When you’re staring at a disassembled binary, the first thing that slaps you in the face is stack management. You see push, call, ret, and sometimes a lea that makes no sense until you know the convention. Calling conventions are the unwritten handshake between functions—how arguments get passed, who cleans the stack, and which registers survive the call. Get this wrong, and you’re reading ghosts in the assembly. For reverse engineers, mastering these conventions is like learning the dialect of the machine you’re interrogating.

Close-up of a computer motherboard with glowing circuits

Why Calling Conventions Matter in Reverse Engineering

Imagine you’ve dumped a suspicious DLL and need to trace its exports. Without knowing the convention, you can’t tell if that mov eax, [esp+4] is grabbing the first argument or leftover stack trash. Conventions dictate the binary’s shape: how the compiler weaves function prologues and epilogues, how it aligns the stack, and how it deals with return values. For an underground analyst, this is your Rosetta Stone. It lets you reconstruct function signatures, spot hand-coded assembly obfuscation, and predict side effects that debuggers might hide.

The x86 world is messy because of its history. You’ve got 32-bit conventions born in the era of slow CPUs and small caches, and 64-bit ones that the AMD architects streamlined. Each one leaves a distinctive fingerprint on the binary. If you’re doing vulnerability research or unpacking malware, you’ll see them all: cdecl in ancient Windows code, stdcall in Win32 APIs, fastcall in driver code, and thiscall in C++ objects. On Linux, the System V AMD64 ABI rules 64-bit land, while the old i386 ABI hangs around in legacy binaries.

The 32-bit Battlefield: cdecl, stdcall, fastcall, and thiscall

Let’s start with the 32-bit conventions because they’re still everywhere in legacy Windows malware and old game hacks. Each one answers three questions: argument order (right-to-left or left-to-right?), stack cleanup (caller or callee?), and register usage (which regs are volatile?).

cdecl: The Default Chaos

cdecl is the standard for C programs on 32-bit x86. Arguments go on the stack right-to-left, the caller cleans the stack, and all registers except EBP and ESP are considered volatile. This is why you see add esp, 0Ch right after a call in disassembly—the caller is popping its own arguments. For variadic functions like printf, cdecl is the only game because the caller knows exactly how many args it pushed. In the wild, you’ll spot cdecl by the call followed by stack adjustment, and the frequent use of push instructions before the call.

stdcall: Windows’ Workhorse

stdcall flips the cleanup duty to the callee. Arguments still go right-to-left, but the function itself uses ret 10h (or similar) to pop arguments and return. This is the calling convention of the Win32 API. When you see ret 4, ret 8, or ret 0Ch, you’re looking at a stdcall function, and you can immediately deduce how many DWORD arguments it takes. For a reverse engineer, this is gold—no need to trace the caller to understand the function’s signature. Many malware droppers wrap API calls with stdcall stubs, so recognizing that ret with an immediate operand is a quick win.

fastcall: Speed Over Clarity

fastcall tries to avoid stack traffic by passing the first two arguments in ECX and EDX (on Windows). The rest go on the stack right-to-left, and the callee cleans. This is common in kernel-mode code and in some performance-sensitive user-mode libraries. The disassembly hallmark is seeing arguments in ECX and EDX without an initial push. It’s easy to mistake for a thiscall if you’re not paying attention—ECX can hold a this pointer or just the first integer arg. Context from surrounding code tells you which is which.

thiscall: C++ Under the Hood

thiscall is Microsoft’s convention for C++ member functions. The this pointer goes into ECX, and the rest of the arguments are pushed right-to-left. The callee cleans the stack if the function is non-variadic (usual case); otherwise, it’s caller-clean. In Visual Studio binaries, you’ll see ecx loaded with an object pointer before the call, and often the function prologue will store ECX into a stack slot or register for later use. When you’re reconstructing C++ vtables, thiscall is your bread and butter.

Digital representation of binary code flowing across a dark background

The 64-bit World: System V AMD64 vs. Microsoft x64

When you jump to 64-bit, the game changes radically. The stack is only used for arguments beyond the first few, and registers are precious. There are two major conventions: the System V AMD64 ABI used on Linux and macOS, and the Microsoft x64 convention on Windows. They look similar at a glance but have critical differences that will trip you up in cross-platform analysis.

System V AMD64 ABI: The Unix Way

On Linux and macOS, the first six integer or pointer arguments go into RDI, RSI, RDX, RCX, R8, and R9. Floating-point args use XMM0–XMM7. The stack is always 16-byte aligned at a call site, and the caller cleans the stack for any overflow arguments. Return values land in RAX (and RDX for 128-bit returns). The stack has a 128-byte red zone below RSP that signal handlers can use without adjustment—a quirk that sometimes confuses new reverse engineers when they see functions accessing negative RSP offsets without a sub.

