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

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


2004-04-30 (Fri)

[Writing] Linux徹底詳解再訪 続報・その2

大チョンボ・・

高原さん、貴重なご指摘ありがとうございました。「$抜け」、まさに大チョンボでございます・・。皆さん、くれぐれもこういうポカをしないように、日頃から気を付けましょう(滝汗;)。

gas における $ 前置の意味

せっかくの機会ですから、「なぜ$を忘れるとダメなのか?」その理由を補講しておきましょう。まず、$を前置しない場合の転送命令です(前回と同じ mov1.s)

mov 0x12345678, %eax

このソースをアセンブルした後に、逆アセンブルすると次のようになります。

$ as -o mov1.o mov1.s
$ objdump -d mov1.o

mov1.o:     file format elf32-i386

Disassembly of section .text:

00000000 <.text>:
   0:   a1 78 56 34 12          mov    0x12345678,%eax

前回説明を忘れましたが、objdump の逆アセンブルオプションは正確には -d (Disassemble) です。objdump には、逆アセンブルオプションとして、-D も用意されていますが、その使い分けについては後述します。

さて、この逆アセンブル結果から見ると、mov 0x12345678, %eax という転送命令の機械語は、0xA1 であることが分かります。次に $ を前置した mov2.s です。

mov $0x12345678, %eax

同じく、アセンブル&逆アセンブル。

$ as -o mov2.o mov2.s
$ objdump -d mov2.o

mov2.o:     file format elf32-i386

Disassembly of section .text:

00000000 <.text>:
   0:   b8 78 56 34 12          mov    $0x12345678,%eax

今度は機械語が 0xB8 に展開されました。これは、mov1.s および mov2.s が全く異なる命令として処理されることを意味しています。果たして、$ 前置の意味はどこにあるのでしょうか?

opcode の重要性

ここで、大切になるのがオペコード(オペレーションコード: OPeration Code)テーブルです(英語表記の場合は opcode、通常 opecode とは略されない)。x86 に限らず、すべての CPU はオペコードにより、実行する命令を決定します。

オペコードについては、GCC プログラミング工房・第10回〜17回で仮想機械 Octopus の構築を通じて詳細に解説を行いました。アセンブリ言語の学習において、オペコードおよびオペランドの理解は極めて大切ですが、例によって多くの機械語入門書は、このあたりを説明していません。

名著「はじめて読む 486」も、悲しいかなオペコードについては言及しておらず、このためにサイズプレフィックスの解説が今ひとつ分かりづらいものになっています。

x86 のインストラクション構成については、英語ではありますが、「Instruction Set Architecture」が良くまとまっています。これは知る人ぞ知る、Art of Assembly language programming (AoA) というオンライン版書籍の1ページであり、アセンブリ野郎は必見です。ちなみに、書籍版はこちら

ただし・・私が見つけ出せていないのかどうか、このサイトには実際のオペコード表が見当たりません。

ということで、オペコードを実感したい方は、80x86 Opcodes のページをご覧ください。Main Instructions から始まるテーブルの真ん中に位置している、Opcode が目指すブツです。アセンブリ言語をマスターできるかどうかは、抽象的なニーモニック(AAA, MOV などの表記)を、このようなビット列として透視できるかどうかにかかっています(暗記する必要は全くありませんが)。

opcode テーブルを読もう!

雰囲気は掴めてきましたが、まだ何かが足りません。私が思うに、CPU のインストラクションセットを理解する上で、最も大切な資料はオペコード・テーブルです。これは、各命令コードの上位ニブル(Nibble: 4 bit)と下位ニブルをそれぞれ縦横軸に取り、256個のマスに並べただけの単純なものですが、この貴重な情報が意外とどこにも書かれていないのです。なんでやねん!

幸い、x86 に関しては Bob Neveln 氏による「LINUX Assembly Language Programming」に掲載されているオペコードテーブルのコピーが授業用の資料として公開されています。

これは、マサチューセッツ大学の Bob Wilson 氏による講義で使われている資料ですが、同氏はこの他にも gas (Gnu ASsembler)に対応した各 386 命令の解説も用意されており、大変実践的な講義であることが伺えます。オペコードテーブルをわざわざ資料として用意しているとは、さすが!

なお、同講義でテキストとして採用されている LINUX Assembly Language Programming の第5章 MACHINE LANGUAGE は、資料性にも富んでおり、お勧めです。

