アセンブラと機械語の対応が分からないよ、でも知りたいよ!のときに読むページ
- (by K, 2006.04.19)
- いろいろQ&Aが集まったと思うので、ここにまとめておこうと思います。
- ここで答えが見つからなければ、q_and_aへ!
前書き
- OSを作るという立場では、それぞれのアセンブラ命令がどんな機械語になるかなんてほとんど重要ではありません。それに、naskが出力する.lstファイルを見れば、この命令はこんなふうに翻訳されるんだーってことも簡単に確認できます。それで満足するのが(=あまりその方面には深追いしないのが)この本の想定読者だったので、それ以上の詳しいことは本文中には書いてきませんでした。
- しかしもっと知りたいと思うことが悪いわけじゃありません。そこで、ここにそういう人がそれなりに満足できるような説明を書いておくことにします。なお、ここにあるのは全体のうちのごく一部ですが、これくらいが分かればあとは.lstの結果と類推でなんとなく分かるはずです。
パラメータのない命令
| CLI | HLT | IRETD | NOP | RET | RETF | STI | | FA | F4 | CF | 90 | C3 | CB | FB |
レジスタ番号表
- reg8
- reg16
- reg32
| EAX | ECX | EDX | EBX | ESP | EBP | ESI | EDI | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
- sreg
一般の命令
- CALL 定数 ([BITS 32]のとき)
- たとえば0x1234番地の関数の呼び出す場合、 E8 34 12 00 00 になるわけではない。E8の後に書かれる数値は、EIPに足し算するべき値である。たとえば、このCALL命令が0x2345番地にあるならCALL命令を読み取った直後のEIPの値は0x234aになっているはずなので、機械語は E8 EA EE FF FF になる(0xffffeeeaは-4374を意味していて、 0x234a + (-4374) = 0x1234 であるから)。
- INT 定数
- INT 3 に限っては、CCという1バイト命令でもよい(もちろん CD 03 でもいい)。
- JMP 定数 ([BITS 32]のとき)
- EIPに足す値が-128〜+127の範囲のときは、
という形式も使える。
- MOV reg8,定数
- MOV reg16,定数 ([BITS 16]のとき)
- MOV reg32,定数 ([BITS 32]のとき)
- MOV reg16,sreg([BITS 16]のとき)
| 8C | C0 + セグメントレジスタ番号 * 8 + レジスタ番号 |
- MOV sreg,reg16
| 8E | C0 + セグメントレジスタ番号 * 8 + レジスタ番号 |
- ちなみに MOV sreg,定数 という機械語はない
- POP reg32
- PUSH reg32
補足
- [BITS 32]のときに MOV AX,0x1234 を翻訳するとどうなるの?・・・頭に66が追加されます。というかこの程度のことは自分でちょっとプログラムを書いて、naskの.lstを見て自分で突き止めましょう。そうすれば力がつきますよ。
- 66は BITS 16/32 を次の1命令に限って逆にする機械語です。
なぜ?
- どうしてMOV命令は代入したいレジスタによっていろいろな機械語になってしまうんだろう?
みたいにすれば分かりやすくて良さそうなのに。
- よい質問です。実はそのような機械語もあります。たとえば8ビットレジスタへの代入なら、
というのがありますし、32ビットレジスタへの代入なら、
というのがあるのです。確かにこの形式のほうが人間には分かりやすいとはいえるでしょう。
- しかしこの書き方だとMOV命令は3バイト命令や6バイト命令になってしまいます。MOV命令はとてもよく使う命令なので、もし短く書ける形式があるのなら、そちらを使うほうがプログラムが短くなって、動作が速くなります(註:キャッシュヒット率が上がるので)。それでアセンブラは基本的に短く書ける形式のほうの機械語しか出力しないようになっています。
- 同様にPUSHやPOPも2バイト形式がありますが、やはり同じ理由で1バイト形式を使うのが普通です。
- どうしてINT命令は INT 3 だけ1バイト形式が用意されているんだろう? INT 3 ってそんなによく使う命令なのかなあ。だとしたらなんに使うの?
- INT 3 は例外の割り込み番号の一つで、ブレークポイント割り込みです。これは何かというと、今ここにバグがありそうな機械語のプログラムがあって、特に0x1234番地の命令を実行するときが怪しいとしましょう。だからそのときのレジスタの値を画面に表示させてチェックしようと考えました。そんなとき、0x1234にJMP命令とかCALL命令を置いて、レジスタ表示ルーチンを呼び出せばいいでしょう。ああしかし、実は0x1234番地の命令は1バイト命令(たとえばRETとか)で、その次の0x1235番地からはテストに必要なデータが入っているんですよ。だからもし0x1234番地に5バイト命令のCALLなんて書いてしまったら、テスト結果がおかしくなってしまいます。普通のINT命令なら2バイトで済みますが、それでも長すぎます。
- もちろんアセンブラのソースが手元にあればそこに命令を追加してアセンブルしなおせばいいのかもしれませんが、命令を挿入すると番地がずれて結果が変わることもたまにはありますし、アセンブラのソースはないけど、とにかくバグを見つけて報告しなければいけない、というむごい状況もたまにはあるのです。そんなときに1バイトで関数を呼び出す機能があれば、ものすごく重宝なのです。 INT 3 はそのときのための割り込み命令です。だからよく使うわけでもないのに、1バイト形式が特別に用意されているわけです。
- そんなわけで、「お、1バイトでかけるINT命令があるのか。じゃあこれをAPIの呼び出しに使ったら、アプリケーションはきっと短くなるぞ。APIを呼び出すためのINTはアプリケーションの中に何度も何度も出てくるから、これは結構効果がありそうだ。」なんて考えちゃダメなんです。そんなことしたら、バグを見つけるための INT 3 とAPIの INT 3 が区別できなくなるので、事実上、バグを見つけるための INT 3 をあきらめなきゃいけないことになり、「なんだよこのOSはデバッグのために INT 3 を使うことはできないのかよ、不便だなあ。」って言われちゃうことになるかもしれないわけです。
- どうしてCALL命令やJMP命令では、EIPに足し算するような機械語なんだろう? 分かりにくいし、CPUは代入の代わりに内部で計算しなきゃいけなくなるから、かえって疲れちゃう気がするんだけど。
- まずJMP命令から。JMP命令はgotoに相当する命令ですが、for (;;) などでも使われます。いずれにしても、たいていの場合、関数の中の別の部分への移動がほとんどなので、行き先は100バイト前とか、123バイト後など、現在地からそれほど遠くないことが多いです。そこでインテルのおじさんたちは、飛びたい番地ではなくEIPに足したい数を機械語では書くことにして、しかも距離が短いときは足したい数を signed char で(つまり1バイトで)書いていいことにしました。これにより、距離が近い多くのJMP命令は2バイト命令になって、機械語は短くなりました。
- 距離が遠いものはEIPに足す値を4バイトで書かないといけないので5バイト命令になりますが、そのときも値は行きたい番地ではなくEIPに足す値で指定します。これは同じJMP命令なのに足したり代入したりというややこしい回路にするよりも、全部足し算で統一したほうがラクだったのでしょう。
- CALL命令はJMP命令とほとんど同じで、違うのはEIPを更新する前に PUSH EIP を実行するかどうかだけなので、JMP命令と同じ仕様にしたのだと思います。しかしCALLでは2バイト形式はありません。5バイト命令だけです。
- このように実際の番地を指定するのではなく、EIPへの加算量で指定する方法を「相対ジャンプ」といいます。
- なお、far-CALLやfar-JMP命令ではどちらもEIPへの足し算ではなく、EIPとCSへの代入になっています。これはその名のとおり遠くへのCALL/JMPなので、1バイト形式を作る意味がなく、それゆえに単純な代入形式だけになったのでしょう。このような普通の指定方法のことを「絶対ジャンプ」といいます。
こめんと欄
- オペコードの頭に0x66がつく話はもうちょい解説してあげるべきではないでしょうか。とは言え私もOperand override prefixの詳しい仕様がどこにあるかしらないのですが…(はじめて読む486にもさっぱり書いていない) -- 永田 2016-12-23 (金) 17:09:18
|