/ «2008-03-04 (Tue) ^ 2008-03-09 (Sun)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

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


2008-03-05 (Wed)

[Thoughts] プログラマの教養は manual pages に宿る (その4)

Linux/GNU 開発環境の役者が揃ったところで、実際にプログラムのビルドに挑戦してみましょう。

最初に binutils ありき

全ての環境において、もっとも原始的かつ基本的な開発ツールは、アセンブラとリンカです。昔のアセンブラは、そのまま実行可能ファイルを出力できていたのですが(a.out の名前は Assembler OUTput に由来)、GNU 開発環境ではアセンブラ (as: ASsembler) が出力したオブジェクトファイルから、リンカ・ローダ (ld: linker LoaDer) が実行可能ファイルを出力します(Netwide Assembler: NASM を利用すれば、アセンブラ単体で実行可能ファイルを生成可能)。

アセンブラ (as) とリンカ・ローダ (ld) は、GNU が提供する binutils (BINary UTILitieS) パッケージに含まれています。「GCC とは別個のパッケージ」であることに注意してください。既に紹介した通り、binutils のバージョンは、as もしくは ld コマンドに -v オプションを渡すことで確認できます(as はソースファイルが指定されていない場合、標準入力よりソースファイルを読み込もうとするため、ソースファイルとして /dev/null を指定する)。

 $ as -v /dev/null
 GNU assembler version 2.18.0 (x86_64-linux-gnu) using BFD version (GNU Binutils
 for Debian) 2.18.0.20080103
 $ ld -v
 GNU ld (GNU Binutils for Debian) 2.18.0.20080103

アセンブラとリンカが確認できたところで、アセンブリ言語によるプログラム作成に入る訳ですが、ここで注意すべき点があります。

Intel 形式 vs AT&T 形式

現在、x86アーキテクチャのアセンブリ言語には、大きく分けるとインテル形式とAT&T形式の2種類が存在しています。8086時代からインテル形式に慣れた方が、AT&T形式のアセンブリソースを見ると、あまりの奇怪さに驚かれることでしょう。ソース・デスティネーションオペランドの位置が "正反対" であることをはじめとして、レジスタや即値(immediate value)にそれぞれ%と$の前置が必要であったり、各命令セットにはオペランドサイズに応じて l (long), w (word), b (byte) などの suffix が必要、間接参照の表記方法は全く異なるなど、最初は困惑する方がほとんどです。

両形式の違いについては、Sig9 というグループによる AT&T Assembly Syntax が簡潔でよくまとまっています。冒頭では、なぜ AT&T 形式がこれほど難解な表記を取っているのか、その理由が解説されており、短いながらもプロ顔負けの記事に仕上がっています。

AT&T文法に関する詳細な解説は、Sun が Solaris 向けに公開している "x86 Assembly Language Reference Manual" が最も優れています(Solaris のアセンブラは AT&T 形式に準拠)。文書の先頭では簡潔に Overview が語られていますが、その直後に記載されている Syntax Differences Between x86 Assemblers を読んで、私は思わず唸ってしまいました。引用してみましょう。

  • The Solaris and Intel assemblers use the opposite order for source and destination operands.
  • The Solaris assembler specifies the size of memory operands by adding a suffix to the instruction mnemonic, while the Intel assembler prefixes the memory operands.
  • The Solaris assembler prefixes immediate operands with a dollar sign ($) (ASCII 0x24), while the Intel assembler does not delimit immediate operands.

Sun のスタッフは、文書の最初において、Intel 形式に慣れた多くのユーザが混乱しやすい、3つの相違点を簡潔にまとめ注意喚起しているのです。意訳しますと(Solaris の表記は AT&T へ変更)、

  • AT&T および Intel 形式では、ソースオペランドとデスティネーションオペランドの位置が互いに逆になっている。
  • AT&T 形式では、それぞれの命令ニーモニック(名称のこと)に対して、オペランドサイズを示す suffix を後置しなければならない。
  • AT&T 形式では、即値に$を前置しなければならない。

