C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、プログラムの終わりに呼び出されるexit()の中身を探る。
書籍の中から有用な技術情報をピックアップして紹介する本シリーズ。今回は、秀和システム発行の書籍『ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ(2015年9月11日発行)』からの抜粋です。
ご注意:本稿は、著者及び出版社の許可を得て、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。
※編集部注:前回記事「あなたが知らないプログラムの真の始まり――main()関数の前にあるスタートアップとは」はこちら
main()関数が呼び出されるまでの処理の内容はだいたいわかってきたが、次はプログラムの終了についてだ。
プログラムの実行は、exit()が呼び出されることで終了する。これはプログラマが明示的に呼び出すこともあれば、main()から戻った後にスタートアップによって呼び出される場合もあるだろう。
しかしexit()からは戻ってくることは無いため呼び出されっぱなしであり、このためexit()の中ででどのような処理が行われているのかを意識することは少ないかもしれない。
ここではGDBによる動的解析と、glibcのソースコード読解による静的解析の両面から、exit()で行われる処理を追ってみよう。
まずはGDBでexit()の処理を追うことで、プログラムの実行が終了するその瞬間を探ってみよう。
GDBでhelloを起動して、exit()にブレークポイントを張る。
[user@localhost hello]$ gdb -q hello Reading symbols from /home/user/hello/hello...done. (gdb) break exit Breakpoint 1 at 0x8048e65 (gdb) run Starting program: /home/user/hello/hello Hello World! 1 /home/user/hello/hello Breakpoint 1, 0x08048e65 in exit () (gdb)
exit()もglibcによって与えられるものなので、ブレークした箇所のC言語のソースコードは出てこない。layout asmでアセンブラを見てみよう。
すると図5.7のようになった。
さて、プログラムが終了する瞬間を見つけるのは簡単だ。この状態でnextiによりステップ実行を繰り返していき、終了するところを探ればいい。
実際に何度か繰り返して試してみると、図5.8の位置でnextiを実行したときにプログラムが終了することがわかってきた。
_exit()という関数を呼び出したときに終了しているようだ。
そこで次は_exit()にブレークポイントを張ってみよう。
(gdb) break _exit Breakpoint 2 at 0x8053c10 (gdb)
再実行してみる。
(gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) Starting program: /home/user/hello/hello Breakpoint 1, 0x08048e65 in exit () (gdb)
まずはexit()の先頭でブレークする。さらにcontinueしてみると_exit()の先頭でブレークし、図5.9のような画面になった。
0x8058304の位置でint $0x80が呼び出されていることに注目してほしい。ということはこれはシステムコールの呼び出しだ。
これは一見すると_exit()の内部でシステムコールが呼ばれているように思えるのだが、実際にステップ実行で処理を進めてみると、0x8053c19にあるcall命令で関数呼び出しされ図5.10のようになり、さらにそこでint $0x80が呼び出されるところでプログラムが終了した。
ということは、何らかのシステムコールによって終了しているようだ。
またexit()は_exit()を呼び出し、実際の終了はそちらで行われる。exit()は終了処理を行った後に_exit()を呼び出すライブラリ関数になっているということがわかるだろう。
どのようなシステムコールが呼ばれているのかを知るには、int $0x80が呼ばれたときのシステムコール番号を見てみればいいだろう。
図5.10の位置で、レジスタの値を見てみる。
(gdb) info registers eax 0xfc 252 ecx 0x1 1 edx 0x80d82b4 135103156 ebx 0x0 0 esp 0xbffffb48 0xbffffb48 ebp 0xbffffb68 0xbffffb68 esi 0x0 0 edi 0x8048c20 134515744 eip 0x110414 0x110414 <__kernel_vsyscall> eflags 0x246 [ PF ZF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb)
EAXの値が252になっている。これがシステムコール番号なわけだが、これに相当するシステムコールは何であろうか。
システムコール番号はLinuxカーネルのシステムコール・テーブルを見ればわかるはずだ。連載第6回の説明を見返すと、arch/x86/kernel/syscall_table_32.Sというファイルにある。
実際にsyscall_table_32.Sを見ると、252番は以下のように定義されている。
... 252: .long sys_fadvise64 /* 250 */ 253: .long sys_ni_syscall 254: .long sys_exit_group ...
「exit_group」というシステムコールがあるようだ。man exit_groupで調べてみると、以下のように書かれている。
NAME exit_group - exit all threads in a process SYNOPSIS #include <linux/unistd.h> void exit_group(int status); DESCRIPTION This system call is equivalent to exit(2) except that it terminates not only the calling thread, but all threads in the calling process's thread group. ...
プロセス内の全スレッドを終了するシステムコールとのことだ。
DESCRIPTIONを読むと、exitシステムコールと等価であるが、exitはそれを呼んだスレッドのみ終了するのに対して、exit_groupはプロセス内の全スレッドを終了する、とある。
ここでもう一度、連載第4回のstraceによるシステムコール・トレースを見てほしい。以下のようなシステムコール呼び出しが検出されており、プログラムの終了時には実はexit_groupが呼ばれていたことがわかる。
exit_group(0) = ?
さらに図5.9を見返してみよう。
callによる関数呼び出しの先ではexit_groupが呼ばれるためにcall以降が実行されることはないはずなのだが、そこにあるコードはこれもint $0x80によるシステムコール呼び出しだ。直前でEAXに1を設定しているのでシステムコール番号は1のようだ。syscall_table_32.Sのテーブルを調べると、以下のようになっている。
ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ .long sys_exit ...
どうやら「exit」というシステムコールがあるようだ。
つまり「exit_group」と「exit」という2種類のシステムコールがあるようなのだが、これはいったいどういうことであろうか。
Copyright © ITmedia, Inc. All Rights Reserved.