Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
Linux/GNU 開発環境の役者が揃ったところで、実際にプログラムのビルドに挑戦してみましょう。
全ての環境において、もっとも原始的かつ基本的な開発ツールは、アセンブラとリンカです。昔のアセンブラは、そのまま実行可能ファイルを出力できていたのですが(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
アセンブラとリンカが確認できたところで、アセンブリ言語によるプログラム作成に入る訳ですが、ここで注意すべき点があります。
現在、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 を読んで、私は思わず唸ってしまいました。引用してみましょう。
Sun のスタッフは、文書の最初において、Intel 形式に慣れた多くのユーザが混乱しやすい、3つの相違点を簡潔にまとめ注意喚起しているのです。意訳しますと(Solaris の表記は AT&T へ変更)、
Intel 形式に経験の深いプログラマがこの3行を最初に読んでおけば、AT&T 形式の落とし穴による時間の浪費は、最小限に抑えることができるでしょう(その昔、私自身がこの3点で見事にハマリました・・)。
本記事に限らず、Sun が公開している文書は、いずれも完成度が極めて高く(Solaris 10 manual pages は必見!)、言葉の選び方や文章の運びにプロの仕事が垣間見えます。何よりも読者に対する配慮は素晴らしく、ユーザが躓きそうなポイントには、実に適切な解説が用意されているのです。GNU が公開している as リファレンスマニュアル と読み比べると、Sun と GNU の文書に対する姿勢の違いが良くわかります。
Sun が培ってきた優れた文書環境は、まさに "ユーザの心が分かる心"、すなわち教養を物語っているようです。
それでは、いよいよ 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 の内部を 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 ユーティリティの出番です。-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 コンパイラのお出ましです。
続く