daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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