x86-64機械語命令
x86-64機械語命令の実行方法
概要:デバッガ上で実行します
機械語命令を実行しても,単にa.out
を実行するだけでは
意図通りに実行できたかの確認が難しいです.
(アセンブリコード内からprintf
を呼び出せばいいのですが,
そのコードもうざいので).
そこで本書ではデバッガを使って機械語命令の実行と確認を行います.
以下では例としてmovq $999, %rax
という機械語命令を実行してみます.
(これらのファイルはサンプルコードから入手できます).
movq $999, %rax
は「定数999
を%rax
レジスタに格納する」という命令ですので,
実行後,%rax
レジスタに999
という値が入っていれば,
うまく実行できたことを確認できます.
# asm/movq-4.s
.text
.globl main
.type main, @function
main:
movq $999, %rax
ret
.size main, .-main
実行確認のためのgdb
のコマンド列を書いたファイルも用意しています.
# asm/movq-4.txt
b 7
r
list 6,6
p $rax
echo # %raxの値が999なら成功\n
quit
デバッガ上で実行:gdb
コマンドを手入力
asm/movq-4.txt
中のgdb
コマンドを
以下のように1行ずつ入力してみて下さい.
(この章でもgdb
の使い方を説明していきますが,
gdb
の使い方の詳細はデバッガgdbの使い方にもまとめてあります).
$ gcc -g movq-4.s
$ gdb ./a.out ❶
(gdb) b 7 ❷
Breakpoint 1 at 0x1130: file movq-4.s, line 6.
(gdb) r ❸
Breakpoint 1, main () at movq-4.s:6
6 ret
(gdb) list 6,6 ❹
5 movq $999, %rax
(gdb) p $rax ❺
$1 = 999
(gdb) quit
- ❶ コンパイルした
a.out
をgdb
上で実行 - ❷ ブレークポイントを7行目(
movq $999, %rax
の次の行)に設定 (b
はbreakの略) - ❸ 実行開始 (
r
は run の略) - ❹ ソースコードの6行目だけを表示
- ❺ レジスタ
%rax
の値を(10進表記で)表示 (p
はprintの略) movq-4.txt
の最後の行echo # %raxの値が999なら成功\n
は, 「どうなると正しく実行できたか」を確認するメッセージを出力するコマンドですので, ここでは入力不要です. ❺の結果と一致したので「正しい実行」と確認できました.
デバッガ上で実行:gdb
コマンドを自動入力 (-x
オプションを使う)
gdb
コマンドを手入力して,よく使うgdb
コマンドを覚えることは良いことです.
とはいえ,手入力は面倒なので,自動入力も使いましょう.
$ gcc -g movq-4.s
$ gdb ./a.out ❶ -x movq-4.txt
Breakpoint 1, main () at movq-4.s:7
7 ret
6 ❷ movq $999, %rax
❸ $1 = 999
❹ # %raxの値が999なら成功
(gdb)
- ❶
-x movq-4.txt
というオプションをつけると,指定したファイル(ここではmovq-4.txt
)の中に書かれているgdb
コマンドを1行ずつ順番に実行してくれます list 6,6
を実行した結果,❷6行目のmovq $999, %rax
が表示されていますp $rax
を実行した結果,%rax
レジスタの値が❸999であると表示されています. ($1
はgdb
が扱う変数です.ここでは無視して下さい)echo # %raxの値が999なら成功\n
をgdb
が実行した結果, ❹# %raxの値が999なら成功
というメッセージが表示されています. このメッセージと❸の実行結果を見比べれば「実行結果が正しい」ことを確認できます.
デバッガ上で実行:gdb
コマンドを自動入力 (source
コマンドを使う)
$ gcc -g movq-4.s
$ gdb ./a.out
(gdb) ❶ source movq-4.txt
Breakpoint 1, main () at movq-4.s:7
7 ret
6 movq $999, %rax
$1 = 999
# %raxの値が999なら成功
gdb
は通常通りに実行開始して,
❶ source
コマンドを使えば,movq-4.txt
中のgdb
コマンドを実行できます.
-x
オプションとsource
コマンドは好きな方を使って下さい.
アドレッシングモード (オペランドの表記方法)
アドレッシングモードの概要
機械語命令は命令(オペコード(opcode))と
その引数のオペランド(operand)から構成されています.
例えば,movq $999, %rax
という命令では,
movq
がオペコードで,$999
と%rax
がオペランドです.
アドレッシングモードとはオペランドの書き方のことです. (元々は「メモリのアドレスを指定する記法」という意味で「アドレッシングモード」という用語が使われています). x86-64では大きく,以下の4種類の書き方ができます.
アドレッシング モードの種類 | オペランドの値 | 例 |
---|---|---|
定数の値 | movq $0x100, %rax | |
movq $foo, %rax | ||
レジスタの値 | movq %rbx, %rax | |
定数で指定した アドレスのメモリ値 | movq 0x100, %rax | |
movq foo, %rax | ||
レジスタ等で計算した アドレスのメモリ値 | movq (%rsp), %rax | |
movq 8(%rsp), %rax | ||
movq foo(%rip), %rax |
foo
はラベル(その値はアドレス)であり,定数と同じ扱い.(定数を書ける場所にはラベルも書ける).- メモリ参照では例えば
-8(%rbp, %rax, 8)
など複雑なオペランドも指定可能. 参照するメモリのアドレスは-8+%rbp+%rax*8
になる. (以下を参照).
アドレッシングモード:即値(定数)
定数 $999
即値(immediate value,定数)には$
をつけます.
例えば$999
は定数999
を意味します.
# asm/movq-4.s
.text
.globl main
.type main, @function
main:
movq $999, %rax
ret
.size main, .-main
movq-4.s
の6行目の
movq $999, %rax
は「定数999
をレジスタ%rax
に格納する」という意味です.
デバッガで動作を確認します
(デバッガの操作手順はmovq-4.txt
にもあります).
$ gcc -g movq-4.s
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1129: file movq-4.s, line 6.
(gdb) r
Breakpoint 1, main () at movq-4.s:6
6 movq $999, %rax
(gdb) si
main () at movq-4.s:7
7 ret
(gdb) p $rax
$1 = 999
確かに%rax
レジスタ中に999
が格納されていました.
なお,多くの場合,即値は32ビットまでで,オペランドのサイズが64ビットの場合,
32ビットの即値は,64ビットの演算前に
64ビットに符号拡張 されます
(ゼロ拡張だと
負の値が大きな正の値になって困るからです).
64ビットに符号拡張される例はこちら
を見て下さい.
例外はmovq
命令で,64ビットの即値を扱えます.
実行例はこちらを見て下さい.
ラベル $main
定数が書ける場所にはラベル(その値はアドレス)も書けます.
ラベルは関数名やグローバル変数の実体があるメモリの先頭番地を
示すために使われます(それ以外にはジャンプのジャンプ先としても使われます).
ですので,main
関数の先頭番地を示すmain
というラベルが
main
関数をコンパイルしたアセンブリコード中に存在します.
# asm/movq-6.s
.text
.globl main
.type main, @function
main:
movq $main, %rax
ret
.size main, .-main
movq-6.sの6行目のmovq $main, %rax
は
「ラベルmain
が表すアドレスを%rax
レジスタに格納する」という意味です.
gdb
で確かめます.
$ gcc ❶ -no-pie -g movq-6.s
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at ❷ 0x40110a: file movq-6.s, line 7.
(gdb) r
Breakpoint 1, main () at movq-6.s:7
7 ❸ movq $main, %rax
(gdb) ❹ si
main () at movq-6.s:8
8 ret
(gdb) p/x $rax
$1 = ❺ 0x40110a
- まず❶
-no-pie
オプションをつけてコンパイルして下さい. (-static
オプションを使ってもうまくいくと思います)
なぜ -no-pieオプション
-no-pie
オプションをつけないと以下のエラーが出てしまうからです.
$ gcc -g movq-6.s
/usr/bin/ld: /tmp/ccqHsPbg.o: relocation R_X86_64_32S against symbol `main' can not be used when making a PIE object; recompile with -fPIE
/usr/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status
-no-pie
は「位置独立実行可能ファイル
(PIEの説明1,PIEの説明2)を生成しない」
というオプションです.
最近のLinuxのgcc
では,PIEがデフォルトで有効になっている事が多いです.
PIC(位置独立コード)やPIEは「再配置(アドレス調整)無しに
どのメモリ番地に配置しても,そのまま実行可能」という機械語命令列です.
そのため,PIEやPICのメモリ参照では絶対アドレス(absolute address)が使えません.
-no-pie
オプションが無いと,
アセンブラはmovq $main, %rax
という命令中のmain
というラベルを
「絶対アドレスだ」と解釈してエラーにするようです.
絶対アドレス,相対アドレスとは
絶対アドレスとは「メモリの先頭0番地から何バイト目か」で示すアドレスです.
上図で青色のメモリ位置の絶対アドレスは0x1000
番地となります.
一方,相対アドレス(relative address)は(0番地ではなく)別の何かを起点とした差分のアドレスです.
x86-64では%rip
レジスタ(プログラムカウンタ)を起点とすることが多いです.
上図では青色のメモリ位置の相対アドレスは
%rip
を起点とすると,-0x500
番地となります(0x1000 - 0x1500 = -0x500
).
また,相対アドレスに起点のアドレスを足すと絶対アドレスになります
(-0x500 + 0x1500 = 0x1000
).
なぜ PICやPIEで絶対アドレスが使えないかと言うと, 機械語命令列を何番地に置くかで,絶対アドレスが変化してしまうからです.
もうちょっと具体的に
例えば,movq $main, %rax
という命令は
main
関数のアドレスを%rax
レジスタに格納するわけですが,
このアドレスが絶対アドレスの場合,出力される機械語命令に
絶対アドレスが埋め込まれてしまいます.
$ gcc -no-pie -g movq-6.s
$ objdump -d ./a.out
(一部略)
000000000040110a <main>:
40110a: 48 c7 c0 ❷ 0a 11 40 00 mov ❶$0x40110a,%rax
401111: c3 ret
上の逆アセンブル結果を見ると,確かにmain
関数のアドレス❶ 0x40110a
が
機械語命令列に❷埋め込まれています.
(x86-64はリトルエンディアンなので,バイトの並びが逆順に見えることに注意).
相対アドレスだと大丈夫なことも見てみます.
leaq-1.s
中の
leaq main(%rip), %rax
は,
「%rip
を起点としたmain
の相対アドレスと,
%rip
の値との和を%rax
レジスタに格納する」という命令です.
(lea
は load effective address の略です.effective addressは日本語では実効アドレスです).
# asm/leaq-1.s
.text
.globl main
.type main, @function
main:
leaq main(%rip), %rax
ret
.size main, .-main
$ gcc -g leaq-1.s
$ objdump -d ./a.out
(一部略)
0000000000001129 <main>:
❶ 1129: 48 8d 05 ❸ f9 ff ff ff lea ❷ -0x7(%rip),%rax # 1129 <main>
❹ 1130: c3 ret
上のように逆アセンブルすると以下が分かります.
main
関数の(ファイルa.out
中での)アドレスは❶0x1129
番地leaq main(%rip), %rax
の%rip
の値は❸0x1130
番地 (プログラムカウンタ%rip
は「次に実行する機械語命令のアドレス」を保持しています).- 機械語命令に埋め込まれているアドレスは相対アドレスで,
❶
0x1129
- ❸0x1130
= ❷-0x7
= ❸0xFFFFFFF9
です.
❶ 0x1129
や ❹ 0x1130
のアドレスは,
main
関数がどのアドレスに配置されるかで変化します.
しかし,この相対アドレス❷ -0x7
は
main
関数がどのアドレスに配置されても変化しないので,
この機械語命令はPICやPIEとして使えるわけです.
❷ -0x7
が ❸ 0xFFFFFFF9
として埋め込まれているのは,
2の補数表現だからですね
なお,相対アドレスが固定にならない場合(例えば,printf
関数のアドレス)もあります.
その場合はGOTやPLTを使います.
printf
関数のアドレスを機械語命令列(.text
セクション)に埋め込むのではなく,
別の書込み可能なセクション(例:got
セクション)に格納し,
そのアドレスを使って間接コール(indirect call)するのです.
-staticオプションとは
-static
オプションは(動的リンクではなく)
静的リンク
せよという,gcc
への指示になります.
-
main
関数の先頭にブレークポイントを設定します.main
関数の先頭アドレスが❷0x40110a
と分かります. -
❸
movq $main, %rax
の実行直前で止まっているので, ❹si
で1命令実行を進めます. -
❺
%rax
レジスタ中にmain
関数のアドレス❷0x40110a
が入っていました.
アドレッシングモード:レジスタ参照
# asm/movq-1.s
.text
.globl main
.type main, @function
main:
movq $999, %rax
movq %rax, %rbx
ret
.size main, .-main
movq-1.s
中のmovq %rax, %rbx
は
「%rax
レジスタ中の値を%rbx
に格納する」という意味です.
$ gcc -g movq-1.s
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1129: file movq-1.s, line 6.
(gdb) r
Breakpoint 1, main () at movq-1.s:6
6 ❶ movq $999, %rax
(gdb) si
7 ❷ movq %rax, %rbx
(gdb) si
main () at movq-1.s:8
8 ret
(gdb) p $rax
$1 = ❸ 999
(gdb) p $rbx
$2 = ❹ 999
gdb
上での実行で,❶ 定数999
が%rax
に格納され,
❷ %rax
中の999
がさらに%rbx
に格納されたことを
❸❹確認できました.
アドレッシングモード:直接メモリ参照
直接メモリ参照はアクセスするメモリ番地が定数となるメモリ参照です.
以下の例ではラベルx
を使ってメモリ参照していますが,
これは直接メモリ参照になります.
アセンブル時に(つまり実行する前に)アドレスが具体的に(以下では0x404028
番地)と決まるからです.
# asm/movq-7.s
.data
x:
.quad 999
.text
.globl main
.type main, @function
main:
movq x, %rax
ret
.size main, .-main
$ gcc -g -no-pie movq-7.s
$ gdb ./a.out -x movq-7.txt
Breakpoint 1, main () at movq-7.s:10
10 ret
9 movq x, %rax
$1 = ❶ 999
# %raxの値が999なら成功
以下の図で0x401106<main>
は「ラベルmain
が示すアドレスは0x401106
番地」
「ラベルx
が示すアドレスは0x404028
番地」であることを示してます.
そしてmovq-7.s
中の以下の3行で,以下は
.data
x:
.quad 999
「.data
セクションにサイズが8バイトのデータとして値999
を配置せよ」
「そのデータの先頭アドレスをラベルx
として定義せよ」を意味しています
(quad
が8バイトを意味しています).
ですので,実行時には上図のように
「.data
セクションのある場所(上図では0x404028
番地)に値999
が入っていて,
ラベルx
の値は0x404028
」となっています.
ですので,movq-7.s
中のmovq x, %rax
は
「ラベルx
が表すアドレス(上図では0x404028
番地)のメモリの中身(上図では999
)
を%rax
レジスタにコピーせよ」を意味します.
実行するとmovq x, %rax
の実行で,x
中の999
が%rax
レジスタに
コピーされたことを確認できました❶.
ここで$マークの有無,つまりx
と$x
の違いに注意しましょう
(上図も参照).
movq x, %rax # x はメモリの中身を表す
movq $x, %rax # $x はアドレスを表す
以下のようにmovq $x, %rax
を実行すると,
%rax
レジスタにはアドレス(ここでは0x404028
番地)が
入っていることを確認できました❷.
-8(%rbp)の-8には(定数なのに)$マークが付かない
以下でも説明しますが,
例えば-8(%rbp)
とオペランドに書いた時,-8
は($マークが無いのに)
定数として扱われます.
そして,-8(%rbp)
は,%rbp - 8
の計算結果をアドレスとするメモリの中身を意味します.
ちなみにこの-8
のことは
Intelのマニュアルでは変位 (displacement)と呼ばれています.
つまり「変位は定数だけど$マークはつきません」.
# asm/movq-8.s
.data
x:
.quad 999
.text
.globl main
.type main, @function
main:
movq $x, %rax
ret
.size main, .-main
$ gcc -g -no-pie movq-8.s
$ gdb ./a.out -x movq-8.txt
Breakpoint 1, main () at movq-8.s:10
10 ret
9 movq $x, %rax
$1 = 0x404028 ❷
nm ./a.out | egrep 'd x'
0000000000404028 d x
# %raxの値と nmコマンドによるxのアドレスが一致すれば成功
ちなみに,x
のアドレスが0x404028
になると分かっていれば,
movq x, %rax # これと
movq 0x404028, %rax # これは同じ意味
上の2行は全く同じ意味(0x404028
番地のメモリの中身)になります.
しかし,何番地になるか事前に分からないのが普通なので,
通常はラベル(ここではx
)を使います.
アドレッシングモード:間接メモリ参照
間接メモリ参照はアクセスするメモリ番地が変数となるメモリ参照です. アセンブリ言語では変数という概念は無いので, 正確には「実行時に決まるレジスタの値を使って, 参照先のメモリアドレスを計算して決める」という参照方式です. 以下では3つの例が出てきます(以下でより複雑な間接メモリ参照を説明します).
間接メモリ参照 | 計算するアドレス |
---|---|
(%rsp) | %rsp |
8(%rsp) | %rsp + 8 |
foo(%rip) | %rip + foo |
以下のmovq-9.sをpushq $777
まで実行すると,
メモリの状態は上図のようになっています.
(%rsp
が指す777
のひとつ下のアドレスが%rsp+8
なのは,
pushq $777
命令が「サイズが8バイトの値777
をスタックにプッシュしたから」です).
# asm/movq-9.s
.data
foo:
.quad 999
.text
.globl main
.type main, @function
main:
pushq $888
pushq $777
movq (%rsp), %rax
movq 8(%rsp), %rbx
movq foo(%rip), %rcx
ret
.size main, .-main
(%rsp)
は「アドレスが%rsp
の値のメモリ」なので値777
が入っている部分を参照します8(%rsp)
は「アドレスが%rsp + 8
の値のメモリ」なので値888
が入っている部分を参照しますfoo(%rip)
はちょっと特殊です.この形式は%rip
相対アドレッシング といいます. この形式の時,ラベルfoo
の値はプログラムカウンタ%rip
中のアドレスを起点とした 相対アドレス になります.ですので,%rip + foo
はfoo
の 絶対アドレス になるので,foo(%rip)
はラベルfoo
のメモリ部分,つまり999
が入っている部分になります.
gdbでの実行結果
$ gcc -g movq-9.s
$ gdb ./a.out -x movq-9.txt
Breakpoint 1, main () at movq-9.s:14
14 ret
11 movq (%rsp), %rax
12 movq 8(%rsp), %rbx
13 movq foo(%rip), %rcx
$1 = 777
$2 = 888
$3 = 999
# 777, 888, 999なら成功
メモリ参照の一般形
前節では,
(%rsp)
,8(%rsp)
,foo(%rip)
という間接メモリ参照の例を説明しました.
ここではメモリ参照の一般形を説明します.
以下がx86-64のメモリ参照の形式です.
AT&T形式 | Intel形式 | 計算されるアドレス | |
---|---|---|---|
通常のメモリ参照 | disp (base, index, scale) | [base + index * scale + disp] | base + index * scale + disp |
%rip 相対参照 | disp (%rip ) | [rip + disp] | %rip + disp |
「segment: メモリ参照」という形式
実は「segment: メモリ参照」という形式もあるのですが, あまり使わないので,ここでは省いて説明します. 興味のある人はこちらを参照下さい.
disp (base, index, scale)
でアクセスするメモリのアドレスは
base + index * scale + disp で計算します.
disp(%rip
)でアクセスするメモリのアドレスは
disp + %rip
で計算します.
disp,base,index,scaleとして指定可能なものは次の節で説明します.
メモリ参照で可能な組み合わせ(64ビットモードの場合)
通常のメモリ参照
通常のメモリ参照では,disp,base,index,scaleに以下を指定できます.
- disp には符号あり定数を指定する.ただし「64ビット定数」は無いことに注意. アドレス計算時に64ビット長に符号拡張される. dispは変位(displacement)を意味する.
- base には上記のいずれかのレジスタを指定可能.省略も可.
- index には上記のいずれかのレジスタを指定可能.省略も可.
%rsp
を指定できないことに注意. - scale を省略すると
1
と同じ
注: dispの例外.
mov␣
命令のみ,64ビットのdispを指定可能. この場合,movabs␣
というニモニックを使用可能. (abs
はおそらく絶対アドレス absolute address から). メモリ参照は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 |
なんでこんな複雑なアドレッシングモード?
x86-64はRISCではなくCISCなので「よく使う1つの命令で複雑な処理が
できれば,それは善」という思想だからです(知らんけど).
例えば,以下のCコードの配列array[i]
へのアクセスはアセンブリコードで
movl (%rdi,%rsi,4), %eax
の1命令で済みます.
(ここではsizeof(int)
が4
なので,scaleが4
になっています.
配列の先頭アドレスがarray
の,i
番目の要素のアドレスは,
array + i * sizeof(int)
で計算できることを思い出しましょう.
なお,array.s
の出力を得るには,gcc -S -O2 array.c
として下さい.
私の環境では-O2
が無いとgcc
は冗長なコードを吐きましたので).
// array.c
int foo (int array [], int i)
{
return array [i];
}
.text
.p2align 4
.globl foo
.type foo, @function
foo:
endbr64
movslq %esi, %rsi
movl (%rdi,%rsi,4), %eax
ret
.size foo, .-foo
オペランドの表記方法
以下の機械語命令の説明で使う記法を説明します. この記法はその命令に許されるオペランドの形式を表します.
オペランド,即値(定数)
記法 | 例 | 説明 |
---|---|---|
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) | メモリ参照 |
x86-64機械語命令:転送など
nop
命令: 何もしない
nop
は転送命令ではありませんが,最も簡単な命令ですので最初に説明します.
記法 | 何の略か | 動作 |
---|---|---|
nop | no operation | 何もしない(プログラムカウンタのみ増加) |
nop op1 | no operation | 何もしない(プログラムカウンタのみ増加) |
nop
は何もしない命令です(ただしプログラムカウンタ%rip
は増加します). フラグも変化しません.- 機械語命令列の間を(何もせずに)埋めるために使います.
nop
の機械語命令は1バイト長です. (なのでどんな長さの隙間にも埋められます).nop
r/m という形式の命令は2〜9バイト長のnop
命令になります. 1バイト長のnop
を9個並べるより, 9バイト長のnop
を1個並べた方が,実行が早くなります.- 「複数バイトの
nop
命令がある」という知識は, 逆アセンブル時にnopl (%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
命令は第1オペランドの値を第2オペランドに転送(コピー)します. 例えば,movq %rax, %rbx
は「%rax
の値を%rbx
にコピー」することを意味します.
movq-1.sの実行例
$ gcc -g movq-1.s
$ gdb ./a.out -x movq-1.txt
Breakpoint 1, main () at movq-1.s:8
8 ret
7 movq %rax, %rbx
# p $rbx
$1 = 999
# %rbxの値が999なら成功
movq-2.sの実行例
$ gcc -g movq-2.s
$ gdb ./a.out -x movq-2.txt
Breakpoint 1, main () at movq-2.s:8
8 ret
7 movq %rax, -8(%rsp)
# x/1gd $rsp-8
0x7fffffffde90: 999
# -8(%rsp)の値が999なら成功
-
オペランドには,即値,レジスタ,メモリ参照を組み合わせて指定できますが, メモリからメモリへの直接データ転送はできません.
-
␣
には命令サフィックス (q
,l
,w
,b
)を指定します. 命令サフィックスは転送するデータのサイズを明示します (順番に,8バイト,4バイト,2バイト,1バイトを示します).movq $0x11, (%rsp)
は値0x11
を8バイトのデータとして(%rsp)
に書き込むmovl $0x11, (%rsp)
は値0x11
を4バイトのデータとして(%rsp)
に書き込むmovw $0x11, (%rsp)
は値0x11
を2バイトのデータとして(%rsp)
に書き込むmovb $0x11, (%rsp)
は値0x11
を1バイトのデータとして(%rsp)
に書き込む
機械語命令のバイト列をアセンブリコードに直書きできる
movq %rax, %rbx
をコンパイルして逆アセンブルすると,
機械語命令のバイト列は48 89 C3
となります.
.byte
というアセンブラ命令を使うと,
アセンブラに指定したバイト列を出力できます.
例えば,次のように.byte 0x48, 0x89, 0xC3
と書くと,
.text
セクションに0x48, 0x89, 0xC3
というバイト列を出力できます.
# asm/byte.s
.text
.globl main
.type main, @function
main:
movq %rax, %rbx # これと
.byte 0x48, 0x89, 0xC3 # これは同じ意味
ret
.size main, .-main
$ gcc -g byte.s
$ objdump -d ./a.out
(中略)
0000000000001129 <main>:
1129: ❶48 89 c3 ❸mov %rax,%rbx
112c: ❷48 89 c3 ❹mov %rax,%rbx
112f: c3 ret
コンパイルして逆アセンブルしてみると,
❷0x48, 0x89, 0xC3
を出力できています.
一方,❶0x48, 0x89, 0xC3
にも同じバイト列が並んでいます.
これは❸movq %rax, %rbx
命令の機械語命令バイト列ですね.
さらに❷0x48, 0x89, 0xC3
の逆アセンブル結果として,
❹movq %rax, %rbx
とも表示されています.
つまり,アセンブラにとっては,
movq %rax, %rbx
というニモニック.byte 0x48, 0x89, 0xC3
というバイト列
は全く同じ意味になるのでした.
ですので,.text
セクションにニモニックで機械語命令を書く代わりに,
.byte
を使って直接,機械語命令のバイト列を書くことができます.
異なる機械語のバイト列で,同じ動作のmov
命令がある
- 質問:
%rax
の値を%rbx
にコピーしたい時,movq
r, r/m とmovq
r/m, r のどちらを使えばいいのでしょう? - 答え: どちらを使ってもいいです.ただし,異なる機械語命令のバイト列に なることがあります.
実は0x48, 0x89, 0xC3
というバイト列は,
movq
r, r/m を使った時のものです.
一方,movq
r/m, r という形式を使った場合は,
バイト列は 0x48, 0x8B, 0xD8
になります.確かめてみましょう.
# asm/byte2.s
.text
.globl main
.type main, @function
main:
.byte 0x48, 0x89, 0xC3
.byte 0x48, 0x8B, 0xD8
ret
.size main, .-main
$ gcc -g byte2.s
$ objdump -d ./a.out
(中略)
0000000000001129 <main>:
1129: ❶48 89 c3 ❸mov %rax,%rbx
112c: ❷48 8b d8 ❹mov %rax,%rbx
112f: c3 ret
❶48 89 c3
と❷48 8b d8
は異なるバイト列ですが
逆アセンブル結果としては
❸mov %rax,%rbx
と❹mov %rax,%rbx
と,どちらも同じ結果になりました.
このように同じニモニック命令に対して,複数の機械語のバイト列が存在する時,
アセンブラは「実行が速い方」あるいは「バイト列が短い方」を適当に選んでくれます.
(そして,アセンブラが選ばない方をどうしても使いたい場合は,
.byte
等を使って機械語のバイト列を直書きするしかありません).
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プリフィクスをつけなくてもアトミックになります)- このアトミックな動作はロックなどの同期機構を作るために使えます.
xchg.sの実行例
$ gcc -g xchg.s
$ gdb ./a.out -x xchg.txt
Breakpoint 1, main () at xchg.s:9
9 xchg %rax, (%rsp)
1: /x $rax = 0x99aabbccddeeff00
2: /x *(void **)($rsp) = 0x1122334455667788
10 xchg (%rsp), %rax
1: /x $rax = 0x1122334455667788
2: /x *(void **)($rsp) = 0x99aabbccddeeff00
11 popq %rax
1: /x $rax = 0x99aabbccddeeff00
2: /x *(void **)($rsp) = 0x1122334455667788
# 値が入れ替わっていれば成功
機械語1命令の実行はアトミックとは限らない
機械語1命令の実行はアトミックとは限りません.
例えば,inc
命令(オペランドを1増やす命令)は
マニュアルによると「LOCK
プリフィックスをつければアトミックに実行される」とあります.
inc
命令にLOCK
プリフィックスがない場合には(たまたまアトミックに実行されるかも知れませんが)
「常にアトミックである」と期待してはいけないのです(マニュアルで「アトミックだ」と明記されていない限り).
なお,inc
は「メモリから読んだ値に1を足して書き戻す」ため
アトミックにならない可能性がありますが,読むだけまたは書くだけでかつ,
適切にアラインメントされていれば,
そのメモリ操作はアトミックになります.
lea
命令: 実効アドレスを計算
記法 | 何の略か | 動作 |
---|---|---|
lea␣ op1, op2 | load effective address | op1 の実効アドレスを op2 に代入する |
詳しい記法 | 例 | 例の動作 | サンプルコード |
---|---|---|---|
lea␣ m, r | leaq -8(%rsp, %rsi, 4), %rax | %rax=%rsp+%rsi*4-8 | leaq-2.s leaq-2.txt |
lea
命令は第1オペランド(常にメモリ参照)の実効アドレスを計算して, 第2オペランドに格納します.lea
命令はアドレスを計算するだけで,メモリにはアクセスしません.
leaq-2.sの実行例
$ gcc -g lea.s
$ gdb ./a.out -x lea.txt
Breakpoint 1, main () at leaq-2.s:8
8 ret
# p/x $rsp
$1 = 0x7fffffffde98
# p/x $rsi
$2 = 0x8
# p/x $rax
$3 = 0x7fffffffdeb0
# %rax == %rsp + %rsi * 4 なら成功
実効アドレスとリニアアドレスの違いは?→(ほぼ)同じ
- 実効アドレス(effective address)は
メモリ参照で
disp (base, index, scale) や disp (
%rip
)から計算したアドレスのことです. - x86-64のアセンブリコード中のアドレスは論理アドレス (logical address)といい, セグメントと実効アドレスのペアとなっています. このペアをx86-64用語でfarポインタとも呼びます. (本書ではfarポインタは扱いません).
- セグメントが示すベースアドレスと実効アドレスを加えたものが リニアアドレス(linear address)です. 例えば64ビットアドレス空間だと,リニアアドレスは0番地から264-1番地 まで一直線に並ぶのでリニアアドレスと呼ばれています. リニアアドレスは仮想アドレス(virtual address)と等しくなります.
- また,x86-64では%fsと%gsを除き, セグメントが示すベースアドレスが0番地なので, 実効アドレスとリニアアドレスは等しくなります.
- リニアアドレス(仮想アドレス)はCPUのページング機構により, 物理アドレスに変換されて,最終的なメモリアクセスが行われます.
- コンパイラは加算・乗算を高速に実行するため
lea
命令を使うことがあります.
例えば,
movq $4, %rax
addq %rbx, %rax
shlq $2, %rsi # 左論理シフト.2ビット左シフトすることで%rsiを4倍にしている
addq %rsi, %rax
は,%rax = %rbx + %rsi * 4 + 4
という計算を4命令でしていますが,
lea
命令なら以下の1命令で済みます
leaq 4(%rbx, %rsi, 4), %rax
注: 実行時間は命令ごとに異なりますので,命令数だけで 実行時間を比較することはできません.
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 |
push
命令はスタックポインタ%rsp
を減らしてから, スタックトップ(スタックの一番上)にオペランドの値を格納します.pop
命令はスタックトップの値をオペランドに格納してから, スタックポインタを増やします.- 64ビットモードでは,32ビットの
push
とpop
はできません. - 抽象データ型のスタックは(スタックトップに対する)プッシュ操作とポップ操作しか
できませんが,x86-64のスタック操作はスタックトップ以外の部分にも自由にアクセス可能です(例えば,
-8(%rsp)
や-8(%rbp)
などへのメモリ参照で). - 一番右側の図(
popq %rbx後
)で,ポップ後も%rsp
よりも上に古い値が残っています (0x11
〜0x88
).このように,ポップしてもスタック上に古い値がゴミとして残ります.
push1.sの実行例
$ gcc -g push1.s
$ gdb ./a.out -x push1.txt
Breakpoint 1, main () at push1.s:6
6 pushq $999
# p/x $rsp
$1 = 0x7fffffffde98
main () at push1.s:7
7 ret
# p/x $rsp
$2 = 0x7fffffffde90
# x/1gd $rsp
0x7fffffffde90: 999
# %rsp が8減って,(%rsp)の値が999なら成功
push2.sの実行例
$ gcc -g push2.s
$ gdb ./a.out -x push2.txt
Breakpoint 1, main () at push2.s:6
6 pushw $999
# p/x $rsp
$1 = 0x7fffffffde98
main () at push2.s:7
7 ret
# p/x $rsp
$2 = 0x7fffffffde96
# x/1hd $rsp
0x7fffffffde96: 999
# %rsp が2減って,(%rsp)の値が999なら成功
pop2.sの実行例
$ gcc -g pop2.s
$ gdb ./a.out -x pop2.txt
Breakpoint 1, main () at pop2.s:7
7 popw %ax
# p/x $rsp
$1 = 0x7fffffffde96
main () at pop2.s:8
8 ret
# p/x $rsp
$2 = 0x7fffffffde98
# p/d $ax
$3 = 999
# %rsp が2増えて,%axの値が999なら成功
push-pop.sの実行例
$ gcc -g push-pop.s
$ gdb ./a.out -x push-pop.txt
Breakpoint 1, main () at push-pop.s:8
8 pushq %rax
# p/x $rsp
$1 = 0x7fffffffde98
main () at push-pop.s:9
9 popq %rbx
# p/x $rsp
$2 = 0x7fffffffde90
# x/8bx $rsp
0x7fffffffde90: 0x88 0x77 0x66 0x55 0x44 0x33 0x22 0x11
# %rsp の値が8減って,スタックトップ8バイトが 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11なら成功
x86-64機械語命令: 算術・論理演算
概要とステータスフラグ
ここでは以下の算術・論理演算を説明します.
演算の種類 | 主な命令 |
---|---|
算術 | add , sub , mul , div , inc , dec , not |
論理 | and , or , not , xor |
シフト | sal , sar , shl , shr , rol , ror , rcl , rcr |
比較 | cmp , test |
変換(拡張) | movs , movz , cbtw , cqto |
これらの命令のほとんどが演算の結果として, ステータスフラグ の値を変化させます. 本書ではステータスフラグの変化を以下の記法で表します.
記法の意味は以下の通りです.
記法 | 意味 |
---|---|
空白 | フラグ値に変化なし |
! | フラグ値に変化あり |
? | フラグ値は未定義(参照禁止) |
0 | フラグ値はクリア(0になる) |
1 | フラグ値はセット(1になる) |
add
命令: 足し算
記法 | 何の略か | 動作 |
---|---|---|
add␣ op1, op2 | add | op1 を op2 に加える |
adc␣ op1, op2 | add with carry | op1 と CF を op2 に加える |
詳しい記法 | 例 | 例の動作 | サンプルコード |
---|---|---|---|
add␣ imm, r/m | addq $999, %rax | %rax += 999 | add-1.s add-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
はオペランドが符号あり整数か符号なし整数かを区別せず, 両方の結果を正しく計算します.adc
は例えば,多倍長整数(任意の桁数の整数)を実装する時の 「繰り上がり」の計算に便利です.
add-1.sの実行例
$ gcc -g add-1.s
$ gdb ./a.out -x add-1.txt
Breakpoint 1, main () at add-1.s:8
8 ret
# p $rax
$1 = 1000
# %raxが1000なら成功
add-2.sの実行例
$ gcc -g add-2.s
$ gdb ./a.out -x add-2.txt
Breakpoint 1, main () at add-2.s:10
10 popq %rbx
# p $rax
$1 = 1001
# x/1gd $rsp
0x7fffffffde90: 1000
# %raxが1001,(%rsp)が1000なら成功
adc-1.sの実行例
$ gcc -g adc-1.s
$ gdb ./a.out -x adc-1.txt
reakpoint 1, main () at adc-1.s:8
8 adcq $2, %rax
# p $rax
$1 = 0
# p $eflags
$2 = [ CF PF AF ZF IF ]
main () at adc-1.s:9
9 ret
# p $rax
$3 = 3
# %rflagsでCFが立っていて,%raxが3なら成功
adc-2.sの実行例
$ gcc -g adc-2.s
$ gdb ./a.out -x adc-2.txt
Breakpoint 1, main () at adc-2.s:9
9 adcq $2, (%rsp)
# p $rax
$1 = 0
# p $eflags
$2 = [ CF PF AF ZF IF ]
main () at adc-2.s:10
10 ret
x/1gd $rsp
0x7fffffffde90: 1002
# %rflagsでCFが立っていて,(%rsp)が1002なら成功
adc-3.sの実行例
$ gcc -g adc-3.s
$ gdb ./a.out -x adc-3.txt
Breakpoint 1, main () at adc-3.s:9
9 adcq (%rsp), %rax
# p $rax
$1 = 0
# p $eflags
$2 = [ CF PF AF ZF IF ]
main () at adc-3.s:10
10 ret
# p $rax
$3 = 1000
# %rflagsでCFが立っていて,%raxが1000なら成功
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
は オペランドが符号あり整数か符号なし整数かを区別せず, 両方の結果を正しく計算します.
sub-1.sの実行例
$ gcc -g sub-1.s
$ gdb ./a.out -x sub-1.txt
Breakpoint 1, main () at sub-1.s:8
8 ret
# p $rax
$1 = 1
# %raxが1なら成功
sub-2.sの実行例
$ gcc -g sub-2.s
$ gdb ./a.out -x sub-2.txt
Breakpoint 1, main () at sub-2.s:10
10 popq %rbx
# p $rax
$1 = -997
# x/1gd $rsp
0x7fffffffde90: 998
# %raxが-997,(%rsp)が998なら成功
sbb-1.sの実行例
$ gcc -g sbb-1.s
$ gdb ./a.out -x sbb-1.txt
Breakpoint 1, main () at sbb-1.s:8
8 sbbq $2, %rax
# p $rax
$1 = 0
# p $eflags
$2 = [ CF PF AF ZF IF ]
main () at sbb-1.s:9
9 ret
# p $rax
$3 = -3
# %rflagsでCFが立っていて,%raxが-3なら成功
sbb-2.sの実行例
$ gcc -g sbb-2.s
$ gdb ./a.out -x sbb-2.txt
Breakpoint 1, main () at sbb-2.s:9
9 sbbq $2, (%rsp)
# p $rax
$1 = 0
# p $eflags
$2 = [ CF PF AF ZF IF ]
10 sbbq (%rsp), %rax
main () at sbb-2.s:11
11 ret
x/1gd $rsp
0x7fffffffde90: 996
# p $rax
$3 = -996
# %rflagsでCFが立っていて,(%rsp)が996,%raxが-996なら成功
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つの形式では乗算結果が8バイトを超えた場合, 越えた分は破棄されます(乗算結果は8バイトのみ).
mul-1.sの実行例
$ gcc -g mul-1.s
$ gdb ./a.out -x mul-1.txt
Breakpoint 1, main () at mul-1.s:9
9 ret
# p $rdx
$1 = 0
# p $rax
$2 = 6
# %rdxが0, %raxが6なら成功
imul-1.sの実行例
$ gcc -g imul-1.s
$ gdb ./a.out -x imul-1.txt
Breakpoint 1, main () at imul-1.s:9
9 ret
# p $rdx
$1 = 0xffffffffffffffff
# p $rax
$2 = -6
# %rdxが0xFFFFFFFFFFFFFFFF, %raxが-6なら成功
imul-2.sの実行例
$ gcc -g imul-2.s
$ gdb ./a.out -x imul-2.txt
Breakpoint 1, main () at imul-2.s:8
8 imulq $4, %rax
9 imulq %rbx, %rax
1: $rax = -8
10 imulq $5, %rbx, %rax
1: $rax = 24
main () at imul-2.s:11
11 ret
1: $rax = -15
# %raxが-8, 24, -15なら成功
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
に符号拡張しておくと良いです.
div-1.sの実行例
$ gcc -g div-1.s
$ gdb ./a.out -x div-1.txt
Breakpoint 1, main () at div-1.s:10
10 ret
# p $rax
$1 = 33
# p $rdx
$2 = 9
# %raxが33, %rdxが9なら成功
idiv-1.sの実行例
$ gcc -g idiv-1.s
$ gdb ./a.out -x idiv-1.txt
Breakpoint 1, main () at idiv-1.s:9
9 idivq %rbx
# p/x $rdx
$1 = 0xffffffffffffffff
main () at idiv-1.s:10
10 ret
# p $rax
$2 = -33
# p $rdx
$3 = -9
# 最初の%rdxが0xFFFFFFFFFFFFFFFF, %raxが-33, 2番目の%rdxが-9なら成功
inc
, dec
命令: インクリメント,デクリメント
記法 | 何の略か | 動作 |
---|---|---|
inc␣ op1 | increment | op1の値を1つ増加 |
dec␣ op1 | decrement | op1の値を1つ減少 |
inc
やdec
はオーバーフローしてもCFが変化しないところがポイントです.
inc-1.sの実行例
$ gcc -g inc-1.s
$ gdb ./a.out -x inc-1.txt
Breakpoint 1, main () at inc-1.s:8
8 ret
# p $rax
$1 = 1
# %raxが1なら成功
dec-1.sの実行例
$ gcc -g dec-1.s
$ gdb ./a.out -x dec-1.txt
reakpoint 1, main () at dec-1.s:8
8 ret
# p $rax
$1 = -1
# %raxが-1なら成功
neg
命令: 符号反転
記法 | 何の略か | 動作 |
---|---|---|
neg␣ op1 | negation | 2の補数によるop1の符号反転 |
neg-1.sの実行例
$ gcc -g neg-1.s
$ gdb ./a.out -x neg-1.txt
Breakpoint 1, main () at neg-1.s:7
7 neg %rax
1: $rax = 999
8 neg %rax
1: $rax = -999
main () at neg-1.s:9
9 ret
1: $rax = 999
# %raxが 999 → -999 → 999 と変化すれば成功
not
命令: ビット論理演算 (1)
記法 | 何の略か | 動作 |
---|---|---|
not␣ op1 | bitwise not | op1の各ビットの反転 (NOT) |
not-1.sの実行例
$ gcc -g not-1.s
$ gdb ./a.out -x not-1.txt
Breakpoint 1, main () at not-1.s:7
7 not %al
1: /t $al = 11001010
8 not %al
1: /t $al = 110101
main () at not-1.s:9
9 ret
1: /t $al = 11001010
# %alが 11001010 → 110101 → 11001010 と変化すれば成功
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言語を復習しましょう).
and-1.sの実行例
$ gcc -g and-1.s
$ gdb ./a.out -x and-1.txt
Breakpoint 1, main () at and-1.s:8
8 pushq $0B00001111
# p/t $al
$1 = 10001000
Breakpoint 2, main () at and-1.s:12
12 ret
# x/1bt $rsp
0x7fffffffde90: 00001000
# p/t $al
$2 = 0
# 表示される値が 10001000, 00001000, 0 なら成功
or-1.sの実行例
$ gcc -g or-1.s
$ gdb ./a.out -x or-1.txt
Breakpoint 1, main () at or-1.s:8
8 pushq $0B00001111
# p/t $al
$1 = 11101110
Breakpoint 2, main () at or-1.s:12
12 ret
# x/1bt $rsp
0x7fffffffde90: 11101111
# p/t $al
$2 = 11111111
# 表示される値が 11101110, 11101111, 11111111 なら成功
xor-1.sの実行例
$ gcc -g xor-1.s
$ gdb ./a.out -x xor-1.txt
Breakpoint 1, main () at xor-1.s:8
8 pushq $0B00001111
# p/t $al
$1 = 1100110
Breakpoint 2, main () at xor-1.s:12
12 ret
# x/1bt $rsp
0x7fffffffde90: 01101001
# p/t $al
$2 = 10011110
# 表示される値が 1100110, 01101001, 10011110 なら成功
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言語で,ビット演算は符号なし整数に対してのみ行うようにしましょう.
sal-1.sの実行例
$ gcc -g sal-1.s
$ gdb ./a.out -x sal-1.txt
Breakpoint 1, main () at sal-1.s:8
8 salq %rax
1: /t $rax = 11111111
9 salq $2, %rax
1: /t $rax = 111111110
10 salq %cl, %rax
1: /t $rax = 11111111000
main () at sal-1.s:11
11 ret
1: /t $rax = 11111111000000
# 表示される値が 11111111, 111111110, 11111111000, 11111111000000 なら成功
shl-1.sの実行例
$ gcc -g shl-1.s
$ gdb ./a.out -x shl-1.txt
reakpoint 1, main () at shl-1.s:8
8 shlq %rax
1: /t $rax = 11111111
9 shlq $2, %rax
1: /t $rax = 111111110
10 shlq %cl, %rax
1: /t $rax = 11111111000
main () at shl-1.s:11
11 ret
1: /t $rax = 11111111000000
# 表示される値が 11111111, 111111110, 11111111000, 11111111000000 なら成功
sar-1.sの実行例
$ gcc -g sar-1.s
$ gdb ./a.out -x sar-1.txt
Breakpoint 1, main () at sar-1.s:8
8 sarq %rax
1: /t $rax = 1111111111111111111111111111111111111111111111111111111100000000
9 sarq $2, %rax
1: /t $rax = 1111111111111111111111111111111111111111111111111111111110000000
10 sarq %cl, %rax
1: /t $rax = 1111111111111111111111111111111111111111111111111111111111100000
main () at sar-1.s:11
11 ret
1: /t $rax = 1111111111111111111111111111111111111111111111111111111111111100
# 表示される値が 1111111111111111111111111111111111111111111111111111111100000000, 1111111111111111111111111111111111111111111111111111111110000000, 1111111111111111111111111111111111111111111111111111111111100000, 1111111111111111111111111111111111111111111111111111111111111100 なら成功
shr-1.sの実行例
$ gcc -g shr-1.s
$ gdb ./a.out -x shr-1.txt
reakpoint 1, main () at shr-1.s:8
8 shrq %rax
1: /t $rax = 1111111111111111111111111111111111111111111111111111111100000000
9 shrq $2, %rax
1: /t $rax = 111111111111111111111111111111111111111111111111111111110000000
10 shrq %cl, %rax
1: /t $rax = 1111111111111111111111111111111111111111111111111111111100000
main () at shr-1.s:11
11 ret
1: /t $rax = 1111111111111111111111111111111111111111111111111111111100
# 表示される値が 1111111111111111111111111111111111111111111111111111111100000000, 111111111111111111111111111111111111111111111111111111110000000, 1111111111111111111111111111111111111111111111111111111100000, 1111111111111111111111111111111111111111111111111111111100 なら成功
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パターンの命令が存在します.
rol-1.sの実行例
$ gcc -g rol-1.s
$ gdb ./a.out -x rol-1.txt
Breakpoint 1, main () at rol-1.s:8
8 rolq %rax
1: /t $rax = 11111111
9 rolq $2, %rax
1: /t $rax = 111111110
10 rolq %cl, %rax
1: /t $rax = 11111111000
main () at rol-1.s:11
11 ret
1: /t $rax = 11111111000000
# 表示される値が 11111111, 111111110, 11111111000, 11111111000000 なら成功
rcl-1.sの実行例
$ gcc -g rcl-1.s
$ gdb ./a.out -x rcl-1.txt
Breakpoint 1, main () at rcl-1.s:10
10 rclq %rax
1: /t $rax = 11111111
11 rclq $2, %rax
1: /t $rax = 111111111
12 rclq %cl, %rax
1: /t $rax = 11111111100
main () at rcl-1.s:13
13 ret
1: /t $rax = 11111111100000
# 表示される値が 11111111, 111111111, 11111111100, 11111111100000 なら成功
ror.sの実行例
$ gcc -g ror.s
$ gdb ./a.out -x ror.txt
Breakpoint 1, main () at ror-1.s:8
8 rorq %rax
1: /t $rax = 11111111
9 rorq $2, %rax
1: /t $rax = 1000000000000000000000000000000000000000000000000000000001111111
10 rorq %cl, %rax
1: /t $rax = 1110000000000000000000000000000000000000000000000000000000011111
main () at ror-1.s:11
11 ret
1: /t $rax = 1111110000000000000000000000000000000000000000000000000000000011
# 表示される値が 11111111, 1000000000000000000000000000000000000000000000000000000001111111, 1110000000000000000000000000000000000000000000000000000000011111, 1111110000000000000000000000000000000000000000000000000000000011 なら成功
rcr-1.sの実行例
$ gcc -g rcr-1.s
$ gdb ./a.out -x rcr-1.txt
Breakpoint 1, main () at rcr-1.s:10
10 rcrq %rax
1: /t $rax = 11111010
11 rcrq $2, %rax
1: /t $rax = 1000000000000000000000000000000000000000000000000000000001111101
12 rcrq %cl, %rax
1: /t $rax = 1010000000000000000000000000000000000000000000000000000000011111
main () at rcr-1.s:13
13 ret
1: /t $rax = 1101010000000000000000000000000000000000000000000000000000000011
# 表示される値が 11111010, 1000000000000000000000000000000000000000000000000000000001111101, 1010000000000000000000000000000000000000000000000000000000011111, 1101010000000000000000000000000000000000000000000000000000000011 なら成功
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
cmp-1.sの実行例
$ gcc -g cmp-1.s
$ gdb ./a.out -x cmp-1.txt
reakpoint 1, main () at cmp-1.s:8
8 cmpq $1, %rax # %rax (=0) - 1
9 cmpq %rax, (%rsp) # (%rsp) (=1) - %rax (=0)
1: $eflags = [ CF PF AF SF IF ]
10 cmpq (%rsp), %rax # %rax (=0) - (%rsp) (=1)
1: $eflags = [ IF ]
main () at cmp-1.s:11
11 ret
1: $eflags = [ CF PF AF SF IF ]
# 表示されるステータスフラグが以下なら成功
# 1: $eflags = [ CF PF AF SF IF ] (SF==1 → 結果は負)
# 1: $eflags = [ IF ] (SF==0 → 結果は0か正)
# 1: $eflags = [ CF PF AF SF IF ] (SF==1 → 結果は負)
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
の方が命令長が短くなるからです.
test-1.sの実行例
$ gcc -g test-1.s
$ gdb ./a.out -x test-1.txt
Breakpoint 1, main () at test-1.s:8
8 testq $0, %rax # %rax (=1) & 0
9 testq %rax, (%rsp) # (%rsp) (=1) & %rax (=1)
1: $eflags = [ PF ZF IF ]
10 testq (%rsp), %rax # %rax (=1) & (%rsp) (=1)
1: $eflags = [ IF ]
main () at test-1.s:11
11 ret
1: $eflags = [ IF ]
# 表示されるステータスフラグが以下なら成功
# 1: $eflags = [ PF ZF IF ] (ZF==1 → 結果は0)
# 1: $eflags = [ IF ] (ZF==0 → 結果は非0)
# 1: $eflags = [ IF ] (ZF==0 → 結果は非0)
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
を使います.
movs-movz.sの実行例
$ gcc -g movs-movz.s
$ gdb ./a.out -x movs-movz.txt
Breakpoint 1, main () at movs-movz.s:7
7 movslq %eax, %rbx
8 movzwq %ax, %rbx
1: /x $rbx = 0xffffffffffffffff
main () at movs-movz.s:9
9 ret
1: /x $rbx = 0xffff
# 以下が表示されれば成功
# 1: /x $rbx = 0xffffffffffffffff
# 1: /x $rbx = 0xffff
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 (double 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形式のニモニックも受け付ける.
cbtw.sの実行例
$ gcc -g cbtw.s
$ gdb ./a.out -x cbtw.txt
Breakpoint 1, main () at cbtw.s:7
7 cbtw # %al -> %ax
9 cwtl # %ax -> %eax
$1 = -1
$2 = 0xffff
11 cwtd # %ax -> %dx:%ax
$3 = -1
$4 = 0xffffffff
13 cltd # %eax -> %edx:%eax
$5 = {-1, -1}
$6 = {0xffff, 0xffff}
15 cltq # %eax -> %rax
$7 = {-1, -1}
$8 = {0xffffffff, 0xffffffff}
17 cqto # %rax -> %rdx:%rax
$9 = -1
$10 = 0xffffffffffffffff
main () at cbtw.s:19
19 ret
$11 = {-1, -1}
$12 = {0xffffffffffffffff, 0xffffffffffffffff}
# 以下が表示されれば成功
# $1 = -1
# $2 = 0xffff
# $3 = -1
# $4 = 0xffffffff
# $5 = {-1, -1}
# $6 = {0xffff, 0xffff}
# $7 = {-1, -1}
# $8 = {0xffffffff, 0xffffffff}
# $9 = -1
# $10 = 0xffffffffffffffff
# $11 = {-1, -1}
# $12 = {0xffffffffffffffff, 0xffffffffffffffff}
ジャンプ命令
- ジャンプとは「次に実行する命令を(『次の番地の命令』ではなく)
『別の番地の命令』にすることです.
ジャンプの仕組みは簡単で「ジャンプ先のアドレスをプログラムカウンタ
%rip
に代入する」だけです. C言語風に書くと%rip = ジャンプ先のアドレス
となります (ジャンプ先が相対アドレスで与えられた場合は,%rip += 相対アドレス
になります). - 無条件ジャンプはC言語の
goto
文と同じで常にジャンプします. 条件付きジャンプは条件が成り立った時だけジャンプします. 条件付きジャンプをC言語風に書くとif (条件) goto ジャンプ先;
になります.
絶対ジャンプと相対ジャンプ
- 絶対ジャンプ (absolute jump)は絶対アドレス,
つまりメモリの先頭からのオフセットでジャンプ先のアドレスを指定するジャンプです.
上の例で,AからBにジャンプする時,
jmp 0x1000
は絶対ジャンプになります. - 相対ジャンプ (relative jump)は
プログラムカウンタ
%rip
を起点とする相対アドレスで ジャンプ先のアドレスを指定するジャンプです. 上の例で,AからBにジャンプする時,jmp -0x500
は相対ジャンプになります. (プログラムカウンタは「次に実行する命令を指すレジスタ」なので, 正確には「Aの一つ前の命令からBにジャンプする時」になります).
直接ジャンプと間接ジャンプ
-
直接ジャンプ (direct jump)はジャンプ先のアドレスを 即値 (定数)で指定するジャンプです. 上の例で一番左の
jmp 0x1000
は直接ジャンプです. -
間接ジャンプ (indirect jump)はジャンプ先のアドレスを レジスタやメモリで指定して,その中に格納されている値を ジャンプ先のアドレスとするジャンプです.
- 上の例で真ん中の
jmp *%rax
はレジスタを使った間接ジャンプです. レジスタ中のアドレス (ここでは0x1000
番地)にジャンプします. (なぜアスタリスク*
が必要なのかは謎です.GNUアセンブラの記法です.) - 上の例で一番右の
jmp *(%rax)
はメモリ参照を使った間接ジャンプです. メモリ中のアドレス (ここでは0x1000
番地)にジャンプします.
- 上の例で真ん中の
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, %rax jg foo | if (%rax >0) goto foo | jg.s jg.txt |
jnle rel | cmpq $0, %rax jnle foo | if (!(%rax <=0)) goto foo | jg.s jg.txt |
jge rel | cmpq $0, %rax jge foo | if (%rax >=0) goto foo | jge.s jge.txt |
jnl rel | cmpq $0, %rax jnl foo | if (!(%rax <0)) goto foo | jge.s jge.txt |
jle rel | cmpq $0, %rax jle foo | if (%rax <=0) goto foo | jle.s jle.txt |
jng rel | cmpq $0, %rax jng foo | if (!(%rax >0)) goto foo | jle.s jle.txt |
jl rel | cmpq $0, %rax jl foo | if (%rax <0) goto foo | jl.s jl.txt |
jnge rel | cmpq $0, %rax jnge 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
も同様です.
なぜ ZF==0&&SF=OF が(符号ありの場合の)op2>op1になるのか
- 復習:
cmp␣
op1, op2は (op2 - op1)という引き算を計算した時の フラグ変化を計算します. - ①: OF==0(オーバーフロー無し)の場合:
- SF==0 だと引き算の結果は0以上→ op2 - op1 >= 0 → op2 >= op1
- ②: OF==1(オーバーフローあり)の場合:
- 結果の正負が逆になる.つまり SF==1 だと引き算の結果は負(OF==1で逆になるので正)→ op2 - op1 >= 0 → op2 >= op1
- ③: ①と②から,(OF==0&&SF==0)||(OF==1&&SF==1)なら,op2 >= op1 になる. (OF==0&&SF==0)||(OF==1&&SF==1)を簡単にすると OF==SF になる.
- ④: ③に ZF==0 (結果はゼロではない)という条件を加えると, ZF==0&&SF=OF が op2 > op1 と等価になる.
- 上の例で,OF==1の時,引き算結果の大小関係(SF)が逆になることを見てみます.
- (+64)-(-64)はオーバーフローが起きて,結果は-128になります(SF==1). 引き算の結果は負ですが,大小関係は (+64) > (-64) です(逆になってます).
- (-64)-(+65)はオーバーフローが起きて,結果は127になります(SF==0). 引き算の結果は正ですが,大小関係は (-64) < (+65) です(逆になってます).
条件付きジャンプ: 符号なし整数用
記法 | 何の略か | 動作 | ジャンプ条件 |
---|---|---|---|
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, %rax ja foo | if (%rax >0) goto foo | ja.s ja.txt |
jnbe rel | cmpq $0, %rax jnbe foo | if (!(%rax <=0)) goto foo | ja.s ja.txt |
jae rel | cmpq $0, %rax jae foo | if (%rax >=0) goto foo | jae.s jae.txt |
jnb rel | cmpq $0, %rax jnb foo | if (!(%rax <0)) goto foo | jae.s jae.txt |
jbe rel | cmpq $0, %rax jbe foo | if (%rax <=0) goto foo | jbe.s jbe.txt |
jna rel | cmpq $0, %rax jna foo | if (!(%rax >0)) goto foo | jbe.s jbe.txt |
jb rel | cmpq $0, %rax jb foo | if (%rax <0) goto foo | jb.s jb.txt |
jnae rel | cmpq $0, %rax jnae 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, %rax je 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, %rax jne 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 |
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 |
call.sの実行例
$ gcc -g call.s
$ gdb ./a.out -x call.txt
reakpoint 1, main () at call.s:12
12 call foo
1: /x $rip = 0x401107
# info address foo
Symbol "foo" is at ❶0x401106 in a file compiled without debugging.
Breakpoint 2 at 0x401106: file call.s, line 6.
❷Breakpoint 2, foo () at call.s:6
6 ret
1: /x $rip = 0x401106
❸Breakpoint 2, foo () at call.s:6
6 ret
1: /x $rip = 0x401106
❹Breakpoint 2, foo () at call.s:6
6 ret
1: /x $rip = 0x401106
# 3回,関数fooを呼び出して,リターンできていれば成功
info address foo
コマンドで,foo
のアドレスは❶0x401106
番地と分かりました.- ❷❸❹より3回,
foo
を呼び出せていることが分かります.
注: 関数呼び出し規約(calling convention),スタックレイアウトなどは ABIが定めるお約束です. 以下ではLinuxのABIに基づいて説明します.
関数の呼び出し時に戻り番地をスタックに積む,リターン時に戻り番地をスタックから取り出す
関数呼び出しとリターンにはスタックを使います(スタック超重要). スタックは以下の図の通り,プロセスが使うメモリの一部の領域です.
関数呼び出しにジャンプ命令(jmp
)を使うと,
(一般的に呼び出す側は複数箇所なので)
リターン時にどこに戻ればよいかが分かりません.
そこで,戻る場所(戻り番地 (return address))をスタックに保存しておきます.
call
命令はこの「戻り番地をスタックに保存する」ことを自動的にやってくれます.
以下で具体例call2.s
を見てみましょう.
call2.s
では関数main
から関数foo
をcall
命令で呼び出して,
関数foo
から関数main
にret
命令でリターンしています.
# asm/call2.s
.text
.type foo, @function
foo:
ret
.size foo, .-foo
.globl main
.type main, @function
main:
call foo
ret
.size main, .-main
$ gcc -g -no-pie call2.s
$ objdump -d ./a.out
(中略)
0000000000401106 <foo>:
❷401106: c3 ret
0000000000401107 <main>:
401107: e8 fa ff ff ff call 401106 <foo>
❶40110c: c3 ret
-no-pie
オプションは
実行するたびにアドレスが変わらないためにつけています.
-no-pie
オプション無しでも仕組みは変わりません.
-
call foo
実行直前: 図(左)が示す通り%rip
はcall foo
命令を指しています. ここで,call foo
命令を実行すると,%rip
はcall foo
命令の次の命令(ここではmain
関数中のret
命令)を指します. (%rip
は「実行中の命令の次の命令」を指すことを思い出しましょう).call foo
はまず%rip
の値(上図では❶0x40110C
)をスタックにプッシュします. その結果,スタック上に0x40110C
が書き込まれます. この0x40110C
が(関数foo
からリターンする際の)戻り番地となります.- 次に,
call foo
は関数foo
の先頭番地(上図では❷0x401106
)にジャンプします.
-
call foo
実行直後: 図(中)が示す通り%rip
はfoo
関数のret
命令を指しています. 一方,スタックトップ(%rsp
が指している場所)には 戻り番地0x40110C
が格納されています. ここで,ret
命令を実行すると,- スタックから戻り番地
0x40110C
をポップして取り出して,%rip
に格納します(つまり0x40110C
番地にジャンプします).
- スタックから戻り番地
-
関数
foo
のret
実行直後: 無事に関数main
のcall foo
命令の次の命令(ここではret
命令)に戻ってこれました.
このように戻り番地をスタックに格納すれば,(メモリ不足にならない限り) どれだけ数多くの関数呼び出しが続いても,正しい順番でリターンすることができます. 戻り番地の格納にスタックを使えば, 「コールした順番とは逆の順序で戻りアドレスを取り出せる」からです.
例えば,上図のようにA→B→C→Dという順番で関数コールをした場合, 上図の順番で「Aへの戻り番地」「Bへの戻り番地」「Cへの戻り番地」が スタックに積まれます. リターンするときはD→C→B→Aという逆の順番になるわけですが, スタックを使っているので, ポップするたびに「Cへの戻り番地」「Bへの戻り番地」「Aへの戻り番地」 という逆の順番で戻り番地を正しく取り出せます.
C言語の関数ポインタと,間接call
命令
// asm/fp.c
int add5 (int n)
{
return n + 5;
}
int main ()
{
int (*fp)(int n);
fp = add5;
return fp (10);
}
$ gcc -g fp.c
$ objdump -d ./a.out
(中略)
000000000000113c <main>:
113c: f3 0f 1e fa endbr64
1140: 55 push %rbp
1141: 48 89 e5 mov %rsp,%rbp
1144: 48 83 ec 10 sub $0x10,%rsp
1148: 48 8d 05 da ff ff ff ❷ lea -0x26(%rip),%rax # 1129 <add5>
114f: 48 89 45 f8 mov %rax,-0x8(%rbp)
1153: 48 8b 45 f8 mov -0x8(%rbp),%rax
1157: bf 0a 00 00 00 mov $0xa,%edi
115c: ff d0 ❶ call *%rax
115e: c9 leave
115f: c3 ret
- C言語で関数ポインタを使うと,間接
call
命令にコンパイルされます.asm/fp.c
中の
int (*fp)(int n);
の部分は「『int
型の引数をもらい,int
型を返す関数』へのポインタを
格納する変数fp
を定義しています.
そして,fp = add5
と代入を行い,fp (10)
することで,
関数ポインタを使って間接的にadd5
関数を呼び出しています.
- このCコードをコンパイルして逆アセンブルすると,
関数ポインタを使った関数呼び出しは,
間接
call
命令 (ここでは❶ call *%rax)になっていることが分かります.%rax
には関数add5
の先頭アドレスが入っています (ここでは ❷lea -0x26(%rip),%rax
を実行することで).
fp = add5 であってる?
fp = add5
ではなくfp = &add5
が正しいのでは?と思った人はいますか?
fp = add5
で正しいです.
(sizeof
や単項演算子&
のオペランドであるときを除いて)
式中では「関数は関数へのポインタ」に暗黙的に型変換されます.
ですので,式中でadd5
の型は「関数へのポインタ」になり,
fp
とadd5
は同じ型になります
(fp = &add5
としても動くんですけどね).
fp (10)
も同様です.「fp
は関数へのポインタなのだから,
(*fp) (10)
が正しいのでは?」と思うかも知れません.
でも,fp (10)
で正しいです.
そもそも関数呼び出しの文法は「関数 ( 引数の列 )」ではなく,
「関数へのポインタ ( 引数の列 )」です.
add5 (10)
のadd5
の型は関数へのポインタなんです.
ちなみに(*fp)(10)
としても動きます.
(*fp)
は「関数へのポインタを関数」に戻しますが,その戻った関数型は
すぐに「関数型へのポインタ」に変換されるからです.
ですので,(******fp)(10)
でも動きます.
enter
, leave
命令: スタックフレームを作成する,解放する
スタックフレーム
- 戻り番地はスタックに格納しますが, それ以外のデータ(例えば,局所変数,引数,返り値,退避したレジスタの値など)も スタックを使います.
- スタック上で管理する,関数呼び出し1回分のデータのことを スタックフレーム (stack frame)といいます.
例えば,main
関数がadd5
関数を呼び出して,add5
からリターンすると以下の図になります.
スタックフレームにはいろいろなデータが入っていますが,
スタックフレームまるごとでプッシュしたりポップしたりします.
ですので,関数を呼び出したりリターンする時はこの
「スタックフレームをプッシュしたり,ポップしたり」,
つまり「スタックフレームを作成したり,破棄したり」する機械語命令列を
使う必要があります(以下で説明します).
そして%rsp
と%rbp
は以下の図のように,
スタック上の一番上のスタックフレームの上下を指す役割を担っています.
(ただし,-fomit-frame-pointer
オプションでコンパイルされている場合を除く).
enter
, leave
命令
記法 | 何の略か | 動作 |
---|---|---|
enter op1, op2 | make stack frame | サイズop1のスタックフレームを作成する |
leave | discard stack frame | 今のスタックフレームを破棄する |
詳しい記法 | 例 | 例の動作 | サンプルコード |
---|---|---|---|
enter imm16, imm8 | enter $0x20, $0 | pushq %rbp movq %rsp, %rbp subq $0x20, %rsp | enter.s enter.txt |
leave | leave | movq %rbp, %rsp popq %rbp | enter.s enter.txt |
enter
命令のop2には関数のネストレベルを指定するのですが, C言語では入れ子の関数がない(つまりネストレベルは常にゼロ)なので 常にゼロを指定します.- ただし,
enter
は遅いので通常は使いません. 代わりに同等の動作をするpushq %rbp; movq %rsp, %rbp; subq $
size, %rsp
を使います.(sizeは新しいスタックフレームで確保するバイトサイズです). スタックは0番地に向かって成長するので,足し算ではなく引き算を使います.
enter命令はどのぐらい遅いのか(3〜4倍?)
$ gcc -g rdtscp-enter.c
$ ./a.out
processor ID = 0
processor ID = 0
processor ID = 0
240966
60796
$ ./a.out
processor ID = 0
processor ID = 0
processor ID = 0
165718
46368
$ ./a.out
processor ID = 1
processor ID = 1
processor ID = 1
204346
49530
インラインアセンブラを使ったCプログラムrdtscp-enter.c
で,以下のコードを10000万回繰り返して,
タイムスタンプカウンタの差分を調べた所,
(単純な調べ方ですが)概ね3〜4倍という結果になりました.
# 遅い
asm volatile ("enter $32, $0; leave");
# 速い
asm volatile ( "pushq %rbp; movq %rsp, %rbp;"
"subq $32, %rsp; leave");
leave
を入れないとスタックを使い切ってしまうのでleave
を入れています.
leave
を除いて計測すればもうちょっと差が開くかも知れません.
というわけで,enter
は遅いので,コンパイラがenter
の代わりに出力する
機械語命令列で説明します.
# asm/stack-frame.s
.text
.type foo, @function
foo:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
# 本来はここにfoo関数本体の機械語列が来る
leave # movq %rbp, %rsp; pop %rbp と同じ
ret
.globl main
.type main, @function
main:
call foo
ret
.size main, .-main
$ gcc -g enter2.s
$ gdb ./a.out -x stack-frame.txt
- 関数
foo
の最初の3行が「関数foo
のスタックフレーム」を作ります.
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
call
前:%rsp
と%rbp
は関数main
のスタックフレームの上下を指しています.call
後:call
命令が戻り番地をプッシュしてから, (図にはありませんが)関数foo
にジャンプします.pushq %rbp
後: スタックに%rbpの値
(図中では古い%rbp
の値)をプッシュします. この値はmain
のスタックフレームの一番下を指しています.movq %rsp, %rbp
後:%rbp
の値をスタック上に退避した(保存した)ので,movq %rsp, %rbp
により,%rbp
が「関数foo
のスタックフレームの一番下」を指すようにします.subq $32, %rsp
により,foo
のスタックフレームを確保しました. これでfoo
のスタックフレームは完成です. ここでは32バイト確保していますが,関数foo
の中身によって適宜,増減します.
- 関数
foo
の最後の2行(leave
とret
)が 「関数foo
のスタックフレーム」を破棄します.leave
命令はmovq %rbp, %rsp; popq %rbp
と同じ動作をします.
leave
ret
leave
前:%rsp
と%rbp
が関数foo
のスタックフレームの上下を指しています.leave
前半(movq %rbp, %rsp
)後:%rsp
が関数foo
のスタックフレームの一番下を指します.leave
後半(popq %rbp
)後: 退避しておいた「古い%rbp
」をポップして%rbp
に格納することで,%rbp
は関数main
のスタックフレームの一番下を指します.ret
後: スタックトップに戻り番地がある状態に戻ったので,ret
命令で関数foo
からmain
にリターンします.ret
命令はスタックからポップして戻り番地を取り出すので, スタック上から戻り番地が無くなります. これでスタックは関数foo
を呼び出す前と同じ状態に戻りました.%rsp
と%rbp
は関数main
のスタックフレームの上下を指しています.
caller-saveレジスタとcallee-saveレジスタ
- レジスタの数は限られているので,必要に応じて, レジスタの値はスタック上に退避(保存)する必要があります.
- その保存の仕方で,レジスタは caller-saveレジスタとcallee-saveレジスタに分類されます.これを以下で説明します.
calleeとcaller
関数Aが関数Bを呼び出す時,
- 関数Aをcaller(呼び出す側),
- 関数Bをcallee(呼び出される側),といいます.
雇用者を employer,被雇用者(雇われてる人)を employee って呼ぶのと同じ言い方ですね. デバッグする側(debugger),デバッグされる側(debuggee), テストする側(tester),テストされる側(testee)という言い方もあります.
レジスタ退避と回復
- 関数呼び出しで,レジスタの退避と回復が必要になることが良くあります. レジスタの数が有限でごく少ないからです.
- レジスタの退避・回復のやり方は大きく2種類あります:
- caller側で退避・回復: caller側でレジスタのプッシュとポップを行う
- callee側で退避・回復: callee側でレジスタのプッシュとポップを行う
LinuxのABIでの caller-saveレジスタとcallee-saveレジスタ
レジスタの退避と回復は,caller側でもcallee側でもできますが, レジスタごとにどちらでやるかを決めておくと便利です.
- caller側で退避・回復を行うレジスタをcaller-saveレジスタと呼びます
- callee側で退避・回復を行うレジスタをcallee-saveレジスタと呼びます
LinuxのABIでは 以下のように,caller-saveレジスタとcallee-saveレジスタが決まっています.
汎用レジスタ | |
---|---|
caller-saveレジスタ | %rax , %rcx , %rdx , %rsi , %rdi , %r8 〜%r11 |
callee-saveレジスタ | %rbx , %rbp , %rsp , %r12 〜%r15 |
%rsp
のcallee側での退避・回復には,
プッシュやポップを使いませんが,
「caller側にリターンする前に元に戻す,という約束をcallee側は守る(責任がある)」
という意味で,%rsp
もcallee-saveレジスタになります.
関数呼び出し規約 (calling convention)
関数呼び出し規約 (calling convention)は ABIが定める「callerとcalle間のお約束」です.例えば,以下を定めます:
- 引数の渡し方 (スタック渡しかレジスタ渡しか)
- スタックフレームのレイアウト (どこに何を置くか)
- レジスタの役割
- caller-saveレジスタとcallee-saveレジスタ
- アラインメント
引数の渡し方
引数 | レジスタ |
---|---|
第1引数 | %rdi |
第2引数 | %rsi |
第3引数 | %rdx |
第4引数 | %rcx |
第5引数 | %r8 |
第6引数 | %r9 |
- 第1引数〜第6引数は上記の通り,レジスタを介して渡します
- 第7引数以降はレジスタではなくスタックを介して渡します
スタックレイアウト
- 上図は典型的なスタックレイアウトです.
- 局所変数と第7以降の引数はスタック上に置きます.
スタック上の局所変数や引数は
%rbp
を使ってアクセスします. 例えば,上図ではメモリ参照-16(%rbp)
は局所変数2, メモリ参照24(%rbp)
は第8引数への参照になります.%rbp
を使う理由は, これらの絶対アドレスがコンパイル時に決まりませんが,%rbp
に対する相対アドレスはコンパイル時に決まるからです. (-fomit-frame-pointer
オプションが 指定された場合は,%rbp
ではなく%rsp
を使ってアクセスします). - スタックに置く局所変数や引数が8バイト未満の場合は アラインメント制約を満たすために, 隙間(パディング)を入れる必要があることがあります.
レジスタの役割
%rsp
と%rbp
は一番上のスタックフレームの上下を指します (-fomit-frame-pointer
オプションが 指定されていなければ).- (8バイト以下の整数であれば)返り値は
%rax
に入れて返します. - 可変長引数の関数(例えば
printf
)を呼び出す時は, 呼び出す前に%al
に「使用するベクタレジスタ(例えば%xmm0
)の数」を入れます.
レッドゾーン (redzone)
- レッドゾーンは
%rsp
レジスタの上,128バイトの領域のことです. この領域には好きに読み書きして良いことになっています.
アラインメント制約
call
命令実行時に%rsp
レジスタは16バイト境界を満たす, つまり%rsp
の値が16の倍数である必要があります. これを守らないとプログラムがクラッシュすることがあるので要注意です
関数プロローグとエピローグ
- 関数本体実行前に準備を行うコードを関数プロローグ(function prologue), 関数本体実行後に後片付けを行うコードを関数エピローグ(function epilogue) といいます.
- 上図は典型的な関数プロローグとエピローグです.
- 関数プロローグでは,スタックフレームの作成, callee-saveレジスタの退避(必要があれば), (局所変数や引数のために必要な)スタックフレーム上での領域の確保, などを行います.
- 関数エピローグでは,概ね,関数プロローグの逆を行います.
callee-saveレジスタの回復の順番も,退避のときと逆になっている点に注意して下さい
(退避の時は
%rbx
→%r12
,回復の時は逆順で%r12
→%rbx
).
- コンパイラに
-O2
などの最適化オプションを指定すると, 不要な命令が削られたり移動するため,プロローグとエピローグの内容が 大きく変わることがあります.
Cコードからアセンブリコードを呼び出す
// asm/mix1/main.c
#include <stdio.h>
int sub (int, int);
int main (void)
{
printf ("%d\n", sub (23, 7));
}
# asm/mix1/sub.s
.text
.globl sub
.type sub, @function
sub:
pushq %rbp
movq %rsp, %rbp
subq %rsi, %rdi
movq %rdi, %rax
leave
ret
.size sub, .-sub
$ gcc -g main.c sub.s
$ ./a.out
16
- 関数規約が守られていれば, Cからアセンブリコードの関数を呼び出したり, アセンブリコードからCの関数を呼び出すことができます.
- 上の例では関数
main
から,アセンブリコード中の関数sub
を呼び出しています.
アセンブリコードからCコードを呼び出す
# asm/mix2/main.s
.text
.globl main
.type sub, @function
main:
pushq %rbp
movq %rsp, %rbp
movq $23, %rdi
movq $7, %rsi
call sub
leave
ret
.size main, .-main
// asm/mix2/sub.c
int sub (int a, int b)
{
return a - b;
}
$ gcc -g main.s sub.c
$ ./a.out
$ echo $?
16
- 上の例ではアセンブリコードからCの関数を呼び出しています.
関数
sub
の計算結果をここでは終了ステータスとして表示しています. 関数sub
が計算結果を%rax
に入れて返した後, 関数main
が%rax
を壊さず終了したので, 引き算の結果がそのまま終了ステータスになっています. - 終了ステータスの値は関数
main
がreturn
した値,またはexit
の引数に渡した値です. ただし,下位1バイトしか受け取れないので,終了ステータスの値は0から255までになります.
アセンブリコードからprintf
を呼び出す
# asm/printf.s
.section .rodata
L_fmt:
.string "%d\n"
.text
.globl main
.type sub, @function
main:
pushq %rbp
movq %rsp, %rbp
leaq L_fmt(%rip), %rdi
movq $999, %rsi
# pushq $888 # ❶このコメントを外すと segmentation fault になることも
movb $0, %al # ❷
call printf
leave
ret
.size main, .-main
$ gcc -g printf.s
$ ./a.out
999
- アセンブリコードから
printf
などのライブラリ関数を呼び出せます. call
命令実行時には%rsp
の値は16の倍数で無くてはいけません (%rsp
のアラインメント制約). なので,❶の行のコメントを外して実行すると,segmentation fault が起きることがあります(起きないこともありますが,それはたまたまです). ❶の行のコメントを外さなければ, 「戻り番地の8バイトと古いrbp
の値の8バイト」でちょうど16バイトが積まれて,%rsp
の値は16の倍数になります.printf
は可変長引数を持つ関数なので,呼び出し前に%al
にベクトルレジスタ(例%xmm0
)の数を入れておく必要があります (ここではベクトルレジスタを使っていないのでゼロに設定).