Skip to content

Assembly Hello World: A Cross-Platform Syscall Deep Dive

Master assembly syscalls across Linux & macOS for x86-64 and ARM64 architectures. This comprehensive assembly syscall tutorial provides working code examples for write and exit syscalls.

Writing assembly code that directly interfaces with the operating system kernel through syscalls provides invaluable insight into how programs communicate with the underlying system. This guide explores “Hello World” implementations across four different platform configurations: AArch64 (ARM64) and AMD64 (x86-64) architectures on both Linux and macOS operating systems.


1. Understanding the Syscalls

The tutorial focuses on the two primary syscalls required for a “Hello World” program. While the register numbers vary by platform, the semantics of these syscalls remain consistent across Unix-like systems.

The Write Syscall

The write syscall is the fundamental way a process requests the kernel to send bytes to an output stream.

  • C Prototype: ssize_t write(int fd, const void *buf, size_t count)
  • Walkthrough:
    1. fd (File Descriptor): This is a small integer that handles the I/O stream. By standard convention, 0 is stdin, 1 is stdout (standard output), and 2 is stderr (standard error).
    2. buf (Buffer Address): This is a pointer to the data you want to write. In assembly, this is the label or immediate address of your string.
    3. count (Byte Count): The exact number of bytes to write. It is critical that this matches your buffer length; writing too few bytes will truncate your message, and writing too many can cause a “buffer over-read,” potentially leaking adjacent memory or causing a crash.

The Exit Syscall

The exit syscall is the proper way to terminate a process. Simply falling off the end of your code will result in a segmentation fault because the CPU will continue executing whatever “garbage” bytes follow your program in memory.

  • C Prototype: void _exit(int status)
  • Walkthrough:
    1. status: This integer is returned to the parent process. A status of 0 traditionally signals success. Any non-zero value (typically 1-255) indicates a specific error condition. Shells like Bash allow you to check this value immediately after execution using echo $?.

2. Register Calling Conventions

x86-64 (AMD64) Syscall Convention

PurposeRegisterDescription
Syscall NumberraxIdentifies which syscall to invoke
Argument 1rdiFirst parameter (e.g., file descriptor)
Argument 2rsiSecond parameter (e.g., buffer pointer)
Argument 3rdxThird parameter (e.g., byte count)
Argument 4r10Fourth parameter (syscall only)
Argument 5r8Fifth parameter
Argument 6r9Sixth parameter
Return ValueraxSyscall return value

The rcx vs r10 Paradox: One of the most common pitfalls for assembly beginners on x86-64 is the transition from function calls to syscalls.

  • Function Calls (User-land): Follow the System V ABI, which uses rdi, rsi, rdx, rcx, r8, r9.
  • Syscalls (Kernel-land): Use rdi, rsi, rdx, r10, r8, r9.

Why the change? The syscall instruction is designed for speed. To facilitate a fast return, the CPU automatically saves the return address (the next instruction pointer) into the rcx register and the processor flags into r11. If the kernel tried to use rcx to pass an argument, that argument would be overwritten the moment the syscall instruction executed. Therefore, the ABI mandates the use of r10 as a surrogate for the fourth argument.

AArch64 (ARM64) Syscall Convention

PurposeLinux RegistermacOS RegisterDescription
Syscall Numberx8x16Identifies which syscall to invoke
Argument 1x0x0First parameter (e.g., file descriptor)
Argument 2x1x1Second parameter (e.g., buffer pointer)
Argument 3x2x2Third parameter (e.g., byte count)
Argument 4x3x3Fourth parameter
Argument 5x4x4Fifth parameter
Argument 6x5x5Sixth parameter
Return Valuex0x0Syscall return value

Interactive Lab: Syscall ABI Diff Tool

Compare syscall numbers, argument registers, and invocation instructions across all four platforms at a glance. Select a group to focus on specific syscall categories.

Interactive Lab: Syscall Trace Simulator

Step through the register setup, kernel entry, and return phases of real Linux system calls. Toggle between x86-64 and ARM64 to compare syscall vs svc invocation.


3. Key Architectural Differences

Linux vs macOS Syscall Numbering

Linux uses a clean, direct mapping. For example, on x86-64, write is always 1. These numbers are defined in the kernel source (usually in unistd_64.h).

macOS x86-64 (The BSD Class Heritage): Apple’s kernel (XNU) inherited much of its syscall logic from FreeBSD. To distinguish between different types of kernel services (BSD, Mach, Private, etc.), macOS uses a “Class” system encoded in the upper bits of the syscall number.

  • BSD Class (0x2): Most standard POSIX syscalls (read, write, open) fall into this class.
  • The Mask: The BSD class is identified by the value 2 in the high-order bits. Specifically, the formula is (2 << 24) | syscall_number.
  • In hexadecimal, this means standard BSD syscalls always start with 0x2000000.
  • write = 0x2000000 + 4 = 0x2000004
  • exit = 0x2000000 + 1 = 0x2000001

macOS AArch64 (The Modernization): When Apple moved to Silicon (M1/M2/M3), they took the opportunity to simplify the interface. For ARM64, the high-order class bits are no longer required for standard BSD calls. You simply put the direct number in x16.

  • write = 4
  • exit = 1

