DFIR^:DFIR

x64 Calling Conventions and Stack Frames

Where arguments live on Windows x64 and System V: the register order, the 32-byte home space, RSP-relative addressing, and how to read an argument out of a frame you did not compile.

advancedupdated 2026-10-04windbg · ghidra · gdb · ida

What this is

A reference for the question you hit constantly in reversing and in reading a crash: this function was called with some arguments, and you need argument number four. There are two x64 conventions in common use and they disagree about almost everything — register order, whether the caller reserves stack space for register arguments, and which register anchors local variables.

Windows x64System V (Linux, macOS)
Integer/pointer argsRCX, RDX, R8, R9RDI, RSI, RDX, RCX, R8, R9
Float argsXMM0–XMM3XMM0–XMM7
Further argspushed, right to leftpushed, right to left
Caller-reserved space for register argsyes — 32 bytes, alwaysno
Frame anchorRSPRBP conventionally, RSP when omitted
Leaf red zonenone128 bytes below RSP
Return valueRAXRAX

Note RCX and RDX appear in both lists in different positions. Reading a Linux binary with the Windows order gives you plausible, wrong values — which is the single most common way to misread a frame.

Prerequisites

  • A disassembler that shows registers at a breakpoint: WinDBG, Ghidra’s decompiler, GDB, IDA.
  • .frame and k in WinDBG; bt and info frame in GDB.
  • Knowing which convention applies. It follows the target platform, not the file format — so a Windows PE uses the Microsoft convention even when you are analysing it on Linux.

Walkthrough

Windows x64: the home space is the thing to understand

Four integer arguments go in RCX, RDX, R8, R9. Arguments five and beyond are pushed. The non-obvious part is that the caller must also reserve 32 bytes immediately above the return address — the home space — whether or not the callee uses it, and whether or not the function takes four arguments at all.

Inside a function that has executed its prologue:

RSP + 0x00   return address
RSP + 0x08   home space slot for arg 1  (the RCX slot)
RSP + 0x10   home space slot for arg 2  (the RDX slot)
RSP + 0x18   home space slot for arg 3  (the R8 slot)
RSP + 0x20   home space slot for arg 4  (the R9 slot)
RSP + 0x28   argument 5
RSP + 0x30   argument 6

The home space exists so the callee has somewhere to spill the register arguments, which it does whenever it needs their addresses or needs the registers back. That spill is why you can often recover arguments one through four after the registers have been clobbered: look in the home space.

The other Windows-specific habit: RBP is not a frame pointer. Locals and arguments are addressed RSP + offset, and RSP does not move through the body of the function — the prologue subtracts the whole frame at once and the epilogue adds it back. So an RSP-relative offset is stable anywhere in the body, which is what makes reading these frames mechanical.

Annotated Windows x64 stack frame diagram marking the register arguments, the stacked arguments, the return address at RSP and the 32-byte home space above it
The Windows x64 frame: register arguments, home space, return address, stacked arguments. ired.team · Mantvydas Baranauskas

In a decompiler this shows up as the first four arguments being read straight out of the registers at the top of the function — often as 32-bit sub-registers such as ECX when the parameter is an int, which is worth noticing because it tells you the declared width.

Ghidra disassembly of a Windows library function showing its first four parameters arriving in ECX, RDX, R8 and R9
A real Windows function taking its first four arguments in ECX, RDX, R8 and R9. ired.team · Mantvydas Baranauskas

Reading a frame in WinDBG:

k
.frame 2
dv /V
dq @rsp L8

dv /V prints locals with the register or RSP offset each one lives at, which saves the arithmetic when symbols are present. Without symbols, dq @rsp L8 and the table above is the whole method.

System V: six registers, no home space, RBP-anchored

Six integer registers in the order RDI, RSI, RDX, RCX, R8, R9, then the stack. Nothing is reserved for the register arguments, so the stacked arguments start directly above the saved frame pointer.

Debugger view of a nine-argument call on Linux x64 with the first six values visible in RDI, RSI, RDX, RCX, R8 and R9 and the remaining three on the stack
A nine-argument call: six in registers, three pushed. ired.team · Mantvydas Baranauskas

With a frame pointer established, the stacked arguments are at fixed positive offsets from RBP:

RBP + 0x00   saved RBP
RBP + 0x08   return address
RBP + 0x10   argument 7
RBP + 0x18   argument 8
RBP + 0x20   argument 9
Annotated Linux x64 stack frame inside a callee, marking which arguments arrived in registers and which were pushed to the stack
The frame inside the callee, with register and stacked arguments marked. ired.team · Mantvydas Baranauskas

The spilled-argument offset that moves

Register arguments are often spilled to negative RBP offsets so the function can treat them like locals. Those offsets are not fixed — they depend on how much local space the function allocated, because the compiler lays out locals first.

A four-byte first argument in a function with no locals lands at rbp - 0x4. Add one local and it moves to rbp - 0x14. Cross 16 bytes of locals and it moves to rbp - 0x24; cross 32 and it is at rbp - 0x34. The shift is 0x10 per 16-byte block, which is the alignment the ABI requires.

Overall Linux x64 process stack layout diagram showing the frame, the argument vector, and the environment block above it
The wider stack: frames below, argv and the environment block above. ired.team · Mantvydas Baranauskas

Two consequences. Do not carry a spilled-argument offset from one function to another, and do not carry one across a recompile. And when you are looking for a value and the obvious offset is wrong, count the locals before assuming you have the wrong register.

At main, the same registers hold the program’s own arguments: RDI is argc and RSI points at argv, whose first element is the program path. Walk further up the stack and you reach the environment block — which is where you read the environment of a process whose /proc entry you cannot trust.

What it tells you

Which byte to read when the symbol is missing. This is the practical payoff. A memory image gives you a stack; a sandbox gives you a call with no prototype. The convention tables above turn “a call happened here” into “argument three was this pointer”, which is what you need to recover a filename, a URL, a buffer length or a key.

How to reconstruct a call you only have the aftermath of. On Windows, the home space frequently still contains the first four arguments after the function has clobbered the registers, because the callee spilled them there. On a frame that has already returned, the values above the old RSP are often intact, since nothing zeroes a stack on return. Reading a stale frame out of a memory image is a standard way to recover an argument whose caller is gone.

Confidence in a hook’s argument indices. Every Interceptor.attach handler in API tracing with Frida indexes args[n], and that index is only correct because of the table at the top of this sheet. When a hook prints nonsense, the first thing to check is whether you are applying the right convention — a Windows API read with System V ordering gives you args[0] and args[1] transposed into the wrong slots and no error anywhere.

Reading the stack trace in an injection investigation. A backtrace that crosses from a non-image-backed region into kernel32 or ntdll is the shape of injected code calling the OS, and the frame below the boundary tells you what it asked for. That is the step between “injected-thread hunting flagged this thread” and “this thread opened that file”. It needs frame-walking, which needs the convention.

Alignment as a sanity check. Both ABIs require RSP to be 16-byte aligned at a call instruction. A frame where RSP is misaligned is either mid-prologue, hand-written assembly, or you have the wrong frame — and shellcode that ignores alignment is a common source of crashes in otherwise working injection, which is worth knowing when a payload faults immediately on entry.

Varargs are special on System V. A variadic call sets AL to the number of vector registers used. If you are reading arguments to a printf-family function and the values are wrong, that is usually why.

References

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.