命令の内部フォーマットとアドレッシングモード

アセンブリ言語のプログラムはアセンブラがバイナリコードに変換します。バイナリコードのフォーマットを知っていたほうが複雑な x86-64 のアドレッシングモード(メモリアドレスの指定方法)を理解しやすくなると思います。ここでは命令の内部フォーマットの概要を説明します。この部分が理解できなくても不正なアドレッシングモードの場合にはエラーで知らせてくれるので心配は要りません。

x86-64 の命令は一般的に以下の構成となっています。最小では1バイト、最大で15バイトの可変長となっているのがRISC CPUと異なる大きな特徴です。

プリフィックス REXプリフィックス オペコード ModR/M SIB アドレス変位 即値データ
複数指定可
(無くてもよい)
0、1 バイト 1、2、3バイト 0、1 バイト 0、 1 バイト 0、1、2、4 バイト 0、1、2、4 バイト

オペランドのレジスタやメモリアドレスの指定方法をアドレッシングモードと呼び、x86-64 には多くの種類があります。レジスタを指定する場合は、レジスタ名 (rax、esi, r8bなど) を直接記述します。転送元(右側)のオペランドに整数値(即値、イミディエイト)を書き、転送先(左側)のレジスタに代入することもできます。

    mov   rax, 1234567890          ; rax = 1234567890
    mov   rax, [rsi + rdi * 4]     ; rax = Memory[rsi + rdi * 4]
    mov   rax, rbx                 ; rax = rbx
    mov   [rsi + rdi * 4], rax     ; Memory[rsi + rdi * 4] = rax

演算命令の場合は左側のオペランドのレジスタに結果が格納されます。加算の例を示します。

    add   rax, 1234567890          ; rax = rax + 1234567890
    add   rax, [rsi + rdi * 4]     ; rax = rax + Memory[rsi + rdi * 4]
    add   rax, rbx                 ; rax = rax + rbx
    add   [rsi + rdi * 4], rax     ; Memory[rsi + rdi * 4] = Memory[rsi + rdi * 4] + rax

上記の [rsi + rdi * 4] のようにメモリアドレスをレジスタ間の計算で求めることができます。 ベースレジスタ + インデックスレジスタ * スケール + 定数 といった複雑な形式のメモリアドレスを指定できます。オペランドで指定されたメモリアドレスを実効アドレス (Effective Address) と呼びます。

この後は、アドレッシングモードの内部フォーマットを説明しますが、古い8086との互換性を維持するため、かなり複雑な仕組みになっています。興味が無ければスキップしても大丈夫です。


REX プリフィックス

オペコードの直前において、レジスタの種類を32bitモードの8本から16本に拡張したり、オペランドのデフォルトサイズを変更します。32bitモードでは 0x4X のオペコードはレジスタの内容に1を加算(INC)、レジスタの内容から1を減算(DEC)する1バイトの命令に使われていました。64bitモードではこの16個のオペコードをREX プリフィックスが使用することになったため、2バイトのINC / DEC 命令を使う必要があります。(アセンブラが適当にやってくれるので気にする必要はありません。)

フィールド名 ビット位置 定義
7:4 0100 (4) に固定
W 3 0 = デフォルトのオペランドサイズ
1 = 64 ビットのオペランド・サイズ
R 2 ModRM のreg フィールドの拡張
X 1 SIB のindex フィールドの拡張
B 0 ModRM の r/m、SIB の base、
またはオペコードのregの各フィールドの拡張

ModR/M (Mode Register Memory) バイト

ModR/M はオペコードの直後においてレジスタとアドレッシングモードを指定するための1バイトのデータです。オペコードの直前にREX プリフィックスがある場合は、R8 - R15 の拡張汎用レジスタを指定できます。

bit 7 6 5 4 3 2 1 0
field mod reg r/m
rex r b

reg

オペコードで使用するオペランドのレジスタを指定する3ビットのフィールドです。R8からR15のレジスタを指定する場合は、REX プレフィックスのRフィールドの指定が必要です。

rex.r reg レジスタ rex.r reg レジスタ
0 000 RAX 1 000 R8
001 RCX 001 R9
010 RDX 010 R10
011 RBX 011 R11
100 RSP 100 R12
101 RBP 101 R13
110 RSI 110 R14
111 RDI 111 R15

mod と r/m

mode フィールドの2ビットとr/m フィールドの3ビット、REXプレフィックスの B フィールドの1ビットの計 6 ビットを使って、64種類のアドレッシングモードの指定を行います。mod が 11 の場合は直接レジスタを指定することになります。転送や計算の対象がレジスタであることを示します。mod がそれ以外の場合は指定されたメモリアドレス(実効アドレス)が格納しているデータが転送や計算の対象となります。mod が 11 の場合は汎用レジスタ以外にもMMX、XMM レジスタを指定できますが、ここでは省略しています。disp8 と disp32 はそれぞれ8ビット整数と32ビット整数を示します。

