プログラミングノート - 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 ビット Windows32 ビット Linux64 ビット Windows64 ビット 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~5655~4847~4039~3231~2423~1615~87~0
64 ビットrax
32 ビット-eax
16 ビット--ax
8 ビット--ahal

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 ステータスレジスタが設定される。

C3C2C0
ST(0) > オペランド000
ST(0) < オペランド001
ST(0) = オペランド100
順序付けできない111

これらのステータスは

fstsw  ax       ; ax ← fpu ステータスレジスタ
sahf            ; flags ← ah

でフラグレジスタにコピーでき、条件分岐に使える。 それぞれ

C3C2C0
ZFPFCF

に対応している。 Pentium Pro 以降では fcomi 系命令 (ただしメモリアクセス不可) で比較結果を直接フラグレジスタにセットできる。

参考文献