x64-stack-frames.md (12662B)
1 --- 2 title: "x64 Calling Conventions and Stack Frames" 3 description: "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." 4 category: dfir 5 subcategory: "Windows Internals" 6 tags: [reverse-engineering, assembly, windows, linux, malware-analysis] 7 tools: [windbg, ghidra, gdb, ida] 8 difficulty: advanced 9 updated: 2026-10-04 10 upstreamName: "ired.team" 11 upstreamUrl: "https://ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/windows-x64-calling-convention-stack-frame" 12 upstreamAuthor: "Mantvydas Baranauskas" 13 upstreamLicense: none 14 upstreamRelation: derived 15 references: 16 - name: "x64 Calling Convention: Stack Frame" 17 url: "https://ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/x64-calling-convention-stack-frame" 18 author: "Mantvydas Baranauskas" 19 license: none 20 relation: derived 21 note: "Duplicate of the Windows guide; contributed the same register order and home-space notes, and the Ghidra example." 22 - name: "Linux x64 Calling Convention: Stack Frame" 23 url: "https://ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame" 24 author: "Mantvydas Baranauskas" 25 license: none 26 relation: derived 27 note: "Contributed the System V register order, the rbp-relative argument offsets and the 0x10 shift per 16 bytes of locals." 28 - name: "x64 stack usage — Microsoft Learn" 29 url: "https://learn.microsoft.com/en-us/cpp/build/stack-usage" 30 relation: inspired 31 note: "Normative statement of the home space, alignment and stack-allocation rules." 32 - name: "System V AMD64 ABI" 33 url: "https://gitlab.com/x86-psABIs/x86-64-ABI" 34 relation: inspired 35 note: "Normative register order and red-zone definition for the Linux half." 36 --- 37 38 ## What this is 39 40 A reference for the question you hit constantly in reversing and in reading a crash: this 41 function was called with some arguments, and you need argument number four. There are two 42 x64 conventions in common use and they disagree about almost everything — register order, 43 whether the caller reserves stack space for register arguments, and which register anchors 44 local variables. 45 46 | | Windows x64 | System V (Linux, macOS) | 47 |---|---|---| 48 | Integer/pointer args | `RCX`, `RDX`, `R8`, `R9` | `RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9` | 49 | Float args | `XMM0`–`XMM3` | `XMM0`–`XMM7` | 50 | Further args | pushed, right to left | pushed, right to left | 51 | Caller-reserved space for register args | yes — 32 bytes, always | no | 52 | Frame anchor | `RSP` | `RBP` conventionally, `RSP` when omitted | 53 | Leaf red zone | none | 128 bytes below `RSP` | 54 | Return value | `RAX` | `RAX` | 55 56 Note `RCX` and `RDX` appear in both lists in different positions. Reading a Linux binary with 57 the Windows order gives you plausible, wrong values — which is the single most common way to 58 misread a frame. 59 60 ## Prerequisites 61 62 - A disassembler that shows registers at a breakpoint: WinDBG, Ghidra's decompiler, GDB, IDA. 63 - `.frame` and `k` in WinDBG; `bt` and `info frame` in GDB. 64 - Knowing which convention applies. It follows the *target platform*, not the file format — 65 so a Windows PE uses the Microsoft convention even when you are analysing it on Linux. 66 67 ## Walkthrough 68 69 ### Windows x64: the home space is the thing to understand 70 71 Four integer arguments go in `RCX`, `RDX`, `R8`, `R9`. Arguments five and beyond are pushed. 72 The non-obvious part is that the caller must also reserve 32 bytes immediately above the 73 return address — the *home space* — whether or not the callee uses it, and whether or not the 74 function takes four arguments at all. 75 76 Inside a function that has executed its prologue: 77 78 ```text 79 RSP + 0x00 return address 80 RSP + 0x08 home space slot for arg 1 (the RCX slot) 81 RSP + 0x10 home space slot for arg 2 (the RDX slot) 82 RSP + 0x18 home space slot for arg 3 (the R8 slot) 83 RSP + 0x20 home space slot for arg 4 (the R9 slot) 84 RSP + 0x28 argument 5 85 RSP + 0x30 argument 6 86 ``` 87 88 The home space exists so the callee has somewhere to spill the register arguments, which it 89 does whenever it needs their addresses or needs the registers back. That spill is why you can 90 often recover arguments one through four *after* the registers have been clobbered: look in 91 the home space. 92 93 The other Windows-specific habit: `RBP` is not a frame pointer. Locals and arguments are 94 addressed `RSP + offset`, and `RSP` does not move through the body of the function — the 95 prologue subtracts the whole frame at once and the epilogue adds it back. So an `RSP`-relative 96 offset is stable anywhere in the body, which is what makes reading these frames mechanical. 97 98 <figure class="shot"> 99 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28594%29.png" 100 alt="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" 101 loading="lazy" referrerpolicy="no-referrer"> 102 <figcaption>The Windows x64 frame: register arguments, home space, return address, stacked arguments. 103 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 104 </figcaption> 105 </figure> 106 107 In a decompiler this shows up as the first four arguments being read straight out of the 108 registers at the top of the function — often as 32-bit sub-registers such as `ECX` when the 109 parameter is an `int`, which is worth noticing because it tells you the declared width. 110 111 <figure class="shot"> 112 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28595%29.png" 113 alt="Ghidra disassembly of a Windows library function showing its first four parameters arriving in ECX, RDX, R8 and R9" 114 loading="lazy" referrerpolicy="no-referrer"> 115 <figcaption>A real Windows function taking its first four arguments in <code>ECX</code>, <code>RDX</code>, <code>R8</code> and <code>R9</code>. 116 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 117 </figcaption> 118 </figure> 119 120 Reading a frame in WinDBG: 121 122 ```erlang 123 k 124 .frame 2 125 dv /V 126 dq @rsp L8 127 ``` 128 129 `dv /V` prints locals with the register or `RSP` offset each one lives at, which saves the 130 arithmetic when symbols are present. Without symbols, `dq @rsp L8` and the table above is the 131 whole method. 132 133 ### System V: six registers, no home space, RBP-anchored 134 135 Six integer registers in the order `RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9`, then the stack. 136 Nothing is reserved for the register arguments, so the stacked arguments start directly above 137 the saved frame pointer. 138 139 <figure class="shot"> 140 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28894%29.png" 141 alt="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" 142 loading="lazy" referrerpolicy="no-referrer"> 143 <figcaption>A nine-argument call: six in registers, three pushed. 144 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 145 </figcaption> 146 </figure> 147 148 With a frame pointer established, the stacked arguments are at fixed positive offsets from 149 `RBP`: 150 151 ```text 152 RBP + 0x00 saved RBP 153 RBP + 0x08 return address 154 RBP + 0x10 argument 7 155 RBP + 0x18 argument 8 156 RBP + 0x20 argument 9 157 ``` 158 159 <figure class="shot"> 160 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28891%29.png" 161 alt="Annotated Linux x64 stack frame inside a callee, marking which arguments arrived in registers and which were pushed to the stack" 162 loading="lazy" referrerpolicy="no-referrer"> 163 <figcaption>The frame inside the callee, with register and stacked arguments marked. 164 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 165 </figcaption> 166 </figure> 167 168 ### The spilled-argument offset that moves 169 170 Register arguments are often spilled to negative `RBP` offsets so the function can treat them 171 like locals. Those offsets are not fixed — they depend on how much local space the function 172 allocated, because the compiler lays out locals first. 173 174 A four-byte first argument in a function with no locals lands at `rbp - 0x4`. Add one local 175 and it moves to `rbp - 0x14`. Cross 16 bytes of locals and it moves to `rbp - 0x24`; cross 32 176 and it is at `rbp - 0x34`. The shift is 0x10 per 16-byte block, which is the alignment the ABI 177 requires. 178 179 <figure class="shot"> 180 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28911%29.png" 181 alt="Overall Linux x64 process stack layout diagram showing the frame, the argument vector, and the environment block above it" 182 loading="lazy" referrerpolicy="no-referrer"> 183 <figcaption>The wider stack: frames below, <code>argv</code> and the environment block above. 184 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 185 </figcaption> 186 </figure> 187 188 Two consequences. Do not carry a spilled-argument offset from one function to another, and do 189 not carry one across a recompile. And when you are looking for a value and the obvious offset 190 is wrong, count the locals before assuming you have the wrong register. 191 192 At `main`, the same registers hold the program's own arguments: `RDI` is `argc` and `RSI` 193 points at `argv`, whose first element is the program path. Walk further up the stack and you 194 reach the environment block — which is where you read the environment of a process whose 195 `/proc` entry you cannot trust. 196 197 ## What it tells you 198 199 **Which byte to read when the symbol is missing.** This is the practical payoff. A memory 200 image gives you a stack; a sandbox gives you a call with no prototype. The convention tables 201 above turn "a call happened here" into "argument three was this pointer", which is what you 202 need to recover a filename, a URL, a buffer length or a key. 203 204 **How to reconstruct a call you only have the aftermath of.** On Windows, the home space 205 frequently still contains the first four arguments after the function has clobbered the 206 registers, because the callee spilled them there. On a frame that has already returned, the 207 values above the old `RSP` are often intact, since nothing zeroes a stack on return. Reading 208 a stale frame out of a memory image is a standard way to recover an argument whose caller is 209 gone. 210 211 **Confidence in a hook's argument indices.** Every `Interceptor.attach` handler in 212 [API tracing with Frida](/sheets/dfir/frida-api-tracing) indexes `args[n]`, and that index is 213 only correct because of the table at the top of this sheet. When a hook prints nonsense, the 214 first thing to check is whether you are applying the right convention — a Windows API read 215 with System V ordering gives you `args[0]` and `args[1]` transposed into the wrong slots and 216 no error anywhere. 217 218 **Reading the stack trace in an injection investigation.** A backtrace that crosses from a 219 non-image-backed region into `kernel32` or `ntdll` is the shape of injected code calling the 220 OS, and the frame below the boundary tells you what it asked for. That is the step between 221 "[injected-thread hunting](/sheets/dfir/injected-thread-hunting) flagged this thread" and 222 "this thread opened that file". It needs frame-walking, which needs the convention. 223 224 **Alignment as a sanity check.** Both ABIs require `RSP` to be 16-byte aligned at a call 225 instruction. A frame where `RSP` is misaligned is either mid-prologue, hand-written assembly, 226 or you have the wrong frame — and shellcode that ignores alignment is a common source of 227 crashes in otherwise working injection, which is worth knowing when a payload faults 228 immediately on entry. 229 230 **Varargs are special on System V.** A variadic call sets `AL` to the number of vector 231 registers used. If you are reading arguments to a `printf`-family function and the values are 232 wrong, that is usually why. 233 234 ## References 235 236 - [x64 stack usage — Microsoft Learn](https://learn.microsoft.com/en-us/cpp/build/stack-usage) 237 - [x64 calling convention — Microsoft Learn](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention) 238 - [System V AMD64 ABI](https://gitlab.com/x86-psABIs/x86-64-ABI) 239 - [API tracing with Frida](/sheets/dfir/frida-api-tracing) — where the argument indices get 240 used in anger