C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、main()の中身や、その前に呼び出されるスタートアップという初期化処理について学ぶ。
書籍の中から有用な技術情報をピックアップして紹介する本シリーズ。今回は、秀和システム発行の書籍『ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ(2015年9月11日発行)』からの抜粋です。
ご注意:本稿は、著者及び出版社の許可を得て、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。
※編集部注:前回記事「Linuxカーネルに見る、システムコール番号と引数、システムコール・ラッパーとは」はこちら
printf()の呼び出しの先にあるものとシステムコールの仕組みは漠然とわかったが、次はプログラムの開始と終了時に何が行われるのかを見てみよう。関数の呼び出し先を追うのではなく、呼び出し元をさかのぼってみるわけだ。
C言語の入門書を開くと、プログラムの実行はmain()関数から開始されると説明されていることがある。しかしこれは嘘であり、その前にスタートアップという初期化処理があると説明されている本もある。
が、そのスタートアップの処理について説明されている本は少ない。いったいスタートアップでは、どのような処理が行われているのだろうか。
またプログラムはexit()を呼び出すことで終了する。ではそのexit()では、何が行われているのだろうか。なぜexit()を呼び出すことで、プログラムは終了するのだろうか。プログラムの終了はmain()関数から戻ることでも行われるが、main()から戻った先にはいったい何があるのだろうか。
本章ではmain()関数が呼び出されるまでと、呼び出された後を見てみよう。
連載第2回ではデバッガによる動的解析でprintf()の呼び出しの先を探ったが、ここでも同じようにして、main()が呼ばれるまでの処理を追ってみよう。
サンプル・プログラムには、連載第2回で解析の対象にした実行ファイルhelloをそのまま利用する。
まずはhelloを指定してGDBを起動して、main()にブレークポイントを張って動作を開始しよう。
[user@localhost hello]$ gdb -q hello Reading symbols from /home/user/hello/hello...done. (gdb) break main Breakpoint 1 at 0x80482c5: file hello.c, line 5. (gdb) run Starting program: /home/user/hello/hello Breakpoint 1, main (argc=1, argv=0xbffffc14) at hello.c:5 5 printf("Hello World! %d %s\n", argc, argv[0]); (gdb)
main()関数の先頭でブレークした。ここでwhereにより、バックトレースを追ってみよう。
(gdb) where #0 main (argc=1, argv=0xbffffc14) at hello.c:5 (gdb)
もしかしたらmain()関数の呼び出し元となっている関数がわかるかもしれないと思ったのだが、そういうわけでもないようだ。
どのようにすれば、main()関数の呼び出し元を調べることができるだろうか。
main()関数から戻ってみたらどうだろうか。サンプル・プログラムでは、main()の最後はreturn 0で呼び出し元に戻っている。
まずnextでprintf()の処理を実行する。
(gdb) next Hello World! 1 /home/user/hello/hello 6 return 0; (gdb)
「Hello World!」のメッセージが出力され、実行はreturnの位置に移動している。さらにstepによりステップ実行してみよう。
(gdb) step 7 } (gdb) step 0x08048478 in __libc_start_main () (gdb)
stepを2回行うと、__libc_start_main()という関数に戻ったようだ。ソースコードは表示されないので、これもアセンブラで見てみよう。
(gdb) layout asm
すると図5.1のような画面になった。
見たところ、確かに__libc_start_main()という関数に戻っているようだ。
ということはまず__libc_start_main()という関数があり、そこからmain()が呼ばれているということだろうか?
__libc_start_main()にブレークポイントを張ることはできるだろうか?やってみよう。
(gdb) break __libc_start_main Breakpoint 2 at 0x80482fb (gdb)
うまく張れたようなので、再実行してみよう。
連載第3回でも説明したが、runを実行すると以下のように聞かれる。プログラムが実行中のため「プログラムを最初から実行するか?」と聞かれているので、これには「y」を答えておけばいい。
(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 2, 0x080482fb in __libc_start_main () (gdb)
__libc_start_main()で、うまくブレークできたようだ。バックトレースはどうなっているだろうか。
(gdb) where #0 0x080482fb in __libc_start_main () #1 0x080481e1 in _start () (gdb)
今度は_start()という関数が出てきたようだ。
_start()にブレークポイントを張り、もう一度実行してみよう。
(gdb) break _start Breakpoint 3 at 0x80481c0 (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 3, 0x080481c0 in _start () (gdb)
うまくブレークできている。バックトレースを見てみよう。
(gdb) where #0 0x080481c0 in _start () (gdb)
とくに何も出力されない。画面は図5.2のようになっている。
アセンブラ出力の下のほうでcall命令により__libc_start_main()が呼び出され、その後にhltという命令が呼ばれていることに注目してほしい。
hltはいわゆるHALT命令というもので、CPUの動作を停止するものだ。つまり__libc_start_main()から戻ってきた場合にはプログラムの動作は停止する。この先の処理は実行されない、ということだ。
ということはこれが、main()を呼び出すためのおおもとの処理だろうか。そもそもプログラムが実行開始される位置は、どこかに情報として格納されていないものだろうか。
Copyright © ITmedia, Inc. All Rights Reserved.