kubo39's blog

ただの雑記です。

DMDでprofileオプションを使ったときだけスタックオーバーフローする不思議なバグの調査

なんかおもしろいコンパイラのバグに遭遇したのでめも。

もともとは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オプションを使っていないのだろうか。

今回の話の教訓としては、「コードは書いたとおりに動くというのは嘘、コンパイラは隙があればおまえのコードを勝手に書き換える」ということですね。