C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。最終回は、Linuxカーネルの中では、プログラムの起動時にはどのような処理が行われているのかを探る。
書籍の中から有用な技術情報をピックアップして紹介する本シリーズ。今回は、秀和システム発行の書籍『ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ(2015年9月11日発行)』からの抜粋です。
ご注意:本稿は、著者及び出版社の許可を得て、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。
※編集部注:前回記事「エンジニアならC言語プログラムの終わりに呼び出されるexit()の中身分かってますよね?」はこちら
ここまでは主にglibcが持つスタートアップと終了処理について見てきた。
しかしOSカーネルの中では、プログラムの起動時にはどのような処理が行われているのだろうか。
例えばプログラムの実行はエントリ・ポイントから開始される、という説明をした。ということはエントリ・ポイントをEIPに設定して実行を開始するような処理が、どこかで行われているはずだ。
次はLinuxカーネルでのプロセス起動処理を見てみよう。
そもそもプログラムの実行は、どのようにして行われるのであろうか。
UNIXライクなシステムでは、新しいプロセスはfork()により生成され、exec()系の関数により新たなプログラムに書きかわることで実行されるというのが基本形だ。「基本形」と言っているのはvfork()などの新たなシステムコールもあるからだが、とりあえずは気にしなくていいだろう。
また「exec()系」というのはプログラムを実行するためのライブラリ関数としてexeclp()やexecvp()などがあり、これらを総称して「exec()系」と俗に呼ばれる。最終的にはexecveシステムコールを発行するため、システムコールとしてはexecveに集約される。
つまりプログラムの起動はfork→execveという流れによって行われる。
execveシステムコールが発行されたら、あとはカーネルの仕事だ。そしてこのときに必要な作業は、以下のようなものだろうか。
これらの処理をOSのカーネルが行っていることになる。そしてたとえばLinuxならば、そのソースコードを読めば、実際にどのような処理が行われるのかがわかるはずだ。
ここでは新たなプロセスを開始するための、カーネル内処理を見てみよう。
まずはexecveシステムコールの処理関数を探してみよう。
これはシステムコール・テーブルを見ればわかるだろうか。Linuxカーネル・ソースコードのarch/x86/kernel/syscall_table_32.Sを見てみると、以下のように登録されている。
1:ENTRY(sys_call_table) ... 12: .long sys_unlink /* 10 */ 13: .long ptregs_execve ...
システムコール番号は11のようだ。さらにexecve で検索すると、kernel/entry_32.Sに以下のような定義があることに気がつく。
715:/* 716: * System calls that need a pt_regs pointer. 717: */ 718:#define PTREGSCALL(name) \ 719: ALIGN; \ 720:ptregs_##name: \ 721: leal 4(%esp),%eax; \ 722: jmp sys_##name; ... 728:PTREGSCALL(execve) ...
ptregs_execveというシンボルが定義され、そこからsys_execveが呼ばれる、ということになるようだ。つまりsys_execveを探せばいいことになる。
これはアーキテクチャ非依存部分にあるだろうから、トップ・ディレクトリ付近で検索すればいいだろう…と思ったのだが実際にやっても見つからない。気を取りなおしてx86依存部で検索すると、kernel/process_32.cというファイルが見つかり、sys_execve()が以下のように定義されていた。
447:/* 448: * sys_execve() executes a new program. 449: */ 450:int sys_execve(struct pt_regs *regs) 451:{ ... 459: error = do_execve(filename, 460: (char __user * __user *) regs->cx, 461: (char __user * __user *) regs->dx, 462: regs); ...
do_execve()が呼ばれるようだ。これを探してみると、今度こそアーキテクチャ非依存部のfs/exec.cに見つかり、以下のように定義されていた。
1357:/* 1358: * sys_execve() executes a new program. 1359: */ 1360:int do_execve(char * filename, 1361: char __user *__user *argv, 1362: char __user *__user *envp, 1363: struct pt_regs * regs) 1364:{ ... 1426: retval = copy_strings(bprm->argc, argv, bprm); 1427: if (retval < 0) 1428: goto out; 1429: 1430: current->flags &= ~PF_KTHREAD; 1431: retval = search_binary_handler(bprm,regs); ...
copy_strings()という関数によってargv[]の準備が行われているようだ。
手始めにdo_execve()の処理を見てみたが、別の目線からもexecve()の処理を見てみよう。
実行ファイルはELFフォーマットという形式になっているので、ELFフォーマットの解析処理を行っている箇所があるはずだ。そしてこれはアーキテクチャ非依存部にあるだろう。
ファイル名に「elf」を含むようなファイルは無いだろうか。
[user@localhost ~]$ cd linux-2.6.32.65 [user@localhost linux-2.6.32.65]$ ls */*elf* fs/binfmt_elf.c lib/locking-selftest-softirq.h fs/binfmt_elf_fdpic.c lib/locking-selftest-spin-hardirq.h fs/compat_binfmt_elf.c lib/locking-selftest-spin-softirq.h lib/locking-selftest-hardirq.h lib/locking-selftest-spin.h lib/locking-selftest-mutex.h lib/locking-selftest-wlock-hardirq.h lib/locking-selftest-rlock-hardirq.h lib/locking-selftest-wlock-softirq.h lib/locking-selftest-rlock-softirq.h lib/locking-selftest-wlock.h lib/locking-selftest-rlock.h lib/locking-selftest-wsem.h lib/locking-selftest-rsem.h lib/locking-selftest.c [user@localhost linux-2.6.32.65]$
fs/binfmt_elf.cというファイルがそれらしく思える。
そしてbinfmt_elf.cの中身を見てみると、load_elf_binary()という関数がある。名前からして、まさにELFフォーマットの実行ファイルのロード処理のように思える。
連載第8回で説明したように、プログラムの実行はELFフォーマットに含まれているエントリ・ポイントから開始される。ということはこの中に、エントリ・ポイントを扱っている箇所があるのではないだろうか。
「entry」というキーワードで検索すると、以下のような箇所が見つかった。
563:static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) 564:{ ... 978: start_thread(regs, elf_entry, bprm->p); ...
start_thread()という関数を呼び出している。さらにレジスタ関連らしき引数と、エントリ・ポイントのアドレスを渡しているようだ。
Copyright © ITmedia, Inc. All Rights Reserved.