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

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


2004-05-01 (Sat)

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

NASM による binary format の落とし穴

opcode table が分かったところで、NASM を用いて、余分な情報が含まれていない binary format で mov1nasm.s をアセンブルしてみましょう。as は直接 binary format を出力することが出来ないため、アセンブル完了後 ELF オブジェクトファイルを objcopy コマンドで変換する必要がありました。これに対して、NASM は前回説明した通りデフォルトでは binary format を用いてコードを出力します。

$ nasm mov1nasm.s
$ hexdump -C mov1nasm    
00000000  66 b8 78 56 34 12                                 |f.xV4.|
00000006

このように -f (Format) オプションを指定しない場合、出力ファイルは binary 型式となり、デフォルトの出力ファイル名はソースファイル名から拡張子を削除したベースネームとなります。

出来上がった mov1nasm は6バイトから構成され、その先頭には 0x66 という意味不明なコードが前置されています。果たしてこれは、何でしょう?

BITS 16 vs BITS 32

それでは、ここで mov1nasm32.s というファイルを用意してください。

bits    32              ; Select 32-bit mode
mov     eax, 0x12345678

プログラムの先頭に「bits 32」という宣言子を追加しています(NASM のコメント開始はセミコロン)。とりあえず、この状態でアセンブルしてみましょう。

$ nasm mov1nasm32.s
$ hexdump -C mov1nasm32
00000000  b8 78 56 34 12                                    |.xV4.|
00000005

先ほどの 0x66 が消え、as で生成したコードと同じものが出力されました。そろそろ、x86 における 16 bit モードと 32 bit モードのからくりに、気づかれた方がいらっしゃるのではないでしょうか?

サイズ・プレフィックス

種明かしをすると、0x66 というのは「Operand size prefix」と呼ばれる前置コードです。前回紹介した opcode table を思い出してください。0xB8 の部分には、「MOV eAX, iv」と書かれていました。この表記は、「CPU が 32 bit モードの時は MOV EAX, iv、16 bit モードの時は MOV AX, iv」という事を意味しています。すなわち、0xB8 はビットモードに応じて、生意気にもふたつの顔を使い分けているのです!

これは、限られた opcode 空間を節約するための、苦肉の策だった訳ですが、その代償として困った問題が浮上してきました。それは、「16 bit モードの時、32 bit レジスターを扱う場合は一体どうすんのよ?」ということ。

二股野郎の運命やいかに・・と思いきや、インテルの技術者はこの問題を「サイズ・プレフィックス」を導入することで切り抜けました。

オペランドサイズ・プレフィックス

サイズプレフィックスが命令コードの前に前置されると、デフォルトのビットモードの意味が反転される仕掛けを用意したのです。16 bit モードで稼働中の CPU が 0xB8 に出会うと MOV AX, iv として解釈しますが、0x66 0xB8 の場合は MOV EAX, iv に読み替える訳です。

通常、Linux 上での開発にいそしんでいる場合、生成されるコードは 32 bit モードで稼働しますので、サイズプレフィックスを気にする必要はありません。しかし、カーネルの起動部やブートローダーなど、リアルモードを経由するプログラムの場合は、このサイズプレフィックスを念頭に置いておかなければ、まともなコードを生成することができません(特に GCC でリアルモード対応のコードを生成する場合)。

それでは、「mov1nasm.s ではなぜオペランドサイズ・プレフィックスが自動的に挿入されたのか?」という疑問が湧いてきますが、これは NASM が binary 型式の出力ファイルを生成する場合、自動的に 16 bit モードを選択してしまうことが原因です。すなわち、mov1nasm.s は

bits    16
mov     eax, 0x12345678

に等しいことになります。16 bit モード上にもかかわらず、32 bit のオペランドを取り扱う命令が出現したため、賢い NASM は自動的に 0x66 を前置してくれていた・・というのが、事の真相です。

ndisasm による逆アセンブル

それではもう少し理解を深めるために、ndisasm による逆アセンブルに挑戦してみましょう。ndisasm は objdump ほど高機能ではないため、バイナリー型式のファイルしか逆アセンブルできません(ELF 型式は不可)。まず最初に mov1nasm の逆アセンブルです。

$ ndisasm mov1nasm  
00000000  66B878563412      mov eax,0x12345678

シラーっと逆アセンブルしてくれていますが、騙されてはいけません。0x66 が前置されているにもかかわらず、どうして何もなかったかのように逆アセンブルできているのでしょうか?

ご想像の通り、ndisasm もまた、バイナリー型式のデフォルトを 16 bit モードとして想定しているのです。この事実は、mov1nasm32 を逆アセンブルすることでより明らかになります。

$ ndisasm mov1nasm32
00000000  B87856            mov ax,0x5678
00000003  3412              xor al,0x12

この結果は非常にタメになる事実を孕んでいますので、一体何が起きているのか、よく理解しておく必要があります。

まず、0xB8 から始まる MOV 命令ですが、16 bit モードを想定していますので、ニーモニックは MOV AX, iv となり、オペランドサイズも 16 bit となります。この結果、CPU は 0xB8 から始まる3バイトを MOV AX, 0x5678 と解釈し、残りの 0x34 0x12 は「次の命令」として解釈します。opcode テーブルで 0x34 を検索すると、XOR AL, ib (ib は Immediate Byte の略)と記述されていますので、ndisasm は xor al, 0x12 と逆アセンブルした訳です。

何やらどんどん脱線していくような気がしますが、これまで再三にわたり登場している「.code16gcc」の真の意味を知るためには、このあたりの知識を疎かにする訳にはいきません。次回は「アドレスサイズ・プレフィックス」から始めましょう。

こんな調子でイスカンダルに到達できるのか?>我 (BGM 交響組曲・宇宙戦艦ヤマト ISKANDALL)