プログラムの実行はどのようにして行われるのか、Linuxカーネルのコードから探る:main()関数の前には何があるのか(終)(2/2 ページ)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。最終回は、Linuxカーネルの中では、プログラムの起動時にはどのような処理が行われているのかを探る。
レジスタの設定処理
start_thread()を探してみると、アーキテクチャ依存部のarch/x86/kernel/process_32.cで以下のように定義されていた。
296:void 297: start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) 298: { 299: set_user_gs(regs, 0); 300: regs->fs = 0; 301: regs->ds = __USER_DS; 302: regs->es = __USER_DS; 303: regs->ss = __USER_DS; 304: regs->cs = __USER_CS; 305: regs->ip = new_ip; 306: regs->sp = new_sp; ...
エントリ・ポイントのアドレスがnew_ipという引数で渡され、それをインストラクション・ポインタ(regs->ip)に設定しているようだ。スタックポインタなどの設定も、ここで行われていることになる。
では、load_elf_binary()はどのようにして呼ばれているのだろうか。load_elf_binaryが参照されている部分を探してみよう。
[user@localhost linux-2.6.32.65]$ grep load_elf_binary */* fs/binfmt_elf.c:static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs * regs); fs/binfmt_elf.c: .load_binary = load_elf_binary, fs/binfmt_elf.c:static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs * regs) [user@localhost linux-2.6.32.65]$
なんらかの構造体のload_binaryというメンバに、load_elf_binary()のアドレスが設定されているようだ。このメンバを経由して呼び出されているのだろう。
呼び出し元はどこであろうか。
[user@localhost linux-2.6.32.65]$ grep load_binary */* fs/binfmt_aout.c: .load_binary = load_aout_binary, fs/binfmt_elf.c: .load_binary = load_elf_binary, fs/binfmt_elf_fdpic.c: .load_binary = load_elf_fdpic_binary, fs/binfmt_em86.c: .load_binary = load_em86, fs/binfmt_flat.c: .load_binary = load_flat_binary, fs/binfmt_misc.c: .load_binary = load_misc_binary, fs/binfmt_script.c: .load_binary = load_script, fs/binfmt_som.c: .load_binary = load_som_binary, fs/exec.c: int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; [user@localhost linux-2.6.32.65]$
fs/exec.cで、fnという変数に代入されているようだ。当該の箇所を見てみると、以下のような関数の中で呼び出されていた。
1279:/* 1280: * cycle the list of binary formats handler, until one recognizes the image 1281: */ 1282:int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs) 1283:{ ... 1307: int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; ... 1314: retval = fn(bprm, regs); ...
ここでもう一度、「execve()の処理」の項を見返してほしい。
search_binary_handler()は、実はdo_execve()の中でargv[]の設定処理の後で呼び出されている。その先で各種レジスタの設定が行われている、ということになる。
argv[]の準備
では、スタートアップに渡されるargvはどこで設定されているのだろうか。連載第8回によれば、argvは配列も配列のポインタが指す先の文字列の本体も、スタック上に置かれているということだった。
load_elf_binary()によるELFファイルのロード処理を見てみると、以下のような関数呼び出しがされている箇所がある。
... 934: retval = create_elf_tables(bprm, &loc->elf_ex, 935: load_addr, interp_load_addr); ...
create_elf_tables()はfs/binfmt_elf.cで、以下のように定義されている。
136:static int 137:create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec, 138: unsigned long load_addr, unsigned long interp_load_addr) 139:{ ... 278: /* Now, let's put argc (and argv, envp if appropriate) on the stack */ 279: if (__put_user(argc, sp++)) 280: return -EFAULT; 281: argv = sp; 282: envp = argv + argc + 1; 283: 284: /* Populate argv and envp */ 285: p = current->mm->arg_end = current->mm->arg_start; 286: while (argc-- > 0) { 287: size_t len; 288: if (__put_user((elf_addr_t)p, argv++)) 289: return -EFAULT; 290: len = strnlen_user((void __user *)p, MAX_ARG_STRLEN); 291: if (!len || len > MAX_ARG_STRLEN) 292: return -EINVAL; 293: p += len; 294: } 295: if (__put_user(0, argv)) 296: return -EFAULT; ...
コメントにあるように、スタック上にargcを積み、その直下にargv[]を作成しているようだ。
実際にはここではポインタの配列の実体をスタック上に作成している。よってアプリケーションのスタートアップには、配列へのポインタが渡されるわけではない。
もう一度、glibcのsysdeps/i386/start.Sにあるスタートアップのソースコードを見てみよう。
55: .text 56: .globl _start 57: .type _start,@function 58: _start: ... 63: /* Extract the arguments as encoded on the stack and set up 64: the arguments for `main': argc, argv. envp will be determined 65: later in __libc_start_main. */ 66: popl %esi /* Pop the argument count. */ 67: movl %esp, %ecx /* argv starts just at the current stack top.*/ ... 108: pushl %ecx /* Push second argument: argv. */ 109: pushl %esi /* Push first argument: argc. */ 110: 111: pushl $main 112: 113: /* Call the user's main function, and exit with its value. 114: But let the libc call main. */ 115: call __libc_start_main ...
先ほどのおさらいになるが、スタックのトップにはargcが積まれている。これがまずpopl命令により、ESIレジスタに格納される。
さらにその直下にargv[]の実体があるわけだが、movl命令でESPをECXにコピーすることで、この先頭アドレスがECXに格納される。
そしてpushl命令でECX、ESIがスタックに積まれ、__libc_start_main()が呼ばれる。これにより__libc_start_main()には、argcとargv[]が引数として渡されることになる。
まとめ
main()関数の前には何があるのか、main()関数からリターンした先には何があるのか、疑問に思ったことのあるかたは多いのではないだろうか。
ふだん何気なく書いているmain()関数であるが、その前と後には実は様々な処理が働いていることがわかる。
C言語の実行はmain()関数から始まるとはよく聞く説明であるが、それは様々なものがその下で動いていることで実現されているわけだ。
そしてそこにはスタートアップなどのライブラリがあり、それを作成してくれたひとがいるために我々は楽をすることができている。筆者は様々なサンプルプログラムも含め今までにmain()を書いた回数はもはや数えることもできないが、そのような先人の成果に感謝しつつmain()を書くことができるようになりたいものだ。
書籍紹介
ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ
坂井弘亮著
秀和システム 3,200円
C言語の入門書では、"Hello, World"と出力するプログラムを最初に作るのが定番です。"Hello, World"は、たった7行の単純なプログラムですが、printf()の先では何が行われているのか、main()の前にはいったい何があるのか、考えてみると謎だらけです。本書は、基礎中の基礎である"Hello, World"プログラムを元に、OSと標準ライブラリの仕組みをあらゆる角度からとことん解析します。資料に頼らず、自分の手で調べる方法がわかります。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- プログラミング言語Cについて知ろう
プログラミング言語の基本となる「C」。正しい文法や作法を身に付けよう。Cには確かに学ぶだけの価値がある(編集部) - シェルコード解析に必携の「5つ道具」
コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部) - 【 od 】コマンド――ファイルを8進数や16進数でダンプする
本連載は、Linuxのコマンドについて、基本書式からオプション、具体的な実行例までを紹介していきます。今回は、「od」コマンドです。