「Hello World」の本に書いてあったことを自分向けにまとめただけ。
簡単にするためにあえて省いたものやただ単に僕が間違えている可能性もあるのでご参考程度に^^
【execve】
プログラムの実行はスタートアップ関数から行われると思われがちだが、実際にはその前にはカーネルが存在する。
なぜなら、例えば実行ファイルを仮想メモリにマップしたり、argc・argv・envpをスタックに積んだり、BSSの初期化、スタートアップ関数のEIPを設定する必要などがあるからだ。
UNIXライクなシステムでは新しいプロセスを作る際にfork()からexec()系のライブラリを使って新しいプログラムを起動する。
exec()系のライブラリであるexeclp()やexecvp()などは最終的にexecveシステムコールを呼び出す。
なのでここではexecveについて見ていきたいと思う。
Linuxカーネルの場合はexecveは11番目のシステムコールだという。
11番目のシステムコールの実装部分を見てみるとptregs_execveとある。
実際にはptregs_execve→sys_execve→do_execveと呼ばれている。
このdo_execveのなかではcopy_stringsという関数でargvの準備がされている。
またその直後でserach_binary_handlerという関数が呼ばれている。
この関数の中からload_elf_binary→start_threadと呼ばれ、レジスタの設定が行われている。
更にload_elf_binaryの中ではstart_threadの後にcreate_elf_tablesという関数が呼ばれており、この関数ではスタック上にargc・argvを積んでいるようだ。
またこの関数では「envp = argv + argc + 1」と書かれており、この後で紹介する__libc_start_mainでの実装と矛盾がない。
そのようなことがあってようやくスタートアップ関数が呼ばれるようだ。
【_start】
GNUプロジェクトが開発したglibcが提供してくれているスタートアップ関数の_startの実行が開始する。
上の処理ですでにargc、argvといった引数はスタック上に展開されているため、それを素直に受け取る。
ここで受け取った上の3つの引数はこの後にあるスタートアップ関数(__libc_start_main)でも引き継がれる。
_start:
/* 省略 */
popl %esi // pop the argument argc
movl %esp, %ecx // argv starts just at the current stack top (just after argc)
/* 省略 */
pushl %ecx // push argv
pushl %esi // push argc
pushl $main // push main's addr to call this function in __libc_start_main
call __libc_start_main
hlt
この中ではargcとargvについては見受けられるが、envpについては見当たらない。
実はこの後の__libc_start_mainで設定されている。
ちなみに_startのアドレスはエントリポイントを調べること(readelf -a)でも見つけることができる。
いきなりpopなどが使えているところを見るとespといった重要な情報はすでにカーネルによって設定されているようだ。
【__libc_start_main】
_startからは__libc_start_mainが呼ばれるが、ここでは主にenvpの設定とmain関数の呼び出し、そしてmain終了後のexit()の実行が行われる。
__libc_start_main:
/* 省略 */
char **ev = &argv[argc + 1];
__environ = ev;
/* 省略 */
int result = main(argc, argv, __environ MAIN_AUXVEC_PARAM);
exit(result);
上のプログラムを見るとevというポインタにargvの最後の要素の2つ先を設定しているようだ。
そのevは__environという変数に格納され、そのしたでは第3引数としてmainが呼び出されている。
つまり、このevは環境変数のmainへの引数だと言えそうだ。
最後にはmainからの引数を格納したresultという変数を引数にとってexit(result)が呼び出されている。
このexit()はプログラムの中でexit_groupシステムコールが呼び出されなかった場合のみ実行されることになる。
【main】
みんな大好きmain関数。
ご存知の通り、アプリケーションプログラマはこの関数からプログラムを作成する。
【exit】
つまり「man 3 exit」で情報が見れる方のexit。
ではmainから戻ってきて__libc_start_mainのexit(result)に差し掛かったところを考えてみる。
exit:
while(*listp != NULL){ // listpはatexitで設定された関数の情報が入っている
// listpからセットされた関数を次々の呼び出す処理
}
_exit(status); // statusはmainから返された戻り値が入っている
このプログラムを見ると、exit()はatexit()で設定された関数を呼び出した後に_exit()という関数を呼び出すらしい。
つまり、C言語のプログラムからexit()と呼び出すのと_exit()を呼び出す違いはatexitによって登録された関数を処理するかしないかだと言えそうだ。(他にもあると思うけど知りません)
【_exit】
上のexit()からmainの戻り値を引数に呼び出されたこの関数はexitシステムコール(これはカーネルのABIレベルのexit)のシステムコールラッパーになっている。
_exit:
movl 0x4(%esp), %ebx // 引数の1つ目(main関数の戻り値)
movl $0xfc, %eax // システムコールの番号(exit_group)
call *0x80d6750 // _dl_sysinfoに入っている値に飛ぶ(__kernel_vsyscall)
movl $0x1, %eax // システムコールの番号(exit)
int $0x80
hlt
上のプログラムを見ると「movl $0x1, %eax」「int $0x80」の部分でシステムコールが呼び出されると思われるかもしれない。
しかしデバッガで実験してみると実際には「call *0x80d6750」で処理が終了する。
この「0x80d6750」には何が入っているのだろうか?
じつはこのアドレスには_dl_sysinfoというシンボルが紐付けられていて、この変数は__kernel_vsyscallというものを指している。
__kernel_vsyscall:
int $0x80
ret
__kernel_vsyscallではただ単にint $0x80しているだけのようだ。
つまりこの「call *0x80d6750」の先でプログラムは終了してしまってその後のint 0x80は呼び出されないことになる。これはなぜか?
実は上のシステムコール(0xfc)ではexit_groupというシステムコールが呼び出される。
このシステムコールは比較的新しいものでおそらくマルチスレッドが実装された時にできたものだ。
処理としてはプロセスの中のスレッドをすべて終了させるものらしい。
打って変わって下のシステムコール(0x1)でexitが呼び出される。
このシステムコールは古く、おそらくはシングルスレッドの際のものだと思う。
処理としては、このシステムコールを呼び出した一つのスレッドのみを停止させるらしい。
これらのものが並べられている理由としては、おそらく新しい方のシステムコール(exit_group)が存在しなかったとき(対応していなかったとき)にはexitを呼び出すようになっているからだろう。
下位互換を大切にしているLinuxらしい設計だと思う。
【exit()と_exit()とexit_groupとexitと】
たくさんのexitが出てきて頭が痛いがこれらの違いについてちょっと考えてみよう。
・exit()はプログラムの終了時(もしくはユーザーが任意のタイミングで)呼び出されるglibcのライブラリ関数
・_exit()はexit()の中で呼ばれているシステムコールラッパーで、最終的にはexit_groupシステムコールを呼び出すことでプログラムを終了する
・exit_groupはプログラムを終了させるLinuxカーネルが提供するシステムコール
・exitはプログラムを終了させる旧来のLinuxカーネルのシステムコールだが、今はexit_groupが使われている
余計にわかりにくくなっただろうか?
これらのことからLinuxカーネルが定義するのはシステムコールのABIであり、APIを提供するのはglibcの役割だ。
よってPOSIXのexit()と_exit()の機能を実現するための機能としてLinuxのexitやexit_groupシステムコールが存在し、glibcはこれらのシステムコールを呼び出すことで_exit()(さらに言えばこれを呼び出しているexit())を実現している。
つまり、_exit()はシステムコールラッパーとしてユーザに提供されるAPIなわけである。
manコマンドではカテゴリ1がコマンド、カテゴリ2がシステムコールAPI、カテゴリ3がライブラリ関数になっている。
よって「man 2 _exit」とすることは正しくても「man 3 _exit」とするのは不適切だし、
「man 3 exit」とするのは正しくても「man 2 exit」とするのは不適切である。(ただman 2 exitではラッパーである_exitがヒットしてしまうらしい)