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.

A function with eight arguments, a local array and two calls, compiled by GCC with and without a 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 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
        ret

Intel 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
        ret

GCC 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
        ret

Intel 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
        ret
The frame of frame with a frame pointer, System V x86-64: arguments above RBP, locals below.
AddressContentsRegister
RBP+24h, the 8th argument 8 bytes
RBP+16g, the 7th argument 8 bytes
RBP+8return address 8 bytes← RSP on entry
RBP+0the caller’s RBP 8 bytes← RBP
RBP-8R13, R12 and RBX, callee-saved 24 bytes
RBP-40buf[3] 8 bytes
RBP-64buf[0] to buf[2] 24 bytes
RBP-72padding that keeps RSP aligned to 16 8 bytes← RSP
below RSPthe 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.

The same frame without a frame pointer: every offset is relative to RSP, and the 7th argument is at 64(%rsp) rather than 16(%rbp).
AddressContentsRegister
RSP+72h, the 8th argument 8 bytes
RSP+64g, the 7th argument 8 bytes
RSP+56return address 8 bytes
RSP+32R12, RBP and RBX, callee-saved 24 bytes
RSP+24buf[3] 8 bytes
RSP+0buf[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.

The same function compiled by Clang for the Microsoft x64 ABI.

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
        retq

Intel 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
        ret
The frame of frame 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.
AddressContentsRegister
RSP+136h, the 8th argument 8 bytes
RSP+128g, the 7th argument 8 bytes
RSP+120f, the 6th argument 8 bytes
RSP+112e, the 5th argument 8 bytes
RSP+80the homes of RCX, RDX, R8 and R9: shadow space the caller reserved, unused here 32 bytes
RSP+72return address 8 bytes
RSP+64RSI, callee-saved 8 bytes
RSP+56buf[3] 8 bytes
RSP+32buf[0] to buf[2] 24 bytes
RSP+0shadow 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.

The frame of the variadic 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.
AddressContentsRegister
RSP+224arguments passed on the stack, if any: overflow_arg_area 8 bytes
RSP+216return address 8 bytes
RSP+208padding 8 bytes
RSP+80XMM0 to XMM7, saved only if AL is not 0 128 bytes
RSP+40RSI, RDX, RCX, R8 and R9 40 bytes
RSP+32the slot of RDI, which holds the named argument fmt and is not saved 8 bytes
RSP+8the va_list: gp_offset 8, fp_offset 48, then the two pointers 24 bytes
RSP+0padding 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.

The frame of the same log_msg in Microsoft x64 (Clang): the homes of the register arguments become the start of the argument array.
AddressContentsRegister
RSP+80the 5th argument and those after it, if any 8 bytes
RSP+72the home of R9, the 4th argument 8 bytes
RSP+64the home of R8, the 3rd argument 8 bytes
RSP+56the home of RDX, the 2nd argument: where va_list starts 8 bytes
RSP+48the home of RCX, fmt, not stored 8 bytes
RSP+40return address 8 bytes
RSP+32the va_list, a char * 8 bytes
RSP+0shadow 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.

A frame of 16 KB. GCC for Linux allocates it in one 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
        ret

Intel 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
        ret

GCC 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
        ret

Intel 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
        ret

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 big-frame.c

big:
        movl    $16424, %eax
        callq   __chkstk
        subq    %rax, %rsp
        leaq    40(%rsp), %rcx
        callq   fill
        addq    $16424, %rsp
        retq

Intel 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
        ret

MinGW-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
        ret

Intel 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
        ret

Clang 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
        retl

Intel 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
        ret

Summary

Stack frames in the four ABIs.
System V x86-64Microsoft x64System V i386Windows x86
Alignment of RSP or ESP at a call1616164
Stack arguments start at, on entryRSP+8RSP+40ESP+4ESP+4
Space the caller reservesnone32 bytes of shadow spacenonenone
Space below the stack pointer128-byte red zonenonenonenone
Large framesprobed only with -fstack-clash-protection__chkstk from 4096 bytesprobed 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

  1. System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: commit e1ce098331da, 2025-03-12
  2. System V Application Binary Interface, Intel386 Architecture Processor Supplement, version 1.2: commit 20ec676cd56d, 2025-08-24
  3. Microsoft Learn: x64 stack usage: commit f70d88cd5da7, 2026-09-24
  4. Microsoft Learn: x64 prolog and epilog: commit f70d88cd5da7, 2026-09-24
  5. GCC 14.2 manual: Program Instrumentation Options: as published on 2026-09-25
  6. Linux 7.2: Documentation/admin-guide/kernel-parameters.txt: commit 8d3ae59288f1, 2026-08-16