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.outgdb上で実行
  • ❷ ブレークポイントを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であると表示されています. ($1gdbが扱う変数です.ここでは無視して下さい)
  • echo # %raxの値が999なら成功\ngdbが実行した結果, ❹ # %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の説明1PIEの説明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関数がどのアドレスに配置されるかで変化します. しかし,この相対アドレス❷ -0x7main関数がどのアドレスに配置されても変化しないので, この機械語命令は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.spushq $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 + foofoo絶対アドレス になるので, 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]disp8
foo[foo]dispfoo
(%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:-4fs:[-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$100imm8, imm16, imm32のどれか
$foo
imm8$1008ビットの即値(定数)
imm16$10016ビットの即値(定数)
imm32$10032ビットの即値(定数)
  • 多くの場合,サイズを省略して単にimmと書きます. 特にサイズに注意が必要な時だけ,imm32などとサイズを明記します.
  • 一部例外を除き, x86-64では64ビットの即値を書けません(32ビットまでです).

汎用レジスタ

記法説明
r%raxr8, r16, r32, r64のどれか
r8%al8ビットの汎用レジスタ
r16%ax16ビットの汎用レジスタ
r32%eax32ビットの汎用レジスタ
r64%rax64ビットの汎用レジスタ

メモリ参照

記法説明
r/m%rbpr/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は転送命令ではありませんが,最も簡単な命令ですので最初に説明します.


記法何の略か動作
nopno operation何もしない(プログラムカウンタのみ増加)
nop op1no operation何もしない(プログラムカウンタのみ増加)

詳しい記法例の動作サンプルコード
nopnop何もしないnop.s nop.txt
nop r/mnopl (%rax)何もしないnop2.s nop2.txt


  • nopは何もしない命令です(ただしプログラムカウンタ%ripは増加します). フラグも変化しません.
  • 機械語命令列の間を(何もせずに)埋めるために使います.
  • nopの機械語命令は1バイト長です. (なのでどんな長さの隙間にも埋められます).
  • nop r/m という形式の命令は2〜9バイト長のnop命令になります. 1バイト長のnopを9個並べるより, 9バイト長のnopを1個並べた方が,実行が早くなります.
  • 「複数バイトのnop命令がある」という知識は, 逆アセンブル時にnopl (%rax)などを見て「なんじゃこりゃ」とビックリしないために必要です.

mov命令: データの転送(コピー)


記法何の略か動作
mov␣ op1, op2moveop1の値をop2にデータ転送(コピー)

詳しい記法例の動作サンプルコード
mov␣ r, r/mmovq %rax, %rbx%rbx = %raxmovq-1.s movq-1.txt
movq %rax, -8(%rsp)*(%rsp - 8) = %raxmovq-2.s movq-2.txt
mov␣ r/m, rmovq -8(%rsp), %rax%rax = *(%rsp - 8)movq-3.s movq-3.txt
mov␣ imm, rmovq $999, %rax%rax = 999movq-4.s movq-4.txt
mov␣ imm, r/mmovq $999, -8(%rsp)*(%rsp - 8) = 999movq-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) は値0x118バイトのデータとして(%rsp)に書き込む
    • movl $0x11, (%rsp) は値0x114バイトのデータとして(%rsp)に書き込む
    • movw $0x11, (%rsp) は値0x112バイトのデータとして(%rsp)に書き込む
    • movb $0x11, (%rsp) は値0x111バイトのデータとして(%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/mmovq 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, op2exchangeop1op2 の値を交換する

詳しい記法例の動作サンプルコード
xchg r, r/mxchg %rax, (%rsp)%rax(%rsp)の値を交換するxchg.s xchg.txt
xchg r/m, rxchg (%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, op2load effective addressop1 の実効アドレスを op2 に代入する

詳しい記法例の動作サンプルコード
lea␣ m, rleaq -8(%rsp, %rsi, 4), %rax%rax=%rsp+%rsi*4-8leaq-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

注: 実行時間は命令ごとに異なりますので,命令数だけで 実行時間を比較することはできません.

pushpop命令: スタックとデータ転送


記法何の略か動作
push␣ op1pushop1 をスタックにプッシュ
pop␣ op1popスタックから op1 にポップ

詳しい記法例の動作サンプルコード
push␣ immpushq $999%rsp-=8; *(%rsp)=999push1.s push1.txt
push␣ r/m16pushw %ax%rsp-=2; *(%rsp)=%axpush2.s push2.txt
push␣ r/m64pushq %rax%rsp-=8; *(%rsp)=%raxpush-pop.s push-pop.txt
pop␣ r/m16popw %ax*(%rsp)=%ax; %rsp += 2pop2.s pop2.txt
pop␣ r/m64popq %rbx%rbx=*(%rsp); %rsp += 8push-pop.s push-pop.txt


  • push命令はスタックポインタ%rsp減らしてから, スタックトップ(スタックの一番上)にオペランドの値を格納します.
  • pop命令はスタックトップの値をオペランドに格納してから, スタックポインタを増やします.
  • 64ビットモードでは,32ビットのpushpopはできません.
  • 抽象データ型のスタックは(スタックトップに対する)プッシュ操作とポップ操作しか できませんが,x86-64のスタック操作はスタックトップ以外の部分にも自由にアクセス可能です(例えば,-8(%rsp)-8(%rbp)などへのメモリ参照で).
  • 一番右側の図(popq %rbx後)で,ポップ後も%rspよりも上に古い値が残っています (0x110x88).このように,ポップしてもスタック上に古い値がゴミとして残ります.
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

これらの命令のほとんどが演算の結果として, ステータスフラグ の値を変化させます. 本書ではステータスフラグの変化を以下の記法で表します.

CFOFSFZFPFAF
 !?01

記法の意味は以下の通りです.

記法意味
空白フラグ値に変化なし
!フラグ値に変化あり
?フラグ値は未定義(参照禁止)
0フラグ値はクリア(0になる)
1フラグ値はセット(1になる)

add命令: 足し算


記法何の略か動作
add␣ op1, op2addop1op2 に加える
adc␣ op1, op2add with carryop1 と CF を op2 に加える

詳しい記法例の動作サンプルコード
add␣ imm, r/maddq $999, %rax%rax += 999add-1.s add-1.txt
add␣ r, r/maddq %rax, (%rsp)*(%rsp) += %raxadd-2.s add-2.txt
add␣ r/m, raddq (%rsp), %rax%rax += *(%rsp)add-2.s add-2.txt
adc␣ imm, r/madcq $999, %rax%rax += 999 + CFadc-1.s adc-1.txt
adc␣ r, r/madcq %rax, (%rsp)*(%rsp) += %rax + CFadc-2.s adc-2.txt
adc␣ r/m, radcq (%rsp), %rax%rax += *(%rsp) + CFadc-3.s adc-3.txt

CFOFSFZFPFAF
!!!!!!
  • addadcはオペランドが符号あり整数か符号なし整数かを区別せず, 両方の結果を正しく計算します.
  • 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, op2subtractop1op2 から引く
sbb␣ op1, op2subtract with borrowop1 と CF を op2 から引く

詳しい記法例の動作サンプルコード
sub␣ imm, r/msubq $999, %rax%rax -= 999sub-1.s sub-1.txt
sub␣ r, r/msubq %rax, (%rsp)*(%rsp) -= %raxsub-2.s sub-2.txt
sub␣ r/m, rsubq (%rsp), %rax%rax -= *(%rsp)sub-2.s sub-2.txt
sbb␣ imm, r/msbbq $999, %rax%rax -= 999 + CFsbb-1.s sbb-1.txt
sbb␣ r, r/msbbq %rax, (%rsp)*(%rsp) -= %rax + CFsbb-2.s sbb-2.txt
sbb␣ r/m, rsbbq (%rsp), %rax%rax -= *(%rsp) + CFsbb-2.s sbb-2.txt

CFOFSFZFPFAF
!!!!!!
  • addと同様に,subsbbは オペランドが符号あり整数か符号なし整数かを区別せず, 両方の結果を正しく計算します.
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␣ op1unsigned multiply符号なし乗算.(%rdx:%rax) = %rax * op1
imul␣ op1signed multiply符号あり乗算.(%rdx:%rax) = %rax * op1
imul␣ op1, op2signed multiply符号あり乗算.op2 *= op1
imul␣ op1, op2, op3signed multiply符号あり乗算.op3 = op1 * op2

詳しい記法例の動作サンプルコード
mul␣ r/mmulq %rbx(%rdx:%rax) = %rax * %rbxmul-1.s mul-1.txt
imul␣ r/mimulq %rbx(%rdx:%rax) = %rax * %rbximul-1.s imul-1.txt
imul␣ imm, rimulq $4, %rax%rax *= 4imul-2.s imul-2.txt
imul␣ r/m, rimulq %rbx, %rax%rax *= %rbximul-2.s imul-2.txt
imul␣ imm, r/m, rimulq $4, %rbx, %rax%rax = %rbx * 4imul-2.s imul-2.txt

CFOFSFZFPFAF
!!????

  • オペランドが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␣ op1unsigned divide符号なし除算と余り
%rax = (%rdx:%rax) / op1
%rdx = (%rdx:%rax) % op1
idiv␣ op1signed divide符号あり除算と余り
%rax = (%rdx:%rax) / op1
%rdx = (%rdx:%rax) % op1

詳しい記法例の動作サンプルコード
div␣ r/mdivq %rbx%rax = (%rdx:%rax) / %rbx
%rdx = (%rdx:%rax) % %rbx
div-1.s div-1.txt
idiv␣ r/midivq %rbx%rax = (%rdx:%rax) / %rbx
%rdx = (%rdx:%rax) % %rbx
idiv-1.s idiv-1.txt

CFOFSFZFPFAF
??????
  • 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␣ op1incrementop1の値を1つ増加
dec␣ op1decrementop1の値を1つ減少

詳しい記法例の動作サンプルコード
inc␣ r/minc %rax%rax++inc-1.s inc-1.txt
dec␣ r/mdec %rax%rax--dec-1.s dec-1.txt

  • incdecはオーバーフローしても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␣ op1negation2の補数によるop1の符号反転

詳しい記法例の動作サンプルコード
neg␣ r/mneg %rax%rax = -%raxneg-1.s neg-1.txt

CFOFSFZFPFAF
!!!!!!
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␣ op1bitwise notop1の各ビットの反転 (NOT)

詳しい記法例の動作サンプルコード
not␣ r/mnotq %rax%rax = ~%raxnot-1.s not-1.txt

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, op2bitwise andop1op2の各ビットごとの論理積(AND)
or␣ op1, op2bitwise orop1op2の各ビットごとの論理和(OR)
xor␣ op1, op2bitwise xorop1op2の各ビットごとの排他的論理和(XOR)

詳しい記法例の動作サンプルコード
and␣ imm, r/mandq $0x0FFF, %rax%rax &= 0x0FFFand-1.s and-1.txt
and␣ r, r/mandq %rax, (%rsp)*(%rsp) &= %raxand-1.s and-1.txt
and␣ r/m, randq (%rsp), %rax%rax &= *(%rsp)and-1.s and-1.txt
or␣ imm, r/morq $0x0FFF, %rax%rax |= 0x0FFF or-1.s or-1.txt
or␣ r, r/morq %rax, (%rsp)*(%rsp) |= %raxor-1.s or-1.txt
or␣ r/m, rorq (%rsp), %rax%rax |= *(%rsp)or-1.s or-1.txt
xor␣ imm, r/mxorq $0x0FFF, %rax%rax ^= 0x0FFFxor-1.s xor-1.txt
xor␣ r, r/mxorq %rax, (%rsp)*(%rsp) ^= %raxxor-1.s xor-1.txt
xor␣ r/m, rxorq (%rsp), %rax%rax ^= *(%rsp)xor-1.s xor-1.txt

CFOFSFZFPFAF
00!!!?

xyx & yx | yx ^ y
00000
01011
10011
11110
  • &, |, ^は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/msalq %rax%raxを1ビット算術左シフトsal-1.s sal-1.txt
sal␣ imm8, r/msalq $2, %rax%raxを2ビット算術左シフトsal-1.s sal-1.txt
sal␣ %cl, r/msalq %cl, %rax%rax%clビット算術左シフトsal-1.s sal-1.txt
shl␣ r/mshlq %rax%raxを1ビット論理左シフトshl-1.s shl-1.txt
shl␣ imm8, r/mshlq $2, %rax%raxを2ビット論理左シフトshl-1.s shl-1.txt
shl␣ %cl, r/mshlq %cl, %rax%rax%clビット論理左シフトshl-1.s shl-1.txt
sar␣ r/msarq %rax%raxを1ビット算術右シフトsar-1.s sar-1.txt
sar␣ imm8, r/msarq $2, %rax%raxを2ビット算術右シフトsar-1.s sar-1.txt
sar␣ %cl, r/msarq %cl, %rax%rax%clビット算術右シフトsar-1.s sar-1.txt
shr␣ r/mshrq %rax%raxを1ビット論理右シフトshr-1.s shr-1.txt
shr␣ imm8, r/mshrq $2, %rax%raxを2ビット論理右シフトshr-1.s shr-1.txt
shr␣ %cl, r/mshrq %cl, %rax%rax%clビット論理右シフトshr-1.s shr-1.txt

CFOFSFZFPFAF
!!!!!?
  • 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 carryCFを含めて左ローテート
ror␣ op1[, op2]rotate right右ローテート
rcr␣ op1[, op2]rotate right through carryCFを含めて右ローテート

詳しい記法例の動作サンプルコード
rol␣ r/mrolq %rax%raxを1ビット左ローテートrol-1.s rol-1.txt
rol␣ imm8, r/mrolq $2, %rax%raxを2ビット左ローテートrol-1.s rol-1.txt
rol␣ %cl, r/mrolq %cl, %rax%rax%clビット左ローテートrol-1.s rol-1.txt
rcl␣ r/mrclq %rax%raxを1ビットCFを含めて左ローテートrcl-1.s rcl-1.txt
rcl␣ imm8, r/mrclq $2, %rax%raxを2ビットCFを含めて左ローテートrcl-1.s rcl-1.txt
rcl␣ %cl, r/mrclq %cl, %rax%rax%clビットCFを含めて左ローテートrcl-1.s rcl-1.txt
ror␣ r/mrorq %rax%raxを1ビット右ローテートror-1.s ror-1.txt
ror␣ imm8, r/mrorq $2, %rax%raxを2ビット右ローテートror-1.s ror-1.txt
ror␣ %cl, r/mrorq %cl, %rax%rax%clビット右ローテートror-1.s ror-1.txt
rcr␣ r/mrcrq %rax%raxを1ビットCFを含めて右ローテートrcr-1.s rcr-1.txt
rcr␣ imm8, r/mrcrq $2, %rax%raxを2ビットCFを含めて右ローテートrcr-1.s rcr-1.txt
rcr␣ %cl, r/mrcrq %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]compareop1op2の比較結果をフラグに格納(比較はsub命令を使用)

詳しい記法例の動作サンプルコード
cmp␣ imm, r/mcmpq $999, %raxsubq $999, %raxのフラグ変化のみ計算.オペランドは変更なしcmp-1.s cmp-1.txt
cmp␣ r, r/mcmpq %rax, (%rsp)subq %rax, (%rsp)のフラグ変化のみ計算.オペランドは変更なしcmp-1.s cmp-1.txt
cmp␣ r/m, rcmpq (%rsp), %raxsubq (%rsp), %raxのフラグ変化のみ計算.オペランドは変更なしcmp-1.s cmp-1.txt

CFOFSFZFPFAF
!!!!!!
  • 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 compareop1op2の比較結果をフラグに格納(比較はand命令を使用)

詳しい記法例の動作サンプルコード
test␣ imm, r/mtestq $999, %raxandq $999, %raxのフラグ変化のみ計算.オペランドは変更なしtest-1.s test-1.txt
test␣ r, r/mtestq %rax, (%rsp)andq %rax, (%rsp)のフラグ変化のみ計算.オペランドは変更なしtest-1.s test-1.txt
test␣ r/m, rtestq (%rsp), %raxandq (%rsp), %raxのフラグ変化のみ計算.オペランドは変更なしtest-1.s test-1.txt

CFOFSFZFPFAF
00!!!?
  • cmp命令と同様に,test命令はフラグ計算だけを行います. (レジスタやメモリは変化しません).
  • cmp命令と同様に,test命令は条件付きジャンプ命令と一緒に使うことが多いです. 例えば以下の2命令で「%raxが0ならジャンプする」という意味になります.
testq %rax, %rax
jz L2
  • 例えば%raxが0かどうかを知りたい場合, cmpq $0, %raxtestq %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, op2movsx op2, op1
movsxd op2, op1
move with sign-extentionop1を符号拡張した値をop2に格納
movz␣␣ op1, op2movzx op2, op1move with zero-extentionop1をゼロ拡張した値をop2に格納

詳しい記法例の動作サンプルコード
movs␣␣ r/m, rmovslq %eax, %rbx%rbx = %eaxを8バイトに符号拡張した値movs-movz.s movs-movz.txt
movz␣␣ r/m, rmovzwq %ax, %rbx%rbx = %axを8バイトにゼロ拡張した値movs-movz.s movs-movz.txt

␣␣に入るもの何の略か意味
bwbyte to word1バイト→2バイトの拡張
blbyte to long1バイト→4バイトの拡張
bqbyte to quad1バイト→8バイトの拡張
wlword to long2バイト→4バイトの拡張
wqword to quad2バイト→8バイトの拡張
lqlong to quad4バイト→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形式)
例の動作サンプルコード
cbtwcbwcbtw%al(byte)を%ax(word)に符号拡張cbtw.s cbtw.txt
cwtlcwdecwtl%ax(word)を%eax(long)に符号拡張cbtw.s cbtw.txt
cwtdcwdcwtd%ax(word)を%dx:%ax(double word)に符号拡張cbtw.s cbtw.txt
cltdcdqcltd%eax(long)を%edx:%eax(double long, quad)に符号拡張cbtw.s cbtw.txt
cltqcdqecltd%eax(long)を%rax(quad)に符号拡張cbtw.s cbtw.txt
cqtocqocqto%rax(quad)を%rdx:%rax(octuple)に符号拡張cbtw.s cbtw.txt

  • cqtoなどはidivで割り算する前に使うと便利(%rdx:%raxidivの隠しオペランドなので).
  • 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 op1jumpop1にジャンプ

詳しい記法例の動作サンプルコード
jmp reljmp 0x10000x1000番地に相対直接ジャンプ (%rip += 0x1000)jmp.s jmp.txt
jmp foofoo番地に相対直接ジャンプ (%rip += foo)jmp.s jmp.txt
jmp r/mjmp *%rax*%rax番地に絶対間接ジャンプ (%rip = *%rax))jmp.s jmp.txt
jmp r/mjmp *(%rax)*(%rax)番地に絶対間接ジャンプ (%rip = *(%rax))jmp.s jmp.txt
---
  • x86-64では,相対・直接と絶対・間接の組み合わせしかありません. (つまり,相対・間接ジャンプや絶対・直接ジャンプはありません. なお,ここで紹介していないfarジャンプでは絶対・直接もあります).
  • 相対・直接ジャンプでは符号ありの8ビット(rel8)か 32ビット(rel32)の整数定数で相対アドレスを指定します. (64ビットの相対アドレスは指定できません.64ビットのジャンプをしたい時は 絶対・間接ジャンプ命令を使います).
  • rel8rel32かはアセンブラが勝手に選んでくれます. 逆にjmpbjmplなどとサフィックスをつけて指定することはできません.
  • なぜか,定数なのにrel8rel32にはドルマーク$をつけません. 逆にr/mの前にはアスタリスク*が必要です. GNUアセンブラのこの部分は一貫性がないので要注意です.

