ここ最近 x86-emulator (ただし現状実態は 8086 emulator) を書いていました。目標の一つであった watcom でコンパイルした MS-DOS exe ファイルを実行できるようにする、というのは達成できたので今後しばらくは片手間に命令を追加していこうかなと考えています。そこで忘れないうちに Intel 64 and IA-32 Architectures Software Developer Manuals を見ながら培った 8086 の命令デコーディングあたりの知識をまとめておこうと思います。このマニュアル自体は x86_64 までカバーされているので情報の取捨選択が必要になるのですが、これに慣れておけば将来 32-bit, 64-bit CPU への知識の移行が楽になるはず (と信じています) です。

記事中の図は Intel 64 and IA-32 Architectures Software Developer Manuals から引用しています。

Instruction Set Reference

この Intel の資料、ただでさえ長大なマニュアルですが、その中でも Volume 2 の Chapter 3, 4, 5 という長さをかけて載せられているのが全命令のリストになります。ここでは各命令毎に

  • Instruction Format
    • 命令のフォーマット。opcode, operand, .etc
  • Instruction Operand Encoding
    • operand がどのようにエンコードされるか
  • Description
    • 文章による命令の説明
  • Operation
    • 疑似コードによる命令の動作説明
  • Flags Affected
    • 影響を受ける EFLAGS レジスタ上のフラグについて
  • Exceptions
    • (発生しうる例外についてだと思いますが、まだあまり見れていません)

といった情報がまとめられています。

一例として MOV 命令の項を見てみます。

Instruction Format は以下のような表でまとめられています (Fig.1 は表中の一行のみを抜き出したもの)。この行により 88 /r という 0x88 から始まるフォーマットは MOV r/m8,r8 という命令を表し、 r8 (8-bit レジスタ) の値を r/m8 (8-bit レジスタあるいはメモリ上の番地) に書き込む、という動作をすることがわかります。r/m8, r8 といった各記号の細かい説明は Chapter 3 の冒頭にまとめられています。

Instruction Format of MOV with opcode 0x88
Fig.1 Instruction Format of MOV with opcode 0x88

operand がどのようにエンコードされているかは上の Op/En 列と対応する表 Instruction Operand Encoding の情報で判断できます。Op/En=MR では ModRM というあとで登場する 1 byte の情報を使用して dest, src が指定されることがわかります。ちなみに (r), (w) という表記はそれぞれ read, write を表すようです。

Instruction Operand Encoding of MOV with Op/En MR
Fig.2 Instruction Operand Encoding of MOV with Op/En MR

各命令の動作をエミュレートする場合、ここにある情報が基になります。一方で命令のデコーディングを実装する場合は先頭に位置する opcode から命令を判断したいので、命令名がキーになるこの reference ではその用途には不向きなことがわかります。

マニュアルにはまさにその用途にうってつけな Opcode Map というものがあります。

Opcode Map

Opcode Map は Volume 2 の Appendix A に存在します。以下は 1-byte の Opcode Map から 0x00 - 0x07 にあたる部分を抜き出したものです。パッと見た通りですが 0x00-0x05 までが ADD 命令、0x06 が PUSH es, 0x07 が POP es 命令だということがわかります。

1-byte Opcode Map for 0x00-0x07
Fig.3 1-byte Opcode Map for 0x00-0x07

現在 Intel CPU としては opcode の最大長は 3 byte のようですが、調べた感じ 8086 に限れば 1-byte だと考えて良さそうです。

表の各セルに並ぶ Eb, GbAL, Ib といった記号は operand の指定です。例えば 0x00 の場合 ADD Eb,Gb のように考えることができ、 Eb が dest, Gb が src に関する指定となります。operand の指定は大文字小文字各 1 文字の組み合わせで表されます。大文字は addressing method を表し、即値だとかレジスタあるいはメモリ番地だとかを指定します。小文字は operand type と呼ばれ 8086 の範囲では operand のサイズ (e.g. byte, word) と考えて良さそうに思います。8086 に限れば operand type についてよく見るのは b, v, w, z ぐらいですが b が 8-bit であり v, w, z が 16-bit と考えていいのかなと思います。このあたりの記号の詳細は Volume 2 A.2 を参照となります。

