GCCとcdecl規約とmrb_value
mrubyのContextThreading化で大変なバグを2つ見つけた。それを直したらブロックの呼び出しができるようになって、mrblibを含めたmrubyが作れた。
一つはOP_JMPIF/OP_JMPNOTのVM命令側で、ここに書くのも恥ずかしいのだが、昨日の記事に書いたコードがすでに恥ずかしいことになっているので致し方ない。修正コードを置いておく。
CASE(OP_JMPIF) { char tt0 = regs[GETARG_A(i)].tt; // ←コレと /* A sBx if R(A) pc+=sBx */ if (mrb_test(regs[GETARG_A(i)])) { pc += GETARG_sBx(i); i=*pc; } else { i=*++pc; } asm("addl $40, %esp;"); // asm("cmpb $0, %0" : : "r" (regs[GETARG_A(i)].tt)); asm("cmpb $0, %0" : : "r" (tt0)); // ←コレ asm("ret $0"); NEXT; }
なんと、元のやつはジャンプ先の命令に埋め込まれたレジスタと比較していたのだ。よく動いていたなほんとに。
そしてもう一つはMAKE_CT_FUNCマクロ内のret命令。
*adr = 0xc2;\ *(adr + 1) = 0x00;\ *(adr + 2) = 0x00;\
となっていたのを
*adr = 0xc2;\ *(adr + 1) = 0x04;\ *(adr + 2) = 0x00;\
に変えた。c2はret nnという形の3byte命令で、nnには16bitの値が入る。リターンアドレスをスタックから取り出した後に、更にnnのぶんだけスタックを移動する。
スタック上のリターンアドレスはcall時に自動的に積まれるもので、これより先頭側は関数内のローカル変数として使うスタックフレームとなる。逆に後ろ側は呼び出し元の関数が使う領域である。ret 4とするこの修正は、自分のスタックフレームではなく、呼び出し元の領域を削り取ることになる。しかしこれで正しく動くようになる。
今回は、なぜこのような修正が発生したのか、という話。長いけどmrubyにはあんまり関係なくて、GCCの吐き出すコードとスタックの使い方、cdecl規約の決まりごとなどの関連となる。
1.序章 - pushが多い話
もともと、ContextThreading化の前にgccに-Sオプションをつけて出力したコードが何か変なことは気づいていた。例えばOP_STRING。
CASE(OP_STRING) { /* A Bx R(A) := str_new(Lit(Bx)) */ regs[GETARG_A(i)] = mrb_str_literal(mrb, pool[GETARG_Bx(i)]); mrb->arena_idx = ai; NEXT; }
ちょっと長いがアセンブラのコード。
L413: subl $40, %esp movl %ebx, %eax shrl $23, %eax sall $4, %eax addl -800(%ebp), %eax movl %eax, -1004(%ebp) shrl $3, %ebx andl $1048560, %ebx movl -868(%ebp), %esi addl %ebx, %esi leal 8(%esp), %edi movl $4, %ecx rep movsl movl 12(%ebp), %ecx movl %ecx, 4(%esp) leal -792(%ebp), %ebx movl %ebx, (%esp) call _mrb_str_literal pushl %ecx movl $4, %ecx movl -1004(%ebp), %edi movl -1324(%ebp), %esi rep movsl movl -848(%ebp), %edi movl 12(%ebp), %esi movl %edi, 4236(%esi) addl $4, -796(%ebp) movl -796(%ebp), %eax movl (%eax), %ebx addl $40, %esp;ret $0 movl %ebx, %eax andl $127, %eax movl _optable.3830(,%eax,4), %eax jmp *%eax
一番上がインラインアセンブラで書いたスタックの調整。次の塊がGETARG_Bx(i)、その次が引数の設定とmrb_str_literal呼び出し、戻り値のコピー、arena_idx、NEXTルーチンとなっている。
俺が疑問に思ったのはcall _mrb_str_literal直後のpushだ。この時点でecxはcall先の動きにより値は不定となっているはずで、スタックにpushする意味がわからない。そもそもこのコードを見る限り、pushしてるのにpopしてない。スタックが伸びているんじゃないか。GCCのバグか?という具合だ。
気づいても意味がわからないしスタックもズレずに何故かちゃんと動いてるようだったから、とりあえず放置してあった。まあ、マシンスタックを調整して無理矢理動かすのがContextThreadingなのだから、こういうスタック関連で理解不能な部分は後々必ず問題にはなるんだろう、とは思いつつ。
2.cdecl規約
Cなどの言語には呼び出し規約というものがある。関数を呼び出すときに、引数や戻り値はどのように渡すのか、という取り決めをしておいて、そのルールに従って呼び出し側/呼び出され側がデータのやり取りをすれば、きちんと動く。そのルールのことを言う。けっこうたくさん種類があって、引数は右から順にスタックに積むだとか、左から積むだとか、レジスタに入れるだとか、引数に使ったスタックはどっち側が解放するだとか、変更するレジスタの保存はどうするだとか、まあいろいろ事細かに。
んで、だいたいのCコンパイラはcdecl規約というのがデフォルトになっている。GCCもそうだ。これは、
・引数は右から順にスタックに積む
・引数に使ったスタックは呼び出し元の関数側で処分する
・戻り値はeaxで返す
・戻り値がeaxに入らないサイズだった場合、戻り値を格納する領域のアドレスをスタックの最後に積む
・関数内でeax、ecx、edxは破壊してよい。それ以外は変えちゃダメ。
といった感じのルールらしい。正確な定義がどこにあるのかはよくわからない。ぼんやりとした決め事なんだろうか。
3.スタックがズレている件
ContextThreading化したmrubyでブロックを呼ぶとコケるので、その原因を調べていた。どうやらブロックのOP_RETURN後にコケていることがわかってきたので、gdbを使ってスタックの状態を調べてみた。
実行時に生成したコードを動かしているのでどうにもデバッグしにくい。ちょっと強引に、
void breakpoint(void){}
とか関数を作っておいて、OP_RETURNの最後に
regs[acc] = v; } breakpoint(); JUMP; }
としてブレークすることに。以下gdbの操作と結果。はじめてのgdbなので微妙なのは笑って許して。
(gdb) b breakpoint Breakpoint 1 at 0x41080c: file vm.c, line 180. (gdb) r test.rb Starting program: xxxxxx\mirichi\ct_mruby\bin\mruby.exe test.rb [New Thread 3608.0xe4c] Breakpoint 1, breakpoint () at vm.c:180 180 void breakpoint(void){} (gdb) n mrb_run (mrb=0x5d4038, proc=0x5d8774, self=...) at vm.c:1237 1237 JUMP; (gdb) l 1232 syms = irep->syms; 1233 1234 regs[acc] = v; 1235 } 1236 breakpoint(); 1237 JUMP; 1238 } 1239 1240 CASE(OP_TAILCALL) { 1241 /* A B C return call(R(A),Sym(B),R(A+1),... ,R(A+C-1)) */ (gdb) x/100xb $esp 0x22f750: 0x38 0x40 0x5d 0x00 0x24 0xa1 0x5d 0x00 0x22f758: 0x01 0x01 0x00 0x00 0xe4 0x86 0x5d 0x00 0x22f760: 0x0c 0x00 0x00 0x00 0x0c 0xa5 0x5d 0x00 0x22f768: 0x30 0xdd 0x5d 0x00 0x5c 0x87 0x5d 0x00 0x22f770: 0x90 0xf9 0x22 0x00 0x94 0xf7 0x22 0x00 0x22f778: 0x0e 0xeb 0x5e 0x00 0x5f 0x7c 0x41 0x00 0x22f780: 0x00 0xf9 0x22 0x00 0x30 0x83 0x5e 0x00 0x22f788: 0x01 0x00 0x00 0x00 0x08 0xff 0x22 0x00 0x22f790: 0x94 0x5c 0xbe 0x77 0x00 0x00 0x00 0x00 0x22f798: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x22f7a0: 0x00 0x00 0x00 0x00 0x76 0x00 0x00 0x00 0x22f7a8: 0x04 0xed 0x5e 0x00 0xb5 0x14 0x41 0x00 0x22f7b0: 0x68 0xfa 0x22 0x00 (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x00000076 in ?? () (gdb)
最後はSEGVして終わっている。0x00000076というアドレスの命令を実行しようとして盛大にコケているわけだ。ブレークした部分でスタックの中身を出力してある。これを読み解いてみる。
まず、OP_RETURNは先頭でスタックを40byte進めているので、esp先頭から40byteはゴミである。その次の4byteがct_func内に戻るためのリターンアドレスで、その次の4byteはOP_SENDなどでct_funcを呼び出しているところに戻るリターンアドレス、そこから40byteがゴミで、トップのct_func内に戻るリターンアドレス、mrb_runに戻るリターンアドレス、という感じになっているはずだ。
つまり、こう。
0x22f750: 0x38 0x40 0x5d 0x00 0x24 0xa1 0x5d 0x00 | 0x22f758: 0x01 0x01 0x00 0x00 0xe4 0x86 0x5d 0x00 | 0x22f760: 0x0c 0x00 0x00 0x00 0x0c 0xa5 0x5d 0x00 | ←ゴミ 0x22f768: 0x30 0xdd 0x5d 0x00 0x5c 0x87 0x5d 0x00 | 0x22f770: 0x90 0xf9 0x22 0x00 0x94 0xf7 0x22 0x00 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
x86アーキテクチャなのでリトルエンディアンになる。上下Byteが逆に並ぶから注意が必要だ。5eで始まるアドレスはmallocで確保したヒープのあたり、41で始まるのはCで書かれた実行コードが入っているあたり、22で始まるとスタックのあたりである。詳細はわからないがだいたいそのへんのアドレスだということがわかれば上出来だろう。だって0x00000076とかが動いてコケてるんだから。
これを見る限り、スタックそのものはきちんと積まれていることがわかる。では0x00000076というのは何かというと、なんかゴミのエリアの中にそれっぽいやつが見える。
+-------------------------------+ 0x22f7a0: 0x00 0x00 0x00 0x00 | 0x76 0x00 0x00 0x00 | ←この右側のやつがいかにも怪しい
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
つまり、ct_func内に戻るはずが、スタックポインタが4byte進んでいて、ゴミ内のデータを拾ってジャンプしているのではないか、と。こうなると例のpush1個が非常に疑わしい。
4.GCCのcdecl実装の詳細(呼び出し側)
mrb_str_literal関数はこのようなプロトタイプ宣言となっている。
mrb_value mrb_str_literal(mrb_state*, mrb_value);
mrb_stateポインタとmrb_valueの値を引数として、mrb_valueを戻り値とする。mrb_valueは16byteなのでレジスタには当然入らない。引数側は愚直にスタックに積むとして、戻り値はcdecl規約だとちょっと特殊になる。おそらく余分なpushはGCCのこのあたりのやり取りが影響しているのではないかと予想できる。
OP_STRINGはちょっと難しいので、簡単なテストコードを作ってそのアセンブラを見てみることにする。
mrb_value callee(mrb_value t) { return t; } void caller(void) { mrb_value v; SET_NIL_VALUE(v); callee(v); }
mrb_valueを作って、それを引数で渡してそのまま値として戻ってくる。呼び出し側のアセンブラはこのようになった。
_caller: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx subl $84, %esp movb $0, -32(%ebp) movl $0, -40(%ebp) leal -56(%ebp), %eax movl %eax, -60(%ebp) leal 4(%esp), %edx leal -40(%ebp), %ebx movl $4, %eax movl %edx, %edi movl %ebx, %esi movl %eax, %ecx rep movsl movl -60(%ebp), %eax movl %eax, (%esp) call _callee subl $4, %esp leal -12(%ebp), %esp addl $0, %esp popl %ebx popl %esi popl %edi leave ret
pushではないがcall直後にespを操作するsublがある。同じ意味だ。スタックの状態図を書きながら動きを追ってみることにする。
まず関数が呼ばれるとスタックの先頭に戻りアドレスがセットされる。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
最初にebpをスタックに保存して、espをebpに入れる。これはCのプログラムの作法のようなもので、これによりebpが突入してきたときのスタックの位置、espは関数内で伸ばしたり縮めたりした結果のスタックの先頭になる。ebpとespではさまれたエリアがスタックフレームである。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp(=esp) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
pushでediとesi、ebxを保存する。これらはcdecl規約では破壊してはダメなので、使う場合はこのように保存してやる必要がある。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
更にsublでスタックを84byte伸ばす。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | esp + 84 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
84byteの根拠がよくわからないが、余裕を持って取っているのかもしれない。O0でコンパイルしたし。
次のmovbとmovbはローカル変数vへのnil値のセットだ。mrubyではMRB_TT_FALSEの値を0にすればNILとなる。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | ebp - 40(ローカル変数v) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 36 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp - 32 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 28 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | esp + 84(ebp - 12) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
んで、leal -56(%ebp), %eaxでeaxに入れた値を-60(%ebp)に格納している。lealは数字とレジスタの値を足してレジスタに入れるアドレス計算用の3オペランド加算命令だ。ebp-56が何なのかはこの時点ではよくわからない。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp-56 | ebp - 60 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
何かの領域 | ebp - 56 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | ebp - 40(ローカル変数v) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 36 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp - 32 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 28 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | esp + 84(ebp - 12) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
続いてesp+4のアドレスをedxに、ebp-40のアドレスをebxに、4をeaxに入れる。これらのレジスタはそのままそれぞれedi、esi、ecxに入れられて、movslで使われる。movslはesiのアドレスからediのアドレスにecxの数だけ4byte単位でコピーする命令で、つまり、ローカル変数vに入っている値を、esp+4にコピーするということだ。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | esp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | esp + 12 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 16 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp-56 | ebp - 60 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
何かの領域 | ebp - 56 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | ebp - 40(ローカル変数v) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 36 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp - 32 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 28 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | esp + 84(ebp - 12) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
最後にebp-60に入っている値をespにコピーして、calleeを呼び出す。スタックの先頭はcdecl規約に従い、戻り値が格納されるアドレスが入っているので、calleeの中でこの値を参照してcallerのスタックフレーム内に値を書き込むこととなる。はずだ。
戻ってきた後でsublしてて意味がわからないが、そのあとでebp-12をespに突っ込んでいるのでespはどうなっていてもまあ、問題は無い。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp-56 | esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | esp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | esp + 12 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 16 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp-56 | ebp - 60 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値用領域 | ebp - 56 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値用領域 | ebp - 52 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値用領域 | ebp - 48 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値用領域 | ebp - 44 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | ebp - 40(ローカル変数v) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 36 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp - 32 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 28 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | esp + 84(ebp - 12) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
5.GCCのcdecl実装の詳細(呼び出され側)
んじゃあcallee側を見てみよう。
_callee: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx subl $20, %esp leal -32(%ebp), %edx leal 12(%ebp), %ebx movl $4, %eax movl %edx, %edi movl %ebx, %esi movl %eax, %ecx rep movsl movl 8(%ebp), %edx leal -32(%ebp), %ebx movl $4, %eax movl %edx, %edi movl %ebx, %esi movl %eax, %ecx rep movsl movl 8(%ebp), %eax addl $20, %esp popl %ebx popl %esi popl %edi leave ret $4
流れとしてはさっきと似たようなもんだ。レジスタを保存してespを進めてスタックフレームを確保する。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esp (ebp - 32) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp - 28 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp - 24 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp - 20 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp - 16 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | ebp - 12 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi | ebp - 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi | ebp - 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値アドレス | ebp + 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | ebp + 12(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp + 16(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp + 20(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp + 24(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp+12からの4byteをebp-32にコピーする。ebp-32からの領域がcalleeのローカル変数tである。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | esp (ebp - 32) ローカル変数t |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 28 ローカル変数t |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp - 24 ローカル変数t |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp - 20 ローカル変数t |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp - 16 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebx | ebp - 12 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
esi | ebp - 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
edi | ebp - 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
元のebp | ebp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | ebp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値アドレス | ebp + 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | ebp + 12(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp + 16(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | ebp + 20(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | ebp + 24(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp-32からの4byteをebp+8に入っているアドレスの領域にコピーする。ローカル変数を戻り値として返すための動きだ。
最後に、ebp+8に入っている値をeaxに入れて、スタックを戻す。ebp+8は戻り値のアドレスなので、eaxにそれを入れるて返しているのだが、これがcdecl規約の決まりごとなのかどうかはわからない。leave命令は最初のpush ebpとmovl esp,ebpを巻戻す便利命令だ(遅いけど)。leaveが終わった時点でスタックはこうなる。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻りアドレス | esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
戻り値アドレス | esp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | esp + 8(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 12(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | esp + 16(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 20(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
で、残ったのはret命令だ。ret $4となっているので、戻りアドレスをpopしてから4byteスタックを捨てる。callerに戻ったときのスタックはこのようになる。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | esp(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 4(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | esp + 8(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 12(引数) |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
caller側からcalleeを呼び出したときのスタックはこのような感じだった。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ebp-56 | esp |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0(NIL) | esp + 4 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 8 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
tt=MRB_TT_FALSE | esp + 12 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ゴミ | esp + 16 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
callerからcalleeを呼び出して戻ったらスタックフレームが4byte縮んでるじゃないですかやだー。
つまり、スタックの整合性が取れなくなるのでcaller側では呼出し後にpushしているのだ。
6.この動きについての考察と問題と対策
通常Cコンパイラの実装では、スタックフレームとして確保するのはローカル変数の部分までで、関数呼び出しの引数は呼び出す直前にpushされ、呼び出し後に捨てられる。GCCが引数の部分までスタックフレームに含めているのが特殊だ。
GCCでは戻り値用のアドレスはcallee側で捨てることになっていて、実際ret 4として捨てているが、caller側は引数部分をスタックフレームとして持っているため、捨てられるとスタックフレームが短くなってしまう。だから戻ってきてから短くなった分を補填して調整しているのだ。
ところでcdecl規約について調べてみても、レジスタに入りきらないサイズの戻り値の場合に、スタックの先頭に入れた戻り値用アドレスをcaller/calleeのどっちが捨てるのかの記述は出てこない。どうやらコンパイラ依存ということらしく、実はVCでこのコードをコンパイルするとcalleeのretは普通にret 0となり、caller側がその領域もまとめて捨てていた。
mrubyのContextThreading化ではmrb_valueを返す関数という定義でct_funcを生成しているので、呼び出し側は戻ってきたときにスタックフレームが短くなっていることを想定している。ct_funcは戻るときにスタックフレームを4byte短くしてやらないと整合性が取れない、というわけだ。
だからスタックがズレたし、ret 4にしてこれが綺麗に流れるようになった。
これがVCだったらret 0できちんと動いていたはずだ。
7.次の展開
ブロックの呼び出しができるようになったのでmrblibをコンパイルしてmrubyに含めることができるようになった。これでpメソッドとかtimesメソッドとかが使える。が、AOベンチを動かしたらコケてしまった。
コケている箇所はArray.new(3)というところ。どうやら省略可能引数の処理が問題になっているようだ。これまたとても厄介なシロモノで、もう一段ややこしいbranch-inliningを作りこむ必要があるかもしれない。
これが解決できたら次のContextThreadingネタということになる。