条件付きジャンプの概要

  • 条件付きジャンプ命令 j␣は  ステータスフラグ (CF, OF, PF, SF, ZF)をチェックして, 条件が成り立てばジャンプします. 条件が成り立たない場合はジャンプせず,次の命令に実行を進めます.
  • 条件付きジャンプは比較命令と一緒に使うことが多いです. 例えば以下の2命令で「%raxが(符号あり整数として)1より大きければジャンプする」という意味になります.
cmpq $1, %rax
jg L2
  • 条件付きジャンプ命令のニモニックでは次の用語を使い分けます
    • 符号あり整数の大小には less/greater を使う
    • 符号なし整数の大小には above/below を使う

条件付きジャンプ: 符号あり整数用


記法何の略か動作ジャンプ条件
jg rel
jnle rel
jump if greater
jump if not less nor equal
op2>op1ならrelにジャンプ
!(op2<=op1)ならrelにジャンプ
ZF==0&&SF==OF
jge rel
jnl rel
jump if greater or equal
jump if not less
op2>=op1ならrelにジャンプ
!(op2<op1)ならrelにジャンプ
SF==OF
jle rel
jng rel
jump if less or equal
jump if not greater
op2<=op1ならrelにジャンプ
!(op2>op1)ならrelにジャンプ
ZF==1||SF!=OF
jl rel
jnge rel
jump if less
jump if not greater nor equal
op2<op1ならrelにジャンプ
!(op2>=op1)ならrelにジャンプ
SF!=OF

