※この記事はリンク切れしたページhttp://milkpot.sakura.ne.jp/note/x86.html の無断転載です。リンクの復活もしくはページ作成者からの指摘があった場合このページを削除する予定です。
プログラミングノート - x86
x86 のアセンブラを勉強するページ。
レジスタ
eax, ecx, edx, ebx, esp, ebp, esi, edi | ... 汎用レジスタ (32 ビット) |
rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8~15 | ... 汎用レジスタ (64 ビット) |
eflags | ... フラグレジスタ (32 ビット) |
rflags | ... フラグレジスタ (64 ビット) |
eip | ... プログラムカウンタ (32 ビット) |
rip | ... プログラムカウンタ (64 ビット) |
st(0)~(7) | ... FPU レジスタ |
FPU ステータスレジスタ | ... FPU ステータスレジスタ |
FPU コントロールレジスタ | ... FPU コントロールレジスタ |
xmm0~7 | ... XMM レジスタ |
mxcsr | ... MXCSR コントロール&ステータスレジスタ |
汎用レジスタの内訳
32 ビット Windows | 32 ビット Linux | 64 ビット Windows | 64 ビット Linux | |
{e|r}ax | 戻り値を入れるレジスタ | |||
{e|r}cx | 自由に使っていいレジスタ | 引数を入れるレジスタ | ||
{e|r}dx | 自由に使っていいレジスタ | 引数を入れるレジスタ | ||
{e|r}bx | 自由に使っていいレジスタ | 保存しなければならないレジスタ | ||
{e|r}sp | スタックポインタ | |||
{e|r}bp | 元の値を保存しなければならないレジスタ | |||
{e|r}si | 元の値を保存しなければならないレジスタ | 引数を入れるレジスタ | ||
{e|r}di | 元の値を保存しなければならないレジスタ | 引数を入れるレジスタ | ||
r8~9 | - | 引数を入れるレジスタ | ||
r10~11 | - | 自由に使っていいレジスタ | ||
r12~15 | - | 元の値を保存しなければならないレジスタ |
rsp は 16 バイト境界に合わせなければならない (64 ビットの場合)。
32 ビットでは引数はすべてスタックに積む。 ただしレジスタに入れる処理系依存の拡張も多い。 Wikipedia が詳しい。
eax は rax の下位 32 ビット部分を指す。 また ax は eax の下位 16 ビット部分を指す。 さらに ax は上下 2 つの 8 ビットレジスタ ah, al に分割して使うことができる。 以下のようなレイアウトになっている。
ビット番号 | 63~56 | 55~48 | 47~40 | 39~32 | 31~24 | 23~16 | 15~8 | 7~0 |
64 ビット | rax | |||||||
32 ビット | - | eax | ||||||
16 ビット | - | - | ax | |||||
8 ビット | - | - | ah | al |
32 ビットレジスタを使う場合、上位 32 ビットはクリアされる。 8/16 ビットレジスタを使う場合、他のビットは変化しない。
64ビットでは以下のレジスタがフルに使える。
rax, | rcx, | rdx, | rbx, | rsp, | rbp, | rsi, | rdi, | r8, | r9, | r10, | r11, | r12, | r13, | r14, | r15 |
eax, | ecx, | edx, | ebx, | esp, | ebp, | esi, | edi, | r8d, | r9d, | r10d, | r11d, | r12d, | r13d, | r14d, | r15d |
ax, | cx, | dx, | bx, | sp, | bp, | si, | di, | r8w, | r9w, | r10w, | r11w, | r12w, | r13w, | r14w, | r15w |
ah, | ch, | dh, | bh | ||||||||||||
al, | cl, | dl, | bl, | spl, | bpl, | sil, | dil, | r8l, | r9l, | r10l, | r11l, | r12l, | r13l, | r14l, | r15l |
sph とか r8h とかは存在しない。
32ビットでは以下のレジスタだけが使える。
eax, | ecx, | edx, | ebx, | esp, | ebp, | esi, | edi |
ax, | cx, | dx, | bx, | sp, | bp, | si, | di |
ah, | ch, | dh, | bh | ||||
al, | cl, | dl, | bl |
フラグレジスタの内訳
ビット 11 | (OF) | ... | オーバーフロー発生 |
ビット 10 | (DF) | ... | ストリング命令の方向を指定するフラグ |
ビット 7 | (SF) | ... | 演算結果が負 |
ビット 6 | (ZF) | ... | 演算結果がゼロ |
ビット 2 | (PF) | ... | 演算結果の奇数パリティ (1 が偶数個のときに立つ) |
ビット 0 | (CF) | ... | キャリー発生 / ボロー発生 |
ビット数によって flags, eflags, rflags と呼び分けたりするが、上記の ax, eax, rax と同様にひとつの領域を共用している。 ちなみに ip, eip, rip も同様。
FPU レジスタの内訳
st(0) | ... | 戻り値を入れるレジスタ |
st(1)~(7) | ... | 自由に使っていいレジスタ |
各 80 ビット (拡張倍精度)。
引数は汎用のスタックに積む。
表記とか
本家インテルの記述方式と GNU as で使われている AT&T; の記述方式はだいぶ違う。 特にソースとデスティネーションが逆になっているのが大きい。 ここではインテル方式を使うことにする。
インテルスタイル | AT&T; (GNU as) スタイル |
mov al, 123 | movb $123, %al |
mov eax, [esi+ebx*2+3] | movl 3(%esi,%ebx,2), %eax |
整数演算
ロード/ストア
mov eax, [4]
mov eax, [ebx]
mov eax, [ebx+4]
mov eax, [ecx*2+4]
mov eax, [ebx+ecx]
mov eax, [ebx+ecx+4]
mov eax, [ebx+ecx*2]
mov eax, [ebx+ecx*2+4]
mov eax, [rip+4]
レジスタ (eax, ebx, ecx の部分) は汎用レジスタどれでも指定できる。 シフト (*2 の部分) は 2, 4, 8 のいずれか。 オフセット (+4 の部分) は符号付き 8 ビットまたは 32 ビット。
64 ビットでは rip 相対アドレッシングが可能。 ただし単純にオフセット (符号付き 32 ビット) を加算する形式のみ。 32 ビットでは eip 相対は不可。 call 命令のリターンアドレスを取り出すしか方法はない。
ストアの場合は単純に逆に書く。
mov [eax], ebx
mov はレジスタ間の移動にも使う。
mov eax, ecx
mov はソースとデスティネーションのサイズが同じでなければならない。 メモリ上の 8 ビット値を 8 ビットレジスタにロードすることはできるが、 8 ビット値を 32 ビットレジスタにロードなどするには movsx (符号拡張) または movzx (ゼロ拡張) を使う。
movzx ax, al ; 16 ビット ← 8 ビット
movzx ax, byte ptr [bx] ; 16 ビット ← 8 ビット
movzx eax, al ; 32 ビット ← 8 ビット
movzx eax, byte ptr [ebx] ; 32 ビット ← 8 ビット
movzx eax, ax ; 32 ビット ← 16 ビット
movzx eax, word ptr [ebx] ; 32 ビット ← 16 ビット
movzx rax, al ; 64 ビット ← 8 ビット
movzx rax, byte ptr [rbx] ; 64 ビット ← 8 ビット
movzx rax, eax ; 64 ビット ← 32 ビット
movzx rax, dword ptr [rbx] ; 64 ビット ← 32 ビット
64 ビット ← 16 ビット はできない。
mov でいちいちロード/ストアしなくても、直接メモリ上の値を演算できる命令も多い。
mov はフラグレジスタを更新しない。
プッシュ&ポップ
{e|r}sp を用いた専用の push 命令と pop 命令がある。
push 4
push eax
push [eax]
pop eax
pop [eax]
スタックは full-descending。 即値は符号付き 8~16 ビットまたは符号なし 32 ビット。
push, pop はフラグレジスタを更新しない。
定数ロード
mov 命令で 64 ビットまでの任意の即値をロードできる。
mov eax, 12345678h
mov rax, 123456789abcdefh
演算
add eax, ecx ; eax ← eax+ecx
adc eax, ecx ; eax ← eax+ecx+CF
sub eax, ecx ; eax ← eax-ecx
sbc eax, ecx ; eax ← eax-ecx-CF
and eax, ecx ; eax ← eax&ecx
or eax, ecx ; eax ← eax|ecx
xor eax, ecx ; eax ← eax^ecx
shl eax, cl ; eax ← eax<<cl
sar eax, cl ; eax ← eax>>cl
shr eax, cl ; eax ← eax>>>cl
neg eax ; eax ← -eax
not eax ; eax ← ~eax
inc eax ; eax ← eax+1
dec eax ; eax ← eax-1
演算結果はフラグレジスタに反映される。 命令によってフラグへの影響が異なるので要マニュアル参照。 また、レジスタの代わりにメモリアクセスおよび即値を指定できる。 add~xor ではオペランドに以下の組み合わせが可能。
add eax, 4
add eax, ecx
add eax, [ecx]
add [eax], 4
add [eax], ecx
shl~shr では以下の組み合わせが可能 (シフト回数のレジスタは cl のみ)。
shl eax, 1
shl eax, cl
shl [eax], 1
shl [eax], cl
neg~dec では以下の指定が可能。
neg eax
neg [eax]
メモリアクセスの [eax] [ecx] 部分はロード/ストアの節で示したアドレッシングモードがどれでも使える。
命令とオペランドから自動的にサイズを判断できないときはサイズを明記する必要がある (常に明記しても構わない)。
add [rax], al ; 1 バイト
add [rax], ax ; 2 バイト
add [rax], eax ; 4 バイト
add [rax], rax ; 8 バイト
add [rax], 3 ; サイズ不明につきエラー
add byte ptr [rax], 3 ; 1 バイト
add word ptr [rax], 3 ; 2 バイト
add dword ptr [rax], 3 ; 4 バイト
add qword ptr [rax], 3 ; 8 バイト
アドレッシングモードが豊富なおかげで、 アドレス計算をする (だけで実際にメモリアクセスはしない) lea 命令が通常の演算命令代わりに大変役立つ。 lea はフラグレジスタを更新しない。
lea eax, [ebx+ecx+1] ; eax ← ebx+ecx+1
lea eax, [ebx+ebx*2] ; eax ← ebx×3
乗除算
mul ecx ; edx:eax ← eax×ecx (符号なし)
imul ecx ; edx:eax ← eax×ecx (符号付き)
div ecx ; eax ← eax÷ecx, edx ← eax mod ecx (符号なし)
idiv ecx ; eax ← eax÷ecx, edx ← eax mod ecx (符号付き)
eax と edx を固定で使う 1 オペランド形式の乗除算。 ecx のところには任意のレジスタまたはメモリアドレスを書ける。
imul ecx, 2 ; ecx ← ecx×2
imul ecx, ebx ; ecx ← ecx×ebx
imul ecx, [ebx] ; ecx ← ecx×mem[ebx]
乗算だけ 2~3 オペランド形式もある。 この形式では上位ビットは捨てられる。
imul ecx, ebx, 3 ; ecx ← ebx×3
imul ecx, [ebx], 3 ; ecx ← mem[ebx]×3
3 オペランド形式では第 3 オペランド即値のみ。
比較
cmp eax, ecx ; eax-ecx
test eax, ecx ; eax&ecx;
それぞれ sub, and の演算結果を保存しないバージョン。 フラグだけ更新する。
フラグは分岐命令で直接参照するほか、 pushf 系命令でスタックに積んだり、 lahf 命令で一部を ah レジスタに転送したり、 set 系命令で汎用レジスタに展開できる。
pushf ; mem[sp-2] ← flags, sp ← sp-2
pushfd ; mem[esp-4] ← eflags, esp ← esp-4
pushfq ; mem[rsp-8] ← rflags, rsp ← rsp-8
lahf ; ah ← flags の下位 8 ビット (SF~CF)
setc al ; al ← CF=1
setz al ; al ← ZF=1
...
set の語尾は下記の分岐命令 j の語尾と同じにつき省略。 条件に応じて 1 または 0 を 8 ビットレジスタまたはメモリアドレスにストアする。
分岐
jo | ... OF=1 |
jno | ... OF=0 |
jb, jc, jnae | ... CF=1 |
jae, jnc, jnb | ... CF=0 |
je, jz | ... ZF=1 |
jne, jnz | ... ZF=0 |
jbe, jna | ... CF=1 and ZF=1 |
ja, jnbe | ... CF=0 and ZF=0 |
js | ... SF=1 |
jns | ... SF=0 |
jp, jpe | ... PF=1 |
jnp, jpo | ... PF=0 |
jl, jnge | ... SF≠OF |
jge, jnl | ... SF=OF |
jle, jng | ... ZF=1 or SF≠OF |
jg, jnle | ... ZF=0 and SF=OF |
jcxz | ... cx=0 |
jecxz | ... ecx=0 |
すべて符号付き 8~32 ビット {e|r}ip 相対ジャンプ。 カンマで区切っているものはニモニックの別名。
無条件分岐の jmp はレジスタ間接ジャンプやメモリ間接ジャンプもできる。
jmp $+4
jmp eax
jmp [eax]
サブルーチンコール (他でいうジャンプ&リンク) は専用の call 命令と ret 命令を用いる。 リンクレジスタは無く、リターンアドレスはスタックにプッシュされ、スタックからポップされる。
call $+4
call eax
call [eax]
ret
浮動小数点演算
ロード/ストア
FPU レジスタはスタック的動作をする。 つまり st(0) に値をプッシュすると、既存の st(0) は st(1) に、st(1) は st(2) に移動する。 st(0) の値をポップすると、逆に st(2) は st(1) に、st(1) は st(0) に移動する。 スタックオーバーフロー例外とかスタックアンダーフロー例外とかある。
ロード (プッシュ) | ストア | ストア+ポップ | |
2 バイト整数 | fild word ptr [eax] | fist word ptr [eax] | fistp word ptr [eax] |
4 バイト整数 | fild dword ptr [eax] | fist dword ptr [eax] | fistp dword ptr [eax] |
8 バイト整数 | fild qword ptr [eax] | - | fistp qword ptr [eax] |
単精度 (4 バイト) | fld dword ptr [eax] | fst dword ptr [eax] | fstp dword ptr [eax] |
倍精度 (8 バイト) | fld qword ptr [eax] | fst qword ptr [eax] | fstp qword ptr [eax] |
拡張倍精度 (10 バイト) | fld tbyte ptr [eax] | - | fstp tbyte ptr [eax] |
レジスタ | fld st(2) | fst st(2) | fstp st(2) |
いずれもレジスタ内では拡張倍精度で保持される。
定数ロード
以下の定数だけが専用の命令でロードできる。 それ以外はすべてメモリからロードする必要がある。
fldz ; 0
fld1 ; 1
fldpi ; 円周率
fldl2t ; log2(10)
fldl2e ; log2(e)
fldlg2 ; log10(2)
fldln2 ; ln(2)
演算
fadd st(0), st(1) ; st(0) ← st(0)+st(1)
fsub st(0), st(1) ; st(0) ← st(0)-st(1)
fsubr st(0), st(1) ; st(0) ← st(1)-st(0)
fmul st(0), st(1) ; st(0) ← st(0)×st(1)
fdiv st(0), st(1) ; st(0) ← st(0)÷st(1)
fdivr st(0), st(1) ; st(0) ← st(1)÷st(0)
いずれも以下のようなバリエーションがある。
fiadd word ptr [eax] ; st(0) ← st(0)+mem[eax] (2 バイト整数)
fiadd dword ptr [eax] ; st(0) ← st(0)+mem[eax] (4 バイト整数)
fadd dword ptr [eax] ; st(0) ← st(0)+mem[eax] (単精度)
fadd qword ptr [eax] ; st(0) ← st(0)+mem[eax] (倍精度)
fadd st(0), st(i) ; st(0) ← st(0)+st(i)
fadd st(i), st(0) ; st(i) ← st(i)+st(0)
faddp st(i), st(0) ; st(i) ← st(i)+st(0), st(0)をポップ
[eax] の部分は通常のアドレッシングモードをなんでも指定できる。 st(i) の部分は st(0)~(7) を指定できる。
語尾の p 付きは st(0) をポップする。 p なしはプッシュもポップもしない。 i 付きは整数 (符号付き)。
fabs ; st(0) ← abs(st(0))
fneg ; st(0) ← -st(0)
fsin ; st(0) ← sin(st(0))
fcos ; st(0) ← cos(st(0))
fsqrt ; st(0) ← sqrt(st(0))
frndint ; st(0) ← int(st(0))
fxch st(i) ; st(0) ← st(i), st(i) ← st(0)
単項演算子はオペランドなしで st(0) 固定。 ポップもできない。
frndint は現在の丸めモードに従って小数点以下を切り捨てる。 fxch は st(0) と st(i) を入れ替える。 他にもいろいろあるけど省略。
比較
ficom word ptr [eax] ; 2 バイト整数
ficom dword ptr [eax] ; 4 バイト整数
fucom dword ptr [eax] ; 単精度
fucom qword ptr [eax] ; 倍精度
fucom st(i)
st(0) とオペランドを比較する。 語尾に p を付けると st(0) をポップする。 fucompp (オペランドなし) は st(0) と st(1) を比較して両方ポップする。
比較結果に基づいて下表の通りに FPU ステータスレジスタが設定される。
C3 | C2 | C0 | |
ST(0) > オペランド | 0 | 0 | 0 |
ST(0) < オペランド | 0 | 0 | 1 |
ST(0) = オペランド | 1 | 0 | 0 |
順序付けできない | 1 | 1 | 1 |
これらのステータスは
fstsw ax ; ax ← fpu ステータスレジスタ
sahf ; flags ← ah
でフラグレジスタにコピーでき、条件分岐に使える。 それぞれ
C3 | C2 | C0 |
↓ | ↓ | ↓ |
ZF | PF | CF |
に対応している。 Pentium Pro 以降では fcomi 系命令 (ただしメモリアクセス不可) で比較結果を直接フラグレジスタにセットできる。
参考文献
- Intel 日本語技術資料のダウンロード
- x86 calling conventions - Wikipedia
- MSDN: x64 Software Conventions - Register Usage
- System V Application Binary Interface - Intel386 Architecture Processor Supplement
- System V Application Binary Interface - AMD64 Architecture Processor Supplement
- How to optimize for the Pentium family of the microprocessors
- Software optimization resources
- The Intel 80386, part 1: Introduction
- The Intel 80386, part 2: Memory addressing modes
- The Intel 80386, part 3: Flags and condition codes
- The Intel 80386, part 4: Arithmetic
- The Intel 80386, part 5: Logical operations
- The Intel 80386, part 6: Data transfer instructions
- The Intel 80386, part 7: Conditional instructions and control transfer
- The Intel 80386, part 8: Block operations
- The Intel 80386, part 9: Stack frame instructions
- The Intel 80386, part 10: Atomic operations and memory alignment
- The Intel 80386, part 11: The TEB
- The Intel 80386, part 12: The stuff you don’t need to know
- The Intel 80386, part 13: Calling conventions
- The Intel 80386, part 14: Rescuing a stack trace after the debugger gave up when it reached an FPO function
- The Intel 80386, part 15: Common compiler-generated code sequences
- The Intel 80386, part 16: Code walkthrough
- The Intel 80386, part 17: Future developments