In practice, you’ll see tight code with minimal stack usage. Functions often avoid using RBP as a frame pointer, relying on debug info instead. When you’re reversing a stripped ELF binary, you have to pay close attention to register initialization before calls to infer argument counts. The prologue is usually just sub rsp, N, and the epilogue is add rsp, N; ret. No ret N here because the callee doesn’t clean up args.

Microsoft x64: The Windows Way

Microsoft’s convention uses RCX, RDX, R8, and R9 for the first four arguments. Any additional args go on the stack right-to-left. The caller must allocate 32 bytes of shadow space on the stack, even if the function takes fewer than four args—this is home space for the callee to spill registers. The stack must be 16-byte aligned, and the caller cleans the stack. Volatile registers include RAX, RCX, RDX, R8–R11, and XMM0–XMM5. Non-volatile registers (RBX, RBP, RDI, RSI, RSP, R12–R15, XMM6–XMM15) must be preserved.

For reverse engineering, the shadow space is a dead giveaway. You’ll see sub rsp, 28h even for a function with two arguments—the extra 8 bytes are for alignment. When you see a function using RBX or RSI and saving them in the prologue, you know it’s preserving non-volatile regs. The Microsoft x64 convention is rigid, which makes decompilation easier: once you learn its signature, you can mechanically reconstruct parameters.

A laptop screen displaying disassembled code in a dark room

Spotting Conventions in the Wild: Practical Tricks

You’re not going to parse every function by hand; you need heuristics. Here’s what I do when I open a binary in IDA, Ghidra, or x64dbg.

Look at the ret instruction. If it’s ret N with N > 0 and you’re in 32-bit mode, it’s almost certainly stdcall or a callee-clean convention. The immediate value divided by 4 gives you the argument count. If it’s plain ret, you could be in cdecl or 64-bit land.

Check the stack pointer after calls. In cdecl, the caller adjusts ESP. You’ll see add esp, 0Ch or pop ecx sequences. In stdcall, no adjustment follows the call. In 64-bit Windows, the shadow space means you might see a larger sub rsp than needed, and no cleanup after the call except to restore the caller’s local space.

Trace register usage before calls. In fastcall or thiscall, ECX gets loaded with something meaningful. In 64-bit Linux, RDI and RSI are the first two args—look for string pointers or integer values. In 64-bit Windows, RCX is the first arg, and it often holds a this pointer if it’s dereferenced early in the function.

Beware of obfuscation. Some packers and protectors intentionally mix conventions or insert junk stack operations. For example, a function might use stdcall-style stack cleanup but be called with cdecl adjustments—this is a sign of hand-crafted assembly or a protector trying to break static analysis. When you see mismatched conventions, you’re probably in interesting territory.

FAQ

What’s the fastest way to identify a calling convention in a disassembler?

Start at the function’s return instruction. A ret N in 32-bit code is a strong signal for stdcall or a similar callee-clean convention. If there’s no immediate and the caller adjusts the stack after the call, you’re in cdecl. In 64-bit, the absence of ret N and the presence of shadow space in Windows point to the platform’s convention. Tools like IDA or Ghidra often auto-detect, but you should verify by checking a few call sites manually.

Can a single binary use multiple calling conventions?

Absolutely. A Windows binary might use stdcall for API calls, cdecl for internal C functions, fastcall for driver communication, and thiscall for C++ objects. It’s common to see them mixed. As a reverse engineer, you need to determine the convention per function. This is especially true in malware that statically links multiple libraries or uses obfuscation that switches conventions mid-stream.

How do variadic functions affect calling conventions?

Variadic functions require the caller to clean the stack because only the caller knows how many arguments were actually pushed. On 32-bit x86, this forces the use of cdecl (or a variant where the caller cleans). On 64-bit, the conventions already have the caller cleaning the stack, so variadic functions just follow the standard ABI. However, they often use AL to pass the number of vector registers used—spotting mov al, N before a call can indicate a variadic function like printf.

Why do some 32-bit functions use ret without an immediate but still clean their own stack?

This can be a sign of a custom convention or an obfuscation trick. For instance, a function might manually pop its arguments off the stack with pop ecx or add esp, N before a plain ret. This is common in code that wants to disguise argument counts or in hand-optimized assembly where the programmer wanted to reuse popped values. When you see this, you have to trace the entire function prologue and epilogue to understand the stack frame.

Mastering calling conventions isn’t glamorous, but it’s the foundation of everything you do in reverse engineering. Once you can read the stack and register dance without thinking, you start to see the programmer’s intent behind the opcodes. That’s where the real fun begins.