@uents blog

Code wins arguments.

プログラムはなぜ動くのか - 関数呼び出しの仕組みを追う(2)

このシリーズの最後は、forループを使った関数呼び出しについて。文章を書くひまがなさげなんで、今日はコードだけアップ。後日追記。⇒追記しました。

いつものようにテキストに合わせたようなコードを用意してみました。forループの中でinc()という関数を呼び出すシンプルなコードです。

int inc( int x )
{
return x + 1;
}

int main( int argc, char *argv )
{
int i, j;

for ( i = 0; i < 10; i ++ ){
j = inc( i );
}

return j;
}


コンパイルしてobjdumpで逆アセンブルしてみます。
08048344 :

int inc( int x )
{
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 8b 45 08 mov 0x8(%ebp),%eax
804834a: 83 c0 01 add $0x1,%eax
return x + 1;
}
804834d: 5d pop %ebp
804834e: c3 ret

0804834f

:

int main( int argc, char *argv )
{
804834f: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048353: 83 e4 f0 and $0xfffffff0,%esp
8048356: ff 71 fc pushl 0xfffffffc(%ecx)
8048359: 55 push %ebp
804835a: 89 e5 mov %esp,%ebp
804835c: 53 push %ebx
804835d: 51 push %ecx
804835e: 83 ec 04 sub $0x4,%esp
8048361: bb 00 00 00 00 mov $0x0,%ebx
int i, j;

for ( i = 0; i < 10; i ++ ){
j = inc( i );
8048366: 89 1c 24 mov %ebx,(%esp)
8048369: e8 d6 ff ff ff call 8048344
804836e: 83 c3 01 add $0x1,%ebx
8048371: 83 fb 0a cmp $0xa,%ebx
8048374: 75 f0 jne 8048366
}

return j;
}
8048376: 83 c4 04 add $0x4,%esp
8048379: 59 pop %ecx
804837a: 5b pop %ebx
804837b: 5d pop %ebp
804837c: 8d 61 fc lea 0xfffffffc(%ecx),%esp
804837f: c3 ret


「関数を呼ぶ前にレジスタをスタックに待避」「関数を呼ぶときは引数をスタックにpushしてcall」「関数から戻ってきたときは待避した値をレジスタに復帰」といった処理は、これまで見てきたものと同じです。(詳しくは前回のエントリーを参考に)

ただ、違うところは繰り返しの手続きが含まれている所。

  for ( i = 0; i < 10; i ++ ){
j = inc( i );
8048366: 89 1c 24 mov %ebx,(%esp)
8048369: e8 d6 ff ff ff call 8048344
804836e: 83 c3 01 add $0x1,%ebx
8048371: 83 fb 0a cmp $0xa,%ebx
8048374: 75 f0 jne 8048366
}

forループのカウンタの役割を果たしている変数“i”の値は、ebxレジスタ(ベース・レジスタ)に格納されて10(=0xa)と比較されていることが分かります。その比較結果を見てループの先頭の手続きに戻るかどうかを、jneコードで処理しています。それ以上でもそれ以下でもない、いたってシンプルなコードです。

ただ、ecxレジスタ(カウント・レジスタ)を使ってくれるのかなと期待してたんですが、必ずしもそうじゃないみたい。そこはコンパイラや解釈系に依存するのかもしれません。

まとめ

Cのコードとアセンブラを色々と比較して、コード上での変数の扱いとスタック・レジスタのイメージが自分の中で明確になったのが、やはり収穫でした。特にCのような手続き型言語の場合は、こういった処理がイメージしやすいのではと感じます。世の中に数多くの言語が出ている中、多くのOSのカーネルやデバイスドライバの実装で未だにCが使われているのは、こういったことが理由のひとつあるのかもしれない。

それにしてもgccやbinutils、gdbなどのGNUツールはかなり危険です。構いすぎると深追い必至、ひたすら遊び続けてしまいます^^; これ以上突き進むとバイナリアンの世界にどっぷりと漬ってしまいそうなので、「プログラムはなぜ動くのか」のエントリーもこの辺りで終わりにしたいと思います。

次は C++ / SQLite / 真面目なpthreadプログラミング、その辺りに手を伸ばしてみる予定です。