x86-64 と x86 のスタックフレーム
関数がスタックに何をどこに置くかを、実際にコンパイルしたコードから図にしています。System V と Microsoft x64 での同じ関数、フレームポインターがある場合とない場合、そしてフレームが 1 ページより大きいときに何が起こるかを示します。
関数のスタックフレームには、レジスターに収まらないものが入ります。スタックで渡す引数、戻りアドレス、関数が使う呼び出し先保存レジスター、ローカル変数です。ABI は、スタック引数の位置や呼び出し時のスタックポインターのアラインメントなど、呼び出し元と呼び出し先が合意しなければならない部分を定め、残りはコンパイラーが配置します。
このページの図は、コンパイルしたコードから描いています。各行は、関数のプロローグの後のスタックポインターまたはフレームポインターを基準としたアドレスを示し、サイトのビルドは、すべてのアドレスを図の元になったリストと照合します。
フレームの各部分
フレームには、上位アドレスから下位アドレスに向かって次のものがあります。
- スタック引数 (呼び出し元のフレームに属し、戻りアドレスの上にある)
callがプッシュした戻りアドレス- 関数がフレームポインターを保持する場合は、呼び出し元のフレームポインター
- 関数が使う呼び出し先保存レジスター
- ローカル変数と、レジスターをスピル (spill) するスロット
- 関数が行う呼び出しに渡す引数 (最下部にあり、呼び出された関数がそこで見つける)
2 つの部分は一方の ABI にだけあります。Microsoft x64 にはシャドウスペース (shadow space) があります。これは各呼び出し元が自分のフレームの最下部に確保する 32 バイトで、呼び出された関数はそこに 4 つのレジスター引数を格納できます。System V x86-64 にはレッドゾーン (red zone) があります。これはスタックポインターの下の 128 バイトで、関数はスタックポインターを動かさずにそこを使えます。
フレームポインターがある場合
フレームポインターを保持する関数は、呼び出し元の RBP をプッシュし、RBP がそれを指すようにします。そのため、RSP がどう動いても、関数自身の引数は RBP から正のオフセットに、ローカル変数は負のオフセットにあります。デバッガーやプロファイラーは、保存された RBP の値をたどってフレームからフレームへ移動できます。Linux 向けの GCC と Clang がこれを -O2 で行うのは、-fno-omit-frame-pointer で指示された場合か、実行時にフレームのサイズが変わる場合だけです。
frame.c
/* A function with a local array, stack arguments and two calls: every
part of a stack frame at once. */
extern void use(long long *p, long long n);
extern long long other(long long x);
long long frame(long long a, long long b, long long c, long long d,
long long e, long long f, long long g, long long h)
{
long long buf[4] = { a, b, c, g };
use(buf, h);
return other(d + e + f) + buf[3];
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fno-omit-frame-pointer -S frame.c
frame:
pushq %rbp
movq %rsp, %rbp
pushq %r13
movq %r8, %r13
pushq %r12
movq %r9, %r12
pushq %rbx
movq %rcx, %rbx
subq $40, %rsp
movq %rsi, -56(%rbp)
movq 16(%rbp), %rax
movq 24(%rbp), %rsi
movq %rdi, -64(%rbp)
leaq -64(%rbp), %rdi
movq %rdx, -48(%rbp)
movq %rax, -40(%rbp)
call use@PLT
leaq (%rbx,%r13), %rdi
addq %r12, %rdi
call other@PLT
addq -40(%rbp), %rax
addq $40, %rsp
popq %rbx
popq %r12
popq %r13
popq %rbp
retIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fno-omit-frame-pointer -masm=intel -S frame.c
frame:
push rbp
mov rbp, rsp
push r13
mov r13, r8
push r12
mov r12, r9
push rbx
mov rbx, rcx
sub rsp, 40
mov QWORD PTR -56[rbp], rsi
mov rax, QWORD PTR 16[rbp]
mov rsi, QWORD PTR 24[rbp]
mov QWORD PTR -64[rbp], rdi
lea rdi, -64[rbp]
mov QWORD PTR -48[rbp], rdx
mov QWORD PTR -40[rbp], rax
call use@PLT
lea rdi, [rbx+r13]
add rdi, r12
call other@PLT
add rax, QWORD PTR -40[rbp]
add rsp, 40
pop rbx
pop r12
pop r13
pop rbp
retGCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S frame.c
frame:
pushq %r12
movq %r8, %r12
pushq %rbp
movq %r9, %rbp
pushq %rbx
movq %rcx, %rbx
subq $32, %rsp
movq 64(%rsp), %rax
movq %rsi, 8(%rsp)
movq 72(%rsp), %rsi
movq %rdi, (%rsp)
movq %rsp, %rdi
movq %rdx, 16(%rsp)
movq %rax, 24(%rsp)
call use@PLT
leaq (%rbx,%r12), %rdi
addq %rbp, %rdi
call other@PLT
addq 24(%rsp), %rax
addq $32, %rsp
popq %rbx
popq %rbp
popq %r12
retIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S frame.c
frame:
push r12
mov r12, r8
push rbp
mov rbp, r9
push rbx
mov rbx, rcx
sub rsp, 32
mov rax, QWORD PTR 64[rsp]
mov QWORD PTR 8[rsp], rsi
mov rsi, QWORD PTR 72[rsp]
mov QWORD PTR [rsp], rdi
mov rdi, rsp
mov QWORD PTR 16[rsp], rdx
mov QWORD PTR 24[rsp], rax
call use@PLT
lea rdi, [rbx+r12]
add rdi, rbp
call other@PLT
add rax, QWORD PTR 24[rsp]
add rsp, 32
pop rbx
pop rbp
pop r12
retframe のフレーム。引数は RBP の上、ローカル変数は下にあります。| アドレス | 内容 | レジスター |
|---|---|---|
RBP+24 | h, the 8th argument 8 バイト | |
RBP+16 | g, the 7th argument 8 バイト | |
RBP+8 | return address 8 バイト | ← RSP on entry |
RBP+0 | the caller’s RBP 8 バイト | ← RBP |
RBP-8 | R13, R12 and RBX, callee-saved 24 バイト | |
RBP-40 | buf[3] 8 バイト | |
RBP-64 | buf[0] to buf[2] 24 バイト | |
RBP-72 | padding that keeps RSP aligned to 16 8 バイト | ← RSP |
below RSP | the red zone, unused: frame makes calls 128 バイト |
GCC 14.2.0 (Debian 14.2.0-19) が x86_64-linux-gnu 向けにコンパイルした frame から描いたもので、ビルド時にすべてのアドレスをそのリストと照合しています。
フレームは戻りアドレスから RSP までの 80 バイトで、16 の倍数です。入口で RSP は 16 の倍数から 8 ずれており、4 回のプッシュと subq $40 で 72 が加わります。これにより、2 つの呼び出しで RSP が 16 にアラインされた状態に保たれます。
フレームポインターがない場合
フレームポインターがない場合、コードはすべてを RSP を基準にアドレス指定し、RBP は呼び出し先保存レジスターの 1 つにすぎなくなります。ここでは GCC は、呼び出しをまたいで生き残る値に RBP を使っています。フレームは小さくなりますが、そのレイアウトはコードからしかわかりません。アンワインドテーブルが存在するのはそのためです。
16(%rbp) ではなく 64(%rsp) にあります。| アドレス | 内容 | レジスター |
|---|---|---|
RSP+72 | h, the 8th argument 8 バイト | |
RSP+64 | g, the 7th argument 8 バイト | |
RSP+56 | return address 8 バイト | |
RSP+32 | R12, RBP and RBX, callee-saved 24 バイト | |
RSP+24 | buf[3] 8 バイト | |
RSP+0 | buf[0] to buf[2] 24 バイト | ← RSP |
GCC 14.2.0 (Debian 14.2.0-19) が x86_64-linux-gnu 向けにコンパイルした frame から描いたもので、ビルド時にすべてのアドレスをそのリストと照合しています。
Microsoft x64
Microsoft x64 では、4 つのレジスター引数のホームが戻りアドレスの上、つまり呼び出し元が確保したシャドウスペースにあり、スタック引数はその後、入口で 40(%rsp) から始まります。同じ関数は、自分が呼び出す関数のために、自分のフレームの最下部にも独自のシャドウスペースを確保します。
frame.c
/* A function with a local array, stack arguments and two calls: every
part of a stack frame at once. */
extern void use(long long *p, long long n);
extern long long other(long long x);
long long frame(long long a, long long b, long long c, long long d,
long long e, long long f, long long g, long long h)
{
long long buf[4] = { a, b, c, g };
use(buf, h);
return other(d + e + f) + buf[3];
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S frame.c
frame:
pushq %rsi
subq $64, %rsp
movq %r9, %rsi
movq 136(%rsp), %rax
movq 128(%rsp), %r9
movq %rcx, 32(%rsp)
movq %rdx, 40(%rsp)
movq %r8, 48(%rsp)
movq %r9, 56(%rsp)
leaq 32(%rsp), %rcx
movq %rax, %rdx
callq use
addq 112(%rsp), %rsi
addq 120(%rsp), %rsi
movq %rsi, %rcx
callq other
addq 56(%rsp), %rax
addq $64, %rsp
popq %rsi
retqIntel 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S frame.c
frame:
push rsi
sub rsp, 64
mov rsi, r9
mov rax, qword ptr [rsp + 136]
mov r9, qword ptr [rsp + 128]
mov qword ptr [rsp + 32], rcx
mov qword ptr [rsp + 40], rdx
mov qword ptr [rsp + 48], r8
mov qword ptr [rsp + 56], r9
lea rcx, [rsp + 32]
mov rdx, rax
call use
add rsi, qword ptr [rsp + 112]
add rsi, qword ptr [rsp + 120]
mov rcx, rsi
call other
add rax, qword ptr [rsp + 56]
add rsp, 64
pop rsi
retframe のフレーム。戻りアドレスの上に呼び出し元のシャドウスペースとスタック引数、最下部に関数が呼び出しのために確保する独自のシャドウスペースがあります。| アドレス | 内容 | レジスター |
|---|---|---|
RSP+136 | h, the 8th argument 8 バイト | |
RSP+128 | g, the 7th argument 8 バイト | |
RSP+120 | f, the 6th argument 8 バイト | |
RSP+112 | e, the 5th argument 8 バイト | |
RSP+80 | the homes of RCX, RDX, R8 and R9: shadow space the caller reserved, unused here 32 バイト | |
RSP+72 | return address 8 バイト | |
RSP+64 | RSI, callee-saved 8 バイト | |
RSP+56 | buf[3] 8 バイト | |
RSP+32 | buf[0] to buf[2] 24 バイト | |
RSP+0 | shadow space for the calls to use and other 32 バイト | ← RSP |
Clang 23.1.2 が x86_64-pc-windows-msvc 向けにコンパイルした frame から描いたもので、ビルド時にすべてのアドレスをそのリストと照合しています。
Microsoft x64 では、プロローグとエピローグの外で RSP が 16 にアラインされたままである必要があり、レッドゾーンは認められていません。RSP より下には安全な場所がないため、フレームは使う前に確保されます。
可変長引数関数
可変長引数関数は引数を 1 つずつ順にたどれなければなりませんが、引数の一部はレジスターで届いています。64 ビットの 2 つの ABI はどちらもこれをフレームで解決しますが、その方法は異なります。
System V は引数レジスターを 176 バイトのレジスター保存領域に保存し、va_list は va_arg がその中のどこまで進んだかと、スタック引数がどこから続くかを記録します。リストは System V x86-64 のページにあります。
log_msg のフレーム。レジスター保存領域と、その中とスタック引数を指す va_list。| アドレス | 内容 | レジスター |
|---|---|---|
RSP+224 | arguments passed on the stack, if any: overflow_arg_area 8 バイト | |
RSP+216 | return address 8 バイト | |
RSP+208 | padding 8 バイト | |
RSP+80 | XMM0 to XMM7, saved only if AL is not 0 128 バイト | |
RSP+40 | RSI, RDX, RCX, R8 and R9 40 バイト | |
RSP+32 | the slot of RDI, which holds the named argument fmt and is not saved 8 バイト | |
RSP+8 | the va_list: gp_offset 8, fp_offset 48, then the two pointers 24 バイト | |
RSP+0 | padding 8 バイト | ← RSP |
GCC 14.2.0 (Debian 14.2.0-19) が x86_64-linux-gnu 向けにコンパイルした log_msg から描いたもので、ビルド時にすべてのアドレスをそのリストと照合しています。
Microsoft x64 には保存領域が要りません。関数は RDX、R8、R9 をスタック引数のすぐ下にあるそれぞれのホームに格納し、すべての引数が 1 つの配列を形作り、単なるポインターである va_list がそれをたどります。リストは Microsoft x64 のページにあります。
log_msg のフレーム。レジスター引数のホームが引数配列の先頭になります。| アドレス | 内容 | レジスター |
|---|---|---|
RSP+80 | the 5th argument and those after it, if any 8 バイト | |
RSP+72 | the home of R9, the 4th argument 8 バイト | |
RSP+64 | the home of R8, the 3rd argument 8 バイト | |
RSP+56 | the home of RDX, the 2nd argument: where va_list starts 8 バイト | |
RSP+48 | the home of RCX, fmt, not stored 8 バイト | |
RSP+40 | return address 8 バイト | |
RSP+32 | the va_list, a char * 8 バイト | |
RSP+0 | shadow space for the call to vlog 32 バイト | ← RSP |
Clang 23.1.2 が x86_64-pc-windows-msvc 向けにコンパイルした log_msg から描いたもので、ビルド時にすべてのアドレスをそのリストと照合しています。
32 ビット x86 では、すべての引数がすでにスタックにあるため、可変長引数関数には追加のものは何も要りません。
大きなフレームとスタックプローブ
スレッドのスタックはまだマップされていないメモリに向かって伸び、その下にあるガードページまたはギャップが、伸びすぎたスタックを捕らえます。1 ページより大きいフレームは、一度にガードを飛び越えてほかのメモリに着地しうるため、コンパイラーは、関数がスタックを確保するときにページごとに順番にアクセスするようにできます。
Windows では、これは ABI の一部です。1 ページ (4096 バイト) 以上のフレームは、スタックポインターを動かす前に、RAX にサイズを入れて __chkstk を呼び出さなければなりません。x64 版はページをプローブして戻り、その後関数が RAX を減算します。一方、32 ビット版は ESP を自分で動かします。MinGW-w64 は独自の ___chkstk_ms を同じ方法で呼び出します。
Linux では、カーネルがメインスレッドのスタックの下に 256 ページのギャップをほかのマッピングのない状態で確保しており、GCC と Clang は -fstack-clash-protection で求められたときだけスタックをプローブします。その場合 GCC は 1 ページずつ確保し、それぞれに orq $0 で書き込みます。
subq で、または -fstack-clash-protection ではページごとに確保します。Windows x64 向けの Clang と MinGW-w64 は、RAX にサイズを入れてプローブを呼び出し、その後サイズを減算します。32 ビットの __chkstk は ESP を自分で動かすため、32 ビットのコードは何も減算しません。big-frame.c
/* A frame larger than a page: the stack must be touched page by page,
in order, so that it grows into the guard page rather than past it. */
extern void fill(char *buf);
void big(void)
{
char buf[16384];
fill(buf);
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S big-frame.c
big:
subq $16392, %rsp
movq %rsp, %rdi
call fill@PLT
addq $16392, %rsp
retIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S big-frame.c
big:
sub rsp, 16392
mov rdi, rsp
call fill@PLT
add rsp, 16392
retGCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fstack-clash-protection -S big-frame.c
big:
leaq -16384(%rsp), %r11
.LPSRL0:
subq $4096, %rsp
orq $0, (%rsp)
cmpq %r11, %rsp
jne .LPSRL0
subq $8, %rsp
movq %rsp, %rdi
call fill@PLT
addq $16392, %rsp
retIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fstack-clash-protection -masm=intel -S big-frame.c
big:
lea r11, -16384[rsp]
.LPSRL0:
sub rsp, 4096
or DWORD PTR [rsp], 0
cmp rsp, r11
jne .LPSRL0
sub rsp, 8
mov rdi, rsp
call fill@PLT
add rsp, 16392
retClang 23.1.2 x86_64-pc-windows-msvc
AT&T 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S big-frame.c
big:
movl $16424, %eax
callq __chkstk
subq %rax, %rsp
leaq 40(%rsp), %rcx
callq fill
addq $16424, %rsp
retqIntel 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S big-frame.c
big:
mov eax, 16424
call __chkstk
sub rsp, rax
lea rcx, [rsp + 40]
call fill
add rsp, 16424
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T 構文 x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S big-frame.c
big:
pushq %rbp
movl $16416, %eax
movq %rsp, %rbp
andq $-16, %rsp
call ___chkstk_ms
subq %rax, %rsp
leaq 32(%rsp), %rcx
call fill
leave
retIntel 構文 x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S big-frame.c
big:
push rbp
mov eax, 16416
mov rbp, rsp
and rsp, -16
call ___chkstk_ms
sub rsp, rax
lea rcx, 32[rsp]
call fill
leave
retClang 23.1.2 i686-pc-windows-msvc
AT&T 構文 clang --target=i686-pc-windows-msvc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S big-frame.c
_big:
movl $16384, %eax
calll __chkstk
movl %esp, %eax
pushl %eax
calll _fill
addl $16388, %esp
retlIntel 構文 clang --target=i686-pc-windows-msvc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S big-frame.c
_big:
mov eax, 16384
call __chkstk
mov eax, esp
push eax
call _fill
add esp, 16388
retまとめ
| System V x86-64 | Microsoft x64 | System V i386 | Windows x86 | |
|---|---|---|---|---|
| 呼び出し時の RSP または ESP のアラインメント | 16 | 16 | 16 | 4 |
| 入口でのスタック引数の開始位置 | RSP+8 | RSP+40 | ESP+4 | ESP+4 |
| 呼び出し元が確保する領域 | なし | 32 バイトのシャドウスペース | なし | なし |
| スタックポインターより下の領域 | 128 バイトのレッドゾーン | なし | なし | なし |
| 大きなフレーム | -fstack-clash-protection の場合のみプローブ | 4096 バイトから __chkstk | -fstack-clash-protection の場合のみプローブ | 4096 バイトから __chkstk |
呼び出し規約の比較では、フレームに何が入るか、つまりどの引数がスタックにあり、どのレジスターを保存しなければならないかを説明しています。
出典
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット
e1ce098331da、2025-03-12 - System V Application Binary Interface, Intel386 Architecture Processor Supplement, version 1.2: コミット
20ec676cd56d、2025-08-24 - Microsoft Learn: x64 stack usage: コミット
f70d88cd5da7、2026-09-24 - Microsoft Learn: x64 prolog and epilog: コミット
f70d88cd5da7、2026-09-24 - GCC 14.2 manual: Program Instrumentation Options: 2026-09-25 の公開版
- Linux 7.3: Documentation/admin-guide/kernel-parameters.txt: コミット
c03c1a14507d、2026-10-18