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 orderRDI, RSI, RDX, RCX, R8, R9 (6 registers)RCX, RDX, R8, R9 (4 registers)
Arguments beyond thatPassed on the stackPassed on the stack
Shadow spaceNone requiredCaller must reserve 32 bytes on the stack, even if all arguments fit in registers
Integer/pointer return valueRAXRAX (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
Shadow space is genuinely Windows-only
Microsoft's convention requires the caller to reserve 32 bytes of stack space before every call — space the called function is allowed to use freely, even though the arguments themselves already fit entirely in registers. There's no System V equivalent at all; code written against one convention that ignores this requirement under the other will misbehave in ways that can be genuinely difficult to trace back to the actual cause.

One more real, shared rule worth naming: in both conventions, RBX, RBP, and R12R15 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.

From an informal habit to a real standard
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

Exercise 1

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 solution
Exercise 2

Explain 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 solution
Exercise 3

Explain 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 solution

Chapter 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