プログラムはなぜ動くのか - 関数呼び出しの仕組みを追う(1)
「プログラムはなぜ動くのか」、もうずいぶん前に読み終わってけど、関数呼び出しについても色々見てみたんでせっかくなんでまとめてみます。
まぁ、ほとんどは以下のページに詳しく載ってたんだけど、自分の理解を深める意味も込めて。
main関数からサブ関数を呼び出した例
テキストpp.2040あたりのコードを元に次のようなコードを用意する。int add( int x, int y )
{
return x + y;
}int main( int argc, char *argv )
{
int a = 128;
int b = 256;
int c;c = add( a, b );
return c;
}
コンパイルし、objdumpで逆アセンブルしてみます。
add: file format elf32-i386Disassembly of section .init:
08048254 <_init>:
8048254: 55 push %ebp
8048255: 89 e5 mov %esp,%ebp
8048257: 83 ec 08 sub $0x8,%esp
804825a: e8 65 00 00 00 call 80482c4
804825f: e8 bc 00 00 00 call 8048320
8048264: e8 97 01 00 00 call 8048400 <__do_global_ctors_aux>
8048269: c9 leave
804826a: c3 ret
Disassembly of section .plt:0804826c <__gmon_start__@plt-0x10>:
804826c: ff 35 3c 95 04 08 pushl 0x804953c
8048272: ff 25 40 95 04 08 jmp *0x8049540
8048278: 00 00 add %al,(%eax)
...0804827c <__gmon_start__@plt>:
804827c: ff 25 44 95 04 08 jmp *0x8049544
8048282: 68 00 00 00 00 push $0x0
8048287: e9 e0 ff ff ff jmp 804826c <_init+0x18>0804828c <__libc_start_main@plt>:
804828c: ff 25 48 95 04 08 jmp *0x8049548
8048292: 68 08 00 00 00 push $0x8
8048297: e9 d0 ff ff ff jmp 804826c <_init+0x18>
Disassembly of section .text:080482a0 <_start>:
80482a0: 31 ed xor %ebp,%ebp
80482a2: 5e pop %esi
80482a3: 89 e1 mov %esp,%ecx
80482a5: 83 e4 f0 and $0xfffffff0,%esp
80482a8: 50 push %eax
80482a9: 54 push %esp
80482aa: 52 push %edx
80482ab: 68 80 83 04 08 push $0x8048380
80482b0: 68 90 83 04 08 push $0x8048390
80482b5: 51 push %ecx
80482b6: 56 push %esi
80482b7: 68 4f 83 04 08 push $0x804834f
80482bc: e8 cb ff ff ff call 804828c <__libc_start_main@plt>
80482c1: f4 hlt
80482c2: 90 nop
80482c3: 90 nop080482c4
:
80482c4: 55 push %ebp
80482c5: 89 e5 mov %esp,%ebp
80482c7: 53 push %ebx
80482c8: 83 ec 04 sub $0x4,%esp
80482cb: e8 00 00 00 00 call 80482d0
80482d0: 5b pop %ebx
80482d1: 81 c3 68 12 00 00 add $0x1268,%ebx
80482d7: 8b 93 fc ff ff ff mov 0xfffffffc(%ebx),%edx
80482dd: 85 d2 test %edx,%edx
80482df: 74 05 je 80482e6
80482e1: e8 96 ff ff ff call 804827c <__gmon_start__@plt>
80482e6: 58 pop %eax
80482e7: 5b pop %ebx
80482e8: c9 leave
80482e9: c3 ret
80482ea: 90 nop
80482eb: 90 nop
80482ec: 90 nop
80482ed: 90 nop
80482ee: 90 nop
80482ef: 90 nop080482f0 <__do_global_dtors_aux>:
80482f0: 55 push %ebp
80482f1: 89 e5 mov %esp,%ebp
80482f3: 83 ec 08 sub $0x8,%esp
80482f6: 80 3d 58 95 04 08 00 cmpb $0x0,0x8049558
80482fd: 74 0c je 804830b <__do_global_dtors_aux+0x1b>
80482ff: eb 1c jmp 804831d <__do_global_dtors_aux+0x2d>
8048301: 83 c0 04 add $0x4,%eax
8048304: a3 54 95 04 08 mov %eax,0x8049554
8048309: ff d2 call *%edx
804830b: a1 54 95 04 08 mov 0x8049554,%eax
8048310: 8b 10 mov (%eax),%edx
8048312: 85 d2 test %edx,%edx
8048314: 75 eb jne 8048301 <__do_global_dtors_aux+0x11>
8048316: c6 05 58 95 04 08 01 movb $0x1,0x8049558
804831d: c9 leave
804831e: c3 ret
804831f: 90 nop08048320
:
8048320: 55 push %ebp
8048321: 89 e5 mov %esp,%ebp
8048323: 83 ec 08 sub $0x8,%esp
8048326: a1 60 94 04 08 mov 0x8049460,%eax
804832b: 85 c0 test %eax,%eax
804832d: 74 12 je 8048341
804832f: b8 00 00 00 00 mov $0x0,%eax
8048334: 85 c0 test %eax,%eax
8048336: 74 09 je 8048341
8048338: c7 04 24 60 94 04 08 movl $0x8049460,(%esp)
804833f: ff d0 call *%eax
8048341: c9 leave
8048342: c3 ret
8048343: 90 nop08048344
: int add( int x, int y )
{
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 8b 45 0c mov 0xc(%ebp),%eax
804834a: 03 45 08 add 0x8(%ebp),%eax
return x + y;
}
804834d: 5d pop %ebp
804834e: c3 ret0804834f
: int main( int argc, char *argv )
{
804834f: 8d 4c 24 04 lea 4(%esp),%ecx
8048353: 83 e4 f0 and $-16,%esp
8048356: ff 71 fc pushl -4(%ecx)
8048359: 55 push %ebp
804835a: 89 e5 mov %esp,%ebp
804835c: 51 push %ecx
804835d: 83 ec 08 sub $8,%esp
int a = 128;
int b = 256;
int c;c = add( a, b );
8048360: c7 44 24 04 00 01 00 movl $256,4(%esp)
8048367: 00
8048368: c7 04 24 80 00 00 00 movl $128,(%esp)
804836f: e8 d0 ff ff ff call 8048344return c;
}
8048374: 83 c4 08 add $8,%esp
8048377: 59 pop %ecx
8048378: 5d pop %ebp
8048379: 8d 61 fc lea -4(%ecx),%esp
804837c: c3 ret
804837d: 90 nop
804837e: 90 nop
804837f: 90 nop08048380 <__libc_csu_fini>:
8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 5d pop %ebp
8048384: c3 ret
8048385: 8d 74 26 00 lea 0x0(%esi),%esi
8048389: 8d bc 27 00 00 00 00 lea 0x0(%edi),%edi08048390 <__libc_csu_init>:
8048390: 55 push %ebp
8048391: 89 e5 mov %esp,%ebp
8048393: 57 push %edi
8048394: 56 push %esi
8048395: 53 push %ebx
8048396: e8 5e 00 00 00 call 80483f9 <__i686.get_pc_thunk.bx>
804839b: 81 c3 9d 11 00 00 add $0x119d,%ebx
80483a1: 83 ec 1c sub $0x1c,%esp
80483a4: e8 ab fe ff ff call 8048254 <_init>
80483a9: 8d 83 18 ff ff ff lea 0xffffff18(%ebx),%eax
80483af: 89 45 f0 mov %eax,0xfffffff0(%ebp)
80483b2: 8d 83 18 ff ff ff lea 0xffffff18(%ebx),%eax
80483b8: 29 45 f0 sub %eax,0xfffffff0(%ebp)
80483bb: c1 7d f0 02 sarl $0x2,0xfffffff0(%ebp)
80483bf: 8b 55 f0 mov 0xfffffff0(%ebp),%edx
80483c2: 85 d2 test %edx,%edx
80483c4: 74 2b je 80483f1 <__libc_csu_init+0x61>
80483c6: 31 ff xor %edi,%edi
80483c8: 89 c6 mov %eax,%esi
80483ca: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
80483d0: 8b 45 10 mov 0x10(%ebp),%eax
80483d3: 83 c7 01 add $0x1,%edi
80483d6: 89 44 24 08 mov %eax,0x8(%esp)
80483da: 8b 45 0c mov 0xc(%ebp),%eax
80483dd: 89 44 24 04 mov %eax,0x4(%esp)
80483e1: 8b 45 08 mov 0x8(%ebp),%eax
80483e4: 89 04 24 mov %eax,(%esp)
80483e7: ff 16 call *(%esi)
80483e9: 83 c6 04 add $0x4,%esi
80483ec: 39 7d f0 cmp %edi,0xfffffff0(%ebp)
80483ef: 75 df jne 80483d0 <__libc_csu_init+0x40>
80483f1: 83 c4 1c add $0x1c,%esp
80483f4: 5b pop %ebx
80483f5: 5e pop %esi
80483f6: 5f pop %edi
80483f7: 5d pop %ebp
80483f8: c3 ret080483f9 <__i686.get_pc_thunk.bx>:
80483f9: 8b 1c 24 mov (%esp),%ebx
80483fc: c3 ret
80483fd: 90 nop
80483fe: 90 nop
80483ff: 90 nop08048400 <__do_global_ctors_aux>:
8048400: 55 push %ebp
8048401: 89 e5 mov %esp,%ebp
8048403: 53 push %ebx
8048404: bb 50 94 04 08 mov $0x8049450,%ebx
8048409: 83 ec 04 sub $0x4,%esp
804840c: a1 50 94 04 08 mov 0x8049450,%eax
8048411: 83 f8 ff cmp $0xffffffff,%eax
8048414: 74 0c je 8048422 <__do_global_ctors_aux+0x22>
8048416: 83 eb 04 sub $0x4,%ebx
8048419: ff d0 call *%eax
804841b: 8b 03 mov (%ebx),%eax
804841d: 83 f8 ff cmp $0xffffffff,%eax
8048420: 75 f4 jne 8048416 <__do_global_ctors_aux+0x16>
8048422: 83 c4 04 add $0x4,%esp
8048425: 5b pop %ebx
8048426: 5d pop %ebp
8048427: c3 ret
Disassembly of section .fini:08048428 <_fini>:
8048428: 55 push %ebp
8048429: 89 e5 mov %esp,%ebp
804842b: 53 push %ebx
804842c: 83 ec 04 sub $0x4,%esp
804842f: e8 00 00 00 00 call 8048434 <_fini+0xc>
8048434: 5b pop %ebx
8048435: 81 c3 04 11 00 00 add $0x1104,%ebx
804843b: e8 b0 fe ff ff call 80482f0 <__do_global_dtors_aux>
8048440: 59 pop %ecx
8048441: 5b pop %ebx
8048442: c9 leave
8048443: c3 ret
長いっす。なぜなら、main()を呼び出す前後でglibcのライブラリ関数の呼び出しが発生するためです。
命令文のセグメントは.text領域で定義されるので、そこをよく探してみると、
08048344: int add( int x, int y )
{
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 8b 45 0c mov 0xc(%ebp),%eax
804834a: 03 45 08 add 0x8(%ebp),%eax
return x + y;
}
804834d: 5d pop %ebp
804834e: c3 ret0804834f
: int main( int argc, char *argv )
{
804834f: 8d 4c 24 04 lea 4(%esp),%ecx
8048353: 83 e4 f0 and $-16,%esp
8048356: ff 71 fc pushl -4(%ecx)
8048359: 55 push %ebp
804835a: 89 e5 mov %esp,%ebp
804835c: 51 push %ecx
804835d: 83 ec 08 sub $8,%esp
int a = 128;
int b = 256;
int c;c = add( a, b );
8048360: c7 44 24 04 00 01 00 movl $256,4(%esp)
8048367: 00
8048368: c7 04 24 80 00 00 00 movl $128,(%esp)
804836f: e8 d0 ff ff ff call 8048344return c;
}
8048374: 83 c4 08 add $8,%esp
8048377: 59 pop %ecx
8048378: 5d pop %ebp
8048379: 8d 61 fc lea -4(%ecx),%esp
804837c: c3 ret
804837d: 90 nop
804837e: 90 nop
804837f: 90 nop
コンパクトなCのコードも、アセンブラに変換するとかなり複雑に見えます。が、これはレジスタとスタックで値のやり取りを行っているためで、実際はそうでもありません。
#読みやすくするために、16進数→10進数に直しています。
順を追って詳しく見てみました。
main()のスタート
関数が呼び出された直後は、スタックに関数のリターンアドレスが格納され、スタックポインタはそこを指すことになっています。もちろん、main()が呼ばれた場合はこれも例外ではありません。それ以下のアドレスには、関数の引数の値などが格納されます。(ただし、gccのコンパイルオプションによっては引数の値は直接レジスタにセットされるので、必ずしもではありません)
main()関数の前処理
見ると分かる通り、main()の内部変数を処理するまでに、それなりに前処理が行われます。int main( int argc, char *argv )"push %ebp"、"push %ecx"などからも分かるように、レジスタに格納されていた値をスタックにコピーしています。これは以降の処理でレジスタを使うことを想定して、その値を待避させるためです。関数を呼び出した直後は、お決まりのようにこのようなことが行われるようです。
{
804834f: 8d 4c 24 04 lea 4(%esp),%ecx
8048353: 83 e4 f0 and $-16,%esp
8048356: ff 71 fc pushl -4(%ecx)
8048359: 55 push %ebp
804835a: 89 e5 mov %esp,%ebp
804835c: 51 push %ecx
804835d: 83 ec 08 sub $8,%esp
図で書いてみるとこんな感じになります。
add()の呼び出し
次にadd()を呼び出す処理が行われます。int a = 128;
int b = 256;
int c;c = add( a, b );
8048360: c7 44 24 04 00 01 00 movl $256,4(%esp)
8048367: 00
8048368: c7 04 24 80 00 00 00 movl $128,(%esp)
804836f: e8 d0 ff ff ff call 8048344
add()の引数の値をスタックに積み、call命令を使ってadd()を呼び出してみます。
スタック領域のイメージは以下のようになります。add()に関しても他の関数と同様、呼ばれた際にはリターンアドレスがスタックに積まれます。
add()内の処理
add()内の処理は、引数の値を足した値を戻り値として返すことです。コードを見てみましょう。
int add( int x, int y )
{
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 8b 45 0c mov 0xc(%ebp),%eax
804834a: 03 45 08 add 0x8(%ebp),%eax
return x + y;
main()関数と同様に、ebpレジスタの値をスタックに待避させています。その後、add()の引数が入っているスタックの値を参照し、アキュムレータ(eaxレジスタ)を使いながら足し算を行います。
add()のリターン時
}add()の呼び出し時にスタックに待避していたebpレジスタの値を元に戻し、retで呼び出し元に返ります。
804834d: 5d pop %ebp
804834e: c3 ret
main()のリターン時
ここも単なる後処理にすぎません。スタックに待避していたレジスタの値やスタックポインタを元に戻し、呼び出し元に返ります。return c;
}
8048374: 83 c4 08 add $8,%esp
8048377: 59 pop %ecx
8048378: 5d pop %ebp
8048379: 8d 61 fc lea -4(%ecx),%esp
804837c: c3 ret
スタック領域のイメージは以下の通りになります。つまり、main()を呼び出した際の状態に戻ったということです。
まとめ
objdumpで吐かせたアセンブリコードから、関数呼び出しの仕組みを追ってみました。ぱっと見はなかなか難しいのですが、スタック領域のイメージをを図に書きながらコードを追うと、- 関数呼び出し直後は、レジスタの値をスタックに待避させる
- 関数の引数はスタックに順に積んで、callを呼び出す
(今回は"-O0"でしたが、コンパイル時の最適化オプションによっては直接レジスタに積まれる)
それ以外の処理は、関数の内部の計算処理ということになります。この辺りのポイントに慣れればアセンブリのコードも急に読めてきて、なかなか楽しいなと思いました。