はじめに
アセンブラについて勉強して気になったので、基礎事項の確認がてらメモ書き。
x86_64では、関数の引数をレジスタを使って受け渡している。
受け渡しに使われるレジスタは、呼出規約でRDI, RSI, RDX, RCX, R8, R9の6つの汎用レジスタが指定されている。(Linuxなどで使われるSystem V AMD64 ABIの場合1)
簡単なC言語のコードを書き、実際にその通りにレジスタが使われているか確認する。
環境
OS: MacOS Catalina
GCC: gcc version 11.2.0
(Mac標準のApple Clangではなく、gccを使っている)
検証
int sum(int a, int b)
{
return a + b;
}
引数2つの関数を書いたコードをコンパイルし、objdumpしてアセンブラを見る。
$ gcc -c test.c
$ objdump -d test.o
test.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
0000000000000000 _sum:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 89 7d fc movl %edi, -4(%rbp)
7: 89 75 f8 movl %esi, -8(%rbp)
a: 8b 55 fc movl -4(%rbp), %edx
d: 8b 45 f8 movl -8(%rbp), %eax
10: 01 d0 addl %edx, %eax
12: 5d popq %rbp
13: c3 retq
1つ目のmovlで第一引数がediから取り出され、2つ目のmovlで第二引数がesiから取り出されている。
次に引数を6つに増やしてアセンブラを見る。
int sum(int a, int b, int c, int d, int e, int f)
{
return a + b + c + d + e + f;
}
0000000000000000 _sum:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 89 7d fc movl %edi, -4(%rbp)
7: 89 75 f8 movl %esi, -8(%rbp)
a: 89 55 f4 movl %edx, -12(%rbp)
d: 89 4d f0 movl %ecx, -16(%rbp)
10: 44 89 45 ec movl %r8d, -20(%rbp)
14: 44 89 4d e8 movl %r9d, -24(%rbp)
18: 8b 55 fc movl -4(%rbp), %edx
1b: 8b 45 f8 movl -8(%rbp), %eax
1e: 01 c2 addl %eax, %edx
20: 8b 45 f4 movl -12(%rbp), %eax
23: 01 c2 addl %eax, %edx
25: 8b 45 f0 movl -16(%rbp), %eax
28: 01 c2 addl %eax, %edx
2a: 8b 45 ec movl -20(%rbp), %eax
2d: 01 c2 addl %eax, %edx
2f: 8b 45 e8 movl -24(%rbp), %eax
32: 01 d0 addl %edx, %eax
34: 5d popq %rbp
35: c3 retq
呼出規約の通り、edi ~ r9の6つのレジスタが使われている。
8つ以上に増やしてみる。
int sum(int a, int b, int c, int d, int e, int f, int g, int h)
{
return a + b + c + d + e + f + g + h;
}
0000000000000000 _sum:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 89 7d fc movl %edi, -4(%rbp)
7: 89 75 f8 movl %esi, -8(%rbp)
a: 89 55 f4 movl %edx, -12(%rbp)
d: 89 4d f0 movl %ecx, -16(%rbp)
10: 44 89 45 ec movl %r8d, -20(%rbp)
14: 44 89 4d e8 movl %r9d, -24(%rbp)
18: 8b 55 fc movl -4(%rbp), %edx
1b: 8b 45 f8 movl -8(%rbp), %eax
1e: 01 c2 addl %eax, %edx
20: 8b 45 f4 movl -12(%rbp), %eax
23: 01 c2 addl %eax, %edx
25: 8b 45 f0 movl -16(%rbp), %eax
28: 01 c2 addl %eax, %edx
2a: 8b 45 ec movl -20(%rbp), %eax
2d: 01 c2 addl %eax, %edx
2f: 8b 45 e8 movl -24(%rbp), %eax
32: 01 c2 addl %eax, %edx
34: 8b 45 10 movl 16(%rbp), %eax
37: 01 c2 addl %eax, %edx
39: 8b 45 18 movl 24(%rbp), %eax
3c: 01 d0 addl %edx, %eax
3e: 5d popq %rbp
3f: c3 retq
7つ目の引数は16(%rbp)、8つ目の引数は24(%rbp)から取られている。
7つ目以降の引数は汎用レジスタではなく、スタックに積まれていることが分かる。
※ スタックには既に関数のリターンアドレスが積まれており、更に関数の冒頭でpushq %rbp
movq %rsp, %rbp
しているため、引数の格納部が16(%rbp)から始まっている。
おわりに
実際にアセンブラを読んで関数の引数とレジスタの対応を確認した。
今回はSystem V AMD64 ABIの呼出規約に沿っていることを確認したが、Windowsなどで使われているMicrosoft x64 calling conventionでは4つの汎用レジスタ(RCX, RDX, R8, R9)を使うことを定めている。
-
Wikipediaに呼び出し規則をまとめた表がある。x86 calling conventions ↩