Talking to the OS

x86-64 Assembly

Chapter 10 · Talking to the OS

assembly2-8 established that ring-3 code can't do privileged things directly — it has to ask. This chapter covers how it actually asks, and the answer genuinely splits in two depending on which operating system is listening.

Recap — assembly1-8's Clean Abstraction

LC-3's TRAP was one unified mechanism: a single instruction, one vector table, a small fixed set of OS services (GETC/OUT/PUTS/HALT) defined by the LC-3 specification itself, identical on every LC-3 system that ever exists. Real x86-64 has no equivalent single mechanism — what "asking the OS" even looks like depends entirely on which operating system the code is running under.

Linux — The SYSCALL Instruction

x86-64 Linux provides a dedicated SYSCALL instruction — a fast, purpose-built replacement for the older, slower software-interrupt-based mechanism (INT 0x80) that 32-bit x86 Linux used. The convention: the syscall number goes in RAX, and arguments go in RDI, RSI, RDX, R10, R8, R9.

MOV RAX, 1     ; syscall number for write()
MOV RDI, 1     ; file descriptor 1 = stdout
MOV RSI, msg   ; pointer to the string
MOV RDX, 5     ; length
SYSCALL          ; make the call — return value comes back in RAX
R10, not RCX — a real, easy mistake
assembly2-5's own System V AMD64 ABI uses RCX as the 4th argument register for ordinary function calls. Syscalls use R10 in that same 4th argument position instead — because the SYSCALL instruction itself internally clobbers RCX as part of how it works. Assuming syscall arguments follow the exact same register order as a normal function call is a real, understandable mistake that silently passes the wrong value.

The return value comes back in RAX — the one point of real consistency with assembly2-5's own ordinary function-call convention.

Windows — No Direct Syscalls for Applications

Windows takes a genuinely different approach. It does not expose a stable, documented, directly-invokable syscall interface to ordinary application code the way Linux does — Windows's own internal syscall numbers are considered private implementation detail, and can (and do) change between versions and even updates. Instead, applications call into the Windows API (Win32 API): a large collection of ordinary functions, living in system DLLs like kernel32.dll, which themselves make the real, internal, unstable syscalls on the application's behalf.

The practical consequence: an x86-64 Windows assembly program calling something like WriteFile or ExitProcess does so with an ordinary CALL instruction, using assembly2-5's own Microsoft x64 calling convention, to a function imported from a DLL — not via any direct syscall-style instruction the programmer writes.

A Genuine, Important Divergence

LC-3 (assembly1-8)Linux x86-64Windows x86-64
MechanismTRAP + vector tableSYSCALL instructionCALL into a Windows API DLL function
StabilityFixed by the LC-3 spec itselfA stable, documented, directly-invokable interfaceInternal syscalls unstable — only the Windows API surface is the real contract
Argument passingFixed register (R0)RDI, RSI, RDX, R10, R8, R9Ordinary Microsoft x64 calling convention (assembly2-5) — RCX, RDX, R8, R9

This is the real point: LC-3 had one portable OS abstraction, identical everywhere. x86-64 genuinely has two different models depending on target OS — code written to make Linux syscalls directly has no Windows equivalent to fall back on, and vice versa, even though the underlying CPU instructions covered in every prior chapter of this course work identically on both.

Real tooling, coming next
assembly2-11 covers the real toolchains (NASM, GAS, ELF, PE) needed to actually assemble and link a program that uses either of these mechanisms — the OS divergence covered here shows up again at the linking stage, not just the instruction level.

Hands-On Exercises

Exercise 1

Explain specifically why the Linux syscall convention uses R10 instead of RCX for the 4th argument, even though assembly2-5's own regular System V calling convention uses RCX in that exact position.

📄 View solution
Exercise 2

Explain why a Windows assembly program calling a Windows API function uses an ordinary CALL instruction rather than anything resembling Linux's own SYSCALL — what's fundamentally different about how each OS exposes its own services to application code?

📄 View solution
Exercise 3

Explain why this chapter describes x86-64's OS interface situation as "genuinely two different models" rather than "one interface with two syntaxes," using assembly1-8's own single unified TRAP mechanism as the contrast point.

📄 View solution

Chapter 10 Quick Reference

  • LC-3's TRAP was one unified, spec-defined OS abstraction — x86-64 has no equivalent single mechanism
  • Linux: the SYSCALL instruction, syscall number in RAX, arguments in RDI/RSI/RDX/R10/R8/R9, return value in RAX
  • Syscalls use R10, not RCX, for the 4th argument — because SYSCALL itself clobbers RCX internally
  • Windows: no stable, documented direct syscall interface for applications — internal syscall numbers can change between versions
  • Windows applications call the Windows API (DLL functions like kernel32.dll) via an ordinary CALL, using assembly2-5's own Microsoft x64 convention
  • This is a genuine divergence, not just a syntax difference — code targeting one OS's mechanism has no equivalent on the other
  • assembly2-11 covers the real, OS-specific tooling (NASM/GAS, ELF/PE) needed to actually build a working program using either mechanism