以上の情報により例えば opcode 0x04 であれば ADD 命令、operand は AL, Ib なので「8-bit 即値を AL レジスタに足す」という動作であるとわかるようになります。

また表中には命令ではなく Grp の番号を提示するものがあります。

1-byte Opcode Map for 0x80-0x83
Fig.4 1-byte Opcode Map for 0x80-0x83

このような opcode は命令を特定するのに ModRM の reg 値というものも必要とするものです。reg 値と対応する命令の関係は Volume 2 A.4 でまとめられます。例えば 0x80 かつ reg=0 の場合 ADD Eb,Ib を表す命令であることがわかります。

Opcode Extensions for 0x80-0x83
Fig.5 Opcode Extensions for 0x80-0x83

ところで Opcode Map 中に出てくる operand をぼんやり眺めるとやけに E, G の指定が多いことに気が付きます。これはここまでにもちょくちょく出てきた ModRM と呼ばれる情報を持つ命令です。

ModRM

ここまで見てきた Instruction Set Reference, Opcode Map はそれぞれ各命令の動作、opcode から命令を特定する情報をまとめたものでした。しかしこれらでは実際の機械語のフォーマットについては触れられていませんでした。これについては Volume 2 Chapter 2 Instruction Format でまとめられています。

Instruction Format
Fig.6 Instruction Format

現状 8086 エミュレータを書いている限りだと中心になるのは Opcode, ModRM, Displacement, Immediate の 4 つです。あとたまに Instruction Prefixes も (確認している限り) segment override のために 8086 でも使われます。SIB はこれまで出てこないので、8086 では無視して良さそう? に思っています。

このうち opcode は常に存在しますが、それ以外は opcode によります。例えば 0x04 という opcode は Opcode Map から ADD AL,Ib であることがわかり、 Opcode - Immediate (1 byte) というフォーマットを持つことがわかります。これに基づいて 0x04 0x01 という機械語からは ADD AL,0x01 という命令がデコードできます。

Instruction Format のうち、x86 に特有でわかりにくくて、しかしデコードに不可欠な情報が ModRM です。ModRM は上の図で示されるように 1 byte の情報で上位 bit から 2 bit, 3 bit, 3 bit がそれぞれ mod, reg, rm と呼ばれます。例えば 0b00001010 の場合 mod=0, reg=1, rm=2 となります。

ModRM は様々な役割を持つので端的な説明が難しいです。上で見たように opcode とともに命令の特定に使われることもあれば、operand の addressing mode を指定するために使用されたりもします。いずれにしても ModRM の解釈は Opcode Map と Chapter 2 の Table 2-1. 16-Bit Addressing Forms with the ModR/M Byte (8086 の場合) を使用して行えます。

16-Bit Addressing Forms with the ModR/M Byte
Fig.7 16-Bit Addressing Forms with the ModR/M Byte

(青線は無視して下さい…)

例えば Opcode Map の G は reg によるレジスタ指定です。Gb かつ reg=0 ならば Fig.7 の列から AL であり、Gv かつ reg=0 ならば AX というようになります。

E は mod, rm による柔軟な指定です。Eb かつ mod=0, rm=6 ならば DS:(16-bit displacement) で表されるメモリ番地から 1 byte を operand として使用します。機械語としては displacement の指定があるので、ModRM のあとに displacement が続くことがわかります。Ev かつ mod=3, rm=2 ならば DX レジスタを operand として使用します。

まとめ

  • x86 の機械語のフォーマットは Fig.6 のようになります
  • 先頭の opcode と Opcode Map から命令の種類を判断できます
  • 8086 命令のデコード時に ModRM が必要かどうかは以下のいずれかから判断します
    • Opcode Map
    • Instruction Set Reference (の Op/En)
  • Immediate が必要かどうかも ModRM と同様に判断できます
  • Displacement が必要かどうかは以下の両者から判断する必要があります
    • Opcode Map の addressing method
    • ModRM

余談ですがマニュアルは最近手に入れた iPad Pro と GoodNotes というアプリで読んでいたのですが恐ろしく読みやすいですね。左画面でマニュアル開きながら右画面でメモをとるとかできますし、スクショとってメモに貼ったりとかできますし、何だか学生のときに授業のまとめノートを作った気分を思い出しました。iPad Pro は PDF 資料を伴う作業にはかなり強そうです。