詳しい記法例の動作サンプルコード
jg relcmpq $0, %rax
jg foo
if (%rax>0) goto foojg.s jg.txt
jnle relcmpq $0, %rax
jnle foo
if (!(%rax<=0)) goto foojg.s jg.txt
jge relcmpq $0, %rax
jge foo
if (%rax>=0) goto foojge.s jge.txt
jnl relcmpq $0, %rax
jnl foo
if (!(%rax<0)) goto foojge.s jge.txt
jle relcmpq $0, %rax
jle foo
if (%rax<=0) goto foojle.s jle.txt
jng relcmpq $0, %rax
jng foo
if (!(%rax>0)) goto foojle.s jle.txt
jl relcmpq $0, %rax
jl foo
if (%rax<0) goto foojl.s jl.txt
jnge relcmpq $0, %rax
jnge foo
if (!(%rax>=0)) goto foojl.s jl.txt

  • op1op2 は条件付きジャンプ命令の直前で使用したcmp命令のオペランドを表します.
  • jgjnleは異なるニモニックですが動作は同じです. その証拠にジャンプ条件はZF==0&&SF==OFと共通です. 他の3つのペア,jgejnljlejngjljngeも同様です.
なぜ 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 rel
jnbe rel
jump if above
jump if not below nor equal
op2>op1ならrelにジャンプ
!(op2<=op1)ならrelにジャンプ
CF==0&ZF==0
jae rel
jnb rel
jump if above or equal
jump if not below
op2>=op1ならrelにジャンプ
!(op2<op1)ならrelにジャンプ
CF==0
jbe rel
jna rel
jump if below or equal
jump if not above
op2<=op1ならrelにジャンプ
!(op2>op1)ならrelにジャンプ
CF==1&&ZF==1
jb rel
jnae rel
jump if below
jump if not above nor equal
op2<op1ならrelにジャンプ
!(op2>=op1)ならrelにジャンプ
CF==1

