なんかおもしろいコンパイラのバグに遭遇したのでめも。
もともとはpeggedを使ったプロジェクトで dub build --build=profile でビルドしたバイナリを実行するとSEGVに遭遇するんだけど・・・、という問題だった。
とりあえずgdbにかけてみると
>>> bt 10 #0 0x0000555555928ecc in trace_pro () #1 0x000055555580cbb8 in _D4core8internal6string__T7dstrcmpZQjFNaNbNiNeMxAaMxQeZi () #2 0x0000555555955b62 in trace_addsym () #3 0x0000555555928fee in trace_pro () #4 0x000055555580cbb8 in _D4core8internal6string__T7dstrcmpZQjFNaNbNiNeMxAaMxQeZi () #5 0x0000555555955b62 in trace_addsym () #6 0x0000555555928fee in trace_pro () #7 0x000055555580cbb8 in _D4core8internal6string__T7dstrcmpZQjFNaNbNiNeMxAaMxQeZi () #8 0x0000555555955b62 in trace_addsym () #9 0x0000555555928fee in trace_pro () (More stack frames follow...)
という感じでどうも相互関数呼び出しによるstack overflowをしている。 そのあとごにょごにょ回り道をしたあげく、DWARFの吐いている core.internal.string.dstrcmp のソースでは存在していないけれど、disassemble では callq trace_pro するようになっていてなんだこれ?ということでDMDのソースをみることに。 すると src/dmd/glue.d に以下のようなコードとコメントが。
/* Doing this in semantic3() caused all kinds of problems: * 1. couldn't reliably get the final mangling of the function name due to fwd refs * 2. impact on function inlining * 3. what to do when writing out .di files, or other pretty printing */ if (global.params.trace && !fd.isCMain() && !fd.naked) { /* The profiler requires TLS, and TLS may not be set up yet when C main() * gets control (i.e. OSX), leading to a crash. */ /* Wrap the entire function body in: * trace_pro("funcname"); * try * body; * finally * _c_trace_epi(); */ StringExp se = StringExp.create(Loc.initial, s.Sident.ptr); se.type = Type.tstring; se.type = se.type.typeSemantic(Loc.initial, null); Expressions *exps = new Expressions(); exps.push(se); FuncDeclaration fdpro = FuncDeclaration.genCfunc(null, Type.tvoid, "trace_pro"); Expression ec = VarExp.create(Loc.initial, fdpro); Expression e = CallExp.create(Loc.initial, ec, exps); e.type = Type.tvoid; Statement sp = ExpStatement.create(fd.loc, e);
つまり dstrcmp 関数がtrace_pro を先頭で呼ぶように書き換えられた結果、 trace_pro -> trace_addsym -> dstrcmp -> trace_pro -> ... のようにひたすら関数呼び出しをし続けてstack overflowになる、ということだ。
peggedを使わない場合の再現コードをなかなか用意できなかったが文字列の大小比較 ("aaa" >= "bbb") は内部で dstrcmp を呼ぶので非常に簡単な再現コードに落とし込むことができた。 つまり文字列の大小比較のあるすべてのコードは -profile オプションを用いると同様にstack oveflowになる、ということである。だれも-profileオプションを使っていないのだろうか。
今回の話の教訓としては、「コードは書いたとおりに動くというのは嘘、コンパイラは隙があればおまえのコードを勝手に書き換える」ということですね。