戻る

x86_64 プログラミング入門

レジスタと算術演算

好きなエディタを開いて、add.s というテキストファイルを作り、中に以下のように書く

x8664_asm_language/add.s

	.globl	main
main:
	add $1, %rax
        ret

続いて、gcc を使って、これを実行ファイルに変換する。

 $ gcc add.s
 $ ls
 add.s
 a.out

間違いがなければ、同じディレクトリに、a.out という実行ファイルができているはずだ。

次に、gdb に a.out を指定して起動する。(デバッガは、CPUやメモリの状態を調べるのに、非常に有用なツールである。必要な使いかたは都度説明するが、可能ならば色々な使いかたを知っておくことをおすすめする)

 $ gdb a.out
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...done.
(gdb) 

gdb のプロンプトが出るはずだ。このプロンプトに対して、

と、打ちこんでみよう。

(gdb) start
Temporary breakpoint 1 at 0x4004d6
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004d6 in main ()
(gdb) info registers
rax            0x4004d6	4195542
rbx            0x0	0
rcx            0x0	0
rdx            0x7ffffffee518	140737488282904
rsi            0x7ffffffee508	140737488282888
rdi            0x1	1
rbp            0x4004e0	0x4004e0 <__libc_csu_init>
rsp            0x7ffffffee428	0x7ffffffee428
r8             0x400550	4195664
r9             0x7fffff410ab0	140737475840688
r10            0x846	2118
r11            0x7fffff050740	140737471907648
r12            0x4003e0	4195296
r13            0x7ffffffee500	140737488282880
r14            0x0	0
r15            0x0	0
rip            0x4004d6	0x4004d6 <main>
eflags         0x246	[ PF ZF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

このような出力がされると思う。(実際の値は、OSやライブラリによって異なるため、同じ値ではなくてもよいです)

現代の多くのCPUは、レジスタ と呼ばれる、 ごく少量のメモリ ———メモリってなんだ?という疑問を忘れないで!それについてはそのうち説明しよう!とりあえずここはデータを保存する領域と思ってもらいたい——— を、搭載している。

gdb の info registers というコマンドは、そのレジスタを表示するコマンドである。

info registers の出力を見て、以下のような情報を読み取ってほしい。


                        値の10進表現
rax            0x4004d6	4195542
レジスタ名     値の16進表現

レジスタには、いくつか種類があり、レジスタによって使える場面に制限がある。 現代の一般的なCPUのレジスタは、以下のように分類される

gdb の info registers 表示されているレジスタの分類は、以下のとおりである。

と、なっている。え…スタックポインタとかセグメントレジスタって何…?

x86_64 は、PCが誕生する(より以前?)から存在したCPUの仕様をいくらかひきずっており、現代のCPUでは見られない *生きた化石* のようなレジスタが見られるのが特徴である。このへんの話は、話がずれるので、そのうち書くことにする。とりあえずセグメントレジスタは忘れてもらって構わない。一時期は消滅したスタックポインタが、aarch64 で復活したのは興味深い点ではある。

まず最初は、汎用レジスタだ。汎用レジスタは、多くの命令の入力、出力用の領域として使うことができ、プログラマから見たとき、一番目にすることが多いはずだ。

最初に書いたプログラムをもう一度見てもらいたい

	.globl	main
main:
	add $1, %rax
        ret

まず、.globl 、 次に main: と、あるが、これはあとのリンカのところで説明しよう。

次に来るのが、 add $1, %rax という行だ。

これは、「アセンブリ言語でのadd命令」を書いた行で、意味は、

	add $1, %rax
                rax レジスタに対して、
            1を
        足す

と、なる。

一般的なCPUでは、多くの命令は、以下のような形式をしている

        instruction    operand0, operand1

- instruction : 命令の名前、何をするかを指示する
- operand0 : 0番目のオペランド、どのレジスタに対して命令を実行するかを指示する
- operand1 : 1番目のオペランド、命令によっては、複数のレジスタを入出力にとることがあり、その場合は、コンマで区切って 1番目、2番目のオペランド、というように指示していく

だがしかし!悲しいかな、x86_64 の Linux では、アセンブリの表記方法が一般的ではなく、

        instruction    operand1, operand0

というように、operand1 と operand0 の順序が入れかわってしまう(AT&T記法)。これは、Intel のマニュアルの表記(Intel記法)とは異なっており、完全な初心者殺しである。

標準にあわせるオプションもあるが、gdbなど各種ツールの出力がAT&T記法なので慣れるしかない。ここ以降、出現する命令の表記では、operand1, operand0 の順番になっており、Intel のマニュアルとは順序が異なっているという点を頭に入れて読んでほしい。

さて、それでは、add 命令の挙動を見てみよう。さきほどの gdb のコンソールに戻ろう(gdbのコンソールをなくしてしまった人は、もう一度gdbを起動して、start を実行しよう)

まず、gdbのコンソール に disassemble と入力する

(gdb) disassemble
Dump of assembler code for function main:
=> 0x00000000004004d6 <+0>:	add    $0x1,%rax
   0x00000000004004da <+4>:	retq   
   0x00000000004004db <+5>:	nopl   0x0(%rax,%rax,1)
End of assembler dump.

このような文字が表示されるはずだ。"disassemble" コマンドは、現在停止中の関数に含まれる命令を表示するコマンドで、今は、プログラムの開始地点、main関数を実行しようというところで停止しているので、main 関数に含まれる命令列を表示している。

いや、"main関数"について説明が足りてない、これは、詳しくはあとでリンクのところで説明するが、一応簡単に説明しておこう。

最初に書いたプログラムを見てほしい。

main:
	add $1, %rax
        ret

"main:" という行を最初に書いたはずだ。C言語を書いたことがある人なら、main関数は見たことあるだろう。 この、"main:" という行は、ラベル と呼ばれる、C言語の関数にかなり近い物体を作るように指示する行で、 このようにラベルを書くことで、 デバッガから見たときに、ここに、C言語のmain関数のようなものが存在するように見えるのである。 (実際に、gdbは、 "Dump of assembler code for function main:"、"main関数のアセンブラコードのダンプ" と言っている点に注目しよう)

さて、最初にgdbを起動したときに、"start" コマンドを実行したことを思い出してほしい。

"start" コマンドは、「プログラムを起動し、main関数の先頭でプログラムを一旦停止する」というコマンドである。 そのため、"start"コマンドを実行した直後に、"disassemble" コマンドを実行すると、main関数に含まれる命令列がダンプされるのだ。

disassemble の出力を見てみよう。

(gdb) disassemble
Dump of assembler code for function main:
=> 0x00000000004004d6 <+0>:	add    $0x1,%rax
   0x00000000004004da <+4>:	retq   
   0x00000000004004db <+5>:	nopl   0x0(%rax,%rax,1)
End of assembler dump.
=> 0x00000000004004d6 <+0>:	add    $0x1,%rax

の、ように、=> で行が矢印で指されているのがわかると思う。これが、現在プログラムが停止している位置を示している。

add.s では、main 関数の先頭に、add 命令を書いたので、これを実行しようという直前で停止しているわけだ。 では、ここで指されている命令を実行してみよう。

gdb には、一命令だけ、命令を実行する、stepi (step instruction)というコマンドがあるこれを実行しよう。そして、一命令実行したら、もう一度 info register として、レジスタを表示しよう。

次のようになるはずだ。

(gdb) info register
rax            0x4004d6	4195542
rbx            0x0	0
rcx            0x0	0
rdx            0x7ffffffee518	140737488282904
rsi            0x7ffffffee508	140737488282888
rdi            0x1	1
rbp            0x4004e0	0x4004e0 <__libc_csu_init>
rsp            0x7ffffffee428	0x7ffffffee428
r8             0x400550	4195664
r9             0x7fffff410ab0	140737475840688
r10            0x846	2118
r11            0x7fffff050740	140737471907648
r12            0x4003e0	4195296
r13            0x7ffffffee500	140737488282880
r14            0x0	0
r15            0x0	0
rip            0x4004d6	0x4004d6 <main>
eflags         0x246	[ PF ZF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) stepi 
0x00000000004004da in main ()
(gdb) info register
rax            0x4004d7	4195543
rbx            0x0	0
rcx            0x0	0
rdx            0x7ffffffee518	140737488282904
rsi            0x7ffffffee508	140737488282888
rdi            0x1	1
rbp            0x4004e0	0x4004e0 <__libc_csu_init>
rsp            0x7ffffffee428	0x7ffffffee428
r8             0x400550	4195664
r9             0x7fffff410ab0	140737475840688
r10            0x846	2118
r11            0x7fffff050740	140737471907648
r12            0x4003e0	4195296
r13            0x7ffffffee500	140737488282880
r14            0x0	0
r15            0x0	0
rip            0x4004da	0x4004da <main+4>
eflags         0x206	[ PF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) 

rax レジスタの値に注目してほしい

(gdb) info register
rax            0x4004d6	4195542
...(略)...
(gdb) stepi 
0x00000000004004da in main ()
(gdb) info register
rax            0x4004d7	4195543
...(略)...

stepi を実行したあとに、rax の値が1増えているのが確認できる。これが、add 命令の効果である。add $1, %rax をという命令を実行すると、 rax レジスタに入っている命令が、1増えるのだ。

ここまで確認したら、一旦gdbを終了しよう。gdb を終了するのは、quit コマンドだ。

(gdb) quit
A debugging session is active.

	Inferior 1 [process 1665] will be killed.

Quit anyway? (y or n) y
 $ 

「プログラムが実行中だが終了してよいか?」と聞かれるが、今は重要なプログラムを実行しているわけではないので、y でいい。 ここで add 命令の次に書いた ret は何?と、疑問に思っている方もいるかもしれない。それについては、あとのOSインターフェースのところで解説する。

いくつか解説をはさんでしまったので、ここまでの流れをざっともう一度流しておこう

$ gdb a.out
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x4004d6
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004d6 in main ()
(gdb) disassemble
Dump of assembler code for function main:
=> 0x00000000004004d6 <+0>:	add    $0x1,%rax
   0x00000000004004da <+4>:	retq   
   0x00000000004004db <+5>:	nopl   0x0(%rax,%rax,1)
End of assembler dump.
(gdb) info registers
rax            0x4004d6	4195542
rbx            0x0	0
rcx            0x0	0
rdx            0x7ffffffee518	140737488282904
rsi            0x7ffffffee508	140737488282888
rdi            0x1	1
rbp            0x4004e0	0x4004e0 <__libc_csu_init>
rsp            0x7ffffffee428	0x7ffffffee428
r8             0x400550	4195664
r9             0x7fffff410ab0	140737475840688
r10            0x846	2118
r11            0x7fffff050740	140737471907648
r12            0x4003e0	4195296
r13            0x7ffffffee500	140737488282880
r14            0x0	0
r15            0x0	0
rip            0x4004d6	0x4004d6 <main>
eflags         0x246	[ PF ZF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) stepi
0x00000000004004da in main ()
(gdb) info registers
rax            0x4004d7	4195543
rbx            0x0	0
rcx            0x0	0
rdx            0x7ffffffee518	140737488282904
rsi            0x7ffffffee508	140737488282888
rdi            0x1	1
rbp            0x4004e0	0x4004e0 <__libc_csu_init>
rsp            0x7ffffffee428	0x7ffffffee428
r8             0x400550	4195664
r9             0x7fffff410ab0	140737475840688
r10            0x846	2118
r11            0x7fffff050740	140737471907648
r12            0x4003e0	4195296
r13            0x7ffffffee500	140737488282880
r14            0x0	0
r15            0x0	0
rip            0x4004da	0x4004da <main+4>
eflags         0x206	[ PF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) quit
A debugging session is active.

	Inferior 1 [process 1770] will be killed.

Quit anyway? (y or n) y

もうひとつ、よく使う gdb のコマンドを紹介しておこう。print コマンドだ。

(gdb) start
Temporary breakpoint 1 at 0x4004d6
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004d6 in main ()
(gdb) print $rax
$1 = 4195542
(gdb) print /x $rax
$2 = 0x4004d6
(gdb) print 45 * 4
$3 = 180
(gdb) print /x 255
$5 = 0xff
(gdb) print $r8
$7 = 4195664
(gdb) print $r9
$8 = 140737475840688
(gdb) print $ds
$9 = 0
(gdb) print $cs
$10 = 51
(gdb) print $rax + 512
$11 = 4196054

print コマンドは、コマンドに与えた式の値を表示してくれる。"/x" を付けると、値を16進で表示だ。 print $rax + 512 のように、頭に'$' を付けて、レジスタ名を書くと、式の中にレジスタの値を入れることができる。

さて、それでは gdb で命令を実行して、結果を表示する方法は解説したので、いくつか基本的な命令を説明しておこう。

レジスタに値を設定する、mov 命令。

x8664_asm_language/mov.s

	.globl	main
main:
	mov $1, %rax
        ret

(gdb) start
Temporary breakpoint 1 at 0x4004d6
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004d6 in main ()
(gdb) disassemble
Dump of assembler code for function main:
=> 0x00000000004004d6 <+0>:	mov    $0x1,%rax
   0x00000000004004dd <+7>:	retq   
   0x00000000004004de <+8>:	xchg   %ax,%ax
End of assembler dump.
(gdb) p $rax
$1 = 4195542
(gdb) stepi
0x00000000004004dd in main ()
(gdb) disassemble
Dump of assembler code for function main:
   0x00000000004004d6 <+0>:	mov    $0x1,%rax
=> 0x00000000004004dd <+7>:	retq   
   0x00000000004004de <+8>:	xchg   %ax,%ax
End of assembler dump.
(gdb) p $rax
$2 = 1

rax の値が 1 になる。

p コマンドは、print の略で、gdb では、よく使うコマンドは、省略できるという機能がある。 printはよく使うので、p 一文字で実行できる。 同じように、info registers も、"i r" で実行可能だ。慣れてくると使ってみるのもよいかもしれない。

多くの演算は、レジスタ間で演算することが可能だ。

x8664_asm_language/add2.s

	.globl	main
main:
	mov $1, %rax
	mov $2, %r8
	add %r8, %rax
	ret

とすると、r8 レジスタの値を、raxレジスタに加算できる。

 
(gdb) start
Temporary breakpoint 1 at 0x4004d6
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004d6 in main ()
(gdb) stepi
0x00000000004004dd in main ()
(gdb) stepi
0x00000000004004e4 in main ()
(gdb) p $rax
$1 = 1
(gdb) p $r8
$2 = 2
(gdb) disassemble
Dump of assembler code for function main:
   0x00000000004004d6 <+0>:	mov    $0x1,%rax
   0x00000000004004dd <+7>:	mov    $0x2,%r8
=> 0x00000000004004e4 <+14>:	add    %r8,%rax
   0x00000000004004e7 <+17>:	retq   
   0x00000000004004e8 <+18>:	nopl   0x0(%rax,%rax,1)
End of assembler dump.
(gdb) stepi
0x00000000004004e7 in main ()
(gdb) p $rax
$3 = 3
(gdb) p $r8
$4 = 2
(gdb) 

rax レジスタの値が、1 + 2 = 3 になっていることを確認しよう。

subで減算、imul で乗算、and,or,xor で bitwise and, or, xor だ。

x8664_asm_language/sub.s

	.globl	main
main:
	mov $1, %rax
	mov $2, %r8
	sub %r8, %rax

	mov $3, %rax
	mov $4, %r8
	imul %r8, %rax

	mov $0xaa, %rax
	mov $0x0f, %r8
	and %r8, %rax

	mov $0xaa, %rax
	mov $0x0f, %r8
	or %r8, %rax

	mov $0xaa, %rax
	mov $0x0f, %r8
	xor %r8, %rax

	ret
Temporary breakpoint 2, 0x00000000004004d6 in main ()
(gdb) stepi 3
0x00000000004004e7 in main ()
(gdb) p $rax
$5 = -1
(gdb) stepi 3
0x00000000004004f9 in main ()
(gdb) p $rax
$6 = 12
(gdb) stepi 3
0x000000000040050a in main ()
(gdb) p /x $rax
$7 = 0xa
(gdb) stepi 3
0x000000000040051b in main ()
(gdb) p /x $rax
$8 = 0xaf
(gdb) stepi 3
0x000000000040052c in main ()
(gdb) p /x $rax
$11 = 0xa5
(gdb) 

gdb の stepi コマンドは、引数に数字を渡すと、その回数だけ繰り返してくれる。stepi 3 の意味は、命令を3個実行だ。

メインメモリ、ロード、ストア命令

x86_64 では、さきほど見たように、汎用レジスタが16個あった。汎用レジスタは一個8byteあるので、16個あれば、128byteのデータを保持できることになる。 128byte というのは、本当に小さなサイズだ。画像で言うと、8x8ピクセルの画像も保持できないし、今書いてるこの文章も扱えないほど、ほんとうに小さなサイズだ。 たった128byteのデータでできることなんてほとんど何もない。月へ行くことだって難しいだろう。

今のコンピュータのように、大きな写真や、動画を見たり編集したりするには、128byteよりも、もっともっと大きなデータを扱える必要がある。 現代の一般的なコンピュータは、この大きなデータを保持するために、「メインメモリ」、日本の情報の教科書的に言うと、「主記憶装置」と呼ばれるものが付いている。

メインメモリは、データを保存するためのデバイスで、保存したデータをアドレス と呼ばれる整数値で指定し、次のふたつの操作をすることができる。

(TODO:図を入れたい)

例えば、次のような操作が可能だ

  1. アドレス 16 番に、データ 0xcc をストア
  2. アドレス 20 番に、データ 0x88 をストア
  3. アドレス 16 番からロード、 データ 0xcc を取り出すことができる
  4. アドレス 20 番からロード、 データ 0x88 を取り出すことができる

各アドレスによって識別できるデータは、完全に独立していて、

  1. アドレス 16 番に、データ 0xcc をストア
  2. アドレス 20 番に、データ 0x88 をストア
  3. アドレス 20 番からロード、 データ 0x88 を取り出すことができる
  4. アドレス 16 番からロード、 データ 0xcc を取り出すことができる

のように、アドレス毎に、最後にストアしたデータが読めるようになっている。

また、ロードする操作では、データは変わらず、何度ロードしても最後にストアしたデータが読める

  1. アドレス 20 番に、データ 0x88 をストア
  2. アドレス 20 番からロード、 データ 0x88 を取り出すことができる
  3. アドレス 20 番からロード、 データ 0x88 を取り出すことができる
  4. アドレス 20 番からロード、 データ 0x88 を取り出すことができる
  5. アドレス 20 番からロード、 データ 0x88 を取り出すことができる
  6. アドレス 20 番に、データ 0xcc をストア
  7. アドレス 20 番からロード、 データ 0xcc を取り出すことができる

"メイン"メモリ ("主"記憶装置) は、"メイン" ("主")と呼ばれるだけあって、コンピュータを使うときに、レジスタの次によく使われる記憶用デバイスだ。 メインメモリはCPUの外側にあるのだが、よく使うデバイスなので、メインメモリを操作するための専用の命令が、CPUの中に用意されている。

その専用命令が、ロード命令ストア命令 だ。どのぐらいよく使うかというと、add 命令と同じか、それ以上の頻度で使われる命令だ。

(実際には、この説明は正確ではない。正しい解説はもう少しあとで書く)

ロード命令 は、メインメモリにアドレスで識別されるデータを要求し、得られたデータをレジスタに格納する命令で、 ストア命令 は、メインメモリにレジスタに含まれるデータを、指定したアドレスに保存するように、メインメモリに要求する命令だ。

x86_64 の AT&T 記法では、それぞれ次のように書く

見てわかるかもしれないが、x86_64 では、ロード命令も、ストア命令も両方とも、"mov命令" だ。ニーモニックとしては区別されず、オペランドの順序が変わっているだけである

これはややこしいかもしれないが、そうなってしまっているので仕方ない。 筆者はロード命令とストア命令は別物だと理解したほうが理解しやすいと思っているので、mov 命令を見たときは、

の、どれになるかをきちんと区別するのをお勧めしたい。区別の仕方を覚えよう

(実際は、もう少し話がややこしい。これはx86_64機械語のところで説明する)

ともかく、言いたかったこととしては、mov命令を使うと、メインメモリの中のアドレスで識別できる領域に対して、データを保存したり、保存したデータを取り出したりできるということだ。

それでは、早速、このmov命令の挙動を見てみよう。

x8664_asm_language/load.s

	.globl	main
main:
	mov $99, %rax
	mov %rax,0  		# アドレス0で識別される領域に、raxの値(99)を保存する
	mov 0, %r8 		# アドレス0で識別される領域から、値を取り出し、その値をr8に格納する
	ret

これをさきほどと同じように、gcc で a.out に変換し、それを gdb で見てみよう

 $ gcc load.s
 $ gdb a.out
GNU gdb (GDB) 8.2
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x401106
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000401106 in main ()
(gdb) stepi
0x000000000040110d in main ()
(gdb) stepi

Program received signal SIGSEGV, Segmentation fault.
0x000000000040110d in main ()
(gdb) stepi

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
(gdb) 

おや…何かがおかしい…プログラムが終了してしまった。

これは、OSのメモリ保護機能が働いた結果だ。

メインメモリは、各コンピュータに一個か、数個ぐらいしかない。 その一個のメインメモリの上で、たくさんのプログラムが動いている。 (例えば、あなたは、この文章をブラウザで見ながら、エディタでプログラムを編集し、gccやgdbを起動している) 一個のメインメモリを、複数のプログラムで共有しているわけだ。

何の仕組みもなく、一個のメモリを複数のプログラムで共有すると、他のプログラムのデータが読み書きできるので、あまり良くない。 他人が動かしたプログラムのデータが読めるのはセキュリティ的によくないし、 一個のプログラムの小さなミスがシステム全体を止めてしまう可能性があるのは、色々問題があるし使いづらいだろう。

これを防ぐために、一般的なOSには、メモリを保護する機能が付いている。 このメモリ保護の仕組みは、低レベルプログラミングをする上で避けて通れない道なので、あとで詳しく書こう。 とりあえず、今はOSにはメモリを保護する仕組みがあって、メインメモリはいつでも自由に使えるわけではないという点だけ覚えておいてほしい。

では、どうするか。 メモリ保護機能のあるOSには、メモリ割り当て(memory allocation)のインターフェースが用意されており、 そのインターフェースを使うことで、メインメモリの一部を、自由に使えるメモリとしてOSから割り当ててもらうことができる。

メモリを割り当てる方法は、いくつかあるが、一番簡単な方法は、単にプログラムを起動するだけだ。

OSはプログラムを起動するように指示されると、起動するプログラムに必要なメモリを割り当ててから、プログラムの起動を行う。 いわゆる実行ファイル(Windowsのexe等や、さっきあなたが作ったa.out)には、この、起動時に必要なメモリサイズを示す情報が含まれており、OSはその情報を見ることで、 プログラムが使うメモリサイズを判断している。

これはリンカのところでもう少し詳しく説明するが、簡単に見ておこう。Linux では、 readelf というコマンドを、-l を付けて実行することで、 実行ファイルが使うメモリサイズを知ることができる。

$ gcc add.s
$ readelf -l a.out

Elf ファイルタイプは DYN (共有オブジェクトファイル) です
Entry point 0x1020
There are 11 program headers, starting at offset 64

プログラムヘッダ:
  タイプ        オフセット          仮想Addr           物理Addr
            ファイルサイズ        メモリサイズ         フラグ 整列
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000528 0x0000000000000528  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001a5 0x00000000000001a5  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000000b8 0x00000000000000b8  R      0x1000
  LOAD           0x0000000000002e28 0x0000000000003e28 0x0000000000003e28
                 0x0000000000000200 0x0000000000000208  RW     0x1000
  DYNAMIC        0x0000000000002e38 0x0000000000003e38 0x0000000000003e38
                 0x00000000000001a0 0x00000000000001a0  RW     0x8
  NOTE           0x00000000000002c4 0x00000000000002c4 0x00000000000002c4
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_EH_FRAME   0x0000000000002004 0x0000000000002004 0x0000000000002004
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RWE    0x10
  GNU_RELRO      0x0000000000002e28 0x0000000000003e28 0x0000000000003e28
                 0x00000000000001d8 0x00000000000001d8  R      0x1

 セグメントマッピングへのセクション:
  セグメントセクション...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn 
   03     .init .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss 
   06     .dynamic 
   07     .note.ABI-tag .note.gnu.build-id 
   08     .eh_frame_hdr 
   09     
   10     .init_array .fini_array .dynamic .got 

以下の部分に注目しよう

  タイプ        オフセット          仮想Addr           物理Addr
            ファイルサイズ        メモリサイズ         フラグ 整列
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000528 0x0000000000000528  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001a5 0x00000000000001a5  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000000b8 0x00000000000000b8  R      0x1000
  LOAD           0x0000000000002e28 0x0000000000003e28 0x0000000000003e28
                 0x0000000000000200 0x0000000000000208  RW     0x1000

PHDR, INTERP は、また説明が難しいので飛ばすとして、重要なのは、 "LOAD" と書かれている箇所だ。

"LOAD" は、プログラムを実行するときに使うメモリについての情報を格納した領域で、

  LOAD           0x0000000000002e28 0x0000000000003e28 0x0000000000003e28
                 0x0000000000000200 0x0000000000000208  RW     0x1000

これは簡単にいうと「実行ファイルの 0x2e28 の位置にあるデータを、メモリ0x3e28 に 0x200 byte 分コピー、領域として、0x208 byte分確保しておく」という意味だ。

OSは、プログラム実行時に、この情報を見て、必要なメモリの確保を行ってからプログラムを起動してくれる。

ただ、これは、まだ少し説明が正しくなくて…これも今は説明するのが難しいので説明はあとにするが… gcc を使うときに、以下のように、"-static -no-pie" を付けてコンパイルすると この説明と正しく一致するようになる。

$ gcc -static -no-pie add.s
~/src/pllp/docs/1 $ readelf -l a.out

Elf ファイルタイプは EXEC (実行可能ファイル) です
Entry point 0x401a30
There are 8 program headers, starting at offset 64

プログラムヘッダ:
  タイプ        オフセット          仮想Addr           物理Addr
            ファイルサイズ        メモリサイズ         フラグ 整列
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000470 0x0000000000000470  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x000000000007c681 0x000000000007c681  R E    0x1000
  LOAD           0x000000000007e000 0x000000000047e000 0x000000000047e000
                 0x00000000000235f0 0x00000000000235f0  R      0x1000
  LOAD           0x00000000000a20a0 0x00000000004a30a0 0x00000000004a30a0
                 0x00000000000051f0 0x0000000000006940  RW     0x1000
  NOTE           0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x0000000000000044 0x0000000000000044  R      0x4
  TLS            0x00000000000a20a0 0x00000000004a30a0 0x00000000004a30a0
                 0x0000000000000020 0x0000000000000060  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RWE    0x10
  GNU_RELRO      0x00000000000a20a0 0x00000000004a30a0 0x00000000004a30a0
                 0x0000000000002f60 0x0000000000002f60  R      0x1

 セグメントマッピングへのセクション:
  セグメントセクション...
   00     .note.ABI-tag .note.gnu.build-id .rela.plt 
   01     .init .plt .text __libc_freeres_fn .fini 
   02     .rodata .eh_frame .gcc_except_table 
   03     .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs 
   04     .note.ABI-tag .note.gnu.build-id 
   05     .tdata .tbss 
   06     
   07     .tdata .init_array .fini_array .data.rel.ro .got 

説明をもう少し単純にするために、これに加えて、gcc のオプションに "-Tbss=0x800000" を付けよう。 こうすることで、OSに対して "自由に読み書きできるメインメモリをアドレス0x800000に割り当ててからプログラムを起動してください" と指示できる実行ファイルができあがる。

$ gcc -static -no-pie -Tbss=0x800000 add.s
$ ./a.out 
$ readelf -l a.out

Elf ファイルタイプは EXEC (実行可能ファイル) です
Entry point 0x401a30
There are 10 program headers, starting at offset 64

プログラムヘッダ:
  タイプ        オフセット          仮想Addr           物理Addr
            ファイルサイズ        メモリサイズ         フラグ 整列
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000004e0 0x00000000000004e0  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x000000000007c681 0x000000000007c681  R E    0x1000
  LOAD           0x000000000007e000 0x000000000047e000 0x000000000047e000
                 0x00000000000235f0 0x00000000000235f0  R      0x1000
  LOAD           0x00000000000a20a0 0x00000000004a30a0 0x00000000004a30a0
                 0x00000000000051f0 0x00000000000051f0  RW     0x1000
  LOAD           0x00000000000a8000 0x0000000000800000 0x0000000000800000
                 0x0000000000000000 0x0000000000001740  RW     0x1000
  NOTE           0x0000000000000270 0x0000000000400270 0x0000000000400270
                 0x0000000000000020 0x0000000000000020  R      0x4
  NOTE           0x0000000000000290 0x0000000000400290 0x0000000000400290
                 0x0000000000000024 0x0000000000000024  R      0x4
  TLS            0x00000000000a20a0 0x00000000004a30a0 0x00000000004a30a0
                 0x0000000000000020 0x0000000000000060  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RWE    0x10
  GNU_RELRO      0x00000000000a20a0 0x00000000004a30a0 0x00000000004a30a0
                 0x0000000000002f60 0x0000000000002f60  R      0x1

 セグメントマッピングへのセクション:
  セグメントセクション...
   00     .note.ABI-tag .note.gnu.build-id .rela.plt 
   01     .init .plt .text __libc_freeres_fn .fini 
   02     .rodata .eh_frame .gcc_except_table 
   03     .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit 
   04     .bss __libc_freeres_ptrs 
   05     .note.ABI-tag 
   06     .note.gnu.build-id 
   07     .tdata .tbss 
   08     
   09     .tdata .init_array .fini_array .data.rel.ro .got 

以下の部分を見てみよう

  LOAD           0x00000000000a8000 0x0000000000800000 0x0000000000800000
                 0x0000000000000000 0x0000000000001740  RW     0x1000

アドレス 0x800000 に 0x1740 byte 分のメモリを割り当てるように指示する情報が作られているのが確認できるはずだ。(0x1740byte という値はどこから来たんだ?これもあとで説明しよう!あとで説明することが多い!)

x8664_asm_language/load-v2.s

	# gcc -static -no-pie -Tbss=0x800000 add.s のようにビルドすれば、0x800000 のアドレスに
	# メインメモリがOSから割り当てられた状態でプログラムが起動する実行ファイルを作ることができる

	.globl	main
main:
	mov $99, %rax
	mov %rax,0x800000 # アドレス0x800000で識別される領域に、raxの値(99)を保存する
	mov 0x800000, %r8 # アドレス0x800000で識別される領域から、値を取り出し、その値をr8に格納する
	ret

このプログラムをgdbで起動して見てみよう

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x401b55
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 2, 0x0000000000401b55 in main ()
(gdb) stepi
0x0000000000401b5c in main ()
(gdb) x/g 0x800000
0x800000 <completed.6735>:	0
(gdb) p /x $rax
$2 = 0x99
(gdb) disassemble
Dump of assembler code for function main:
   0x0000000000401b55 <+0>:	mov    $0x99,%rax
=> 0x0000000000401b5c <+7>:	mov    %rax,0x800000
   0x0000000000401b64 <+15>:	mov    0x800000,%r8
   0x0000000000401b6c <+23>:	retq   
   0x0000000000401b6d <+24>:	nopl   (%rax)
End of assembler dump.
(gdb) stepi
0x0000000000401b64 in main ()
(gdb) x/g 0x800000
0x800000 <completed.6735>:	0x0000000000000099
(gdb) disassemble
Dump of assembler code for function main:
   0x0000000000401b55 <+0>:	mov    $0x99,%rax
   0x0000000000401b5c <+7>:	mov    %rax,0x800000
=> 0x0000000000401b64 <+15>:	mov    0x800000,%r8
   0x0000000000401b6c <+23>:	retq   
   0x0000000000401b6d <+24>:	nopl   (%rax)
End of assembler dump.
(gdb) stepi
0x0000000000401b6c in main ()
(gdb) p /x $r8
$3 = 0x99

また新しいgdbコマンドを使ってしまった。ここで使っているのは、"x" コマンドだ。

"x" コマンドは、メインメモリに格納されているデータを表示するコマンドで、"x アドレス" というようにして実行する。 "x" のあとに"/" と文字を書くと、表示方法を指定できる。/g だと、メモリに格納されたデータを、64bit 値として16進数で表示する。 上の例では、"x"コマンドを使って、0x800000 のアドレスに格納されているデータを表示している。(completed.6735 ってなんだ!?これはデバッガかリンカの説明ところで掘り下げよう。今は無視してもらって構わない)

%rax に、0x99 を格納した状態で、%rax の値を 0x800000にストアしたあとに、"x /g 0x800000" コマンドを実行すると、メモリに0x99 が格納されていること、 その次にアドレス 0x800000 から %r8 に0x800000からデータをロードすると、%r8 の値が0x99 になっていることを確認してほしい。

あ、アクセスサイズの説明をしていなかった。(TODO:あとで構成を考える)

上の例では、メモリアクセス命令のオペランドとして、rax と r8 を使っていたので、 メモリアクセスのサイズは64bit(8byteだった)

メインメモリのアクセス単位は1byteだが、このメインメモリに対して、 8byteのロードをすると、指定したアドレスから、連続する8個分のデータをロードしてくる。 同様に、8byteのストアをすると、8byteの連続した領域に、8個分のデータをストアする。

この、2byte以上のデータをロード、ストアする方式には、流派がふたつある。リトルエンディアンと、ビッグエンディアンだ。

x86_64 は、リトルエンディアンを採用している。 リトルエンディアン環境でメモリアクセスを行う環境では、8byte のストアを行うと、順に

  1. アドレス+0 の位置に、レジスタの0bit目から7bit目までの8bitを格納
  2. アドレス+1 の位置に、レジスタの8bit目から15bit目までの8bitを格納
  3. アドレス+2 の位置に、レジスタの16bit目から23bit目までの8bitを格納
  4. アドレス+3 の位置に、レジスタの24bit目から31bit目までの8bitを格納
  5. アドレス+4 の位置に、レジスタの32bit目から39bit目までの8bitを格納
  6. アドレス+5 の位置に、レジスタの40bit目から47bit目までの8bitを格納
  7. アドレス+6 の位置に、レジスタの48bit目から55bit目までの8bitを格納
  8. アドレス+7 の位置に、レジスタの56bit目から63bit目までの8bitを格納

と、なる。ロードは逆に、アドレス+0番の位置にあるデータを、レジスタの0bit目から7bit目まで…というようになる。

これは、値を表示したとき、人間が見る表示と、メモリに格納されているデータが逆になることを覚えておこう。

例えば、64bitの 0x0000000011223344 という値をメモリに格納すると、

0byte目 1byte目 2byte目 3byte目 4byte目 5byte目 6byte目 7byte目
0x44 0x33 0x22 0x11 0x00 0x00 0x00 0x00

こうなる

さきほどの例でも、"x" コマンドで 64bit 値 0x99 を格納したあとに、格納されたデータをバイト単位で8byte表示すると(フォーマット指定は8bxだ)、64bit 値が逆順に表示される。

(gdb) x/8bx 0x800000
0x800000 <completed.6735>:	0x99	0x00	0x00	0x00	0x00	0x00	0x00	0x00

一般的なCPUは、レジスタ幅でロードストアする以外に、8bit、16bit、32bit 単位でのロードストアができるようになっている。 もちろん、x86_64 もそれが可能だ。それぞれ、命令は以下のようになる。

bit幅 符号拡張ロード ゼロ拡張ロード ストア
32bit movsl アドレス,64bit_register mov アドレス,32bit_register mov 32bit_register, アドレス
16bit movsw アドレス,64bit register movzw アドレス,64bit register mov 16bit_register, アドレス
8bit movsb アドレス,64bit register movzb アドレス,64bit register mov 8bit_register, アドレス

この表を読むには 2点追加で説明が必要だ。符号拡張と、レジスタ幅だ。

まず符号拡張。コンピュータで扱う整数には、符号付き整数と、符号無し整数がある。C 言語でいうと、signed と unsigned の違いだ。

符号付き整数の場合、ある整数ビット列をより大きなビットを持つ整数ビット列に変換するとき、 値の整合性を維持する場合、大きくした部分に、変換前の符号ビットをコピーして入れなければならない。

例えば、符号付き整数-128 は、

となる。また、符号付き整数 +127 は、

となる。これを見れば拡張されたビット部分に、拡張前の値の符号が入っていることが確認できるはずだ。

ロード命令では、このビット幅の拡張が発生するので、符号拡張ロード命令(signed用)と、ゼロ拡張ロード命令(unsigned用)が用意されている。

ストア命令は、ビット幅が小さくなるほうに変換するので、この問題は発生しない。そのため、符号拡張ストアは存在しない。

この符号拡張ロードの挙動はどのPUでも大体同じである。次に、レジスタ幅だ。

x86系列のCPUでは、レジスタの下位ビットに名前が付いており、 このレジスタの下位ビットを示す名前を使うことで、64bitレジスタから、32bit、16bit、8bit値を取り出すことができるようになっている。

x86_64 では、次のようになる

64bit レジスタ 32bit レジスタ 16bit レジスタ 8bit レジスタ
rax eax ax al
rbx ebx bx bl
rcx ecx cx cl
rdx edx dx dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
r8 r8d r8w r8b
r9 r9d r9w r9b
rX rXd(r8-r15はdが付く) rXw(r8-r15はwが付く) rXb(r8-r15はbが付く)
r15 r15d r15w r15b

命名規則がグダグダなのは、x86_64 が 16bit だった時代の名残を背負ってしまっているからだ。このへんの名残は本当にひどくて、さきほどの表

bit幅 符号拡張ロード ゼロ拡張ロード ストア
32bit movsx アドレス,64bit_register mov アドレス,32bit_register mov 32bit_register, アドレス
16bit movsx アドレス,64bit register movzx アドレス,64bit register mov 16bit_register, アドレス
8bit movsx アドレス,64bit register movzx アドレス,64bit register mov 8bit_register, アドレス

を見ても、何かルールがありそうな、なさそうな形をしているし、あと、add などの算術演算は、レジスタの下位16bit や 下位32bit だけで演算できて、add のオペランドに、alレジスタなどを入れると8bit加算ができたりするのだけど、8bit、16bit算術演算は性能上の理由で使用が推奨されていない、が、64bit環境でも、32bit 算術演算が使える場合は使ったほうが性能上わずかに有利とか、そういう初見殺しルールがあったりする。(この話は nopの話 とあわせて結構好きなネタなので、気が向いたら書く)

その話はともかく、現代のx86_64では、名前のルールにひどい仕様が残っているものの、正しい命令を使えば実行時の悪影響はゼロにすることができて、コンパイラは効率良いコードを出すので安心してほしい。 基本的には、上の表に書いた命令を使っていればよい。

符号拡張の挙動を見ておこう

x8664_asm_language/small-load-store.s

	# gcc -static -no-pie -Tbss=0x800000 small-load-store.s でビルドすること

	.globl main
main:
	mov	$0xff, %rax
	mov	%al, 0x800000 	# 1byte の 0xff を 0x800000 にストア
	movsxb	0x800000, %r8

	ret
(gdb) display /4i $pc
1: x/4i $pc
<error: No registers.>
(gdb) start
Temporary breakpoint 1 at 0x401106
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000401106 in main ()
1: x/4i $pc
=> 0x401106 <main>:	mov    $0xff,%rax
   0x40110d <main+7>:	mov    %al,0x800000
   0x401114 <main+14>:	movsbq 0x800000,%r8
   0x40111d <main+23>:	retq   
(gdb) stepi
0x000000000040110d in main ()
1: x/4i $pc
=> 0x40110d <main+7>:	mov    %al,0x800000
   0x401114 <main+14>:	movsbq 0x800000,%r8
   0x40111d <main+23>:	retq   
   0x40111e <main+24>:	xchg   %ax,%ax
(gdb) stepi
0x0000000000401114 in main ()
1: x/4i $pc
=> 0x401114 <main+14>:	movsbq 0x800000,%r8
   0x40111d <main+23>:	retq   
   0x40111e <main+24>:	xchg   %ax,%ax
   0x401120 <__libc_csu_init>:	endbr64 
(gdb) x/8bx 0x800000
0x800000 <completed.7286>:	0xff	0x00	0x00	0x00	0x00	0x00	0x00	0x00
(gdb) stepi
0x000000000040111d in main ()
1: x/4i $pc
=> 0x40111d <main+23>:	retq   
   0x40111e <main+24>:	xchg   %ax,%ax
   0x401120 <__libc_csu_init>:	endbr64 
   0x401124 <__libc_csu_init+4>:	push   %r15
(gdb) p $r8
$1 = -1
(gdb) p /x $r8
$2 = 0xffffffffffffffff

一個便利な gdb コマンドを説明しておこう。"display" だ。 "display" は、"x"や"print" とほぼ同じコマンドだが、 一度 "display" コマンドを実行すると、以降は、gdbのコマンドを実行するごとに、毎回式やメモリのダンプ結果を表示してくれる。 "display /4i $pc" は、"プログラムカウンタが指す位置にあるメモリの中身を命令として4個分表示しろ"と指示するコマンドだ。 一旦これを書いておけば、以降はgdbのコマンドを実行するごとにプログラムカウンタ周辺の命令をダンプしてくれるようになる。

rax レジスタに、1byteの-1 (0xff) を格納後、その下位8bit をストア、それを符号拡張してロードし、r8レジスタに入れると、r8 レジスタの値が64bit値の-1 (0xffff_ffff_ffff_ffff) になっていることを確認しよう。

そしてまたもう一個必要な説明が抜けていた。メモリオペランドのサイズだ。

movsx は、単なる mov と違い、ニーモニックとオペランドのペアから実行する機械語が一意に定まらない。

     movsx $0x800000, %rax  # 8bit 符号拡張ロード (?)
     movsx $0x800000, %rax  # 16bit 符号拡張ロード (?)
     movsx $0x800000, %rax  # 32bit 符号拡張ロード (?)

何かしらの方法で、ロードするデータのサイズを明示的に指示する必要がある。 これもまた初心者殺しで、AT&T記法と、Intel記法で指示する方法が違う。

AT&T 記法では、命令のニーモニックに、ロードするサイズを指示する接尾辞を付ける。 8bit なら "b"、16bit なら "w"、32bit なら "l" だ。

     movsxb $0x800000, %rax  # 8bit 符号拡張ロード
     movsxw $0x800000, %rax  # 16bit 符号拡張ロード
     movsxl $0x800000, %rax  # 32bit 符号拡張ロード

Intel 記法では、メモリオペランドのほうに、ロードするデータのサイズを書く。 (Intel記法にもさらに流派があって、nasm では PTR を書かないようだ。 筆者はもうよくわからないので詳細は近くの詳しい人に聞いてほしい)

     movsx rax, BYTE PTR [0x800000] ; 8bit 符号拡張ロード
     movsx rax, WORD PTR [0x800000] ; 8bit 符号拡張ロード
     movsx rax, DWORD PTR [0x800000] ; 8bit 符号拡張ロード

さて、これで必要なことはひととおり説明したはずなので、ロードストア命令について掘り下げていこう。

メインメモリのメモリアドレスは、整数値だと書いた。この整数値は、レジスタに入っていても構わない。

レジスタに含まれる整数値を使って、メインメモリを参照することを、レジスタ間接参照 (register indirect addresssing) と呼ぶ。

x86_64 の AT&T 記法では、レジスタ名を丸括弧'()'で囲むと、そのレジスタを使ったレジスタ間接参照になる。

x8664_asm_language/register-indirect.s

	# gcc -static -no-pie -Tbss=0x800000 load-v2.s のようにビルドすれば、0x800000 のアドレスに
	# メインメモリがOSから割り当てられた状態でプログラムが起動する実行ファイルを作ることができる

	.globl	main
main:
	mov $0x99, %rax
	mov %rax, 0x800000      # 0x800000 に 0x99 をストア

	mov $0x800000, %rdx 	# RDX に整数値0x800000 を入れる
	mov (%rdx), %r8         # RDXの値(0x800000)をアドレスとして、メインメモリからロード
	
	ret

これを実行してみよう

(gdb) display /4i $pc
1: x/4i $pc
<error: No registers.>
(gdb) start
Temporary breakpoint 1 at 0x401106
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000401106 in main ()
1: x/4i $pc
=> 0x401106 <main>:	mov    $0x99,%rax
   0x40110d <main+7>:	mov    %rax,0x800000
   0x401115 <main+15>:	mov    $0x800000,%rdx
   0x40111c <main+22>:	mov    (%rdx),%r8
(gdb) stepi
0x000000000040110d in main ()
1: x/4i $pc
=> 0x40110d <main+7>:	mov    %rax,0x800000
   0x401115 <main+15>:	mov    $0x800000,%rdx
   0x40111c <main+22>:	mov    (%rdx),%r8
   0x40111f <main+25>:	retq   
(gdb) 
0x0000000000401115 in main ()
1: x/4i $pc
=> 0x401115 <main+15>:	mov    $0x800000,%rdx
   0x40111c <main+22>:	mov    (%rdx),%r8
   0x40111f <main+25>:	retq   
   0x401120 <__libc_csu_init>:	endbr64 
(gdb) 
0x000000000040111c in main ()
1: x/4i $pc
=> 0x40111c <main+22>:	mov    (%rdx),%r8
   0x40111f <main+25>:	retq   
   0x401120 <__libc_csu_init>:	endbr64 
   0x401124 <__libc_csu_init+4>:	push   %r15
(gdb) 
0x000000000040111f in main ()
1: x/4i $pc
=> 0x40111f <main+25>:	retq   
   0x401120 <__libc_csu_init>:	endbr64 
   0x401124 <__libc_csu_init+4>:	push   %r15
   0x401126 <__libc_csu_init+6>:	mov    %rdx,%r15
(gdb) p /x $rdx
$1 = 0x800000
(gdb) p /x $r8
$2 = 0x99

ここで重要なことは、アドレスは整数値で、その整数値がレジスタに入っているということだ。 レジスタに入っている整数値は、add 命令などを使って算術演算を行うことができた。 つまり、アドレスは算術演算することが可能だ。

(TODO:"算術演算"の説明を書いてない)

x8664_asm_language/address-arith.s

	# gcc -static -no-pie -Tbss=0x800000 address-arith.s のようにビルドする

	.globl	main
main:
	movb $0x0, 0x800000 # 0x800000 から順番に 8個データを保存
	movb $0x1, 0x800001
	movb $0x2, 0x800002
	movb $0x3, 0x800003

	mov $0x800000, %rax # %rax に整数値0x800000を格納
	mov $0, %r8         # %r8 ゼロ初期化

	movsxb (%rax), %r9
	add %r9, %r8
	add $1, %rax

	movsxb (%rax), %r9
	add %r9, %r8
	add $1, %rax

	movsxb (%rax), %r9
	add %r9, %r8
	add $1, %rax
	
	ret
(gdb) start
Temporary breakpoint 1 at 0x401106
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000401106 in main ()
(gdb) display /4xb 0x800000
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x00	0x00	0x00
(gdb) display /x $rax
2: /x $rax = 0x401106
(gdb) display $r8
3: $r8 = 140737353694176
(gdb) display $r9
4: $r9 = 140737353694176
(gdb) display /4i $pc
5: x/4i $pc
=> 0x401106 <main>:	mov    $0x0,%r8
   0x40110d <main+7>:	mov    $0x0,%r9
   0x401114 <main+14>:	movb   $0x0,0x800000
   0x40111c <main+22>:	movb   $0x1,0x800001
(gdb) stepi
0x000000000040110d in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x00	0x00	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 140737353694176
5: x/4i $pc
=> 0x40110d <main+7>:	mov    $0x0,%r9
   0x401114 <main+14>:	movb   $0x0,0x800000
   0x40111c <main+22>:	movb   $0x1,0x800001
   0x401124 <main+30>:	movb   $0x2,0x800002
(gdb) stepi
0x0000000000401114 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x00	0x00	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x401114 <main+14>:	movb   $0x0,0x800000
   0x40111c <main+22>:	movb   $0x1,0x800001
   0x401124 <main+30>:	movb   $0x2,0x800002
   0x40112c <main+38>:	movb   $0x3,0x800003
(gdb) 
0x000000000040111c in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x00	0x00	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x40111c <main+22>:	movb   $0x1,0x800001
   0x401124 <main+30>:	movb   $0x2,0x800002
   0x40112c <main+38>:	movb   $0x3,0x800003
   0x401134 <main+46>:	mov    $0x800000,%rax
(gdb) 
0x0000000000401124 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x00	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x401124 <main+30>:	movb   $0x2,0x800002
   0x40112c <main+38>:	movb   $0x3,0x800003
   0x401134 <main+46>:	mov    $0x800000,%rax
   0x40113b <main+53>:	movsbq (%rax),%r9
(gdb) 
0x000000000040112c in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x40112c <main+38>:	movb   $0x3,0x800003
   0x401134 <main+46>:	mov    $0x800000,%rax
   0x40113b <main+53>:	movsbq (%rax),%r9
   0x40113f <main+57>:	add    %r9,%r8
(gdb) 
0x0000000000401134 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x401134 <main+46>:	mov    $0x800000,%rax
   0x40113b <main+53>:	movsbq (%rax),%r9
   0x40113f <main+57>:	add    %r9,%r8
   0x401142 <main+60>:	add    $0x1,%rax
(gdb) 
0x000000000040113b in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800000
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x40113b <main+53>:	movsbq (%rax),%r9
   0x40113f <main+57>:	add    %r9,%r8
   0x401142 <main+60>:	add    $0x1,%rax
   0x401146 <main+64>:	movsbq (%rax),%r9
(gdb) 
0x000000000040113f in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800000
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x40113f <main+57>:	add    %r9,%r8
   0x401142 <main+60>:	add    $0x1,%rax
   0x401146 <main+64>:	movsbq (%rax),%r9
   0x40114a <main+68>:	add    %r9,%r8
(gdb) 
0x0000000000401142 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800000
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x401142 <main+60>:	add    $0x1,%rax
   0x401146 <main+64>:	movsbq (%rax),%r9
   0x40114a <main+68>:	add    %r9,%r8
   0x40114d <main+71>:	add    $0x1,%rax
(gdb) 
0x0000000000401146 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800001
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x401146 <main+64>:	movsbq (%rax),%r9
   0x40114a <main+68>:	add    %r9,%r8
   0x40114d <main+71>:	add    $0x1,%rax
   0x401151 <main+75>:	movsbq (%rax),%r9
(gdb) 
0x000000000040114a in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800001
3: $r8 = 0
4: $r9 = 1
5: x/4i $pc
=> 0x40114a <main+68>:	add    %r9,%r8
   0x40114d <main+71>:	add    $0x1,%rax
   0x401151 <main+75>:	movsbq (%rax),%r9
   0x401155 <main+79>:	add    %r9,%r8
(gdb) 
0x000000000040114d in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800001
3: $r8 = 1
4: $r9 = 1
5: x/4i $pc
=> 0x40114d <main+71>:	add    $0x1,%rax
   0x401151 <main+75>:	movsbq (%rax),%r9
   0x401155 <main+79>:	add    %r9,%r8
   0x401158 <main+82>:	add    $0x1,%rax
(gdb) 
0x0000000000401151 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800002
3: $r8 = 1
4: $r9 = 1
5: x/4i $pc
=> 0x401151 <main+75>:	movsbq (%rax),%r9
   0x401155 <main+79>:	add    %r9,%r8
   0x401158 <main+82>:	add    $0x1,%rax
   0x40115c <main+86>:	movsbq (%rax),%r9
(gdb) 
0x0000000000401155 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800002
3: $r8 = 1
4: $r9 = 2
5: x/4i $pc
=> 0x401155 <main+79>:	add    %r9,%r8
   0x401158 <main+82>:	add    $0x1,%rax
   0x40115c <main+86>:	movsbq (%rax),%r9
   0x401160 <main+90>:	add    %r9,%r8
(gdb) 
0x0000000000401158 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800002
3: $r8 = 3
4: $r9 = 2
5: x/4i $pc
=> 0x401158 <main+82>:	add    $0x1,%rax
   0x40115c <main+86>:	movsbq (%rax),%r9
   0x401160 <main+90>:	add    %r9,%r8
   0x401163 <main+93>:	retq   
(gdb) 
0x000000000040115c in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800003
3: $r8 = 3
4: $r9 = 2
5: x/4i $pc
=> 0x40115c <main+86>:	movsbq (%rax),%r9
   0x401160 <main+90>:	add    %r9,%r8
   0x401163 <main+93>:	retq   
   0x401164 <main+94>:	nopw   %cs:0x0(%rax,%rax,1)
(gdb) 
0x0000000000401160 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800003
3: $r8 = 3
4: $r9 = 3
5: x/4i $pc
=> 0x401160 <main+90>:	add    %r9,%r8
   0x401163 <main+93>:	retq   
   0x401164 <main+94>:	nopw   %cs:0x0(%rax,%rax,1)
   0x40116e <main+104>:	xchg   %ax,%ax
(gdb) 
0x0000000000401163 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x01	0x02	0x03
2: /x $rax = 0x800003
3: $r8 = 6
4: $r9 = 3
5: x/4i $pc
=> 0x401163 <main+93>:	retq   
   0x401164 <main+94>:	nopw   %cs:0x0(%rax,%rax,1)
   0x40116e <main+104>:	xchg   %ax,%ax
   0x401170 <__libc_csu_init>:	endbr64 
(gdb) 

rax レジスタに入れた値を +1 していくと、メインメモリの連続した領域に格納した値を順番に読んでいけることを確認してほしい。 (重要な部分なので、できればちゃんと理解できるまで手元で実行して確認することをおすすめする。また、余裕があれば、プログラムを改変して、動きがどう変わるかなども確認してほしい)

またいくつか説明していないことが出てきたので説明しておこう。

	movb $0x0, 0x800000 # 0x800000 から順番に 8個データを保存
	movb $0x1, 0x800001
	movb $0x2, 0x800002
	movb $0x3, 0x800003

x86_64 では、即値を直接メインメモリにストアすることができる。 (これができるCPUは現代ではx86系のCPUぐらいで、ARMやPowerPC、MIPSはできない) operand0 にメモリアドレス、operand1 に即値を書く。 詳しくは、またあとのx86_64機械語の説明のところで説明しよう。

この場合は、mov ではなく、mov"b" 命令を使っている点に注意してほしい。 この "b" は、movsx のところで書いたサイズを指定する接尾辞と同じもので、 この mov 命令が1byteの値をmovするように指定している。

ここで、単に "mov" 命令を使ってしまうと、

	mov $0x0, 0x800000 # 0x800000 から順番に 8個データを保存
	mov $0x1, 0x800001
	mov $0x2, 0x800002
	mov $0x3, 0x800003

ストアするバイト数が一意に定まらない。このような場合には、明示的に "b" を付ける。

この接尾辞は、命令がオペランドサイズから一意に定まる場合にも付けてよくて、 例えばレジスタ間転送する場合は、レジスタ名から転送サイズが一意に定まるが、その場合にも接尾辞を付けてよい

  mov %rax, %rcx    # 8 byte 転送
  movq %rax, %rcx   # 8 byte 転送 (q は 8byte の意味)

次に、gdb のコマンドを見てほしいが、

(gdb) stepi
0x0000000000401114 in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x00	0x00	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x401114 <main+14>:	movb   $0x0,0x800000
   0x40111c <main+22>:	movb   $0x1,0x800001
   0x401124 <main+30>:	movb   $0x2,0x800002
   0x40112c <main+38>:	movb   $0x3,0x800003
(gdb) 
0x000000000040111c in main ()
1: x/4xb 0x800000
0x800000 <completed.7286>:	0x00	0x00	0x00	0x00
2: /x $rax = 0x401106
3: $r8 = 0
4: $r9 = 0
5: x/4i $pc
=> 0x40111c <main+22>:	movb   $0x1,0x800001
   0x401124 <main+30>:	movb   $0x2,0x800002
   0x40112c <main+38>:	movb   $0x3,0x800003
   0x401134 <main+46>:	mov    $0x800000,%rax

ここで、二回目は、"stepi" コマンドを入力していない点が気になる人もいるかもしれない。

gdb のプロンプトでは、単にエンターキーを押すと、最後に入力したコマンドがもう一度実行される。 ここでは、直前のコマンドは"stepi"なので、この何もコマンドを入れていない行は、 "stepi"をコマンドを入力したことになっている。

もうひとつ、ディスプレースメントの説明をしておこう。 ディスプレースメント(displacement) は、命令のメモリオペランドに付くオフセットのことだ。 レジスタ間接参照する場合、レジスタに含まれている値から、少しだけオフセットを付けてアクセスしたい場合はよくある。

現代の多くのCPUでは、レジスタ間接参照する時に、レジスタに含まれる整数値にいくらかオフセットを付けることができる。例えば、x86_64だと

  mov $8, %rax
  mov 16(%rax), %rcx

と書くと、rax の値に +16 した値(この場合24)をアドレスとして使ってロードすることができる。このオフセットの値を ディスプレースメント と呼ぶ。

x86_64 では、レジスタ間接参照は他のCPUよりも強力で、レジスタ加算、符号付き32bitディスプレースメント加算、2,4,8倍のレジスタのスケールができる。

x8664_asm_language/x86_mem_operand.s

	# gcc -static -no-pie -Tbss=0x800000 x86_mem_operand.s のようにビルドすれば、0x800000 のアドレスに
	# メインメモリがOSから割り当てられた状態でプログラムが起動する実行ファイルを作ることができる

	.globl	main
main:
	mov $0x99, %rax
	mov %rax, 0x800000 # アドレス0x800000で識別される領域に、raxの値(99)を保存する

	mov $0x200000, %rax
	mov $0x100000, %rcx
	mov 0x400000(%rax,%rcx,2), %r8 #0x400000 + 0x20000(rax) + 0x100000(rcx)*2 = 0x800000 からロード

	ret
(gdb) start
Temporary breakpoint 1 at 0x4009de
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004009de in main ()
(gdb) stepi 4
0x00000000004009fb in main ()
(gdb) disassemble
Dump of assembler code for function main:
   0x00000000004009de <+0>:	mov    $0x99,%rax
   0x00000000004009e5 <+7>:	mov    %rax,0x800000
   0x00000000004009ed <+15>:	mov    $0x200000,%rax
   0x00000000004009f4 <+22>:	mov    $0x100000,%rcx
=> 0x00000000004009fb <+29>:	mov    0x400000(%rax,%rcx,2),%r8
   0x0000000000400a03 <+37>:	retq   
   0x0000000000400a04 <+38>:	nopw   %cs:0x0(%rax,%rax,1)
   0x0000000000400a0e <+48>:	xchg   %ax,%ax
End of assembler dump.
(gdb) stepi 
0x0000000000400a03 in main ()
(gdb) p /x $r8
$2 = 0x99
(gdb) 

add 命令は、一回しか加算できないのに対して、x86_64 のメモリオペランドのアドレス加算は、 レジスタ + レジスタ + ディスプレースメントの二回加算ができる。 命令単位で見ると加算命令よりもロード命令のほうが加算性能が高いのが x86_64 だ。 (実際の性能は、色々な事情があってadd命令のほうが性能高い場合もよくある)

ロードストアの説明はこのあたりにしておこう。

低レベルプログラミングをする場合、ロードストア命令についての正しい理解が求められる場面がかなり多い。 きちんと正しい理解が得られるまで、ゆっくり色々試行錯誤してみるのがよいと思う。

プログラムカウンタと分岐

レジスタについて解説したところで、

と、いうのを書いていた。

現代の一般的なCPUでは、プログラムは、データと本質的な違いはなく、 メモリ上に配置されたデータと同じように、メインメモリ上の"どこか"に配置されたバイト列である。 CPU は命令を実行するとき、このメインメモリ上に配置された命令バイト列を、メインメモリからロードしてくる(このロードはフェッチ(fetch)と呼ばれる)。

ロードストア命令のところで、

  mov (%rax), %rdx

のように書くと、レジスタに含まれる整数値をアドレスとして使ってメインメモリを参照する、「レジスタ間接参照」ができると説明した。

フェッチは、プログラムカウンタを使ってレジスタ間接参照して、命令バイト列をロードしてくる動作だとも言える。

まずは、CPUの命令は単なるバイト列でしかないことを確認しておこう

x8664_asm_language/self-modified.s

	.section "axw", "axw"
	.globl main

main:
	movl	$0x11223344, mov_inst+3

	jmp	1f              # プログラムを書きかえたあとに必要
1:	nop                     # プログラムを書きかえたあとに必要

mov_inst:
	mov	$0x1, %rax 	# 0x48 0xc7 0xc0 0x01 0x00 0x00 0x00
	ret

いくつか説明していないことが含まれているが、とりあえず実行してみる。

(gdb) start
Temporary breakpoint 1 at 0x404028
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000404028 in main ()
(gdb) display /x5i $pc
Invalid number "5i".
(gdb) display /5i $pc
1: x/5i $pc
=> 0x404028 <main>:	movl   $0x11223344,0x404039
   0x404033 <main+11>:	jmp    0x404035 <main+13>
   0x404035 <main+13>:	nop
   0x404036 <mov_inst>:	mov    $0x1,%rax
   0x40403d <mov_inst+7>:	retq   
(gdb) display /8xb mov_inst
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x01	0x00	0x00	0x00	0xc3
(gdb) stepi
0x0000000000404033 in main ()
1: x/5i $pc
=> 0x404033 <main+11>:	jmp    0x404035 <main+13>
   0x404035 <main+13>:	nop
   0x404036 <mov_inst>:	mov    $0x11223344,%rax
   0x40403d <mov_inst+7>:	retq   
   0x40403e:	rex.RXB
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x44	0x33	0x22	0x11	0xc3
(gdb) stepi
0x0000000000404035 in main ()
1: x/5i $pc
=> 0x404035 <main+13>:	nop
   0x404036 <mov_inst>:	mov    $0x11223344,%rax
   0x40403d <mov_inst+7>:	retq   
   0x40403e:	rex.RXB
   0x40403f:	rex.XB
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x44	0x33	0x22	0x11	0xc3
(gdb) stepi
0x0000000000404036 in mov_inst ()
1: x/5i $pc
=> 0x404036 <mov_inst>:	mov    $0x11223344,%rax
   0x40403d <mov_inst+7>:	retq   
   0x40403e:	rex.RXB
   0x40403f:	rex.XB
   0x404040:	rex.XB cmp (%r8),%spl
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x44	0x33	0x22	0x11	0xc3
(gdb) stepi
0x000000000040403d in mov_inst ()
1: x/5i $pc
=> 0x40403d <mov_inst+7>:	retq   
   0x40403e:	rex.RXB
   0x40403f:	rex.XB
   0x404040:	rex.XB cmp (%r8),%spl
   0x404043:	sub    %al,0x4e(%rdi)
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x44	0x33	0x22	0x11	0xc3
(gdb) p /x $rax
$1 = 0x11223344
(gdb) 

以下の部分で、main の先頭にある mov 命令によるストアの実行後、mov $1, %rax の命令が書きかわっていることを確認しよう。

(gdb) display /5i $pc
1: x/5i $pc
=> 0x404028 <main>:	movl   $0x11223344,0x404039 <= このストアの実行後
   0x404033 <main+11>:	jmp    0x404035 <main+13>
   0x404035 <main+13>:	nop
   0x404036 <mov_inst>:	mov    $0x1,%rax            <= mov $0x1, %rax が
   0x40403d <mov_inst+7>:	retq   
(gdb) display /8xb mov_inst
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x01	0x00	0x00	0x00	0xc3
(gdb) stepi
0x0000000000404033 in main ()
1: x/5i $pc
=> 0x404033 <main+11>:	jmp    0x404035 <main+13>
   0x404035 <main+13>:	nop
   0x404036 <mov_inst>:	mov    $0x11223344,%rax     <= mov $0x11223344, %rax に書きかわっている
   0x40403d <mov_inst+7>:	retq   
   0x40403e:	rex.RXB
2: x/8xb mov_inst
0x404036 <mov_inst>:	0x48	0xc7	0xc0	0x44	0x33	0x22	0x11	0xc3

また、最後に、rax の値が、0x11223344 になっており、実際に命令が書きかわって効果があらわれていることを確認しよう。

(gdb) p /x $rax
$1 = 0x11223344

"mov $0x1, %rax" という文は、メインメモリ上では、0x48 0xc7 0xc0 0x01 0x00 0x00 0x00 というバイト列として表現される。

objdump というコマンドに、"-d" と、実行ファイルを渡すと、 その実行ファイルに含まれる命令一覧と その命令のバイト列を知ることができる

$ objdump -d a.out
セクション axw の逆アセンブル:

0000000000404028 <main>:
  404028:       c7 04 25 39 40 40 00    movl   $0x11223344,0x404039
  40402f:       44 33 22 11 
  404033:       eb 00                   jmp    404035 <main+0xd>
  404035:       90                      nop

0000000000404036 <mov_inst>:
  404036:       48 c7 c0 01 00 00 00    mov    $0x1,%rax
  40403d:       c3                      retq   

これを見れば、mov $0x1,%rax が、0x48 0xc7 0xc0 0x01 0x00 0x00 0x00 になることが確認できる。(筆者も別に覚えているわけではなくて、これを見てカンニングしている)

このバイト列の意味は、

 
# x86_64 はリトルエンディアンなので、32bit値をバイト毎に表示すると順序が反対に表示されることに注意
48 c7 c0       01 00 00 00    mov    $0x1,%rax
mov imm,rax    imm=0x00000001

と、なっていて、最初の3byteで、即値(immediate)をraxに転送するmov命令だとあらわしていて、後ろの4byteで、転送するimmの32bit値をあらわしている。 後ろの4byteはそのまま32bit値なので、これを書きかえれば、命令中の即値が変わる。

 
# x86_64 はリトルエンディアンなので、32bit値をバイト毎に表示すると順序が反対に表示されることに注意
48 c7 c0       44 33 22 11    mov    $0x11223344,%rax
mov imm,rax    imm=0x11223344

このように書きかえているのが、上のプログラムだ。 このプログラムの挙動を見れば、CPUの命令というのはメインメモリに配置されたバイト列だということがわかるだろう。 (これも大事なのでわからない人は連絡ください)

いくつか説明していないことが出てきたのでその点の説明をしておこう。

まず、

	.section "axw", "axw"

これは…これはですね…リンカのところで説明します(そればっかやな…)。

簡単に説明すると、OSの上で動くプログラムのアドレスは、「読み取り可能」「書き込み可能」「実行可能」というみっつの属性を持っていて、その属性を指示する文がこの.sectionになる。

OS上のプログラムでは、アドレスに対する操作と、アドレスの属性があっていないと、 メモリ保護の仕組みが働いて、プログラムが止められてしまう。 今は、命令バイト列に対する書き込み操作をしたいのだが、一般的なプログラムでは、 命令バイト列に対して書き込みをすることはまずないので、命令バイト列に対する書き込みは禁止されている。 つまり、この.section文を書かないでプログラムを実行すると、OSによってプログラムが停止させられてしまう。 (section文を消して確認してみてほしい)

次に、

	jmp	1f              # プログラムを書きかえたあとに必要
1:	nop                     # プログラムを書きかえたあとに必要

この部分は、Intel のマニュアルにこうしろと書いてあるから書いている。 x86_64 では、命令列を書きかえたあとは、一旦この命令を実行しないといけない。 これが必要な理由は、CPUハードウェアの実装によるので、正しい説明はできないが、 簡単に説明しておくと、CPUの命令をバイト列として実行時に書きかえるのは、滅多にやらないことなので、 CPU内部では、データ用バイト列が流れる道と、命令用のバイト列が流れる道が別々に設計してあることが多い。 そのふたつの道をちゃんと同期するために、この処理が必要だ。

それから、

	movl	$0x11223344, mov_inst+3
        ...
mov_inst:
        ...

この、mov_inst…これも…詳しくはリンカのところで説明しゅる…

一応、簡単に説明しておこう。"main関数" のところで

main:

と、書けば、"ラベルというCのmain関数のようなもの"になると説明した。

ここの

mov_inst:

も、main: と同じくラベルになる。ラベルは、整数値アドレスと同じように、アセンブリ言語内で、アドレスのように扱うことができる。

label:
        mov 0x800000, %rax  # movのようにオペランドにメモリアドレスを取れる命令は、
        mov label, %rax     # 同じようにラベルをオペランドに取ることができる

リンカは、大量の命令バイト列を一個のファイルにまとめるという処理をし、命令が配置されるアドレスを確定する。 ということは、つまり、命令が配置されるアドレスは、リンクの処理が終わるまで確定しないということである。

label:
        add $1, %rax        # この add 命令が配置されるアドレスは、リンクが終わるまで確定しない
        mov 0x800000, %rax  #
        mov label, %rax     #

しかし、データや命令が配置されるアドレスが知りたい場合はよくあるので、これをなんとかしたい。 そういう時にラベルが使われる。

        nop
        nop
        nop
label:
        add $1, %rax        #
        mov 0x800000, %rax  #
        mov label, %rax     #

リンク後、このようにadd命令の上に 3byte 分の命令が追加されたとしよう。 プログラムがアドレス0番から始まっているとすると、add 命令が配置されるアドレスは、3 byte目だ。

この時、リンカは、ラベルの置かれているアドレスを確定させて、 整合性が取れるようにそのラベルを参照している命令の一部を書きかえる。

        nop
        nop
        nop
label:  # ← ここは3byte目
        add $1, %rax        #
        mov 0x800000, %rax  #
        mov 0x3, %rax       # ラベル "label" への参照を、確定したlabelのアドレス(0x3) におきかえる。

このようになる。

上で書いた例をもう一度見てみよう

	.section "axw", "axw"
	.globl main

main:
	movl	$0x11223344, mov_inst+3   # ラベルmov_inst から +3 byte した位置を参照

	jmp	1f
1:	nop

mov_inst:                       # 書きかえたい命令の位置にラベルを置く
	mov	$0x1, %rax 	# 0x48 0xc7 0xc0 0x01 0x00 0x00 0x00 のうち、書きかえたいのは命令の中の3byte目から
	ret

今やりたいことは、mov 命令の中の32bit値の書きかえだった。 ところが、リンクが終わるまで、mov 命令のアドレスは確定しない。 そこで、書きかえたいmov命令にラベルを置いて、リンク後に確定したmov命令の位置を参照できるようにしている。

gdb が出力した命令のダンプを見てほしい

(gdb) display /5i $pc
1: x/5i $pc
=> 0x404028 <main>:	movl   $0x11223344,0x404039
   0x404033 <main+11>:	jmp    0x404035 <main+13>
   0x404035 <main+13>:	nop
   0x404036 <mov_inst>:	mov    $0x1,%rax
   0x40403d <mov_inst+7>:	retq   

書きかえたい対象の mov 命令のアドレスが0x404036(環境に依存して変わります)になっていて、 それを書き変えるストアのmov 命令が、movl $0x11223344,0x404039 (= 0x404036 + 3) になって、 ラベルを参照していた命令のオペランドが、確定後のラベルのアドレスに書き換わっている点を確認しよう。

(よくわからなければ、先にリンカの説明を見たほうがいいかもしれない (注:まだリンカの説明は書いてない))

さて、命令は単なるバイト列だというのが確認できたところで、次へ進もう。次は分岐命令だ。 分岐命令は、プログラムの流れを変える命令で、高級言語から使えるループ、if文、関数などを実現するために使われている。

通常、命令実行後、プログラムカウンタは命令サイズ分インクリメントされて次の命令のアドレスを指すようになるが、 分岐命令の実行後は、次の命令ではなく、オペランドで指定されたアドレスを指すようになり、 次の命令は、そこから実行される。

x8664_asm_language/branch.s

 
	.globl	main
main:
	mov	$0, %rax

loop:
	add	$1, %rax
	jmp	loop

x86_64 では、分岐命令の名前は "jmp" になっている。 このプログラムを実行してみよう。

(gdb) display /1i $pc
1: x/i $pc
<error: No registers.>
(gdb) display $pc
2: $pc = <error: No registers.>
(gdb) display $rax
3: $rax = <error: No registers.>
(gdb) start
Temporary breakpoint 1 at 0x401106
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000401106 in main ()
1: x/i $pc
=> 0x401106 <main>:	mov    $0x0,%rax
2: $pc = (void (*)()) 0x401106 <main>
3: $rax = 4198662
(gdb) stepi
0x000000000040110d in loop ()
1: x/i $pc
=> 0x40110d <loop>:	add    $0x1,%rax
2: $pc = (void (*)()) 0x40110d <loop>
3: $rax = 0
(gdb) 
0x0000000000401111 in loop ()
1: x/i $pc
=> 0x401111 <loop+4>:	jmp    0x40110d <loop>
2: $pc = (void (*)()) 0x401111 <loop+4>
3: $rax = 1
(gdb) 
0x000000000040110d in loop ()
1: x/i $pc
=> 0x40110d <loop>:	add    $0x1,%rax
2: $pc = (void (*)()) 0x40110d <loop>
3: $rax = 1
(gdb) 
0x0000000000401111 in loop ()
1: x/i $pc
=> 0x401111 <loop+4>:	jmp    0x40110d <loop>
2: $pc = (void (*)()) 0x401111 <loop+4>
3: $rax = 2
(gdb) 
0x000000000040110d in loop ()
1: x/i $pc
=> 0x40110d <loop>:	add    $0x1,%rax
2: $pc = (void (*)()) 0x40110d <loop>
3: $rax = 2
(gdb) 

stepi を繰り返すと、プログラムカウンタが、loop ラベルの付いた命令にもどって、何度もadd命令を繰り返し実行し、raxレジスタの値が一個ずつ増えていくことを確認しよう。

プログラムカウンタの値も単なる整数だ。rax などの汎用レジスタに格納された整数値(アドレス)を、プログラムカウンタにコピーすることもできる。 汎用レジスタが指すアドレスへの分岐を、間接分岐 (indirect branch) と呼ぶ。

x8664_asm_language/indirect-branch.s

	.globl	main
main:
	mov	$loop, %rcx

	mov	$0, %rax

loop:
	add	$1, %rax
	jmp	*%rcx

loopラベルのアドレス値を、rcxレジスタに入れ、最後に jmp 命令を使って rcx レジスタの値をプログラムカウンタへコピーし、プログラムの流れをloopラベルが指す位置に戻している。これもgdbで実行して動作を確認しておいてほしい。

"mov $loop, %rax"というのは、少しわかりにくいかもしれない。この x86_64 アセンブリ言語固有のわかりにくい点について説明しておこう。

x86_64 では、オペランドの数字文字列に "$" を付けるか付けないかは大きな違いがある。 "$" が付いていればそのオペランドは即値、"$" が付いていなければそのオペランドはアドレス値だ。

  mov 16, %rax  // アドレス値 16 から、64bit 値をロードしてraxに格納する
  mov $16, %rax // 即値16をraxに格納する

これは、とにかく紛らわしくて、慣れても何度も間違えてしまうのだが、間違えないように注意してほしい。

このルールは、ラベルにも適用されて、"$"が付いたら即値、"$"が付かなければメモリオペランドだ。

  mov label, %rax  // リンク後解決された label のアドレス値からロード
  mov $label, %rax // リンク後解決された label のアドレスを即値としてレジスタに格納

上の例では、

	mov	$loop, %rcx

と、書いているが、これは、「loopラベルが置かれたアドレスの値を即値として rcx に格納する」という意味だ。

ここで、"$" を書き忘れると、

	mov	loop, %rcx

「loopラベルが置かれたアドレスから64bit 値をロードして、rcxに格納する」という意味になる。このふたつは、*意味が全く違う* 点に注意してほしい。

(x86系以外の多くのCPUでは、ロード命令と即値movに別のニーモニックが割り当てられていることが多いので、もっと区別しやすい)

続いて説明するのは、条件分岐命令 だ。

上の分岐命令を使ったプログラムは、無限ループになっていて、一旦実行すると終了しない。 これをもう少し発展させよう。

x8664_asm_language/sum.s

	.globl main

main:
	mov	$10, %rcx
	mov	$0, %rax

loop:
	add	%rcx, %rax
	sub	$1, %rcx
	jnz	loop

end:
	ret

これは、1〜10 までの総和 を求めるプログラムだ。gdbで以下のようにすれば rax が 55 になることが確認できる。

(gdb) start
Temporary breakpoint 1 at 0x401106
Starting program: /home/w0/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x0000000000401106 in main ()
(gdb) break end
Breakpoint 2 at 0x40111d
(gdb) continue
Continuing.

Breakpoint 2, 0x000000000040111d in end ()
(gdb) p $rax
$1 = 55
(gdb) 

また新たな gdb のコマンドを使っているので説明しておこう。breakとcontinueだ。

"break" コマンドは、コマンド引数に渡したアドレスに ブレークポイント と呼ばれるものを設定するコマンドだ。 このアドレスには、ラベルも使える。上の例では "break end" と書いたが、これは、end ラベルが置かれたアドレスにブレークポイントを設定する。 "break" コマンドは、非常によく使うコマンドなので、1文字で書くことができて、"b" でもよい。

"continue" コマンドは、"break"コマンドで設定したブレークポイント まで、プログラムの実行を進めるコマンドだ。 上の例では、直前に end: ラベルの位置に、ブレークポイントを設定したので、その位置までプログラムを進める。

上のgdbの結果は、プログラムカウンタが end ラベルに到達したときに、rax の値を表示すると 55 が表示されるという結果だ。

分岐の説明にもどろう。 jnz 命令は、条件分岐命令 (conditional branch) と呼ばれる命令の一種だ。 条件分岐命令 は演算の結果(条件)を見て、条件が成立した場合に、分岐する(オペランドの値をプログラムカウンタに格納する)命令である。 使われる条件には色々あるが、jnz 命令では、直前の演算結果がゼロでなかった場合に分岐(jnz = Jump if Not Zero)する。

プログラムの挙動を解説していこう。

	mov	$10, %rcx
	mov	$0, %rax

まずRCX、RAXを初期化する。RCXは、ループカウンタとして、RAXは合計値を入れるレジスタとして使う。

	add	%rcx, %rax

合計値をとりたいので、RCXをRAXに足す。 (RCXの値は下のsub命令で減っていくので、ここで加算する値は、10,9,8,...,1 というように減っていく)

	sub	$1, %rcx
	jnz	loop

RCXの値から1を引いて、その結果がゼロでなければ loop: ラベルが置かれたアドレスへジャンプ

end:
	ret

RCX の値が、ゼロになれば、終了。

というプログラムだ。長くなるのでもう書かないが、納得するまでgdbで動作を確認してほしい。

条件分岐命令について、もう少し詳しく説明しよう。条件分岐は、「演算の結果(条件)」を見る、と書いた。 x86_64 では、この条件は、eflagsというフラグレジスタの値のことだ。

レジスタの説明のところで、

と、書いていた。x86_64 の eflags レジスタは、このフラグレジスタに該当するレジスタで、演算結果の追加情報を格納する (MIPSのようにフラグレジスタに該当するレジスタが無いアーキテクチャもある)

x8664_asm_language/status0.s

	.globl	main
main:
	mov $0xffffffffffffffff, %rax
	add $1, %rax
	ret
(gdb) start
Temporary breakpoint 1 at 0x4004d6
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004d6 in main ()
(gdb) stepi
0x00000000004004dd in main ()
(gdb) p $rax
$1 = -1
(gdb) p $eflags
$2 = [ PF ZF IF ]
(gdb) stepi
0x00000000004004e1 in main ()
(gdb) p $eflags
$3 = [ CF PF AF ZF IF ]

gdb のプロンプトから print $eflags すると eflags の値を確認できる。 eflags は 32bit のレジスタだが、gdbは気を効かせて、各ビットの名前を表示してくれる。

ユーザーが書くプログラムで重要なのは、CF,ZF と、ここでは消えているが、OF,ZFだ。 PF, AF は、もう過去の遺物なので二度と見なくていい。 他のフラグはOS、仮想マシンを書く人向けなので、ユーザーは見る必要はないだろう。 (あ、DFがあった。DFは説明しないので各自で調べて)

それぞれの簡単な意味は以下のとおりだ

詳細な意味は、命令によって変わるので、詳しく知る必要がある場合は、Intel の命令マニュアルを読もう。 (正直なところ、筆者も詳しく知る必要に迫られたことがないので、挙動を完全に把握しているわけではない)

jnz 命令は、このうち、ZF を条件として使う条件分岐で、ZF フラグがセットされていなかった場合に、分岐する。 他にも色々な条件分岐命令が用意されているが、それはC言語との対応を見るところで説明しよう。(注 : まだ書いてないです)

	sub	$1, %rcx
	jnz	loop

もう一度この部分を見てみよう。sub 命令は結果がゼロになると、eflagsのZFをセットする。ゼロでない場合は、ZFをクリアする。

jnz 命令は、eflags を見て、ZF がクリアされていれば、オペランドで指定されたアドレスへ分岐、 ZF がセットされていれば、オペランドを無視して、直後にある命令を引き続き実行していく。

つまり、上のように書くことで、RCX がゼロになるまで回り続けるループを表現できる。

次に紹介する分岐命令は、関数呼び出し命令だ。

x8664_asm_language/call.s

	.globl main

func:
	add	$1, %rax
	ret

func2:
	call	func
	ret

func3:
	call	func2
	ret

main:
	mov	$0, %rax
	call	func
	call	func3
	ret
(gdb) display /4gx $rsp
1: x/4xg $rsp
<error: No registers.>
(gdb) display $rsp
2: $rsp = <error: No registers.>
(gdb) display $rax
3: $rax = <error: No registers.>
(gdb) display /2i $pc
4: x/2i $pc
<error: No registers.>
(gdb) start
Temporary breakpoint 1 at 0x4009bf
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004009bf in main ()
1: x/4xg $rsp
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
0x7ffffffee398:	0x0000000100000000	0x00007ffffffee4c8
2: $rsp = (void *) 0x7ffffffee388
3: $rax = 4196799
4: x/2i $pc
=> 0x4009bf <main>:	mov    $0x0,%rax
   0x4009c6 <main+7>:	callq  0x4009ae <func>
(gdb) stepi
0x00000000004009c6 in main ()
1: x/4xg $rsp
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
0x7ffffffee398:	0x0000000100000000	0x00007ffffffee4c8
2: $rsp = (void *) 0x7ffffffee388
3: $rax = 0
4: x/2i $pc
=> 0x4009c6 <main+7>:	callq  0x4009ae <func>
   0x4009cb <main+12>:	callq  0x4009b9 <func3>
(gdb) 
0x00000000004009ae in func ()
1: x/4xg $rsp
0x7ffffffee380:	0x00000000004009cb	0x0000000000400c26
0x7ffffffee390:	0x0000000000000000	0x0000000100000000
2: $rsp = (void *) 0x7ffffffee380
3: $rax = 0
4: x/2i $pc
=> 0x4009ae <func>:	add    $0x1,%rax
   0x4009b2 <func+4>:	retq   
(gdb) 
0x00000000004009b2 in func ()
1: x/4xg $rsp
0x7ffffffee380:	0x00000000004009cb	0x0000000000400c26
0x7ffffffee390:	0x0000000000000000	0x0000000100000000
2: $rsp = (void *) 0x7ffffffee380
3: $rax = 1
4: x/2i $pc
=> 0x4009b2 <func+4>:	retq   
   0x4009b3 <func2>:	callq  0x4009ae <func>
(gdb) 
0x00000000004009cb in main ()
1: x/4xg $rsp
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
0x7ffffffee398:	0x0000000100000000	0x00007ffffffee4c8
2: $rsp = (void *) 0x7ffffffee388
3: $rax = 1
4: x/2i $pc
=> 0x4009cb <main+12>:	callq  0x4009b9 <func3>
   0x4009d0 <main+17>:	retq   
(gdb) 
0x00000000004009b9 in func3 ()
1: x/4xg $rsp
0x7ffffffee380:	0x00000000004009d0	0x0000000000400c26
0x7ffffffee390:	0x0000000000000000	0x0000000100000000
2: $rsp = (void *) 0x7ffffffee380
3: $rax = 1
4: x/2i $pc
=> 0x4009b9 <func3>:	callq  0x4009b3 <func2>
   0x4009be <func3+5>:	retq   
(gdb) 
0x00000000004009b3 in func2 ()
1: x/4xg $rsp
0x7ffffffee378:	0x00000000004009be	0x00000000004009d0
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
2: $rsp = (void *) 0x7ffffffee378
3: $rax = 1
4: x/2i $pc
=> 0x4009b3 <func2>:	callq  0x4009ae <func>
   0x4009b8 <func2+5>:	retq   
(gdb) 
0x00000000004009ae in func ()
1: x/4xg $rsp
0x7ffffffee370:	0x00000000004009b8	0x00000000004009be
0x7ffffffee380:	0x00000000004009d0	0x0000000000400c26
2: $rsp = (void *) 0x7ffffffee370
3: $rax = 1
4: x/2i $pc
=> 0x4009ae <func>:	add    $0x1,%rax
   0x4009b2 <func+4>:	retq   
(gdb) 
0x00000000004009b2 in func ()
1: x/4xg $rsp
0x7ffffffee370:	0x00000000004009b8	0x00000000004009be
0x7ffffffee380:	0x00000000004009d0	0x0000000000400c26
2: $rsp = (void *) 0x7ffffffee370
3: $rax = 2
4: x/2i $pc
=> 0x4009b2 <func+4>:	retq   
   0x4009b3 <func2>:	callq  0x4009ae <func>
(gdb) 
0x00000000004009b8 in func2 ()
1: x/4xg $rsp
0x7ffffffee378:	0x00000000004009be	0x00000000004009d0
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
2: $rsp = (void *) 0x7ffffffee378
3: $rax = 2
4: x/2i $pc
=> 0x4009b8 <func2+5>:	retq   
   0x4009b9 <func3>:	callq  0x4009b3 <func2>
(gdb) 
0x00000000004009be in func3 ()
1: x/4xg $rsp
0x7ffffffee380:	0x00000000004009d0	0x0000000000400c26
0x7ffffffee390:	0x0000000000000000	0x0000000100000000
2: $rsp = (void *) 0x7ffffffee380
3: $rax = 2
4: x/2i $pc
=> 0x4009be <func3+5>:	retq   
   0x4009bf <main>:	mov    $0x0,%rax
(gdb) 
0x00000000004009d0 in main ()
1: x/4xg $rsp
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
0x7ffffffee398:	0x0000000100000000	0x00007ffffffee4c8
2: $rsp = (void *) 0x7ffffffee388
3: $rax = 2
4: x/2i $pc
=> 0x4009d0 <main+17>:	retq   
   0x4009d1 <main+18>:	nopw   %cs:0x0(%rax,%rax,1)
(gdb) 

x86_64 の call 命令は、1命令で次の動作をする。

  1. rsp を 8 減らす
  2. rsp が指すアドレスに、call命令の次の命令のアドレスを格納
  3. オペランドで指定されたアドレスをプログラムカウンタに格納

ret 命令は、call 命令と対応する命令で、次の動作をする。

  1. rsp が指すアドレスから8byte値をロードし、その値をプログラムカウンタに格納
  2. rsp を 8 増やす

と、なる。この挙動がなぜ関数呼び出しになるのか。

プログラムで出てくる基本的なデータ構造のひとつに、スタックと呼ばれるものがある。

スタックは、push、pop というふたつの操作ができるデータ構造で、pop すると、最後にpush したデータを取り出すことができる。

  1. push 2
  2. push 3
  3. push 4
  4. pop   → 4が取り出せる
  5. pop   → 3が取り出せる
  6. push 5
  7. pop   → 5が取り出せる
  8. pop   → 2が取り出せる

(TODO:わかりやすい図を入れたい)

プログラムを書くときには、よく使う処理を関数にまとめることが多い(関数の説明やメリットは、別の書籍等にたくさん書かれているのでそちらを参照してほしい)。 関数を使うときの挙動は、このスタックと同じ構造をしている。

  1. 関数2を呼び出す
  2. 関数3を呼び出す
  3. 関数4を呼び出す
  4. 関数4が終了 → 関数3に戻る
  5. 関数3が終了 → 関数2に戻る
  6. 関数5を呼び出す
  7. 関数5が終わる → 関数2に戻る
  8. 関数2が終わる

これは、スタックがあれば、関数を呼んだあとに、正しく元の位置に戻ってくるという動作が実現できることを意味している。

スタックは、メインメモリとアドレス値一個あれば実現できる単純なデータ構造なので、昔のCPUでは命令として実装されていた。 今のCPUでは、スタックは命令レベルでは実装されていないが、スタックの構造は関数呼び出しを実現するために有用なので、 命令を組み合わせてスタックを作るのが一般的だ。

文脈なしでスタックと言った場合、抽象的なデータ構造のことを言うが、 アセンブリの近くにいるときの文脈で、スタックと言った場合、もう少し意味が狭く、具体的な意味を持つ。

アセンブリ言語の文脈では、スタックとは、OSから割り当てられた特定のメモリ領域のことを言う。

スタックポインタは、このスタックを指すレジスタのことを言う。 スタックを命令レベルでサポートするCPUでは、CPUの仕様で決められたレジスタをスタックポインタとして使う。 スタックを命令レベルでサポートしないCPUでは、ソフトウェア的に特定のレジスタをスタックポインタとして決めて使う。

x86_64は昔のCPU仕様を引き継いでいるので、スタックを命令レベルでサポートする側のCPUだ。 x86_64では、RSP レジスタをスタックポインタとして使う。

(gdb) start
Temporary breakpoint 1 at 0x4004db
Starting program: /mnt/d/wsl/src/pllp/docs/x8664_asm_language/a.out 

Temporary breakpoint 1, 0x00000000004004db in main ()
(gdb) p /x $rsp
$1 = 0x7ffffffee3e8

プログラム開始直後に RSP の値を表示してみよう。このアドレスが、OSから割り当てられたスタック領域を指すアドレスだ。

スタックのサイズは、OS毎に異なるが、Linuxでは、ulimitという、プロセス毎に指定された値で決まる。

ulimit の値は、ulimit コマンドで確認できる。ulimitで指定できる値は色々あるが、スタックサイズを指定する場合は、ulimit -s だ

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7823
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7823
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
$ ulimit -s
8192

単位はkbytes単位と書いてあって、値は 8192 なのでスタックは8MB分割り当てられる

call,ret 命令の説明に戻ろう。

call 命令はさきほど、以下のように説明した。

  1. rsp を 8 減らす
  2. rsp が指すアドレスに、call命令の次の命令のアドレスを格納
  3. オペランドで指定されたアドレスをプログラムカウンタに格納

これは、「スタックに戻りアドレスをpushして、指定されたアドレスへジャンプする」命令だ。逆にretは

  1. rsp が指すアドレスから8byte値をロードし、その値をプログラムカウンタに格納
  2. rsp を 8 増やす

「スタックに積んである戻りアドレスをpopして、そのアドレスへジャンプする」命令だ。

挙動を細かく見ていこう。

0x00000000004009c6 in main ()
1: x/4xg $rsp
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
0x7ffffffee398:	0x0000000100000000	0x00007ffffffee4c8
2: $rsp = (void *) 0x7ffffffee388
3: $rax = 0
4: x/2i $pc
=> 0x4009c6 <main+7>:	callq  0x4009ae <func>
   0x4009cb <main+12>:	callq  0x4009b9 <func3>
(gdb) 
0x00000000004009ae in func ()
1: x/4xg $rsp
0x7ffffffee380:	0x00000000004009cb	0x0000000000400c26
0x7ffffffee390:	0x0000000000000000	0x0000000100000000
2: $rsp = (void *) 0x7ffffffee380
3: $rax = 0
4: x/2i $pc
=> 0x4009ae <func>:	add    $0x1,%rax
   0x4009b2 <func+4>:	retq   

0x4009c6 にあるcallq命令の動作として、

の、3点を確認しよう。

次に、ret 命令

0x00000000004009b2 in func ()
1: x/4xg $rsp
0x7ffffffee380:	0x00000000004009cb	0x0000000000400c26
0x7ffffffee390:	0x0000000000000000	0x0000000100000000
2: $rsp = (void *) 0x7ffffffee380
3: $rax = 1
4: x/2i $pc
=> 0x4009b2 <func+4>:	retq   
   0x4009b3 <func2>:	callq  0x4009ae <func>
(gdb) 
0x00000000004009cb in main ()
1: x/4xg $rsp
0x7ffffffee388:	0x0000000000400c26	0x0000000000000000
0x7ffffffee398:	0x0000000100000000	0x00007ffffffee4c8
2: $rsp = (void *) 0x7ffffffee388
3: $rax = 1
4: x/2i $pc
=> 0x4009cb <main+12>:	callq  0x4009b9 <func3>
   0x4009d0 <main+17>:	retq   

ret 命令の動作として、

の二点を確認しよう。

これらが確認できたら、call func3 の挙動も同じように確認して、 関数呼び出しが、func3 → func2 → func と深くなっていっても、元のmainの位置を忘れることなく正確に戻ってこれる点も確認しておいてほしい。

まとめ

簡単ではあるが、x86_64 のアセンブリの読み方、書き方、実行のしかたについて説明した。

アセンブリ言語は、難しいと主張する人がいるが、仕様を完全に理解しやすいという点では、高級言語よりずっと簡単だ。

例えば、C 言語で、

  int a = 5;

と書いた場合、これが何をやっているか、正確に、不足なく説明できる人はいるだろうか。

それに対して、アセンブリでは

  mov $5, %rax

と書いた場合、「64bitレジスタRAXに64bit値の5を格納する」と言えば、正確に、不足なく説明できていると言えるだろう。 (まあ筆者もこの命令バイト列が実行可能ページと実行不可ページにまたがってた場合の挙動なんて知らんけど…)

アセンブリ言語は、一個づつ分解して理解していくことが可能だ。 入力が何で、どういう副作用が起こるのか、マニュアルを見れば数ページの中に正確に全て書かれている。

一個づつ確認して勉強できるのは、高級言語にはないアセンブリ言語のメリットだ。 ここで紹介した命令は、数多くある命令のうちのほんの一部だが、わからない命令が出てきたときも、ゆっくり一個づつ確認していけばいいと思う。

戻る