詳しい記法例の動作サンプルコード
ja relcmpq $0, %rax
ja foo
if (%rax>0) goto fooja.s ja.txt
jnbe relcmpq $0, %rax
jnbe foo
if (!(%rax<=0)) goto fooja.s ja.txt
jae relcmpq $0, %rax
jae foo
if (%rax>=0) goto foojae.s jae.txt
jnb relcmpq $0, %rax
jnb foo
if (!(%rax<0)) goto foojae.s jae.txt
jbe relcmpq $0, %rax
jbe foo
if (%rax<=0) goto foojbe.s jbe.txt
jna relcmpq $0, %rax
jna foo
if (!(%rax>0)) goto foojbe.s jbe.txt
jb relcmpq $0, %rax
jb foo
if (%rax<0) goto foojb.s jb.txt
jnae relcmpq $0, %rax
jnae foo
if (!(%rax>=0)) goto foojb.s jb.txt

  • op1op2 は条件付きジャンプ命令の直前で使用したcmp命令のオペランドを表します.
  • jajnbeは異なるニモニックですが動作は同じです. その証拠にジャンプ条件はCF==0&&ZF==0と共通です. 他の3つのペア,jaejnbjbejnajbjnaeも同様です.

条件付きジャンプ: フラグ用


記法何の略か動作ジャンプ条件
jc reljump if carryCF==1ならrelにジャンプCF==1
jnc reljump if not carryCF==0ならrelにジャンプCF==0
jo reljump if overflowOF==1ならrelにジャンプOF==1
jno reljump if not overflowOF==0ならrelにジャンプOF==0
js reljump if signSF==1ならrelにジャンプSF==1
jns reljump if not signSF==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 reljc fooif (CF==1) goto foojc.s jc.txt
jnc reljnc fooif (CF==0) goto foojc.s jc.txt
jo reljo fooif (OF==1) goto foojo.s jo.txt
jno reljno fooif (OF==0) goto foojo.s jo.txt
js reljs fooif (SF==1) goto foojs.s js.txt
jns reljns fooif (SF==0) goto foojs.s js.txt
jz reljz fooif (ZF==1) goto foojz.s jz.txt
je relcmpq $0, %rax
je foo
if (%rax==0) goto foojz.s jz.txt
jnz reljnz fooif (ZF==0) goto foojz.s jz.txt
jne relcmpq $0, %rax
jne foo
if (%rax!=0) goto foojz.s jz.txt
jp reljp fooif (PF==1) goto foojp.s jp.txt
jpe reljpe fooif (PF==1) goto foojp.s jp.txt
jnp reljnp fooif (PF==0) goto foojp.s jp.txt
jpo reljpo fooif (PF==0) goto foojp.s jp.txt

  • jzjeは異なるニモニックですが動作は同じです. その証拠にジャンプ条件はZF==1と共通です. 他の3つのペア,jnzjnejpjpejnpjpoも同様です.
  • AFフラグのための条件付きジャンプ命令は存在しません.

