The Stack and Calling Conventions
x86-64 Assembly
Chapter 5 · The Stack and Calling Conventions
assembly1-7 built one simple, self-invented LC-3 calling convention. cpu8bit1-12 used one straightforward subroutine convention per chip. This chapter is where that simplicity ends: x86-64 doesn't have a calling convention — it has (at least) two real, competing, mutually incompatible standards, and getting the wrong one is a genuine way to crash a real program.
The Stack — RSP and RBP
RSP is a full 64-bit stack pointer, free to point anywhere — as fully general as the Z80's own SP from cpu8bit1-7, with no 6502-style page lock. PUSH/POP are native instructions, operating on 64-bit values by default in long mode.
RBP plays a role neither LC-3 nor the 6502/Z80 ever formally needed: a stable frame pointer. As a function runs, RSP itself keeps moving — every push and pop shifts it. RBP is set once, at the start of a function, and held fixed for its entire duration, giving a stable reference point for that function's own parameters and local variables regardless of how RSP moves around them. It's conceptually the same idea as the 6502/Z80's own convention of dedicating a register to a specific job (cpu8bit1-3's own R6-as-stack-pointer precedent from assembly1-7) — just applied to a genuinely new problem, deep local-variable frames, that this arc's simpler subroutines never had.
Worth an honest, brief note: modern compiled code often skips the frame pointer entirely ("frame pointer omission"), computing local-variable offsets directly from RSP instead, as a performance optimization. RBP is a real, common convention, not a hardware requirement.
Register-Based Argument Passing — A Genuine Departure
Neither assembly1-7's LC-3 subroutines nor cpu8bit1-12's own capstone formally passed "arguments" at all — a value was just already sitting in whatever register the subroutine happened to expect, by ad-hoc agreement between caller and callee. Real x86-64 calling conventions formalize this completely: the first several arguments to a function are passed directly in specific, standardized registers, not via the stack — the stack is reserved for overflow arguments beyond that fixed count, and for local storage.
Two Competing Standards
This is the real complexity: which registers hold which arguments depends entirely on which operating system the code targets.
| System V AMD64 ABI (Linux, macOS, BSD) | Microsoft x64 (Windows) | |
|---|---|---|
| First integer/pointer arguments, in order | RDI, RSI, RDX, RCX, R8, R9 (6 registers) | RCX, RDX, R8, R9 (4 registers) |
| Arguments beyond that | Passed on the stack | Passed on the stack |
| Shadow space | None required | Caller must reserve 32 bytes on the stack, even if all arguments fit in registers |
| Integer/pointer return value | RAX | RAX (the one genuine point of agreement) |
A concrete example — calling a hypothetical function with three integer arguments:
; System V AMD64 ABI (Linux/macOS) MOV RDI, 10 ; 1st argument MOV RSI, 20 ; 2nd argument MOV RDX, 30 ; 3rd argument CALL some_function ; Microsoft x64 (Windows) — same call, different registers MOV RCX, 10 ; 1st argument MOV RDX, 20 ; 2nd argument MOV R8, 30 ; 3rd argument SUB RSP, 32 ; reserve the required 32-byte shadow space first CALL some_function
One more real, shared rule worth naming: in both conventions, RBX, RBP, and R12–R15 are callee-saved — a called function must preserve their values before returning, exactly the register-preservation discipline assembly1-7 first established for R7 and cpu8bit1-12's own capstone reinforced by deliberately choosing C over B.
Why Real Programs Must Actually Care
In every prior course, "the calling convention" was something the programmer invented for their own self-contained program — nothing outside that program ever needed to agree with it. x86-64 calling conventions are a real contract: honoring them correctly is what lets hand-written assembly interoperate with the operating system, system libraries, and code compiled by an entirely different compiler. Get it wrong, and the failure isn't a logic bug in your own code — it's a silent violation of an agreement code outside your control was relying on, often surfacing as a crash or corrupted data far from the actual mistake.
assembly1-7 taught calling conventions as good practice within one program. This chapter is the same underlying idea, scaled up into something genuinely external and non-negotiable — the exact kind of real-world complexity a teaching architecture and two 1970s-era chips never needed to force onto the reader.
Hands-On Exercises
A function is called with three integer arguments: 100, 200, 300. State exactly which register holds each argument under System V AMD64 ABI, and separately under Microsoft x64.
📄 View solutionExplain what "shadow space" is in the Microsoft x64 calling convention, using this chapter's own example, and explain why System V has no equivalent requirement.
📄 View solutionExplain why this chapter describes x86-64's calling conventions as a genuine "contract," and contrast that against assembly1-7's own single, self-invented LC-3 calling convention.
📄 View solutionChapter 5 Quick Reference
- RSP — fully flexible 64-bit stack pointer, like the Z80's own SP, with no 6502-style page lock
- RBP — a stable frame pointer for a function's own locals, a genuinely new need neither prior course's simpler subroutines had
- Arguments are passed in specific registers first, the stack only for overflow — a real, formalized contract, not an ad-hoc habit
- System V AMD64 ABI (Linux/macOS): RDI, RSI, RDX, RCX, R8, R9 — 6 register arguments, no shadow space
- Microsoft x64 (Windows): RCX, RDX, R8, R9 — 4 register arguments, plus mandatory 32-byte caller-reserved shadow space
- Both conventions agree: the integer/pointer return value goes in RAX, and RBX/RBP/R12–R15 are callee-saved
- These conventions are a real, external contract — violating them can silently break interoperability with the OS or other compiled code