x86-64 命令一覧
概要と記号
add5.cをgcc -S add5.cでコンパイルした結果
add5.s(余計なものの削除後)を用いて,
x86-64アセンブリ言語の概要と記号を説明します.
$ gcc -S add5.cを実行(コンパイル)すると,アセンブリコードadd5.sが生成されます.(結果は環境依存なので,add5.sの中身が違ってても気にしなくてOK)- ドット記号
.で始まるのはアセンブラ命令(assembler directive)です. - コロン記号
:で終わるのはラベル定義です. - シャープ記号
#から行末までと,/*と*/で囲まれた範囲(複数行可)はコメントです. movq %rsp, %rbpは機械語命令(2進数)の記号表現(ニモニック(mnemonic))です.movqが命令でオペコード(opcode),%rspと%rbpは引数でオペランド(operand)と呼ばれます.- ドル記号
$で始まるのは即値(immediate value,定数)です. - パーセント記号
%で始まるのはレジスタです. -4(%rbp)は間接メモリ参照です.この場合は「%rbp-4の計算結果」をアドレスとするメモリの内容にアクセスすることを意味します.
命令サフィックス
| AT&T形式の サイズ指定 | Intel形式の サイズ指定 | メモリオペランドの サイズ | AT&T形式での例 | Intel形式での例 |
|---|---|---|---|---|
b | BYTE PTR | 1バイト(8ビット) | movb $10, -8(%rbp) | mov BYTE PTR [rbp-8], 10 |
w | WORD PTR | 2バイト(16ビット) | movw $10, -8(%rbp) | mov WORD PTR [rbp-8], 10 |
l | DWORD PTR | 4バイト(32ビット) | movl $10, -8(%rbp) | mov DWORD PTR [rbp-8], 10 |
q | QWORD PTR | 8バイト(64ビット) | movq $10, -8(%rbp) | mov QWORD PTR [rbp-8], 10 |
- 間接メモリ参照ではサイズ指定が必要です (「何バイト読み書きするのか」が決まらないから)
- AT&T形式では命令サフィックス(instruction suffix)でサイズを指定します.
例えば
movq $10, -8(%rbp)のqは, 「メモリ参照-8(%rbp)への書き込みサイズが8バイト」であることを意味します.
サフィックスとプリフィックス
サフィックス(suffix)は接尾語(後ろに付けるもの), プリフィックス(prefix)は接頭語(前に付けるもの)という意味です.
- Intel形式ではメモリ参照の前に,
BYTE PTRなどと指定します. - 他のオペランドからサイズが決まる場合は命令サフィックスを省略可能です.
例えば,
movq %rax, -8(%rsp)はmov %rax, -8(%rsp)にできます.mov命令の2つのオペランドはサイズが同じで%raxレジスタのサイズが8バイトだからです.
即値(定数)
| 種類 | 説明 | 例 |
|---|---|---|
| 10進数定数 | 0で始まらない | pushq $74 |
| 16進数定数 | 0xか0Xで始まる | pushq $0x4A |
| 8進数定数 | 0で始まる | pushq $0112 |
| 2進数定数 | 0bか0Bで始まる | pushq $0b01001010 |
| 文字定数 | '(クオート)で始まる | pushq $'J |
'(クオート)で囲む | pushq $'J' | |
\バックスラッシュでエスケープ可 | pushq $'\n |
- 即値(immediate value,定数)には
$をつけます - 上の例の定数は最後以外は全部,値が同じです
- GNUアセンブラでは文字定数の値はASCIIコードです.
上の例では,文字
'J'の値は74です. - バックスラッシュでエスケープできる文字は,
\b,\f,\n,\r,\t,\",\\です. また\123は8進数,\x4Fは16進数で指定した文字コードになります. - 多くの場合,即値は32ビットまでで, オペランドのサイズが64ビットの場合, 32ビットの即値は,64ビットの演算前に 64ビットに符号拡張 されます (ゼロ拡張だと 負の値が大きな正の値になって困るから)
64ビットに符号拡張される例(1)
# asm/add-imm2.s
.text
.globl main
.type main, @function
main:
movq $0, %rax
addq $-1, %rax
ret
.size main, .-main
上のaddq $-1, %rax命令の即値$-1は
32ビット(以下の場合もあります)の符号あり整数$0xFFFFFFFFとして
機械語命令に埋め込まれます.
addq命令が実行される時は,
この$0xFFFFFFFFが64ビットに符号拡張されて$0xFFFFFFFFFFFFFFFFとなります.
以下の実行例でもこれが確認できました.
0 + 0xFFFFFFFFFFFFFFFF = 0xFFFFFFFFFFFFFFFF
$ gcc -g add-imm2.s
$ gdb ./a.out -x add-imm2.txt
Breakpoint 1, main () at add-imm2.s:8
8 ret
7 addq $-1, %rax
$1 = 0xffffffffffffffff
# 0xffffffffffffffff が表示されていれば成功
64ビットに符号拡張される例(2)
32ビットの即値が,64ビットの演算前に64ビットに符号拡張されることを見てみます.
32ビット符号あり整数が表現できる範囲は-0x80000000から0x7FFFFFFFです.
# asm/add-imm.s
.text
.globl main
.type main, @function
main:
movq $0, %rax
addl $0xFFFFFFFF, %eax # OK (0xFFFFFFFF = -1)
# addq $0xFFFFFFFF, %rax # NG (0x7FFFFFFFを超えている)
addq $0xFFFFFFFFFFFFFFFF, %rax # OK (0xFFFFFFFFFFFFFFFF = -1)
addq $0x7FFFFFFF, %rax # OK
addq $-0x80000000, %rax # OK
addq $0xFFFFFFFF80000000, %rax # OK (0xFFFFFFFF80000000 = -0x80000000)
ret
.size main, .-main
-
7行目の
$0xFFFFFFFFは32ビット符号あり整数として-1, つまり32ビット符号あり整数が表現できる範囲内なのでOKです. -
一方,8行目の
$0xFFFFFFFFは 64ビット符号あり整数として$0x7FFFFFFFを超えてるのでNGです. (アセンブルするとエラーになります) -
9行目の
$0xFFFFFFFFFFFFFFFFは一見NGですが, 64ビット符号あり整数としての値は-1なので, GNUアセンブラはこの即値を-1として機械語命令に埋め込みます. -
いちばん大事なのは最後の2つの
addq命令です.addq $-0x80000000, %raxの 即値$-0x80000000は(機械語命令中では32ビットで埋め込まれますが) 足し算を実行する前に64ビットに符号拡張されるので,$0xFFFFFFFF80000000という値で足し算されます. (つまり,$0x80000000を引きます). 以下の実行例を見ると,その通りの実行結果になっています.❶ 0x17ffffffd - $0x80000000 = ❷ 0xfffffffd❷ 0xfffffffd - $0x80000000 = ❸ 0x7ffffffd
一方,もし
$-0x80000000を(符号拡張ではなく) ゼロ拡張 すると,$0x0000000080000000となるので,$0x80000000を(引くのではなく)足すことになってしまいます.
$ gcc -g add-imm.s
$ gdb ./a.out -x add-imm.txt
Breakpoint 1, main () at add-imm.s:7
7 addl $0xFFFFFFFF, %eax # OK (0xFFFFFFFF = -0x1)
9 addq $0xFFFFFFFFFFFFFFFF, %rax # OK (0xFFFFFFFFFFFFFFFF = -0x1)
1: /x $rax = 0xffffffff
10 addq $0x7FFFFFFF, %rax # OK
1: /x $rax = 0xfffffffe
11 addq $-0x80000000, %rax # OK
1: /x $rax = ❶ 0x17ffffffd
12 addq $0xFFFFFFFF80000000, %rax # OK (0xFFFFFFFF80000000 = -0x80000000)
1: /x $rax = ❷ 0xfffffffd
main () at add-imm.s:13
13 ret
1: /x $rax = ❸ 0x7ffffffd
#以下が表示されていれば成功
#1: /x $rax = 0xffffffff
#1: /x $rax = 0xfffffffe
#1: /x $rax = 0x17ffffffd
#1: /x $rax = 0xfffffffd
#1: /x $rax = 0x7ffffffd
以下の通り,逆アセンブルすると, 32ビット以下の即値として機械語命令中に埋め込まれていることが分かります.
$ gcc -g add-imm.s
$ objdump -d ./a.out
0000000000001129 <main>:
1129: 48 c7 c0 00 00 00 00 mov $0x0,%rax
1130: 83 c0 ff add $0xffffffff,%eax
1133: 48 83 c0 ff add $0xffffffffffffffff,%rax
1137: 48 05 ff ff ff 7f add $0x7fffffff,%rax
113d: 48 05 00 00 00 80 add $0xffffffff80000000,%rax
1143: 48 05 00 00 00 80 add $0xffffffff80000000,%rax
1149: c3 ret
mov命令は例外で64ビットの即値を扱えます
64ビットの即値を扱う例
# asm/movqabs-1.s
.text
.globl main
.type main, @function
main:
movq $0x1122334455667788, %rax
movabsq $0x99AABBCCDDEEFF00, %rax
ret
.size main, .-main
$ gcc -g movqabs-1.s
$ gdb ./a.out -x movqabs-1.txt
Breakpoint 1, main () at movqabs-1.s:6
6 movq $0x1122334455667788, %rax
7 movabsq $0x99AABBCCDDEEFF00, %rax
1: /x $rax = 0x1122334455667788
main () at movqabs-1.s:8
8 ret
1: /x $rax = 0x99aabbccddeeff00
# 以下が表示されれば成功
# 1: /x $rax = 0x1122334455667788
# 1: /x $rax = 0x99aabbccddeeff00
- ジャンプでは64ビットの相対ジャンプができないので, 間接ジャンプを使う必要がある
64ビットの相対ジャンプの代わりに間接ジャンプを使う例
# asm/jmp-64bit.s
.text
.globl main
.type main, @function
main:
# jmp 0x1122334455667788 # NG
movq $0x1122334455667788, %rax
jmp *%rax
ret
.size main, .-main
0x1122334455667788はいい加減なアドレスなので,
コンパイルは可能ですが,実行すると segmentation fault になります.
レジスタ
汎用レジスタ
- 上記16個のレジスタが汎用レジスタ(general-purpose register)です. 原則として,プログラマが自由に使えます.
- ただし,
%rspはスタックポインタ,%rbpはベースポインタと呼び, 一番上のスタックフレームの上下を指す という役割があります. (ただし,-fomit-frame-pointer オプションでコンパイルされたa.out中では,%rbpはベースポインタとしてではなく, 汎用レジスタとして使われています).
caller-save/callee-saveレジスタ
| 汎用レジスタ | |
|---|---|
| caller-saveレジスタ | %rax, %rcx, %rdx, %rsi, %rdi, %r8〜%r11 |
| callee-saveレジスタ | %rbx, %rbp, %rsp, %r12〜%r15 |
引数
| 引数 | レジスタ |
|---|---|
| 第1引数 | %rdi |
| 第2引数 | %rsi |
| 第3引数 | %rdx |
| 第4引数 | %rcx |
| 第5引数 | %r8 |
| 第6引数 | %r9 |
- 第7引数以降はレジスタではなくスタックを介して渡します
プログラムカウンタ(命令ポインタ)
ステータスレジスタ(フラグレジスタ)
本書で扱うフラグ
ステータスレジスタのうち,本書は以下の6つのフラグを扱います.
| フラグ | 名前 | 説明 |
|---|---|---|
CF | キャリーフラグ | 算術演算で結果の最上位ビットにキャリーかボローが生じるとセット.それ以外はクリア.符号なし整数演算でのオーバーフロー状態を表す. |
OF | オーバーフローフラグ | 符号ビット(MSB)を除いて,整数の演算結果が大きすぎるか小さすぎるかするとセット.それ以外はクリア.2の補数表現での符号あり整数演算のオーバーフロー状態を表す. |
ZF | ゼロフラグ | 結果がゼロの時にセット.それ以外はクリア. |
SF | 符号フラグ | 符号あり整数の符号ビット(MSB)と同じ値をセット.(0は正の数,1は負の数であることを表す) |
PF | パリティフラグ | 結果の最下位バイトの値1のビットが偶数個あればセット,奇数個であればクリア. |
AF | 調整フラグ | 算術演算で,結果のビット3にキャリーかボローが生じるとセット.それ以外はクリア.BCD演算で使用する(本書ではほとんど使いません). |
ステータスフラグの変化の記法
x86-64命令を実行すると,ステータスフラグが変化する命令と 変化しない命令があります. ステータスフラグの変化は以下の記法で表します.
記法の意味は以下の通りです.
| 記法 | 意味 |
|---|---|
| 空白 | フラグ値に変化なし |
| ! | フラグ値に変化あり |
| ? | フラグ値は未定義(参照禁止) |
| 0 | フラグ値はクリア(0になる) |
| 1 | フラグ値はセット(1になる) |
レジスタの別名
%raxレジスタの別名 (%rbx, %rcx, %rdxも同様)
%raxの下位32ビットは%eaxとしてアクセス可能%eaxの下位16ビットは%axとしてアクセス可能%axの上位8ビットは%ahとしてアクセス可能%axの下位8ビットは%alとしてアクセス可能
%rbpレジスタの別名 (%rsp, %rdi, %rsiも同様)
%rbpの下位32ビットは%ebpとしてアクセス可能%ebpの下位16ビットは%bpとしてアクセス可能%bpの下位8ビットは%bplとしてアクセス可能
%r8レジスタの別名 (%r9〜%r15も同様)
%r8の下位32ビットは%r8dとしてアクセス可能%r8dの下位16ビットは%r8wとしてアクセス可能%r8wの下位8ビットは%r8bとしてアクセス可能
同時に使えない制限
- 一部のレジスタは
%ah,%bh,%ch,%dhと一緒には使えない. - 例:
movb %ah, (%r8)やmovb %ah, %bplはエラーになる. - 正確には
REXプリフィクス付きの命令では,%ah,%bh,%ch,%dhを使えない.
32ビットレジスタ上の演算は64ビットレジスタの上位32ビットをゼロにする
- 例:
movl $0xAABBCCDD, %eaxを実行すると%raxの上位32ビットが全てゼロになる - 例:
movw $0x1122, %axやmovb $0x11, %alでは上位をゼロにすることはない
上位32ビットをゼロにする実行例
# asm/zero-upper32.s
.text
.globl main
.type main, @function
main:
movq $0x1122334455667788, %rax
movl $0xAABBCCDD, %eax
movq $0x1122334455667788, %rax
movw $0x1122, %ax
movq $0x1122334455667788, %rax
movb $0x11, %al
ret
.size main, .-main
$ gcc -g zero-upper32.s
$ gdb ./a.out -x zero-upper32.txt
Breakpoint 1, main () at zero-upper32.s:7
7 movl $0xAABBCCDD, %eax
6 movq $0x1122334455667788, %rax
7 movl $0xAABBCCDD, %eax
$1 = 0x1122334455667788
8 movq $0x1122334455667788, %rax
$2 = 0x00000000aabbccdd
# 以下が出力されれば成功
# $1 = 0x1122334455667788 (%raxは8バイトの値を保持)
# $2 = 0x00000000aabbccdd (%raxの上位4バイトがゼロになった)
アドレッシングモード(オペランドの記法)
アドレッシングモードの種類
| アドレッシング モードの種類 | オペランドの値 | 例 | 計算するアドレス |
|---|---|---|---|
| 定数の値 | movq $0x100, %rax | ||
movq $foo, %rax | |||
| レジスタの値 | movq %rbx, %rax | ||
| 定数で指定した アドレスのメモリ値 | movq 0x100, %rax | 0x100 | |
movq foo, %rax | foo | ||
| レジスタ等で計算した アドレスのメモリ値 | movq (%rsp), %rax | %rsp | |
movq 8(%rsp), %rax | %rsp+8 | ||
movq foo(%rip), %rax | %rip+foo |
fooはラベルであり,定数と同じ扱い.(定数を書ける場所にはラベルも書ける).- メモリ参照では例えば
-8(%rbp, %rax, 8)など複雑なオペランドも指定可能. 参照するメモリのアドレスは-8+%rbp+%rax*8になる. (以下を参照).
メモリ参照の形式
| AT&T形式 | Intel形式 | 計算されるアドレス | |
|---|---|---|---|
| 通常のメモリ参照 | disp (base, index, scale) | [base + index * scale + disp] | base + index * scale + disp |
%rip相対参照 | disp (%rip) | [rip + disp] | %rip + disp |
注: Intelのマニュアルには「segment: メモリ参照」という形式もあるとありますが, segment: はほとんど使わないので,省いて説明します.
segmentは使わないの?(いえ,ちょっと使います)
%fsはセグメントレジスタと呼ばれる16ビット長のレジスタで,
他には%cs,%ds,%ss,%es,%gsがあります.
x86-64では%cs,%ds,%ss,%esは常にベースアドレスが0と扱われます.
%fs:という記法が使われた時は,
「%fsレジスタが示すベースアドレスをアクセスするアドレスに加える」
ことを意味します.
%fsのベースレジスタの値を得るには次の方法があります.
arch_prctl()システムコールを使う (ここでは説明しません).gdbでp/x $fs_baseを実行する. (なお,p/x $fsを実行すると0が返りますがこの値は嘘です)rdfsbase命令を使う.
rdfsbase命令はCPUとOSの設定に依存
rdfsbase命令が使えるかどうかは,CPUとOSの設定に依存します.
/proc/cpuinfoのflagsの表示にfsgsbaseがあれば,rdfsbase命令は使えます.
(以下の出力例ではfsgsbaseが入っています).
$ less /proc/cpuinfo
processor : 0
cpu family : 6
model name : Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
(一部略)
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves arat md_clear flush_l1d arch_capabilities
Linuxでは%fs:や%gs:を使って
スレッドローカルストレージ(TLS)を実現しています.
スレッドローカルストレージとは「スレッドごとの(一種の)グローバル変数」です.
マルチスレッドはスレッド同士がメモリ空間を共有しているので,
普通にグローバル変数を使うと,他のスレッドに内容が破壊される可能性があります.
スレッドローカルストレージを使えば,他のスレッドに破壊される心配がなくなります.
スレッドごとに%fsレジスタの値を変えて,
(プログラム上では同じ変数に見えても)スレッドごとに別のアドレスを
参照させて実現しているのでしょう.
(CPUがスレッドの実行を停止・再開するたびに,
スレッドが使用しているレジスタの値も退避・回復するので,
見た目上,「スレッドはそれぞれ独自のレジスタセットを持っている」ように動作します).
C11からは_Thread_local,gccでは__threadというキーワードで,
スレッドローカルな変数を宣言できます.
// tls.c
#include <stdio.h>
❶ __thread int x = 0xdeadbeef;
int main ()
{
printf ("x=%x\n", x);
}
$ gcc -g tls.c
$ ./a.out
x=deadbeef
$ objdump -d ./a.out | less
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 64 8b 04 25 fc ff ff mov ❷ %fs:0xfffffffffffffffc,%eax
1158: ff
1159: 89 c6 mov %eax,%esi
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1151: file tls.c, line 5.
(gdb) r
Breakpoint 1, main () at tls.c:5
5 printf ("x=%x\n", x);
(gdb) p/x x
$1 = 0xdeadbeef
(gdb) ❸ p/x $fs_base
$2 = ❹ 0x7ffff7fa9740
(gdb) x/1wx $fs_base - 4
0x7ffff7fa973c: ❺ 0xdeadbeef
- ❶
__threadキーワードを使って,変数xをスレッドローカルにします. - コンパイルした
a.outを逆アセンブルすると, ❷%fs:0xfffffffffffffffcというメモリ参照があります. これがスレッドローカルな変数xの実体の場所で, 「%fsのベースレジスタ - 4」がxのアドレスになります. - ❸
p/x $fs_baseを使って,%fsのベースレジスタの値を調べると ❹0x7ffff7fa9740と分かりました. - アドレス
$fs_base - 4のメモリの中身(4バイト)を調べると, 変数xの値である❺0xDEADBEEFが入っていました.
0xDEADBEEFとは
バイナリ上でデバッグする際,「ありそうもない値」を使うと便利なことがあります.
1や2だと偶然の一致がありますが,「ありそうもない値」を使うと
「高い確率でこれは私が書き込んだ値だよね」と分かるからです.
0xDEADBEEFは正しい16進数でありながら,英単語としても読めるので,
「ありそうもない値」として便利です.
他にはCAFEBABEもよく使われます.
0xDEADBEEFや0xCAFEBABEはバイナリを識別するマジックナンバーとしても使われます.
%fs:はスタック保護でも使われる
$ gcc -S -fstack-protector-all add5.c
add5.cを-fstack-protector-allオプションで
スタック保護機能をオンにしてコンパイルします.
(最近のLinuxのgccのデフォルトでは,-fstack-protector-strongが有効に
なっています.これはバッファを使用する関数のみにスタック保護機能を加えます.
ここでは-fstack-protector-allを使って全関数にスタック保護機能を加えました).
.text
.globl add5
.type add5, @function
add5:
endbr64
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movl %edi, -20(%rbp)
movq ❶ %fs:40, %rax
movq ❷ %rax, -8(%rbp)
xorl %eax, %eax
movl -20(%rbp), %eax
addl $5, %eax
movq ❸ -8(%rbp), %rdx
subq ❹ %fs:40, %rdx
je .L3
call ❺ __stack_chk_fail@PLT
.L3:
leave
ret
.size add5, .-add5
- 関数の最初の方で,スレッドローカルストレージの❶
%fs:40の値を, (%rax経由で)スタック上の-8(%rbp)に書き込みます. - 関数の終わりの方で,❸
-8(%rbp)の値を%rdxレジスタにコピーし, ❹%fs:40の値を引き算しています. - もし,引き算の結果がゼロでなければ(つまり❸と❹の値が異なれば),
「バッファオーバーフローが起きた」と判断して,
❺
__stack_chk_fail@PLTを呼び出して プロセスを強制終了させます(つまりバッファオーバーフロー攻撃を防げたことになります).
メモリ参照で可能な組み合わせ(64ビットモードの場合)
通常のメモリ参照
- disp には符号あり定数を指定する.ただし「64ビット定数」は無いことに注意. アドレス計算時に64ビット長に符号拡張される. dispは変位(displacement)を意味する.
- base には上記のいずれかのレジスタを指定可能.省略も可.
- index には上記のいずれかのレジスタを指定可能.省略も可.
%rspを指定できないことに注意. - scale を省略すると
1と同じ
注: dispの例外.
mov␣命令のみ,64ビットのdispを指定可能. この場合,movabs␣というニモニックを使用可能. メモリ参照はdispのみで,base,index,scaleは指定不可. 他方のオペランドは%raxのみ指定可能.movq 0x1122334455667788, %rax movabsq 0x1122334455667788, %rax movq %rax, 0x1122334455667788 movabsq %rax, 0x1122334455667788
%rip相対参照
%rip相対参照では32ビットのdispと%ripレジスタのみが指定可能です.
メモリ参照の例
| AT&T形式 | Intel形式 | 指定したもの | 計算するアドレス |
|---|---|---|---|
8 | [8] | disp | 8 |
foo | [foo] | disp | foo |
(%rbp) | [rbp] | base | %rbp |
8(%rbp) | [rbp+8] | dispとbase | %rbp + 8 |
foo(%rbp) | [rbp+foo] | dispとbase | %rbp + foo |
8(%rbp,%rax) | [rbp+rax+8] | dispとbaseとindex | %rbp + %rax + 8 |
8(%rbp,%rax, 2) | [rbp+rax*2+8] | dispとbaseとindexとscale | %rbp + %rax*2 + 8 |
(%rip) | [rip] | base | %rip |
8(%rip) | [rip+8] | dispとbase | %rip + 8 |
foo(%rip) | [rip+foo] | dispとbase | %rip + foo |
%fs:-4 | fs:[-4] | segmentとdisp | %fsのベースレジスタ - 4 |
- メモリに読み書きするサイズの指定方法 (先頭アドレスだけだと,何バイト読み書きすればいいのか不明):
-
AT&T形式では命令サフィックス (
q,l,w,b)で指定する.例:movq $4, 8(%rbp) -
Intel形式では,メモリ参照の前に
QWORD PTR,DWORD PTR,WORD PTR,BYTE PTRを付加する (順番に,8バイト,4バイト,2バイト,1バイトを意味する). 例: `mov QWORD PTR [rbp+8], 4
-
「記法」「詳しい記法」欄で用いるオペランドの記法と注意
以下の機械語命令の説明で使う記法を説明します. この記法はその命令に許されるオペランドの形式を表します.
オペランド,即値(定数)
| 記法 | 例 | 説明 |
|---|---|---|
| op1 | 第1オペランド | |
| op2 | 第2オペランド | |
| imm | $100 | imm8, imm16, imm32のどれか |
$foo | ||
| imm8 | $100 | 8ビットの即値(定数) |
| imm16 | $100 | 16ビットの即値(定数) |
| imm32 | $100 | 32ビットの即値(定数) |
- 多くの場合,サイズを省略して単にimmと書きます. 特にサイズに注意が必要な時だけ,imm32などとサイズを明記します.
- 一部例外を除き, x86-64では64ビットの即値を書けません(32ビットまでです).
汎用レジスタ
| 記法 | 例 | 説明 |
|---|---|---|
| r | %rax | r8, r16, r32, r64のどれか |
| r8 | %al | 8ビットの汎用レジスタ |
| r16 | %ax | 16ビットの汎用レジスタ |
| r32 | %eax | 32ビットの汎用レジスタ |
| r64 | %rax | 64ビットの汎用レジスタ |
メモリ参照
| 記法 | 例 | 説明 |
|---|---|---|
| r/m | %rbp | r/m8, r/m16, r/m32, r/m32, r/m64のどれか |
100 | ||
-8(%rbp) | ||
foo(%rbp) | ||
| r/m8 | -8(%rbp) | r8 または 8ビットのメモリ参照 |
| r/m16 | -8(%rbp) | r16 また は16ビットのメモリ参照 |
| r/m32 | -8(%rbp) | r32 また は32ビットのメモリ参照 |
| r/m64 | -8(%rbp) | r64 また は64ビットのメモリ参照 |
| m | -8(%rbp) | メモリ参照 |
ジャンプ・コール用のオペランド
| 記法 | 例 | 説明 |
|---|---|---|
| rel | 0x100 | rel8, rel32のどちらか |
foo | ||
| rel8 | 0x100 | 8ビット相対アドレス(直接ジャンプ,定数だが$は不要) |
| rel32 | 0x1000 | 32ビット相対アドレス(直接ジャンプ,定数だが$は不要) |
| *r/m64 | *%rax | r64 または64ビットのメモリ参照による絶対アドレス (間接ジャンプ, *が前に必要) |
*(%rax) | ||
*-8(%rax) |
-
GNUアセンブラの記法のおかしな点
- 直接ジャンプ先の指定relは,定数なのに
$をつけてはいけない - 間接ジャンプ先の指定**r/m64は,
(他のr/m*オペランドでは不要だったのに)
*をつけなくてはいけない - 相対アドレスで
rel8かrel32をプログラマは選べない (jmp命令に命令サフィックスbやlをつけると怒られる.アセンブラにお任せするしか無い)
- 直接ジャンプ先の指定relは,定数なのに
-
*%raxと*(%rax)の違いに注意(以下の図を参照)
データ転送(コピー)系の命令
mov命令: データの転送(コピー)
| 記法 | 何の略か | 動作 |
|---|---|---|
mov␣ op1, op2 | move | op1の値をop2にデータ転送(コピー) |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
mov␣ r, r/m | movq %rax, %rbx | %rbx = %rax | movq-1.s movq-1.txt |
movq %rax, -8(%rsp) | *(%rsp - 8) = %rax | movq-2.s movq-2.txt | |
mov␣ r/m, r | movq -8(%rsp), %rax | %rax = *(%rsp - 8) | movq-3.s movq-3.txt |
mov␣ imm, r | movq $999, %rax | %rax = 999 | movq-4.s movq-4.txt |
mov␣ imm, r/m | movq $999, -8(%rsp) | *(%rsp - 8) = 999 | movq-5.s movq-5.txt |
␣は命令サフィックスmov命令(および他のほとんどのデータ転送命令)はステータスフラグの値を変更しないmov命令はメモリからメモリへの直接データ転送はできない
xchg命令: オペランドの値を交換
| 記法 | 何の略か | 動作 |
|---|---|---|
xchg op1, op2 | exchange | op1 と op2 の値を交換する |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
xchg r, r/m | xchg %rax, (%rsp) | %raxと(%rsp)の値を交換する | xchg.s xchg.txt |
xchg r/m, r | xchg (%rsp), %rax | (%rsp)と%raxの値を交換する | xchg.s xchg.txt |
xchg命令はアトミックに2つのオペランドの値を交換します.(LOCKプリフィクスをつけなくてもアトミックになります)- このアトミックな動作はロックなどの同期機構を作るために使えます.
lea命令: 実効アドレスを計算
| 記法 | 何の略か | 動作 |
|---|---|---|
lea␣ op1, op2 | load effective address | op1 の実効アドレスを op2 に代入する |
pushとpop命令: スタックとデータ転送
| 記法 | 何の略か | 動作 |
|---|---|---|
push␣ op1 | push | op1 をスタックにプッシュ |
pop␣ op1 | pop | スタックから op1 にポップ |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
push␣ imm | pushq $999 | %rsp-=8; *(%rsp)=999 | push1.s push1.txt |
push␣ r/m16 | pushw %ax | %rsp-=2; *(%rsp)=%ax | push2.s push2.txt |
push␣ r/m64 | pushq %rax | %rsp-=8; *(%rsp)=%rax | push-pop.s push-pop.txt |
pop␣ r/m16 | popw %ax | *(%rsp)=%ax; %rsp += 2 | pop2.s pop2.txt |
pop␣ r/m64 | popq %rbx | %rbx=*(%rsp); %rsp += 8 | push-pop.s push-pop.txt |
四則演算・論理演算の命令
add, adc命令: 足し算
| 記法 | 何の略か | 動作 |
|---|---|---|
add␣ op1, op2 | add | op1 を op2 に加える |
adc␣ op1, op2 | add with carry | op1 と CF を op2 に加える |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
add␣ imm, r/m | addq $999, %rax | %rax += 999 | sub-1.s sub-1.txt |
add␣ r, r/m | addq %rax, (%rsp) | *(%rsp) += %rax | add-2.s add-2.txt |
add␣ r/m, r | addq (%rsp), %rax | %rax += *(%rsp) | add-2.s add-2.txt |
adc␣ imm, r/m | adcq $999, %rax | %rax += 999 + CF | adc-1.s adc-1.txt |
adc␣ r, r/m | adcq %rax, (%rsp) | *(%rsp) += %rax + CF | adc-2.s adc-2.txt |
adc␣ r/m, r | adcq (%rsp), %rax | %rax += *(%rsp) + CF | adc-3.s adc-3.txt |
addとadcはオペランドが符号あり整数か符号なし整数かを区別せず, 両方の結果を正しく計算する.
sub, sbb命令: 引き算
| 記法 | 何の略か | 動作 |
|---|---|---|
sub␣ op1, op2 | subtract | op1 を op2 から引く |
sbb␣ op1, op2 | subtract with borrow | op1 と CF を op2 から引く |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
sub␣ imm, r/m | subq $999, %rax | %rax -= 999 | sub-1.s sub-1.txt |
sub␣ r, r/m | subq %rax, (%rsp) | *(%rsp) -= %rax | sub-2.s sub-2.txt |
sub␣ r/m, r | subq (%rsp), %rax | %rax -= *(%rsp) | sub-2.s sub-2.txt |
sbb␣ imm, r/m | sbbq $999, %rax | %rax -= 999 + CF | sbb-1.s sbb-1.txt |
sbb␣ r, r/m | sbbq %rax, (%rsp) | *(%rsp) -= %rax + CF | sbb-2.s sbb-2.txt |
sbb␣ r/m, r | sbbq (%rsp), %rax | %rax -= *(%rsp) + CF | sbb-2.s sbb-2.txt |
addと同様に,subとsbbは オペランドが符号あり整数か符号なし整数かを区別せず, 両方の結果を正しく計算する.
mul, imul命令: かけ算
| 記法 | 何の略か | 動作 |
|---|---|---|
mul␣ op1 | unsigned multiply | 符号なし乗算.(%rdx:%rax) = %rax * op1 |
imul␣ op1 | signed multiply | 符号あり乗算.(%rdx:%rax) = %rax * op1 |
imul␣ op1, op2 | signed multiply | 符号あり乗算.op2 *= op1 |
imul␣ op1, op2, op3 | signed multiply | 符号あり乗算.op3 = op1 * op2 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
mul␣ r/m | mulq %rbx | (%rdx:%rax) = %rax * %rbx | mul-1.s mul-1.txt |
imul␣ r/m | imulq %rbx | (%rdx:%rax) = %rax * %rbx | imul-1.s imul-1.txt |
imul␣ imm, r | imulq $4, %rax | %rax *= 4 | imul-2.s imul-2.txt |
imul␣ r/m, r | imulq %rbx, %rax | %rax *= %rbx | imul-2.s imul-2.txt |
imul␣ imm, r/m, r | imulq $4, %rbx, %rax | %rax = %rbx * 4 | imul-2.s imul-2.txt |
- オペランドが1つの形式では,
%raxが隠しオペランドになる. このため,乗算の前に%raxに値をセットしておく必要がある. また,8バイト同士の乗算結果は最大で16バイトになるので, 乗算結果を%rdxと%raxに分割して格納する (16バイトの乗算結果の上位8バイトを%rdxに,下位8バイトを%raxに格納する). これをここでは(%rdx:%rax)という記法で表現している. imulだけ例外的に,オペランドが2つの形式と3つの形式がある. 2つか3つの形式では乗算結果が64ビットを超えた場合, 越えた分は破棄される(乗算結果は8バイトのみ).
div, idiv命令: 割り算,余り
| 記法 | 何の略か | 動作 |
|---|---|---|
div␣ op1 | unsigned divide | 符号なし除算と余り%rax = (%rdx:%rax) / op1 %rdx = (%rdx:%rax) % op1 |
idiv␣ op1 | signed divide | 符号あり除算と余り%rax = (%rdx:%rax) / op1 %rdx = (%rdx:%rax) % op1 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
div␣ r/m | divq %rbx | %rax = (%rdx:%rax) / %rbx %rdx = (%rdx:%rax) % %rbx | div-1.s div-1.txt |
idiv␣ r/m | idivq %rbx | %rax = (%rdx:%rax) / %rbx %rdx = (%rdx:%rax) % %rbx | idiv-1.s idiv-1.txt |
- 16バイトの値
%rdx:%raxを第1オペランドで割った商が%raxに入り, 余りが%rdxに入る. - 隠しオペランドとして
%rdxと%raxが使われるので,事前に値を設定しておく必要がある.idivを使う場合,もし%rdxを使わないのであれば,cqto命令で%raxを%rdx:%raxに符号拡張しておくと良い.
inc, dec命令: インクリメント,デクリメント
| 記法 | 何の略か | 動作 |
|---|---|---|
inc␣ op1 | increment | op1の値を1つ増加 |
dec␣ op1 | decrement | op1の値を1つ減少 |
incやdecはオーバーフローしてもCFが変化しないところがポイント.
neg命令: 符号反転
| 記法 | 何の略か | 動作 |
|---|---|---|
neg␣ op1 | negation | 2の補数によるop1の符号反転 |
not命令: ビット論理演算 (1)
| 記法 | 何の略か | 動作 |
|---|---|---|
not␣ op1 | bitwise not | op1の各ビットの反転 (NOT) |
and, or, xor命令: ビット論理演算 (2)
| 記法 | 何の略か | 動作 |
|---|---|---|
and␣ op1, op2 | bitwise and | op1とop2の各ビットごとの論理積(AND) |
or␣ op1, op2 | bitwise or | op1とop2の各ビットごとの論理和(OR) |
xor␣ op1, op2 | bitwise xor | op1とop2の各ビットごとの排他的論理和(XOR) |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
and␣ imm, r/m | andq $0x0FFF, %rax | %rax &= 0x0FFF | and-1.s and-1.txt |
and␣ r, r/m | andq %rax, (%rsp) | *(%rsp) &= %rax | and-1.s and-1.txt |
and␣ r/m, r | andq (%rsp), %rax | %rax &= *(%rsp) | and-1.s and-1.txt |
or␣ imm, r/m | orq $0x0FFF, %rax | %rax |= 0x0FFF | or-1.s or-1.txt |
or␣ r, r/m | orq %rax, (%rsp) | *(%rsp) |= %rax | or-1.s or-1.txt |
or␣ r/m, r | orq (%rsp), %rax | %rax |= *(%rsp) | or-1.s or-1.txt |
xor␣ imm, r/m | xorq $0x0FFF, %rax | %rax ^= 0x0FFF | xor-1.s xor-1.txt |
xor␣ r, r/m | xorq %rax, (%rsp) | *(%rsp) ^= %rax | xor-1.s xor-1.txt |
xor␣ r/m, r | xorq (%rsp), %rax | %rax ^= *(%rsp) | xor-1.s xor-1.txt |
&,|,^はC言語で,それぞれ,ビットごとの論理積,論理和,排他的論理積です (忘れた人はC言語を復習しましょう).
sal, sar, shl, shr命令: シフト
| 記法 | 何の略か | 動作 |
|---|---|---|
sal␣ op1[, op2] | shift arithmetic left | 算術左シフト |
shl␣ op1[, op2] | shift logical left | 論理左シフト |
sar␣ op1[, op2] | shift arithmetic right | 算術右シフト |
shr␣ op1[, op2] | shift logical right | 論理右シフト |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
sal␣ r/m | salq %rax | %raxを1ビット算術左シフト | sal-1.s sal-1.txt |
sal␣ imm8, r/m | salq $2, %rax | %raxを2ビット算術左シフト | sal-1.s sal-1.txt |
sal␣ %cl, r/m | salq %cl, %rax | %raxを%clビット算術左シフト | sal-1.s sal-1.txt |
shl␣ r/m | shlq %rax | %raxを1ビット論理左シフト | shl-1.s shl-1.txt |
shl␣ imm8, r/m | shlq $2, %rax | %raxを2ビット論理左シフト | shl-1.s shl-1.txt |
shl␣ %cl, r/m | shlq %cl, %rax | %raxを%clビット論理左シフト | shl-1.s shl-1.txt |
sar␣ r/m | sarq %rax | %raxを1ビット算術右シフト | sar-1.s sar-1.txt |
sar␣ imm8, r/m | sarq $2, %rax | %raxを2ビット算術右シフト | sar-1.s sar-1.txt |
sar␣ %cl, r/m | sarq %cl, %rax | %raxを%clビット算術右シフト | sar-1.s sar-1.txt |
shr␣ r/m | shrq %rax | %raxを1ビット論理右シフト | shr-1.s shr-1.txt |
shr␣ imm8, r/m | shrq $2, %rax | %raxを2ビット論理右シフト | shr-1.s shr-1.txt |
shr␣ %cl, r/m | shrq %cl, %rax | %raxを%clビット論理右シフト | shr-1.s shr-1.txt |
- op1[, op2] という記法は「op2は指定してもしなくても良い」という意味です.
- シフトとは(指定したビット数だけ)右か左にビット列をずらすことを意味します. op2がなければ「1ビットシフト」を意味します.
- 論理シフトとは「空いた場所に0を入れる」, 算術シフトとは「空いた場所に符号ビットを入れる」ことを意味します.
- 左シフトの場合は(符号ビットを入れても意味がないので),論理シフトでも算術シフトでも,0を入れます.その結果,算術左シフト
salと論理左シフトshlは全く同じ動作になります. - C言語の符号あり整数に対する右シフト(>>)は算術シフトか論理シフトかは 決まっていません(実装依存です). C言語で,ビット演算は符号なし整数に対してのみ行うようにしましょう.
rol, ror, rcl, rcr命令: ローテート
| 記法 | 何の略か | 動作 |
|---|---|---|
rol␣ op1[, op2] | rotate left | 左ローテート |
rcl␣ op1[, op2] | rotate left through carry | CFを含めて左ローテート |
ror␣ op1[, op2] | rotate right | 右ローテート |
rcr␣ op1[, op2] | rotate right through carry | CFを含めて右ローテート |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
rol␣ r/m | rolq %rax | %raxを1ビット左ローテート | rol-1.s rol-1.txt |
rol␣ imm8, r/m | rolq $2, %rax | %raxを2ビット左ローテート | rol-1.s rol-1.txt |
rol␣ %cl, r/m | rolq %cl, %rax | %raxを%clビット左ローテート | rol-1.s rol-1.txt |
rcl␣ r/m | rclq %rax | %raxを1ビットCFを含めて左ローテート | rcl-1.s rcl-1.txt |
rcl␣ imm8, r/m | rclq $2, %rax | %raxを2ビットCFを含めて左ローテート | rcl-1.s rcl-1.txt |
rcl␣ %cl, r/m | rclq %cl, %rax | %raxを%clビットCFを含めて左ローテート | rcl-1.s rcl-1.txt |
ror␣ r/m | rorq %rax | %raxを1ビット右ローテート | ror-1.s ror-1.txt |
ror␣ imm8, r/m | rorq $2, %rax | %raxを2ビット右ローテート | ror-1.s ror-1.txt |
ror␣ %cl, r/m | rorq %cl, %rax | %raxを%clビット右ローテート | ror-1.s ror-1.txt |
rcr␣ r/m | rcrq %rax | %raxを1ビットCFを含めて右ローテート | rcr-1.s rcr-1.txt |
rcr␣ imm8, r/m | rcrq $2, %rax | %raxを2ビットCFを含めて右ローテート | rcr-1.s rcr-1.txt |
rcr␣ %cl, r/m | rcrq %cl, %rax | %raxを%clビットCFを含めて右ローテート | rcr-1.s rcr-1.txt |
- op1[, op2] という記法は「op2は指定してもしなくても良い」という意味です.
- ローテートは,シフトではみ出したビットを空いた場所に入れます.
- ローテートする方向(右か左),CFを含めるか否かで,4パターンの命令が存在します.
cmp, test命令: 比較
cmp命令
| 記法 | 何の略か | 動作 |
|---|---|---|
cmp␣ op1[, op2] | compare | op1とop2の比較結果をフラグに格納(比較はsub命令を使用) |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
cmp␣ imm, r/m | cmpq $999, %rax | subq $999, %raxのフラグ変化のみ計算.オペランドは変更なし | cmp-1.s cmp-1.txt |
cmp␣ r, r/m | cmpq %rax, (%rsp) | subq %rax, (%rsp)のフラグ変化のみ計算.オペランドは変更なし | cmp-1.s cmp-1.txt |
cmp␣ r/m, r | cmpq (%rsp), %rax | subq (%rsp), %raxのフラグ変化のみ計算.オペランドは変更なし | cmp-1.s cmp-1.txt |
cmp命令はフラグ計算だけを行います. (レジスタやメモリは変化しません).cmp命令は条件付きジャンプ命令と一緒に使うことが多いです. 例えば以下の2命令で「%raxが(符号あり整数として)1より大きければジャンプする」という意味になります.
cmpq $1, %rax
jg L2
test命令
| 記法 | 何の略か | 動作 |
|---|---|---|
test␣ op1[, op2] | logical compare | op1とop2の比較結果をフラグに格納(比較はand命令を使用) |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
test␣ imm, r/m | testq $999, %rax | andq $999, %raxのフラグ変化のみ計算.オペランドは変更なし | test-1.s test-1.txt |
test␣ r, r/m | testq %rax, (%rsp) | andq %rax, (%rsp)のフラグ変化のみ計算.オペランドは変更なし | test-1.s test-1.txt |
test␣ r/m, r | testq (%rsp), %rax | andq (%rsp), %raxのフラグ変化のみ計算.オペランドは変更なし | test-1.s test-1.txt |
cmp命令と同様に,test命令はフラグ計算だけを行います. (レジスタやメモリは変化しません).cmp命令と同様に,test命令は条件付きジャンプ命令と一緒に使うことが多いです. 例えば以下の2命令で「%raxが0ならジャンプする」という意味になります.
testq %rax, %rax
jz L2
- 例えば
%raxが0かどうかを知りたい場合,cmpq $0, %raxとtestq %rax, %raxのどちらでも調べることができます. どちらの場合も,ZF==1なら,%raxが0と分かります (testq %rax, %raxはビットごとのANDのフラグ変化を計算するので,%raxがゼロの時だけ,ZF==1となります). コンパイラはtestq %rax, %raxを使うことが多いです.testq %rax, %raxの方が命令長が短くなるからです.
movs, movz, cbtw, cqto命令: 符号拡張とゼロ拡張
movs, movz命令
| 記法(AT&T形式) | 記法(Intel形式) | 何の略か | 動作 |
|---|---|---|---|
movs␣␣ op1, op2 | movsx op2, op1 movsxd op2, op1 | move with sign-extention | op1を符号拡張した値をop2に格納 |
movz␣␣ op1, op2 | movzx op2, op1 | move with zero-extention | op1をゼロ拡張した値をop2に格納 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
movs␣␣ r/m, r | movslq %eax, %rbx | %rbx = %eaxを8バイトに符号拡張した値 | movs-movz.s movs-movz.txt |
movz␣␣ r/m, r | movzwq %ax, %rbx | %rbx = %axを8バイトにゼロ拡張した値 | movs-movz.s movs-movz.txt |
␣␣に入るもの | 何の略か | 意味 |
|---|---|---|
bw | byte to word | 1バイト→2バイトの拡張 |
bl | byte to long | 1バイト→4バイトの拡張 |
bq | byte to quad | 1バイト→8バイトの拡張 |
wl | word to long | 2バイト→4バイトの拡張 |
wq | word to quad | 2バイト→8バイトの拡張 |
lq | long to quad | 4バイト→8バイトの拡張 |
movs,movz命令はAT&T形式とIntel形式でニモニックが異なるので注意です.- GNUアセンブラではAT&T形式でも実は
movsx,movzxのニモニックが使用できます. ただし逆アセンブルすると,movslq,movzwqなどのニモニックが表示されるので,movslq,movzwqなどを使う方が良いでしょう. movzlq(Intel形式ではmovzxd)はありません.例えば,%eaxに値を入れると,%raxの上位32ビットはクリアされるので,movzlqは不要だからです.- Intel形式では,4バイト→8バイトの拡張の時だけ,
(
movsxではなく)movsxdを使います.
cbtw, cqto命令
| 記法(AT&T形式) | 記法(Intel形式) | 何の略か | 動作 |
|---|---|---|---|
**c␣t␣ | c␣␣␣ | convert ␣ to ␣ | %rax (または%eax, %ax, %al)を符号拡張 |
| 詳しい記法 (AT&T形式) | 詳しい記法 (Intel形式) | 例 | 例の動作 | サンプルコード |
|---|---|---|---|---|
cbtw | cbw | cbtw | %al(byte)を%ax(word)に符号拡張 | cbtw.s cbtw.txt |
cwtl | cwde | cwtl | %ax(word)を%eax(long)に符号拡張 | cbtw.s cbtw.txt |
cwtd | cwd | cwtd | %ax(word)を%dx:%ax(double word)に符号拡張 | cbtw.s cbtw.txt |
cltd | cdq | cltd | %eax(long)を%edx:%eax(doube long, quad)に符号拡張 | cbtw.s cbtw.txt |
cltq | cdqe | cltd | %eax(long)を%rax(quad)に符号拡張 | cbtw.s cbtw.txt |
cqto | cqo | cqto | %rax(quad)を%rdx:%rax(octuple)に符号拡張 | cbtw.s cbtw.txt |
cqtoなどはidivで割り算する前に使うと便利(%rdx:%raxがidivの隠しオペランドなので).- GNUアセンブラはIntel形式のニモニックも受け付ける.
ジャンプ命令
jmp: 無条件ジャンプ
| 記法 | 何の略か | 動作 |
|---|---|---|
jmp op1 | jump | op1にジャンプ |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
jmp rel | jmp 0x1000 | 0x1000番地に相対・直接ジャンプ (%rip += 0x1000) | jmp.s jmp.txt |
jmp foo | foo番地に相対・直接ジャンプ (%rip += foo) | jmp.s jmp.txt | |
jmp r/m | jmp *%rax | *%rax番地に絶対・間接ジャンプ (%rip = *%rax)) | jmp.s jmp.txt |
jmp r/m | jmp *(%rax) | *(%rax)番地に絶対・間接ジャンプ (%rip = *(%rax)) | jmp.s jmp.txt |
- x86-64では,相対・直接と絶対・間接の組み合わせしかありません. (つまり,相対・間接ジャンプや絶対・直接ジャンプはありません. なお,ここで紹介していないfarジャンプでは絶対・直接もあります).
- 相対・直接ジャンプでは符号ありの8ビット(rel8)か 32ビット(rel32)の整数定数で相対アドレスを指定します. (64ビットの相対アドレスは指定できません.64ビットのジャンプをしたい時は 絶対・間接ジャンプ命令を使います).
- rel8かrel32かはアセンブラが勝手に選んでくれます.
逆に
jmpbやjmplなどとサフィックスをつけて指定することはできません. - なぜか,定数なのにrel8やrel32にはドルマーク
$をつけません. 逆にr/mの前にはアスタリスク*が必要です. GNUアセンブラのこの部分は一貫性がないので要注意です.
条件付きジャンプの概要
- 条件付きジャンプ命令
j␣は ステータスフラグ (CF, OF, PF, SF, ZF)をチェックして, 条件が成り立てばジャンプします. - 条件付きジャンプは比較命令と一緒に使うことが多いです.
例えば以下の2命令で「
%raxが(符号あり整数として)1より大きければジャンプする」という意味になります.
cmpq $1, %rax
jg L2
- 条件付きジャンプ命令のニモニックでは次の用語を使い分けます
- 符号あり整数の大小には less/greater を使う
- 符号なし整数の大小には above/below を使う
条件付きジャンプ: 符号あり整数用
| 記法 | 何の略か | 動作 | ジャンプ条件 |
|---|---|---|---|
jg reljnle rel | jump if greater jump if not less nor equal | op2>op1ならrelにジャンプ !(op2<=op1)ならrelにジャンプ | ZF==0&&SF==OF |
jge reljnl rel | jump if greater or equal jump if not less | op2>=op1ならrelにジャンプ !(op2<op1)ならrelにジャンプ | SF==OF |
jle reljng rel | jump if less or equal jump if not greater | op2<=op1ならrelにジャンプ !(op2>op1)ならrelにジャンプ | ZF==1||SF!=OF |
jl reljnge rel | jump if less jump if not greater nor equal | op2<op1ならrelにジャンプ !(op2>=op1)ならrelにジャンプ | SF!=OF |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
jg rel | cmpq $0, %raxjg foo | if (%rax>0) goto foo | jg.s jg.txt |
jnle rel | cmpq $0, %raxjnle foo | if (!(%rax<=0)) goto foo | jg.s jg.txt |
jge rel | cmpq $0, %raxjge foo | if (%rax>=0) goto foo | jge.s jge.txt |
jnl rel | cmpq $0, %raxjnl foo | if (!(%rax<0)) goto foo | jge.s jge.txt |
jle rel | cmpq $0, %raxjle foo | if (%rax<=0) goto foo | jle.s jle.txt |
jng rel | cmpq $0, %raxjng foo | if (!(%rax>0)) goto foo | jle.s jle.txt |
jl rel | cmpq $0, %raxjl foo | if (%rax<0) goto foo | jl.s jl.txt |
jnge rel | cmpq $0, %raxjnge foo | if (!(%rax>=0)) goto foo | jl.s jl.txt |
- op1 と op2 は条件付きジャンプ命令の直前で使用した
cmp命令のオペランドを表します. jgとjnleは異なるニモニックですが動作は同じです. その証拠にジャンプ条件はZF==0&&SF==OFと共通です. 他の3つのペア,jgeとjnl,jleとjng,jlとjngeも同様です.
条件付きジャンプ: 符号なし整数用
| 記法 | 何の略か | 動作 | ジャンプ条件 |
|---|---|---|---|
ja reljnbe rel | jump if above jump if not below nor equal | op2>op1ならrelにジャンプ !(op2<=op1)ならrelにジャンプ | CF==0&ZF==0 |
jae reljnb rel | jump if above or equal jump if not below | op2>=op1ならrelにジャンプ !(op2<op1)ならrelにジャンプ | CF==0 |
jbe reljna rel | jump if below or equal jump if not above | op2<=op1ならrelにジャンプ !(op2>op1)ならrelにジャンプ | CF==1&&ZF==1 |
jb reljnae rel | jump if below jump if not above nor equal | op2<op1ならrelにジャンプ !(op2>=op1)ならrelにジャンプ | CF==1 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
ja rel | cmpq $0, %raxja foo | if (%rax>0) goto foo | ja.s ja.txt |
jnbe rel | cmpq $0, %raxjnbe foo | if (!(%rax<=0)) goto foo | ja.s ja.txt |
jae rel | cmpq $0, %raxjae foo | if (%rax>=0) goto foo | jae.s jae.txt |
jnb rel | cmpq $0, %raxjnb foo | if (!(%rax<0)) goto foo | jae.s jae.txt |
jbe rel | cmpq $0, %raxjbe foo | if (%rax<=0) goto foo | jbe.s jbe.txt |
jna rel | cmpq $0, %raxjna foo | if (!(%rax>0)) goto foo | jbe.s jbe.txt |
jb rel | cmpq $0, %raxjb foo | if (%rax<0) goto foo | jb.s jb.txt |
jnae rel | cmpq $0, %raxjnae foo | if (!(%rax>=0)) goto foo | jb.s jb.txt |
- op1 と op2 は条件付きジャンプ命令の直前で使用した
cmp命令のオペランドを表します. jaとjnbeは異なるニモニックですが動作は同じです. その証拠にジャンプ条件はCF==0&&ZF==0と共通です. 他の3つのペア,jaeとjnb,jbeとjna,jbとjnaeも同様です.
条件付きジャンプ: フラグ用
| 記法 | 何の略か | 動作 | ジャンプ条件 |
|---|---|---|---|
jc rel | jump if carry | CF==1ならrelにジャンプ | CF==1 |
jnc rel | jump if not carry | CF==0ならrelにジャンプ | CF==0 |
jo rel | jump if overflow | OF==1ならrelにジャンプ | OF==1 |
jno rel | jump if not overflow | OF==0ならrelにジャンプ | OF==0 |
js rel | jump if sign | SF==1ならrelにジャンプ | SF==1 |
jns rel | jump if not sign | SF==0ならrelにジャンプ | SF==0 |
jz rel je rel | jump if zero jump if equal | ZF==1ならrelにジャンプ op2==op1ならrelにジャンプ | ZF==1 |
jnz rel jne rel | jump if not zero jump if not equal | ZF==0ならrelにジャンプ op2!=op1ならrelにジャンプ | ZF==0 |
jp rel jpe rel | jump if parity jump if parity even | PF==1ならrelにジャンプ | PF==1 |
jnp rel jpo rel | jump if not parity jump if parity odd | PF==0ならrelにジャンプ | PF==0 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
jc rel | jc foo | if (CF==1) goto foo | jc.s jc.txt |
jnc rel | jnc foo | if (CF==0) goto foo | jc.s jc.txt |
jo rel | jo foo | if (OF==1) goto foo | jo.s jo.txt |
jno rel | jno foo | if (OF==0) goto foo | jo.s jo.txt |
js rel | js foo | if (SF==1) goto foo | js.s js.txt |
jns rel | jns foo | if (SF==0) goto foo | js.s js.txt |
jz rel | jz foo | if (ZF==1) goto foo | jz.s jz.txt |
je rel | cmpq $0, %raxje foo | if (%rax==0) goto foo | jz.s jz.txt |
jnz rel | jnz foo | if (ZF==0) goto foo | jz.s jz.txt |
jne rel | cmpq $0, %raxjne foo | if (%rax!=0) goto foo | jz.s jz.txt |
jp rel | jp foo | if (PF==1) goto foo | jp.s jp.txt |
jpe rel | jpe foo | if (PF==1) goto foo | jp.s jp.txt |
jnp rel | jnp foo | if (PF==0) goto foo | jp.s jp.txt |
jpo rel | jpo foo | if (PF==0) goto foo | jp.s jp.txt |
- op1 と op2 は条件付きジャンプ命令の直前で使用した
cmp命令のオペランドを表します. jzとjeは異なるニモニックですが動作は同じです. その証拠にジャンプ条件はZF==1と共通です. 他の3つのペア,jnzとjne,jpとjpe,jnpとjpoも同様です.- AFフラグのための条件付きジャンプ命令は存在しません.
関数呼び出し(コール命令)
call, ret命令: 関数を呼び出す,リターンする
| 記法 | 何の略か | 動作 |
|---|---|---|
call op1 | call procedure | %ripをスタックにプッシュしてから op1にジャンプする( pushq %rip; %rip = op1) |
ret | return from procedure | スタックからポップしたアドレスにジャンプする ( popq %rip) |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
call rel | call foo | 相対・直接の関数コール | call.s call.txt |
call r/m | call *%rax | 絶対・間接の関数コール | call.s call.txt |
ret | ret | 関数からリターン | call.s call.txt |
enter, leave命令: スタックフレームを作成する,破棄する
| 記法 | 何の略か | 動作 |
|---|---|---|
enter op1, op2 | make stack frame | サイズop1のスタックフレームを作成する |
leave | discard stack frame | 今のスタックフレームを破棄する |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
enter imm16, imm8 | enter $0x20, $0 | pushq %rbpmovq %rsp, %rbpsubq $0x20, %rsp | enter.s enter.txt |
leave | leave | movq %rbp, %rsppopq %rbp | enter.s enter.txt |
enter命令のop2には関数のネストレベルを指定するのですが, C言語では入れ子の関数がない(つまりネストレベルは常にゼロ)なので 常にゼロを指定します.- ただし,
enterは遅いので通常は使いません. 代わりに同等の動作をするpushq %rbp; movq %rsp, %rbp; subq $n, %rspを使います.
その他
nop命令
| 記法 | 何の略か | 動作 |
|---|---|---|
nop | no operation | 何もしない(プログラムカウンタのみ増加) |
nop op1 | no operation | 何もしない(プログラムカウンタのみ増加) |
nopは何もしない命令です(ただしプログラムカウンタ%ripは増加します).- 機械語命令列の間を(何もせずに)埋めるために使います.
nopの機械語命令は1バイト長です.nopr/m という形式の命令は2〜9バイト長のnop命令になります. 1バイト長のnopを9個並べるより, 9バイト長のnopを1個並べた方が,実行が早くなります.- 「複数バイトの
nop命令がある」という知識は, 逆アセンブル時にnopl (%rax)などを見てビックリしないために必要です.
cmpxchg, cmpxchg8b, cmpxchg16b命令: CAS (compare-and-swap)命令
cmpxchg命令
| 記法 | 何の略か | 動作 |
|---|---|---|
cmpxchg op1, op2 | compare and exchange | %raxとop2を比較し,同じならop2=op1,異なれば %rax=op2 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
cmpxchg r, r/m | cmpxchg %rbx, (%rsp) | if (*(%rsp)==%rax) *(%rsp)=%rbx;else %rax=*(%rsp); | cmpxchg.s cmpxchg.txt |
cmpxchg命令などのCAS命令は,lock-free,つまりロックを使わず 同期機構を実現するために使われます. アトミックに実行する必要があるため,通常,LOCKプリフィックスをつけて使います.- 気持ち:
- あるメモリにあるop2を新しい値op1で書き換えたい.
- ただし,代入前のop2の値は
%raxと同じはずで, もし(割り込まれて)知らない間に別の値になっていたら,この代入は失敗させる. - 代入が失敗したことを知るために,
(他の誰かが更新した最新の)op2の値を
%raxに入れる.cmpxchg実行後に%raxの値を調べれば,無事にop1への代入ができたかどうかが分かる.
cmpxchg8b, cmpxchg16b命令
| 記法 | 何の略か | 動作 |
|---|---|---|
cmpxchg8b op1 | compare and exchange bytes | %edx:%eaxとop1を比較し,同じならop1=%ecx:%ebx,異なれば %edx:%eax=op1 |
cmpxchg16b op1 | compare and exchange bytes | %rdx:%raxとop1を比較し,同じならop1=%rcx:%rbx,異なれば %rdx:%rax=op1 |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
cmpxchg8b m64 | cmpxchg8b (%rsp) | if (*(%rsp)==%edx:%eax) *(%rsp)=%ecx:%ebx;else %edx:%eax=*(%rsp); | cmpxchg8b.s cmpxchg8.txt |
cmpxchg16b m128 | cmpxchg16b (%rsp) | if (*(%rsp)==%rdx:%rax) *(%rsp)=%rcx:%rbx;else %rdx:%rax=*(%rsp); | cmpxchg16b.s cmpxchg16.txt |
cmpxchg8b,cmpxchg16bもCAS命令の一種ですが,cmpxchgとステータスフラグの変化が異なるので,分けて書いています.cmpxchg16b命令が参照するメモリは16バイト境界のアラインメントが必要です. (つまりメモリアドレスが16の倍数である必要があります).
rdtsc, rdtscp命令: タイムスタンプを読む
| 記法 | 何の略か | 動作 |
|---|---|---|
rdtsc | read time-stamp counter | %edx:%eax = 64ビットタイムスタンプカウンタ |
rdtscp | read time-stamp counter and processor ID | %edx:%eax = 64ビットタイムスタンプカウンタ %ecx = 32ビットプロセッサID |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
rdtsc | rdtsc | %edx:%eax = 64ビットタイムスタンプカウンタ | rdtsc.s rdtsc.txt |
rdtscp | rdtscp | %edx:%eax = 64ビットタイムスタンプカウンタ %ecx = 32ビットプロセッサID | rdtscp.s rdtscp.txt |
- x86-64は64ビットのタイムスタンプカウンタ (TSC: time stamp counter)を備えており, リセット後のCPUのサイクル数を数えています. 原理的には「サイクル数の差分をCPUのクロック周波数で割れば実行時間が得られる」 はずですが,実際にはout-of-order実行や, 内部クロックの変化などを考慮する必要があります. 詳しくはHow to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures を参照して下さい.
rdtscp命令を使うと,プロセッサIDも取得できます.rdtscとrdtscpではシリアライズ処理が異なるため,得られるサイクル数も異なります. 詳しくは x86-64のマニュアルSDM を参照して下さい.
int3命令
| 記法 | 何の略か | 動作 |
|---|---|---|
int3 | call to interrupt procedure | ブレークポイントトラップを発生 |
int3命令はブレークポイントトラップ(ソフトウェア割り込みの一種)を発生させます. 通常実行ではint3を実行した時点でプロセスは強制終了となりますが, デバッガ上ではその時点でブレークします.continueコマンドでその後の実行も継続できます.ブレークしたい場所が分かっている場合は, Cコード中にasm ("int3");と書くことでデバッガ上でブレークさせることができます.
ud2命令
| 記法 | 何の略か | 動作 |
|---|---|---|
ud2 | undefined instruction | 無効オペコード例外を発生させる |
ud2命令は無効オペコード例外を発生させます. 通常実行ではud2を実行した時点でプロセスは, シグナルSIGILL(illegal instruction)を受け取り,強制終了となります デバッガ上でも,Program received signal SIGILL, Illegal instruction.というメッセージが出て,プロセスは終了になります. 本書では「実行が通るはずがない場所が本当かどうか」の確認のためud2を使います.(通るはずがない場所にud2を置いて,SIGILLが発生しなければOKです)
例外 (exception)とは
例外(exception)はCPUが発生させる割り込み(ソフトウェア割り込み)です. Intel用語で,例外はさらにフォールト(fault),トラップ(trap), アボート(abort)に分類されます. 例えばゼロ割はフォールト,ブレークポイントはトラップです. マイOS作りたい人は頑張って勉強して下さい.
endbr64命令
| 記法 | 何の略か | 動作 |
|---|---|---|
endbr64 | end branch 64 bit | 間接ジャンプ先として許す |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
endbr64 | endbr64 | 間接ジャンプ先として許す | endbr64.s endbr64.txt |
- Intel CET IBT技術に対応したCPUの場合,
間接ジャンプ後のジャンプ先が
endbr64以外だった場合, 例外が発生してプログラムは強制終了となります. - Intel CET IBT技術に未対応のCPUの場合は,
nop命令として動作します. - 逆アセンブルして
endbr64を見てもビックリしないためにこの説明を書いています. - 私のPCが古すぎて,Intel CET未対応だったため,2023/8/17現在,クラッシュが発生するサンプルコードを作れていません.
bndプリフィクス
Intel MPX (Memory Protection Extensions)の機能の一部で,
制御命令 (ジャンプ命令やリターン命令など)に指定できます.
BND0からBND3レジスタに指定した境界に対して境界チェックを行います.
この機能をサポートしてないCPUではnopとして動作します.
- 逆アセンブルして
bndを見てもビックリしないためにこの説明を書いています.
以下のようにPLTセクションを見ると❶bndが使われています.
$ objdump -d /bin/ls | less
(中略)
Disassembly of section .plt:
0000000000004030 <.plt>:
4030: ff 35 2a dc 01 00 push 0x1dc2a(%rip) # 21c60 <_ob
stack_memory_used@@Base+0x114b0>
4036: f2 ff 25 2b dc 01 00 ❶ bnd jmp *0x1dc2b(%rip) # 21c68 <_obstack_memory_used@@Base+0x114b8>
403d: 0f 1f 00 nopl (%rax)
set␣命令: ステータスフラグの値を取得
| 記法 | 何の略か | 動作 |
|---|---|---|
set␣ op1 | set byte on condition | if (条件␣が成立) op1=1; else op1=0; |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
set␣ r/m8 | setz %al | %al = ZF | setz.s setz.txt |
setg %al | より大きい(greater)条件が成立なら%al =1,違えば%al =0 | setg.s setg.txt |
set␣命令はステータスフラグの値を取得します.␣には条件付きジャンプ命令j␣の␣と同じものをすべて入れられます.
ストリング命令
movsなどのストリング命令はREPプリフィクスと組み合わせて使います.
- REPプリフィクス
| 記法 | 何の略か | 動作 |
|---|---|---|
rep insn | repeat | %ecx==0になるまで命令insnと %ecx--を繰り返し実行 |
repe insn | repeat while equal | %ecx==0またはフラグZF==0になるまで命令insnと %ecx--を繰り返し実行 |
repz insn | repeat while zero | |
repne insn | repeat while not equal | %ecx==0またはフラグZF==1になるまで命令insnと %ecx--を繰り返し実行 |
repnz insn | repeat while not zero |
| 詳しい記法 | 例 | 例の動作 | サンプルコード |
|---|---|---|---|
rep insn | rep movsb | while (%ecx-- > 0) (*%rdi++) = (*%rsi++); # 1バイトずつコピー | rep.s rep.txt |
repe insn repz insn | repe cmpsb repz cmpsb | while (%ecx-- > 0 && (*%rdi++ == *%rsi++)); # 1バイトずつ比較 | repe.s repe.txt repz.s repz.txt |
repne insn repnz insn | repne cmpsb repnz cmpsb | while (%ecx-- > 0 && (*%rdi++ != *%rsi++)); # 1バイトずつ比較 | repne.s repne.txt repnz.s repnz.txt |
注意: DFフラグ(direction flag)が0の場合,
%rsiと%rdiを増やす.DFが1の場合は減らす.上記の説明はDF==0を仮定.
注意: ストリング命令はセグメントレジスタ
%dsと%esを使って,%ds:(%rsi)と%es:(%rdi)にアクセスします.が,x86-64では%dsも%esもベースレジスタをゼロと扱うので,%dsと%esは無視して構いません.
-
ストリング命令
記法 何の略か 動作 movs␣move string (%rsi)を(%rdi)にnバイト転送;%rsi+= n;%rdi+= n;lods␣load string (%rsi)を%raxにnバイト転送;%rsi+= n;stos␣store string %raxを(%rdi)にnバイト転送;%rdi+= n;ins␣input string I/Oポート %dxから(%rdi)にnバイト転送;%rdi+= n;outs␣output string (%rsi)からI/Oポート%dxにnバイト転送;%rsi+= n;cmps␣compare string (%rsi)と(%rdi)をnバイト比較;%rsi+= n;%rdi+= n;scas␣scan string %raxと(%rdi)をnバイト比較;%rdi+= n;詳しい記法 例 例の動作 サンプルコード rep movsbrep movsbwhile (%ecx-- > 0) (*%rdi++) = (*%rsi++);
# 1バイトずつコピーrep.s rep.txt rep lodsbrep lodsbwhile (%ecx-- > 0) %al = (*%rsi++);
# 1バイトずつコピーlods.s lods.txt rep stosqrep stosqwhile (%ecx-- > 0) {(*%rdi) = %rax; %rdi+=8; }
# 8バイトずつコピーstos.s stos.txt repe cmpsbrepe cmpsbwhile (%ecx-- > 0 && (*%rdi++) == (*%rsi++);
# 1バイトずつ比較repe.s repe.txt repne scasbrepne scasbwhile (%ecx-- > 0 && (*%rdi++) != %rax);
# 1バイトずつ比較scas.s scas.txt ␣にはb,w,l,qが入り,それぞれ, メモリ参照のサイズ(上ではnと表記)が1バイト,2バイト,4バイト,8バイトになる. (ただし,insとoutsはb,w,lのみ指定可能).%raxはオペランドサイズにより,%rax,%eax,%ax,%alのいずれかになる.ins␣とout␣の実例はここでは無し.
-
DFフラグ(方向フラグ)と
cld命令・std命令記法 何の略か 動作 cldclear direction flag DF=0 stdset direction flag DF=1 - DFフラグはストリング命令で,
%rsiと%rdiを増減する方向を決めます.- DF=0 の時は
%rsiと%rdiを増やします - DF=1 の時は
%rsiと%rdiを減らします
- DF=0 の時は
- DFフラグの変更は
cldやstdで行います (一般的にフラグレジスタの値を変更する場合,pushfでフラグレジスタの値を保存し,popfで元に戻すのが安全です). - Linux AMD64のABIにより,
関数の出入り口ではDF=0に戻す必要があります.このお約束のため,
自分で
stdしていなければ,必ずDF==0となります(わざわざcldする必要はありません).
- DFフラグはストリング命令で,