命令の内部フォーマットとアドレッシングモード
アセンブリ言語のプログラムはアセンブラがバイナリコードに変換します。バイナリコードのフォーマットを知っていたほうが複雑な 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 でわざわざアセンブリプログラミングに挑戦するような人にとっては、押さえておいた方が人に自慢する時に役立つかもしれません。