Syscall Invocation Instructions

  • Linux AArch64: Uses svc #0 (Supervisor Call) to invoke the kernel.
  • macOS AArch64: Uses svc #0x80.
  • Deep Dive on svc: The svc instruction triggers a synchronous exception, causing the CPU to switch from User Mode (EL0) to Kernel Mode (EL1). The immediate value (the #0 or #0x80) is technically a payload that can be read by the exception handler, but on modern Linux and macOS, it is largely ignored. The kernel instead examines the syscall number register to determine which service to provide. This is a shift from older 32-bit ARM (A32), where the immediate value was frequently used to encode the syscall number directly.

4. Troubleshooting Common Syscall Errors

Even with the correct registers, syscalls can fail. Here are the most frequent issues encountered when writing “Hello World” in assembly:

1. The Segmentation Fault (Falling off the Cliff)

If you forget the exit syscall, your program will crash. After the svc or syscall for write returns, the CPU will fetch the next byte in memory. If that byte isn’t a valid instruction, or if it belongs to a different memory segment, the OS will kill your process with a SIGSEGV.

2. Invalid Buffer Addresses

On x86-64 Linux, using mov $msg, %rsi instead of lea msg(%rip), %rsi can cause issues if your code is compiled as Position Independent Executable (PIE). mov might load a 32-bit absolute address which is no longer valid in a randomized 64-bit address space. Always prefer RIP-relative addressing (lea msg(%rip)) for data access.

3. Clobbered Registers

The syscall instruction on x86-64 clobbers rcx and r11. If you were relying on those registers to hold important data across a syscall, that data is now gone. ARM64’s svc is more conservative but still requires careful management of the caller-saved registers (x0-x18).

4. macOS “lSystem” requirement

On macOS, even a pure assembly program often needs to be linked against libSystem.dylib (via -lSystem) if you want it to run on modern versions of the OS. This ensures the kernel correctly identifies the binary’s entry point and security entitlements.


5. Implementation: Hello World in Assembly

Below are the complete source files for each platform.

Linux AArch64

hello_linux_aarch64.s asm

// hello_linux_aarch64.s
.global _start
.section .data
msg: .ascii "Hello, World!
"
msg_len = . - msg

.section .text
_start:
  mov x0, #1          // fd = 1 (stdout)
  ldr x1, =msg        // buf = address of msg
  mov x2, #msg_len    // count = msg_len
  mov x8, #64         // write syscall number (Linux AArch64)
  svc #0              // invoke kernel

  mov x0, #0          // status = 0
  mov x8, #93         // exit syscall number (Linux AArch64)
  svc #0              // invoke kernel

macOS AArch64 (Apple Silicon)

hello_macos_aarch64.s asm

// hello_macos_aarch64.s
.global start
.align 2
.section __DATA,__data
msg: .ascii "Hello, World!
"
msg_len = . - msg

.section __TEXT,__text
start:
  mov x0, #1          // fd = 1 (stdout)
  adr x1, msg         // buf = relative address of msg
  mov x2, #msg_len    // count = msg_len
  mov x16, #4         // write syscall number (macOS AArch64)
  svc #0x80           // invoke kernel

  mov x0, #0          // status = 0
  mov x16, #1         // exit syscall number (macOS AArch64)
  svc #0x80           // invoke kernel

Linux x86-64

hello_linux_x86_64.s asm

# hello_linux_x86_64.s
.global _start
.section .data
msg: .ascii "Hello, World!
"
msg_len = . - msg

.section .text
_start:
  mov $1, %rax        # write syscall number (Linux x86-64)
  mov $1, %rdi        # fd = 1 (stdout)
  lea msg(%rip), %rsi # buf = address of msg
  mov $msg_len, %rdx  # count = msg_len
  syscall             # invoke kernel

  mov $60, %rax       # exit syscall number (Linux x86-64)
  xor %rdi, %rdi      # status = 0
  syscall             # invoke kernel

macOS x86-64 (Intel)

hello_macos_x86_64.s asm

# hello_macos_x86_64.s
.global start
.section __DATA,__data
msg: .ascii "Hello, World!
"
msg_len = . - msg

.section __TEXT,__text
start:
  mov $0x2000004, %rax # write syscall (macOS x86-64 BSD class)
  mov $1, %rdi         # fd = 1 (stdout)
  lea msg(%rip), %rsi  # buf = address of msg
  mov $msg_len, %rdx   # count = msg_len
  syscall              # invoke kernel

  mov $0x2000001, %rax # exit syscall (macOS x86-64 BSD class)
  xor %rdi, %rdi       # status = 0
  syscall              # invoke kernel

5. Compilation and Execution

Linux AArch64

bash

as -o hello.o hello_linux_aarch64.s
ld -o hello hello.o
./hello

macOS AArch64

bash

as -o hello.o hello_macos_aarch64.s
ld -o hello hello.o -lSystem
./hello

Linux x86-64

bash

as -o hello.o hello_linux_x86_64.s
ld -o hello hello.o
./hello

macOS x86-64

bash

as -o hello.o hello_macos_x86_64.s
ld -o hello hello.o -lSystem
./hello
Coder Musings

A modern technical laboratory for systems programming. Master Assembly, Compilers, and Low-Level Engineering through curated paths and interactive visualizations.

© 2026 Coder Musings. All rights reserved. Built for the systems community.