call, ret命令: 関数を呼び出す,リターンする


記法何の略か動作
call op1call procedure%ripをスタックにプッシュしてから op1にジャンプする
(pushq %rip; %rip = op1)
retreturn from procedureスタックからポップしたアドレスにジャンプする
(popq %rip)

詳しい記法例の動作サンプルコード
call relcall foo相対・直接の関数コールcall.s call.txt
call r/mcall *%rax絶対・間接の関数コールcall.s call.txt
retret関数からリターン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から関数foocall命令で呼び出して, 関数fooから関数mainret命令でリターンしています.

# 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実行直前: 図(左)が示す通り%ripcall foo命令を指しています. ここで,call foo命令を実行すると,

    • %ripcall foo命令の次の命令(ここではmain関数中のret命令)を指します. (%ripは「実行中の命令の次の命令」を指すことを思い出しましょう).
    • call fooはまず%ripの値(上図では❶0x40110C)をスタックにプッシュします. その結果,スタック上に0x40110Cが書き込まれます. この0x40110Cが(関数fooからリターンする際の)戻り番地となります.
    • 次に,call fooは関数fooの先頭番地(上図では❷0x401106)にジャンプします.
  • call foo実行直後: 図(中)が示す通り%ripfoo関数のret命令を指しています. 一方,スタックトップ(%rspが指している場所)には 戻り番地0x40110Cが格納されています. ここで,ret命令を実行すると,

    • スタックから戻り番地 0x40110Cをポップして取り出して, %ripに格納します(つまり0x40110C番地にジャンプします).
  • 関数fooret実行直後: 無事に関数maincall 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の型は「関数へのポインタ」になり, fpadd5は同じ型になります (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, op2make stack frameサイズop1のスタックフレームを作成する
leavediscard stack frame今のスタックフレームを破棄する

詳しい記法例の動作サンプルコード
enter imm16, imm8enter $0x20, $0pushq %rbp
movq %rsp, %rbp
subq $0x20, %rsp
enter.s enter.txt
leaveleavemovq %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行(leaveret)が 「関数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間のお約束」です.例えば,以下を定めます:

引数の渡し方

引数レジスタ
第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を壊さず終了したので, 引き算の結果がそのまま終了ステータスになっています.
  • 終了ステータスの値は関数mainreturnした値,または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)の数を入れておく必要があります (ここではベクトルレジスタを使っていないのでゼロに設定).