x86-64 で新しく採用されたアドレッシングモードにRIP相対アドレッシングがあります。実行中の(次の)命令を示すインストラクションポインタ(プログラムカウンタ)との相対値でアドレスを指定する方法は、x86の分岐命令では昔から使われていましたが、RIP相対アドレッシングはModR/Mを使うすべての命令で使用することができるようになりました。プログラムは命令とデータで構成されますが、例えば実行する命令群の直後にデータを配置する方法を採っている場合、特定の命令と特定のデータの位置の距離(RIPとの相対値)はプログラムがメモリのどこに配置されても変化しません。したがってデータ位置をRIP相対アドレッシングで指定しているプログラムの場合には、メモリ中のどこにプログラムを配置されてもプログラム自身を変更する必要がありません。

RIP相対アドレッシングが使えないCPUでは、OS(のローダ)は実行するためにプログラムをハードディスクから読み込んで、メモリに配置した位置(メモリアドレス)に合わせてプログラムを書き換えてから、ロードしたプログラム自身を実行します。一般的にRIP相対アドレッシングが使える場合はローダは何もすることは無いため、プログラム起動時の負荷を小さくすることができます。

mod
00 01 10 11
r/m rex.b=0 000 [RAX] [RAX + disp8] [RAX + disp32] RAX
001 [RCX] [RCX + disp8] [RCX + disp32] RCX
010 [RDX] [RDX + disp8] [RDX + disp32] RDX
011 [RBX] [RBX + disp8] [RBX + disp32] RBX
100 [SIB] [SIB + disp8] [SIB + disp32] RSP
101 [RIP + disp32] [RBP + disp8] [RBP+ disp32] RBP
110 [RSI] [RSI + disp8] [RSI + disp32] RSI
111 [RDI] [RDI + disp8] [RDI + disp32] RDI
rex.b=1 000 [R8] [R8 + disp8] [R8 + disp32] R8
001 [R9] [R9 + disp8] [R9+ disp32] R9
010 [R10] [R10 + disp8] [R10 + disp32] R10
011 [R11] [R11 + disp8] [R11 + disp32] R11
100 [SIB] [SIB + disp8] [SIB + disp32] R12
101 [RIP + disp32] [R13 + disp8] [R13+ disp32] R13
110 [R14] [R14 + disp8] [R14 + disp32] R14
111 [R15] [R15 + disp8] [R15 + disp32] R15

SIB (Scale Index Base) バイト

ModR/MバイトがSIBを指定している場合は、ベースレジスタ + インデックスレジスタ * スケール + 定数 の形式の演算でメモリアドレスを指定することができます。ベースレジスタとインデックスレジスタにはすべての汎用レジスタを指定できます。スケールは 1、2、4、8 の中から選びます。 例えば プログラミング言語 C の場合、ベースレジスタに配列の先頭アドレス、インデックスレジスタに配列のインデックス、スケールに配列要素のサイズを設定しておけば、配列からの読み出し、書き込み、演算が1命令で実行できます。うまく使えば非常に強力な手段となります。LEA命令で演算に使うこともできますが、詳細は後から解説する予定です。

bit 7 6 5 4 3 2 1 0
field scale index base
rex x b

Base

ベースレジスタを指定する 3bit のフィールドです。REXプレフィックスのBフィールドで拡張汎用レジスタであるR8 - R15 が指定できます。

reb.b base mod レジスタ reb.b base mod レジスタ
0 000 RAX 1 000 R8
001 RCX 001 R9
010 RDX 010 R10
011 RBX 011 R11
100 RSP 100 R12
101 00 disp32 101 00 disp32
01 RBP + disp8 01 R13 + disp8
10 RBP + disp32 10 R13 + disp32
110 RSI 110 R14
111 RDI 111 R15

index

インデックスレジスタを指定する 3bit のフィールドです。REXプレフィックスの X フィールドで拡張汎用レジスタであるR8 - R15 が指定できます。インデックスレジスタはスケールを乗算することができるため、配列のインデックスとして使用できます。

rex.x index レジスタ rex.x index レジスタ
0 000 RAX 1 000 R8
001 RCX 001 R9
010 RDX 010 R10
011 RBX 011 R11
100 RSP 100 R12
101 RBP 101 R13
110 RSI 110 R14
111 RDI 111 R15

Scale

インデックスレジスタの倍率を指定します。00 は乗算しない(つまり1)ことの指定となります。

scale 倍数
00 なし
01 2
10 4
11 8

命令の内部フォーマットとアドレッシングモードのまとめ

分かりやすく解説するのは難しいですね。アセンブラが勝手にしてくれる部分なので普通は気にする必要はありません。

1バイトでもプログラムを小さくしたい場合以外は命令の内部フォーマットまで理解する必要はありませんが、64bit CPU でわざわざアセンブリプログラミングに挑戦するような人にとっては、押さえておいた方が人に自慢する時に役立つかもしれません。



続く...