話を元に戻しましょう。$が前置されていない時の命令コードは 0xA1、$ がない時の命令コードは 0xB8 でした。オペコードテーブル上から、それぞれに対応するニーモニックを探してみましょう。縦列が上位ニブル、横列が下位ニブルですから、0xA1 は MOV eAX, [iv]、0xB8 は MOV eAX, iv となります(iv は Immediate Value の略)。

このテーブルはインテル型式ニーモニックで記述されているため、AT&T 型式の gas とは、ソースおよびデスティネーション・オペランドの順序が正反対になっている点に注意してください。

MOV eAX, [iv] は、即値で指定されたアドレスから始まるメモリ内容を eAX レジスターに転送することを意味しています。カギ括弧はインテル形式において、「間接アドレッシング」を指します。AX の前に、小文字の e が前置されている謎については、次回以降で解説します。

次に、MOV eAX, iv ですが、こちらは文字通り即値を eAX レジスターに転送することを意味しています。

以上より、gas の AT&T 型式では、数値に$を前置すると「即値」として扱われ、ただの数値の場合は「間接アドレス」として解釈されることが分かります。具体例を示しましょう。0x12340000 番地から4バイトにわたり、 0x87654321 という値が格納されていたとします。この時、

mov $0x12340000, %eax ---> EAX = 0x12340000
mov 0x12340000, %eax   ---> EAX = 0x87654321

となります。一般的なインテル表記で記述すると、これらは

mov eax, 0x12340000
mov eax, [ 0x12340000 ]

となる訳ですが、こちらの方が遙かに直感的で分かりやすいですね。$の付け忘れで、とんでもないバグに悩むこともありません。

gas を使うことのメリットは、拡張インラインアセンブラーを用いることで、C言語とアセンブリ言語間のインターフェースを容易に記述できる点にあります。しかし、C言語関数との引数渡しをアセンブリ言語で記述する根性があるのであれば、何も難解な AT&T 文法に付き合う必要はありません。

ということで、今回の補講ではインテル型式を採用している Netwide Assembler: NASM で機械語部分を作成することにしましょう。

NASM のインストール

NASM の最新版は、0.98.38 です。ビルドはいつも通り。

$ tar xfj nasm-0.98.38.tar.bz2
$ cd nasm-0.98.38
$ mkdir build
$ cd build
$ ../configure
$ make
$ make install

この結果、nasm および ndisasm が /usr/bin ディレクトリに、各 man ファイルが /usr/man/man1 ディレクトリにインストールされます。無事インストールされたかどうか、確認しておきましょう。

$ nasm -v
NASM version 0.98.38 compiled on May  1 2004

NASM 版 mov1.s

それでは、mov1.s をインテル型式で mov1nasm.s に書き換えてみましょう。

mov     eax, 0x12345678

オペランドが逆転しているので、gas に慣れてしまうと気持ち悪い感じがしますが、Z80, 8086 世代にとってはこちらが正常です。また、インテル型式では AT&T 型式のように命令の直後に l,w,b などのサイズ指定子を後置する必要はありませんので、注意してください。

$ nasm -f elf mov1nasm.s
$ objdump -d mov1nasm.o

mov1nasm.o:     file format elf32-i386

Disassembly of section .text:

00000000 <.text>:
   0:   b8 78 56 34 12          mov    $0x12345678,%eax

NASM でアセンブルするとデフォルトでバイナリー型式で出力されます。このため、上記例では -f (Format)を指定して ELF オブジェクトファイルを生成しています。ファイル名を -o (Output) オプションで指定しない場合は、ソースファイルの拡張子を .o に変更した名前が自動的に採用されます。

アセンブルされたインストラクションコードは 0xB8 ですから、確かに 32bit 即値の転送命令が選択されています。ついでに、mov2.s も書き直しておきましょう。

mov     eax, [ 0x12345678 ]

以下、同様にアセンブル&逆アセンブル(ndisasm を用いた逆アセンブル方法は、次回説明)。

$ nasm -f elf mov2nasm.s  
$ objdump -d mov2nasm.o

mov2nasm.o:     file format elf32-i386

Disassembly of section .text:

00000000 <.text>:
   0:   a1 78 56 34 12          mov    0x12345678,%eax

こちらは、0xA1 になっていますので、私達の意図通り「間接アドレッシング」用の命令コードが選択されていることが分かります。

間違いを指摘して頂いたおかげで、内容はより深まったようです。今後も、「へ?」という箇所がありましたら、どしどしご指摘ください。お待ちしております。