なんとな~くしあわせ?の日記

「そしてそれゆえ、知識そのものが力である」 (Nam et ipsa scientia potestas est.) 〜 フランシス・ベーコン

x86 汎用命令 - ModR/M の解説

ModR/Mについては前もちょっと記事を書いたけど、まだちょっと中途半端だったのでもう一度まとめます。

nantonaku-shiawase.hatenablog.com

ModR/Mの実際の例

例えば以下のような命令がある、 /r の意図がみなさんわからないのではないでしょうか?

; IMUL r32, imm32
0x69 /r iw

また、次のような命令、 /7 の意図がみなさんわからないのではないでしょうか?

; CMP r/m8, imm8
0x80 /7 ib

ModR/Mの構造

全体の概要についてはこのページをみたほうがよい

また、このページも役立つ

ModR/Mバイトの構造
  • ModR/Mバイトは画像のような構造になっており、それを指定された場合アセンブラは「mod/reg/rm」の3つの要素を1byteで出力する

https://freestylewiki.xyz/fswiki/wiki.cgi?page=%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9%28ModR%2FM%29&file=modrm%2Epng&action=ATTACH

  • でも実際はregとr/mはそれぞれレジスタを指すので、ほぼ同じようなものだと思っていい
'''/r'''および'''/7'''の意味

解説を Assembly Programming on x86-64 Linux (06) から引用する

この記事で注目している項目を赤字で表現。

記法 解説
/0 - /7 ModR/M バイト の reg フィールドの 0 から 7 の数字はオペコードの拡張用に使われる。r/m フィールドだけをオペランドに使用する。
/r 命令には ModR/M バイトが続き、レジスタオペランドと r/m オペランドの両方を使う。
cb、cw、cd、cp, co, ct オペコードの後に 1 バイト(cb)、2 バイト(cw)、4 バイト(cd)、6 バイト(cp)、 8 バイト(co)または 10 バイト(ct)が続く。
ib, iw, id, io オペコード、ModM/R バイト、または SIB の後に続く 1 バイト(ib)、2 バイト(iw)、4 バイト(id)または 8 バイト(io)の定数(即値)。
+rb, +rw, +rd, +ro + の左側のオペコードに加算される 0 から 7 までのレジスタコード。 結果として 1 バイトのオペコードとなる。
regとr/mで使われるレジスタコード
レジスタコード レジスタ
000 al ax eax
001 cl cx ecx
010 dl dx edx
011 bl bx ebx
100 ah sp esp
101 ch bp ebp
110 dh si esi
111 bh di edi

mod

reg(/0 - /7指定の場合)

  • regは、/0 - /7を指定された場合はそれをそのまま使う

もちろんこれは10進数表記なので、2進数に直すと以下のようになる。ModR/Mバイトのregフィールドはこれで埋まるわけだ。

/0 - /7 2進数表記
/0 000
/1 001
/2 010
/3 011
/4 100
/5 101
/6 110
/7 111

reg(/0 - /7指定の場合)の実例

; 0x80 /7 ib
CMP r/m8, imm8
; 例
CMP CL,18
; オペコードは0x80で確定
; ---------------------
; ModR/Mの値は
; [mod] 11
; [reg] 111
; [r/m] 001
; => "11111001" 
; => 0xf9
; ---------------------
; 18は16進数で0x12
; ---------------------
; よって、以下がアセンブルされると
CMP CL,18
; 以下のバイナリが出力される
0x80, 0xf9, 0x12

reg('''/r'''指定の場合)

つまり、mod2bitに続いて、regr/mが3bitずつ続くことになる

reg(/r指定の場合)の実例

; 0x69 /r iw
IMUL r32, imm32
; 例
IMUL ECX,4608
; オペコードは0x69で確定
; ---------------------
; ModR/Mの値は
; [mod] 11
; [reg] 001
; [r/m] 001
; => "11001001" 
; => 0xc9
; ---------------------
; 4608は16進数で0x1200
; リトルエンディアンのため、 0x00, 0x12と並ぶ
; ---------------------
; よって、以下がアセンブルされると
IMUL ECX,4608
; 以下のバイナリが出力される
0x69, 0xc9, 0x00, 0x12

/r指定の場合のレジスタの細かい指定

さて、実はここまでregとr/mのどちらがどちらのレジスタを指定しているのか気になった人がいるかもしれない。その指摘はめっちゃ正しい。

ここのサイトに答えが書いてある。

The d bit in the opcode determines which operand is the source, and which is the destination:

d=0: MOD R/M <- REG, REG is the source
d=1: REG <- MOD R/M, REG is the destination

"d"bitというのは、1byte(8bit)のデータを先頭から数えて4番目のbitである。*1
つまり、"d"bitが0か1かによってmodとr/mの3bitをそれぞれ入れ替えなければいけない。

C++の疑似コード、std::bitsetを使うとやりやすい

uint8_t op = 0x8e;
std::bitset<8> bsl(op); // オペコード

if (bsl[3]) { // d=0 or 1
  // d=1: REG <- MOD R/M, REG is the destination
} else {
  // d=0: MOD R/M <- REG, REG is the source
}

はじめて読む486、ModRMとかに関してはほとんど役に立たず…

はじめて読む486―32ビットコンピュータをやさしく語る

はじめて読む486―32ビットコンピュータをやさしく語る

*1:わかるか…!こんな細かい仕様…!