Intel 形式に経験の深いプログラマがこの3行を最初に読んでおけば、AT&T 形式の落とし穴による時間の浪費は、最小限に抑えることができるでしょう(その昔、私自身がこの3点で見事にハマリました・・)。

本記事に限らず、Sun が公開している文書は、いずれも完成度が極めて高く(Solaris 10 manual pages は必見!)、言葉の選び方や文章の運びにプロの仕事が垣間見えます。何よりも読者に対する配慮は素晴らしく、ユーザが躓きそうなポイントには、実に適切な解説が用意されているのです。GNU が公開している as リファレンスマニュアル と読み比べると、Sun と GNU の文書に対する姿勢の違いが良くわかります。

Sun が培ってきた優れた文書環境は、まさに "ユーザの心が分かる心"、すなわち教養を物語っているようです。

GNU アセンブリ言語版 Hello, world!

それでは、いよいよ GNU アセンブリ言語版 Hello, world! プログラム、hello.s の登場です(拡張子 .s はアセンブリ言語ソースファイルを意味する)。

 #---------------------------------------------------------------------
       .data                   # .data section starts
 #---------------------------------------------------------------------
 msg:
       .ascii  "Hello, world!\n"
 mend:
 
 #---------------------------------------------------------------------
       .text                   # .text section starts
 #---------------------------------------------------------------------
       .global main            # Declare 'main' as a global symbol
 main:
       movl $4, %eax           # EAX = "write" system call
       movl $1, %ebx           # EBX = STDOUT descriptor
       movl $msg, %ecx         # ECX = message address
       movl $(mend-msg), %edx  # EDX = message length
       int $0x80               # Execute write system call
 
       movl $1, %eax           # EAX = "_exit" system call
       movl $123, %ebx         # EBX = exit status
       int $0x80               # Execute _exit system call

hello.s は printf ライブラリ関数を呼び出す代わりに、write システムコールを用いて標準出力へ "Hello, world!" を出力し、_exit システムコール(ライブラリ関数と区別するためアンダースコアが前置されている)によりプロセスを終了します。このプログラムはカーネルと直接やり取りしているため、"外部ライブラリを必要としません"。

