/ «2004-02-21 (Sat) ^ 2004-02-28 (Sat)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2004-02-22 (Sun)

[Writing] Hello, world! 物語・その2

Hello, world! 物語・その後

「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 文しか違わないにもかかわらず・・。プログラムの世界も、美人が持てはやされる世間と、どこかしら似ているのかもしれない。

main はどこに返るのか?

「それでは、ここで Question です。main 関数は、一体どこに返るのでしょう?」

実は、この問いはかなりの難問である。試しに、教官の先生に尋ねてみると良い。

「君ねぇ、main 関数を抜けると、プログラムは終了するんだよ」
『プログラムが終了するって、どういうことですか?』
「はぁ?終わるったら、終わるの、The End なの!」
『???』

main 関数内の return 文の行き着く先は、動作環境に強く依存している。Linux におけるプロセス終了の流れについては、Embedded UNIX Vol.6 にて詳しく解説したが、最終的にはシステムコール 1 番に至る。

NASM アセンブリ言語ソース

「論よりコード」、実際にアセンブリ言語を使って確認してみよう。雑誌上の連載では、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 バイトが何とも気になる。とってもとっても、損をしたような気分だ。これでは、悔しくて寝られそうもないよ・・ウッキ〜〜!

ということで、物語は最終章へと続くのである。