Stack frames on x86-64 and x86
What a function keeps on the stack and where, drawn from real compiled code. The same function in System V and Microsoft x64, with and without a frame pointer, and what happens when a frame is larger than a page.
A function’s stack frame holds what does not fit in registers: the arguments it passes on the stack, its return address, the callee-saved registers it uses, and its local variables. The ABI fixes the parts that caller and callee must agree on, such as where the stack arguments are and how the stack pointer is aligned at a call; the compiler arranges the rest.
The figures on this page are drawn from compiled code. Each row names an address relative to the stack pointer or the frame pointer after the function’s prolog, and the site’s build checks every address against the listing the figure comes from.
The parts of a frame
From high addresses to low, a frame has:
- the stack arguments, which belong to the caller’s frame and are above the return address;
- the return address, pushed by
call; - the caller’s frame pointer, if the function keeps one;
- the callee-saved registers the function uses;
- local variables, and slots where registers are spilled;
- the outgoing arguments of the calls it makes, at the bottom, where the called function will find them.
Two parts belong to one ABI only. Microsoft x64 has the shadow space: 32 bytes that each caller reserves at the bottom of its frame, where the called function may store its four register arguments. System V x86-64 has the red zone: 128 bytes below the stack pointer that a function may use without moving it.
With a frame pointer
A function that keeps a frame pointer pushes the caller’s RBP and
points RBP at it, so its own arguments are at positive offsets from
RBP and its locals at negative ones, wherever RSP moves. Debuggers and
profilers can then walk from frame to frame through the saved RBP
values. GCC and Clang for Linux only do this at -O2 when told to,
with -fno-omit-frame-pointer, or when the frame changes size at run
time.
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 syntax 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 syntax 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 syntax 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 syntax 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 with a frame pointer, System V x86-64: arguments above RBP, locals below.| Address | Contents | Register |
|---|---|---|
RBP+24 | h, the 8th argument 8 bytes | |
RBP+16 | g, the 7th argument 8 bytes | |
RBP+8 | return address 8 bytes | ← RSP on entry |
RBP+0 | the caller’s RBP 8 bytes | ← RBP |
RBP-8 | R13, R12 and RBX, callee-saved 24 bytes | |
RBP-40 | buf[3] 8 bytes | |
RBP-64 | buf[0] to buf[2] 24 bytes | |
RBP-72 | padding that keeps RSP aligned to 16 8 bytes | ← RSP |
below RSP | the red zone, unused: frame makes calls 128 bytes |
Drawn from frame as GCC 14.2.0 (Debian 14.2.0-19) compiles it for x86_64-linux-gnu; the build checks every address against that listing.
The frame takes 80 bytes, from the return address to RSP, a multiple
of 16: on entry RSP is 8 past a multiple of 16, and the four pushes
and subq $40 add 72. That keeps RSP aligned to 16 for the two calls.
Without a frame pointer
Without a frame pointer, the code addresses everything relative to RSP, and RBP is one more callee-saved register, which GCC uses here for a value that survives a call. The frame is smaller, but its layout can only be known from the code, which is why the unwind tables exist.
64(%rsp) rather than 16(%rbp).| Address | Contents | Register |
|---|---|---|
RSP+72 | h, the 8th argument 8 bytes | |
RSP+64 | g, the 7th argument 8 bytes | |
RSP+56 | return address 8 bytes | |
RSP+32 | R12, RBP and RBX, callee-saved 24 bytes | |
RSP+24 | buf[3] 8 bytes | |
RSP+0 | buf[0] to buf[2] 24 bytes | ← RSP |
Drawn from frame as GCC 14.2.0 (Debian 14.2.0-19) compiles it for x86_64-linux-gnu; the build checks every address against that listing.
Microsoft x64
In Microsoft x64, the four register arguments have homes above the
return address, in the shadow space that the caller reserved, and the
stack arguments come after them, from 40(%rsp) on entry. The same
function also reserves a shadow space of its own, at the bottom of its
frame, for the functions it calls.
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 syntax 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 syntax 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 in Microsoft x64: the caller’s shadow space and stack arguments above the return address, and the function’s own shadow space for its calls at the bottom.| Address | Contents | Register |
|---|---|---|
RSP+136 | h, the 8th argument 8 bytes | |
RSP+128 | g, the 7th argument 8 bytes | |
RSP+120 | f, the 6th argument 8 bytes | |
RSP+112 | e, the 5th argument 8 bytes | |
RSP+80 | the homes of RCX, RDX, R8 and R9: shadow space the caller reserved, unused here 32 bytes | |
RSP+72 | return address 8 bytes | |
RSP+64 | RSI, callee-saved 8 bytes | |
RSP+56 | buf[3] 8 bytes | |
RSP+32 | buf[0] to buf[2] 24 bytes | |
RSP+0 | shadow space for the calls to use and other 32 bytes | ← RSP |
Drawn from frame as Clang 23.1.2 compiles it for x86_64-pc-windows-msvc; the build checks every address against that listing.
Microsoft x64 requires RSP to stay aligned to 16 outside the prolog and epilog, and allows no red zone: nothing below RSP is safe, so the frame is allocated before it is used.
Variadic functions
A variadic function must be able to walk its arguments one after another, but some of them arrived in registers. Both 64-bit ABIs solve this in the frame, differently.
System V saves the argument registers in a register save area of 176
bytes, and va_list records how far va_arg has got in it and where
the stack arguments continue. The listing is on the
System V x86-64 page.
log_msg in System V x86-64 (GCC): the register save area, and the va_list that points into it and to the stack arguments.| Address | Contents | Register |
|---|---|---|
RSP+224 | arguments passed on the stack, if any: overflow_arg_area 8 bytes | |
RSP+216 | return address 8 bytes | |
RSP+208 | padding 8 bytes | |
RSP+80 | XMM0 to XMM7, saved only if AL is not 0 128 bytes | |
RSP+40 | RSI, RDX, RCX, R8 and R9 40 bytes | |
RSP+32 | the slot of RDI, which holds the named argument fmt and is not saved 8 bytes | |
RSP+8 | the va_list: gp_offset 8, fp_offset 48, then the two pointers 24 bytes | |
RSP+0 | padding 8 bytes | ← RSP |
Drawn from log_msg as GCC 14.2.0 (Debian 14.2.0-19) compiles it for x86_64-linux-gnu; the build checks every address against that listing.
Microsoft x64 needs no save area: the function stores RDX, R8 and R9 in
their homes, which lie right below the stack arguments, and all
arguments form one array that va_list, a plain pointer, walks. The
listing is on the Microsoft x64 page.
log_msg in Microsoft x64 (Clang): the homes of the register arguments become the start of the argument array.| Address | Contents | Register |
|---|---|---|
RSP+80 | the 5th argument and those after it, if any 8 bytes | |
RSP+72 | the home of R9, the 4th argument 8 bytes | |
RSP+64 | the home of R8, the 3rd argument 8 bytes | |
RSP+56 | the home of RDX, the 2nd argument: where va_list starts 8 bytes | |
RSP+48 | the home of RCX, fmt, not stored 8 bytes | |
RSP+40 | return address 8 bytes | |
RSP+32 | the va_list, a char * 8 bytes | |
RSP+0 | shadow space for the call to vlog 32 bytes | ← RSP |
Drawn from log_msg as Clang 23.1.2 compiles it for x86_64-pc-windows-msvc; the build checks every address against that listing.
On 32-bit x86, every argument is on the stack already, so a variadic function needs nothing extra.
Large frames and stack probes
The stack of a thread grows into memory that is not mapped yet, and a guard page or gap below it catches a stack that grows too far. A frame larger than a page can skip over the guard in one step and land in other memory, so compilers can make functions touch the stack page by page as they allocate it, in order.
On Windows this is part of the ABI. A frame of a page or more, 4096
bytes, must call __chkstk with its size in RAX before it moves the
stack pointer; the x64 version probes the pages and returns, and the
function then subtracts RAX, while the 32-bit version moves ESP itself.
MinGW-w64 calls its own ___chkstk_ms the same way.
On Linux, the kernel keeps a gap of 256 pages below the main thread’s
stack free of other mappings, and GCC and Clang probe the stack only
when asked to, with -fstack-clash-protection: GCC then allocates one
page at a time and writes to each with orq $0.
subq, or page by page with -fstack-clash-protection. Clang for Windows x64 and MinGW-w64 call their probe with the size in RAX, then subtract it; the 32-bit __chkstk moves ESP itself, so the 32-bit code subtracts nothing.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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax clang --target=i686-pc-windows-msvc -march=pentium4 -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 syntax clang --target=i686-pc-windows-msvc -march=pentium4 -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
retSummary
| System V x86-64 | Microsoft x64 | System V i386 | Windows x86 | |
|---|---|---|---|---|
| Alignment of RSP or ESP at a call | 16 | 16 | 16 | 4 |
| Stack arguments start at, on entry | RSP+8 | RSP+40 | ESP+4 | ESP+4 |
| Space the caller reserves | none | 32 bytes of shadow space | none | none |
| Space below the stack pointer | 128-byte red zone | none | none | none |
| Large frames | probed only with -fstack-clash-protection | __chkstk from 4096 bytes | probed only with -fstack-clash-protection | __chkstk from 4096 bytes |
The comparison of calling conventions covers what the frames hold: which arguments are on the stack, and which registers must be saved.
Sources
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: commit
e1ce098331da, 2025-03-12 - System V Application Binary Interface, Intel386 Architecture Processor Supplement, version 1.2: commit
20ec676cd56d, 2025-08-24 - Microsoft Learn: x64 stack usage: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: x64 prolog and epilog: commit
f70d88cd5da7, 2026-09-24 - GCC 14.2 manual: Program Instrumentation Options: as published on 2026-09-25
- Linux 7.2: Documentation/admin-guide/kernel-parameters.txt: commit
8d3ae59288f1, 2026-08-16