13日目: [x86] ModRM
この記事はひとりでCPUとエミュレータとコンパイラを作る Advent Calendar 2017の13日目の記事です。
昨日は数値についてまとめた。
今日はModRMを説明する。
ModRMを一言で表すと、必須オプションだ。
たとえばadd
命令の場合、何と何を足すかを指定するのがModRMの役目と言える。
add命令
具体的に説明するため、考える対象をadd
命令に絞る。
一昨日のx86の命令セット表
の最上段左側のadd
命令を切り出すと
機械語の0x01
はニーモニックのadd [M+imm], R
に対応し、0x03
はadd R, [M+imm]
に対応する。
ここで[hoge]
はメモリのhoge
番地を表している。
つまりadd [M+imm], R
を数式的に書くと(M+imm)番地のメモリ = (M+imm)番地のメモリ + R
になり、メモリの値を変更する命令になっている。
一方add R, [M+imm]
ではメモリの値は変更されない。
これらを表に記すと
機械語 | ニーモニック |
---|---|
0x01 ???? ... | add, [address], register |
0x03 ???? ... | add, register, [address] |
????の部分をうまく調整することで何と何を足すか指定できる。 この指定子がModRMだ。
[M+imm], R 型
add [M+imm], R
命令(機械語0x01
)を知るための画像を用意した。これをじーっくり眺めて欲しい。
右列は機械語を1byteずつ区切ったものだ。
最初に0x01
、次に1byteのModRMが来て、最後に即値(無い場合もある)が来る。
ModRMについては、中央のカラフルな列を見れば分かるように、最初の2bitがmod、次の3bitがR、最後の3bitがMと命名されている。
左列の下四段のニーモニックを見ると、[M+imm]
の形式が異なっている。これらの形式はmodで指定される。
mod | [M+imm] の型 |
---|---|
00 | [レジスタ] |
01 | [レジスタ+imm8] |
10 | [レジスタ+imm32] |
11 | レジスタ |
RとMについては、以下のようにレジスタと対応している。
R or Mの2進表示 | レジスタ |
---|---|
000 | eax |
001 | ecx |
010 | edx |
011 | ebx |
100 | esp |
101 | ebp |
110 | esi |
111 | edi |
なんだかややこしいが、そういう仕様なので仕方ない。
R, [M+imm] 型
機械語0x03
、つまりadd R, [M+imm]
の命令を見てみる。
さっきの[M+imm], R
と比較すると、R
と[M+imm]
の位置が入れ替わっただけだ。
ModRMの役割は全く同じなので、説明は略す。
なおadd
以外の命令(sub
やcmp
やmov
など)についても、命令セット表に[M+imm], R
やR, [M+imm]
が出てきたら、ModRMは今説明したadd
命令とおなじ型になる。
ModRMの精神について述べると、modは挙動の種類を指定し、Rはレジスタを指定し、Mはメモリを指定する。 この雰囲気を掴むと納得しやすい。
Rで計算種別を指定する型
今までに解説した2タイプのModRMが分かれば、ほぼ困らない。 しかし他の型のModRMもいるので、軽く説明しておく。
命令セット表の8段目の左側0x81
, 0x83
の命令は、calc
になっている。
これらはadd ecx, 即値
やsub edx, 即値
を計算する命令だ。
この場合は、ModRMのRで計算の種類を指定し、Mでレジスタを指定する。
2進表示 | オペコード(R) | レジスタ(M) |
---|---|---|
000 | add | eax |
001 | or | ecx |
010 | adc | edx |
011 | sbb | ebx |
100 | and | esp |
101 | sub | ebp |
110 | xor | esi |
111 | cmp | edi |
なお、modは11
に固定しておく1。
最後に、calc eax, imm32
型の命令については、機械語0x81
ではなく機械語0x05
等も使用できる。
つまりeaxだけ例外扱いされているのだ2。こういう例外は辛い。
即値を2つ取る型
mov [M+imm], imm32_next
0xc7 ModRM imm imm32_next
となる。この場合のModRMは:
ModRM | 説明 |
---|---|
mod | [M+imm] の形式をadd 命令の場合と同様に指定 |
R | 000で固定。ということにしてくれ |
M | レジスタの種類をadd 命令の場合と同様に指定 |
どうでもいい話
今日のModRMの解説で、 x86の命令セット表 の大半は理解できるようになったと思う。
残りはcall
, leave
, ret
だけだ。これらは関数呼び出しのための命令で、少しややこしいので明日説明する。
今まで見てきたように、x86は各命令のサイズがバラバラだ。
hlt
のように1byteの命令もあれば、add [eax+0x12345678], 0x1234
のような10byte命令もある。
これをちゃんと読み取るCPUをFPGAで書くのは、結構難しい。
3日前に作ったCPUは、異サイズの命令読み取りの部分でずるいことをして、問題を回避していた。
MIPSならば、この辺の面倒な問題が起きない3のでCPUを作りやすい。 逆に言うとx86がしんどいのだ。x86の辛さを表すエピソードを紹介すると
#低レイヤ x86-32bitの機械語で、movの引数に[esp]がくる場合が気持ち悪い。例えばmov [レジスタ] imm32型の命令で、[esa]や[ebp]ならModRMの後に即値がくる。しかし [esp]の場合はModRMのあとに余計な0x24が入る。こういう例外はやめて
— 解答略 (@kaitou_ryaku) 2017年2月1日
SIB拡張のためにどれかのレジスタを読み換える必要があったがESPが一番1バイト長くなっても支障なかったからね
— (:∠) まぐろ㌠ (@MagurosanTeam) 2017年4月10日
スタック上にローカル変数展開するにもEBP相対のアドレッシングで読み書きすれば十分だしね https://t.co/a46ddBAzhq
辛い。