簡単に hello.s の概要を説明しておきますと、このプログラムは大きくふたつのパートに分かれています(#以降はコメント)。前半は .data セクションと呼ばれるデータ領域であり、出力メッセージ "Hello, world!" の ASCII コードが格納されます。後半は、.text セクションと呼ばれる実行コードの領域であり、write システムコールと _exit システムコールの実行を担当する機械語が格納されます。Cコンパイラが出力するアセンブリソースでも、目的に応じて4つの基本セクションが使い分けられています(.text, .rodata, .data, .bss セクション: 詳細は GNU開発ツール にて解説)。

実行コードの先頭には、Cソースにならい main というラベル(C言語でいう関数名)を置き、以下に write システムコールと _exit システムコールの実行コードが続きます。システムコールは EAX レジスタの値によって区別され、4の場合は write システムコール、1の場合は _exit システムコールが実行されます。データ領域には、"Hello, world!" の文字列が ASCII コードとして展開され、最終尾には行末コード(Line Feed: ASCII code 10)が添付されています。

以上のアセンブリソースを疑似Cソースに置き換えると、次のようになります。

 char msg[] = "Hello, world!\n";
 
 main() {
        write(1, msg, 14);
        _exit(123);
 }

雰囲気として、アセンブリ言語とC言語のソースリストがほぼ一対一対応していることが、お分かり頂けるかと思います。古来、C言語が「高級アセンブリ言語」と呼ばれてきた所以です。

なお、アセンブリ言語の経験がない方には、hello.s は呪文にしか見えないと思いますが、心配ありません。それが「普通」です。"コンピュータ学習におけるT型フォード" でも書きましたが、コンピュータシステムが複雑化した現代において、アセンブリ言語などコンピュータの基本を学ぶことは至難の業となっています。数少ない名著が絶版になると同時に、初心者が基本を学ぶために必要な8ビット/16ビット環境も破棄されてしまったからです。Computer Architecture Series 第四巻では、失われた学習環境の再現を目指しています。

アセンブル

それでは、hello.s をアセンブルしてみましょう。

 $ as -o hello.o hello.s
 $ wc -c hello.o
 888 hello.o
 $ file hello.o
 hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

as にソースファイルを指定すると、アセンブルが実行されますが、デフォルトの出力ファイル名は a.out (まさに Assembler OUTput) に設定されています。a.out とは異なるファイル名でオブジェクトファイルを出力する場合は、-o (Output file name) オプションを指定してください。

私の環境では888バイトのファイルが生成され(以降、ファイルサイズはGCCのバージョンや環境により大きく異なる)、そのファイル形式は「64ビット版のELFリロケータブル」と表示されています。32ビット環境でアセンブルした場合は、次のように表示されます。

 hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

hello.s は、どちらの環境でも動作します。

リンク

hello.o はリロケータブル(再配置可能)なオブジェクトファイルであり、そのままでは実行できません。最後にリンクを実行し、実行可能ファイルを生成する必要があります。

 $ ld -e main -o hello hello.o
 $ ls -l hello
 -rwxr-xr-x 1 wataru wataru 911 2008-03-04 21:22 hello
 $ file hello
 hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

リンクは ld コマンドが担当しますが、ELF (Executable and Linking Format) の場合、デフォルトの開始アドレスを示すシンボル名は "_start" に決まっています。hello.s の実行は、Cプログラムにならい、main から始まるため -e (Entry) オプションを用いエントリアドレスを main に変更しています。出力ファイル名のデフォルトは、as コマンドと同じく a.out が設定されているため、-o オプションでプログラム名を指定します。

生成された hello のファイルモードには、実行可能を示す x (eXecutable) が設定されており、ファイルサイズは911バイト、ファイル形式は「64ビット版ELF 実行可能」となっています。relocatable が executable に変化している点に注目してください。さらに続く "statically linked" は、hello が静的リンクで作成されたことを現し、Cライブラリなど外部ライブラリには依存していないことを意味しています。

実行可能ファイル hello の中身

次に、出来上がった hello の内部を hexdump ユーティリティでダンプ表示してみます。

 $ hexdump -C hello
 00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
 00000010  02 00 3e 00 01 00 00 00  b0 00 40 00 00 00 00 00  |..>.......@.....|
 00000020  40 00 00 00 00 00 00 00  10 01 00 00 00 00 00 00  |@...............|
 00000030  00 00 00 00 40 00 38 00  02 00 40 00 06 00 03 00  |....@.8...@.....|
 00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000050  00 00 40 00 00 00 00 00  00 00 40 00 00 00 00 00  |..@.......@.....|
 00000060  d2 00 00 00 00 00 00 00  d2 00 00 00 00 00 00 00  |................|
 00000070  00 00 20 00 00 00 00 00  01 00 00 00 06 00 00 00  |.. .............|
 00000080  d4 00 00 00 00 00 00 00  d4 00 60 00 00 00 00 00  |..........`.....|
 00000090  d4 00 60 00 00 00 00 00  0e 00 00 00 00 00 00 00  |..`.............|
 000000a0  0e 00 00 00 00 00 00 00  00 00 20 00 00 00 00 00  |.......... .....|
 000000b0  b8 04 00 00 00 bb 01 00  00 00 b9 d4 00 60 00 ba  |.............`..|
 000000c0  0e 00 00 00 cd 80 b8 01  00 00 00 bb 7b 00 00 00  |............{...|
 000000d0  cd 80 00 00 48 65 6c 6c  6f 2c 20 77 6f 72 6c 64  |....Hello, world|
 000000e0  21 0a 00 2e 73 79 6d 74  61 62 00 2e 73 74 72 74  |!...symtab..strt|
 000000f0  61 62 00 2e 73 68 73 74  72 74 61 62 00 2e 74 65  |ab..shstrtab..te|
 00000100  78 74 00 2e 64 61 74 61  00 00 00 00 00 00 00 00  |xt..data........|
 00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 *
 00000150  1b 00 00 00 01 00 00 00  06 00 00 00 00 00 00 00  |................|
 00000160  b0 00 40 00 00 00 00 00  b0 00 00 00 00 00 00 00  |..@.............|
 00000170  22 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |"...............|
 00000180  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000190  21 00 00 00 01 00 00 00  03 00 00 00 00 00 00 00  |!...............|
 000001a0  d4 00 60 00 00 00 00 00  d4 00 00 00 00 00 00 00  |..`.............|
 000001b0  0e 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 000001c0  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 000001d0  11 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
 000001e0  00 00 00 00 00 00 00 00  e2 00 00 00 00 00 00 00  |................|
 000001f0  27 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |'...............|
 00000200  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000210  01 00 00 00 02 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000220  00 00 00 00 00 00 00 00  90 02 00 00 00 00 00 00  |................|
 00000230  d8 00 00 00 00 00 00 00  05 00 00 00 05 00 00 00  |................|
 00000240  08 00 00 00 00 00 00 00  18 00 00 00 00 00 00 00  |................|
 00000250  09 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000260  00 00 00 00 00 00 00 00  68 03 00 00 00 00 00 00  |........h.......|
 00000270  27 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |'...............|
 00000280  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000290  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 000002a0  00 00 00 00 00 00 00 00  00 00 00 00 03 00 01 00  |................|
 000002b0  b0 00 40 00 00 00 00 00  00 00 00 00 00 00 00 00  |..@.............|
 000002c0  00 00 00 00 03 00 02 00  d4 00 60 00 00 00 00 00  |..........`.....|
 000002d0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 02 00  |................|
 000002e0  d4 00 60 00 00 00 00 00  00 00 00 00 00 00 00 00  |..`.............|
 000002f0  05 00 00 00 00 00 02 00  e2 00 60 00 00 00 00 00  |..........`.....|
 00000300  00 00 00 00 00 00 00 00  0a 00 00 00 10 00 f1 ff  |................|
 00000310  e2 00 60 00 00 00 00 00  00 00 00 00 00 00 00 00  |..`.............|
 00000320  16 00 00 00 10 00 01 00  b0 00 40 00 00 00 00 00  |..........@.....|
 00000330  00 00 00 00 00 00 00 00  1b 00 00 00 10 00 f1 ff  |................|
 00000340  e2 00 60 00 00 00 00 00  00 00 00 00 00 00 00 00  |..`.............|
 00000350  22 00 00 00 10 00 f1 ff  e8 00 60 00 00 00 00 00  |".........`.....|
 00000360  00 00 00 00 00 00 00 00  00 6d 73 67 00 6d 65 6e  |.........msg.men|
 00000370  64 00 5f 5f 62 73 73 5f  73 74 61 72 74 00 6d 61  |d.__bss_start.ma|
 00000380  69 6e 00 5f 65 64 61 74  61 00 5f 65 6e 64 00     |in._edata._end.|
 0000038f

先頭には File signature として ELF の文字が見えますし、中程には Hello, world! が埋め込まれていることが分かります。一部、msg, main などのシンボル名が見えますが、それ以外の部分はほとんど意味不明のデータです。

objdump による逆アセンブル

こんな時は、objdump ユーティリティの出番です。-d (Disassemble) オプションを用い、.text セクションに配置された機械語からアセンブリ言語を再構成してみます(逆アセンブルと呼ぶ)。

 $ objdump -d hello
 
 hello:     file format elf64-x86-64
 
 Disassembly of section .text:
 
 00000000004000b0 <main>:
   4000b0:       b8 04 00 00 00          mov    $0x4,%eax
   4000b5:       bb 01 00 00 00          mov    $0x1,%ebx
   4000ba:       b9 d4 00 60 00          mov    $0x6000d4,%ecx
   4000bf:       ba 0e 00 00 00          mov    $0xe,%edx
   4000c4:       cd 80                   int    $0x80
   4000c6:       b8 01 00 00 00          mov    $0x1,%eax
   4000cb:       bb 7b 00 00 00          mov    $0x7b,%ebx
   4000d0:       cd 80                   int    $0x80

一番左は機械語の格納アドレス、2番目のカラムは機械語、一番右のカラムは機械語から逆アセンブルされたアセンブリ言語です。B8, 04, 00, 00, 00, BB, 01, 00, 00 から始まる機械語を先ほどの hexdump 出力中で探してみてください。見比べてみると、0xB0 (176) 番地から 0xD1 (209) 番地までの34バイトにわたり、機械語が埋め込まれていることが分かります。

実行コード本体である機械語が34バイト、メッセージデータが14バイトであれば、実行可能ファイルのサイズは本来48バイトで済むはずですが、残り863バイトの冗長なデータは ELF ファイル形式の付属データ構造を実現するために使用されています。

セクション配置アドレス

なお、逆アセンブルリストの3行目で、ECX レジスタに即値が転送されていますが、その値に注目してください。"0x6000D4" はリンカが設定した値ですが、なぜこの値が選択されたのでしょうか?

この答えを知るためには、objdump コマンドの -h (section Headers) オプションを用いて、hello 内部のセクション配置状況をチェックする必要があります。

 $ objdump -h hello
 
 hello:     file format elf64-x86-64
 
 Sections:
 Idx Name          Size      VMA               LMA               File off  Algn
   0 .text         00000022  00000000004000b0  00000000004000b0  000000b0  2**2
                   CONTENTS, ALLOC, LOAD, READONLY, CODE
   1 .data         0000000e  00000000006000d4  00000000006000d4  000000d4  2**2
                   CONTENTS, ALLOC, LOAD, DATA

objdump によると hello 内部には、.text と .data の2セクションが格納されており、それぞれの開始アドレス(VMA: Virtual Memory Address フィールドに記載)は、0x4000B0 番地と 0x6000D4 番地になっています(配置アドレスは環境により異なる)。

実際に 0x6000D4 番地 に割り当てられた .data セクションの内容は、objdump コマンドの -s オプションでダンプ表示することができますが、この際 -j オプションを併用してダンプ対象となるセクション名を指定する必要があります。

 $ objdump -j .data -s hello
 
 hello:     file format elf64-x86-64
 
 Contents of section .data:
  6000d4 48656c6c 6f2c2077 6f726c64 210a      Hello, world!.  

0x6000D4 番地から、"Hello, world!" の ASCII コードが順に並んでおり、最終尾には行末コードである 0x0A が見えます。これで、ECX レジスタへ転送される即値に 0x6000D4 番地が指定された理由が分かりました。

実行

解析が終わったところで、意を決して hello を実行してみましょう。

 $ ./hello ; echo $?
 Hello, world!
 123
 $

見事 Hello, world! が表示され、親プロセスである bash には終了ステータスコードとして 123 が返されています。hello は、Cコンパイラはもちろん、GNU C ライブラリや各種ヘッダーファイルなど、他者の力を一切借りることなく完成させた実行可能プログラムです。

「我が両手に as と ld を与えよ、されば a.out を与えん」

アセンブラとリンカさえあれば、世界を創造できる。そんな気分に浸る日があっても良いでしょう。GNU 開発ツールの中でも、binutils パッケージに含まれる as と ld が、最も重要な役者であることが理解できれば、大きな進歩です(この知識は、将来クロス開発環境をマニュアルでビルドする時に役立ちます)。

次回は、いよいよ GNU C コンパイラのお出ましです。

続く