Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
「Hello, world! 物語・その1」を書いてから、1ヵ月以上が経過した。gcc ドライバーの謎などについては、「ゼロから始める組み込み Linux」で詳細に紹介したので、本メモ上では、違う視点から実行可能ファイルの謎に切り込んでみよう。
まず、天才愛ちゃんが書いた hello.c を、極限まで削ぎ落としてみる。言い方を変えれば、「世界一小さなCプログラムはなぁに?」ということになるが、その答えは、次に示す exit.c 。
1 int main() { 2 return 123; 3 }
見ての通り、たったの3行である。プログラムらしいところは、return 文一行のみという寂しさだが、馬鹿にしてはいけない。単純なものの中に、実は真実が隠されているのである。早速、ビルドしてみよう。
/usr/src/work/memo-2003-0106 # gcc -Wall -s -o exit exit.c /usr/src/work/memo-2003-0106 # wc -c exit 2548 exit /usr/src/work/memo-2003-0106 # ./exit ; echo $? 123
gcc の -s は、シンボル情報を削除(strip)するためのオプションであり、生成された実行コードは 2548 バイトである。試しに実行してみると、確かに 123 がシェルに「返されている」。ちなみに、$? はシェル変数であり、直前に実行されたフォアグラウンドプロセスの終了ステータス値を格納している。
さて、この exit.c は余りにも簡単過ぎて、教科書に掲載されることはない。有名な hello.c とは printf 文しか違わないにもかかわらず・・。プログラムの世界も、美人が持てはやされる世間と、どこかしら似ているのかもしれない。
「それでは、ここで Question です。main 関数は、一体どこに返るのでしょう?」
実は、この問いはかなりの難問である。試しに、教官の先生に尋ねてみると良い。
「君ねぇ、main 関数を抜けると、プログラムは終了するんだよ」 『プログラムが終了するって、どういうことですか?』 「はぁ?終わるったら、終わるの、The End なの!」 『???』
main 関数内の return 文の行き着く先は、動作環境に強く依存している。Linux におけるプロセス終了の流れについては、Embedded UNIX Vol.6 にて詳しく解説したが、最終的にはシステムコール 1 番に至る。
「論よりコード」、実際にアセンブリ言語を使って確認してみよう。雑誌上の連載では、GNU 開発ツールとの関連から、やむなく GNU assembler gas (AT&T 型式ニーモニック)を使用しているが、Web 上では文法がスッキリしている NASM (インテル型式ニーモニック)を使ってみよう。exit.c をアセンブリ言語で書き直すと、次のようになる(exit.asm)。
1 bits 32 2 global _start 3 4 _start: 5 mov eax, 1 ; EAX = 1 (exit system call#) 6 mov ebx, 123 ; EBX = 123 (status code) 7 int 0x80 ; Execute system call 8 ; No one reaches here...
Linux システムコールは BSD とは異なり、引数はレジスタを介してカーネルに渡される。具体的には、EAX レジスタにシステムコール番号を設定し、EBX/ECX/EDX/ESI/EDI レジスタに引数が設定される。exit システムコールの番号1を EAX、ステータスコード 123 を EBX レジスタにセットした上で、ソフトウェア割り込み 128 番を実行している。これが、「プロセス終了」の真の姿なのである。残念ながら、Cソースを追いかけている限り、この姿が見えることはない。
なお、「なぜ、先頭ラベルが main ではなく _start なのか?」という鋭い質問をお持ちの方は、これまでの記事内容を参照して欲しい。また、1行目の bits 32 は 32 ビットモードに設定するための、おまじないである。ビットモードを適切に設定しておかなければ、NASM は誤ったサイズプレフィックスを前置してしまうため、注意してほしい(サイズプレフィックスの詳細については、今後紹介予定)。
さて、それでは「青春をアセンブル」してみよう!(おじさんは「青春をコンパイル」とは言わないのである)。
/usr/src/work/memo-2003-0106 # nasm -o exit_asm exit.asm /usr/src/work/memo-2003-0106 # wc -c exit_asm 12 exit_asm
愛ちゃんが「あたいのバナナがこんなに小さくなっちゃった、ウッキ〜!!」と、興奮しておられる。無理もない、2kgもあったバナナがわずか12gになってしまったのだから・・。一体、exit_asm に何が起こったのだろうか?
/usr/src/work/memo-2003-0106 # hexdump -C exit_asm 00000000 b8 01 00 00 00 bb 7b 00 00 00 cd 80 |......{.....| 0000000c
ダンプリストで確認したが、益々「訳分かんない」のである。困った時に頼りにならぬは統合開発環境、頼りになるのはやっぱり binutils である。ここで、objdump 師匠に登場頂こう。
/usr/src/work/memo-2003-0106 # objdump -b binary -m i386 -D exit_asm exit_asm: file format binary Disassembly of section .data: 00000000 <.data>: 0: b8 01 00 00 00 mov $0x1,%eax 5: bb 7b 00 00 00 mov $0x7b,%ebx a: cd 80 int $0x80
さすがは、我らが objdump 師匠。逆アセンブルにより、exit_asm の正体を1バイト残らず暴き出してくださった(オプションの与え方がポイント)。
「それでは、小さいバナナを食してみようかのぅ」と行きたいところだが、残念ながら、このバナナは食べられないのである。愛ちゃんのために、食べられるバナナにチチンプイプイしてみよう。
/usr/src/work/memo-2003-0106 # nasm -f elf -o exit.o exit.asm /usr/src/work/memo-2003-0106 # wc -c exit.o 512 exit.o /usr/src/work/memo-2003-0106 # file exit.o exit.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped /usr/src/work/memo-2003-0106 # nm exit.o 00000000 T _start
ここで言う「食べられる」とは「Linux 上で実行出来る」ことであり、正確には ELF (Executable and Linking Format)と呼ばれる実行可能ファイル形式に変換することを意味している。NASM では、-f オプションに elf を指定することで、ELF 型式の「オブジェクトファイル」が出力される。
NASM は、最初から実行可能ファイルを出力するのではなく、オブジェクトファイルを出力する点に、注目してほしい。アセンブラーの次には、リンカーローダーが控えている。
/usr/src/work/memo-2003-0106 # ld -o exit_asm exit.o /usr/src/work/memo-2003-0106 # wc -c exit_asm 733 exit_asm /usr/src/work/memo-2003-0106 # file exit_asm exit_asm: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped /usr/src/work/memo-2003-0106 # ./exit_asm ; echo $? 123
リンカーローダー ld により、オブジェクトファイルは実行可能ファイルに生まれ変わる。そのサイズは、再び733gまで大きくなった。実行してみると、きちんと 123 がシェルへ返されている。exit プロセスは、確かに生まれ、そして消え去ったようだ。
アセンブリ言語により、プロセスの正体はかなり明瞭になった。その本体が実は12バイトに過ぎないことも、私達は理解した。となると、水増し分である、721 バイトが何とも気になる。とってもとっても、損をしたような気分だ。これでは、悔しくて寝られそうもないよ・・ウッキ〜〜!
ということで、物語は最終章へと続くのである。