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.

Last Updated on March 16, 2026 by Vivekanand

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.

Understanding Syscalls Across Platforms

Before diving into the code, it’s crucial to understand that syscalls represent the fundamental interface between user-space programs and the kernel. Each architecture and operating system combination has unique conventions for making these calls, including different syscall numbers, register usage, and invocation mechanisms.

Assembly Code
Linux AArch64 Hello World
ASM
// hello_linux_aarch64.s
// Assemble: as -o hello.o hello_linux_aarch64.s
// Link: ld -o hello hello.o

.global _start                  // Entry point for the linker (Linux uses underscore)

.section .data
msg:
    .ascii "Hello, World!\n"    // Message string (no null terminator needed)
    msg_len = . - msg           // Calculate message length at assembly time

.section .text
_start:
    // syscall: write(int fd, const void *buf, size_t count)
    // Syscall number for write on Linux AArch64: 64
    
    mov     x0, #1              // x0: fd = 1 (stdout file descriptor)
    ldr     x1, =msg            // x1: buf = address of message string
    mov     x2, #msg_len        // x2: count = length of message
    mov     x8, #64             // x8: syscall number for write
    svc     #0                  // Supervisor call - invoke kernel
    
    // syscall: exit(int status)
    // Syscall number for exit on Linux AArch64: 93
    
    mov     x0, #0              // x0: status = 0 (success exit code)
    mov     x8, #93             // x8: syscall number for exit
    svc     #0                  // Invoke kernel to terminate process

macOS AArch64 Hello World
ASM
// hello_macos_aarch64.s
// Assemble: as -o hello.o hello_macos_aarch64.s
// Link: ld -o hello hello.o -lSystem

.global start                   // Entry point (macOS uses no underscore for entry)
.align 2                        // Ensure proper alignment

.section __DATA,__data
msg:
    .ascii "Hello, World!\n"
    msg_len = . - msg

.section __TEXT,__text
start:
    // syscall: write(int fd, const void *buf, size_t count)
    // macOS ARM64 uses DIRECT syscall numbers (no BSD class prefix needed)
    // write syscall number = 4
    
    mov     x0, #1              // x0: fd = 1 (stdout)
    adr     x1, msg             // x1: buf = address of message (position-independent)
    mov     x2, #msg_len        // x2: count = message length
    
    // macOS AArch64 uses x16 for syscall number (not x8 like Linux)
    mov     x16, #4             // x16: syscall number for write
    svc     #0x80               // Invoke kernel with 0x80 (macOS convention)
    
    // syscall: exit(int status)
    // exit syscall number = 1
    
    mov     x0, #0              // x0: status = 0 (success)
    mov     x16, #1             // x16: syscall number for exit
    svc     #0x80               // Invoke kernel to exit

Linux x86-64 Hello World
ASM
# hello_linux_x86_64.s
# Assemble: as -o hello.o hello_linux_x86_64.s
# Link: ld -o hello hello.o

.global _start                  # Entry point (Linux uses underscore)

.section .data
msg:
    .ascii "Hello, World!\n"    # Message with newline (10 = '\n')
    msg_len = . - msg           # Calculate message length

.section .text
_start:
    # syscall: write(int fd, const void *buf, size_t count)
    # Syscall number for write on Linux x86-64: 1
    
    mov     $1, %rax            # rax: syscall number for write
    mov     $1, %rdi            # rdi: fd = 1 (stdout)
    lea     msg(%rip), %rsi     # rsi: buf = address of message (RIP-relative)
    mov     $msg_len, %rdx      # rdx: count = length of message
    syscall                     # Invoke kernel via syscall instruction
    
    # syscall: exit(int status)
    # Syscall number for exit on Linux x86-64: 60
    
    mov     $60, %rax           # rax: syscall number for exit
    xor     %rdi, %rdi          # rdi: status = 0 (using xor for zero)
    syscall                     # Invoke kernel to terminate

macOS x86-64 Hello World
ASM
# hello_macos_x86_64.s
# Assemble: as -o hello.o hello_macos_x86_64.s
# Link: ld -o hello hello.o -lSystem

.global start                   # Entry point (macOS uses no underscore)

.section __DATA,__data
msg:
    .ascii "Hello, World!\n"    # Message with newline
    msg_len = . - msg           # Calculate length

.section __TEXT,__text
start:
    # syscall: write(int fd, const void *buf, size_t count)
    # macOS x86-64 uses BSD-style syscall numbers with class prefix
    # write = 0x2000000 + 4 = 0x2000004
    
    mov     $0x2000004, %rax    # rax: BSD write syscall number
    mov     $1, %rdi            # rdi: fd = 1 (stdout)
    lea     msg(%rip), %rsi     # rsi: buf = message address (RIP-relative)
    mov     $msg_len, %rdx      # rdx: count = message length
    syscall                     # Invoke kernel
    
    # syscall: exit(int status)
    # exit = 0x2000000 + 1 = 0x2000001
    
    mov     $0x2000001, %rax    # rax: BSD exit syscall number
    xor     %rdi, %rdi          # rdi: status = 0
    syscall                     # Invoke kernel to exit

Platform Comparison

Syscall Numbers
SyscallLinux x86-64Linux AArch64macOS x86-64macOS AArch64
write1640x20000044
exit60930x20000011

Syscall Invocation
PlatformSyscall InstructionSyscall Number Register
Linux x86-64syscallrax
Linux AArch64svc #0x8
macOS x86-64syscallrax
macOS AArch64svc #0x80x16

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, not standard function ABI)
Argument 5r8Fifth parameter
Argument 6r9Sixth parameter
Return ValueraxSyscall return value

Critical Note on Fourth Argument: The x86-64 architecture has different conventions for syscalls versus normal function calls. For normal function calls (System V ABI), the fourth argument uses rcx. However, for syscalls, the fourth argument uses r10 instead. This is because the syscall instruction itself clobbers rcx by storing the return address there, making it unavailable for passing arguments.

AArch64 (ARM64) Syscall Convention

PurposeLinux RegistermacOS RegisterDescription
Syscall Numberx8(orw8)x16Identifies 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

Key Architectural Differences

Linux vs macOS Syscall Numbering

Linux uses direct syscall numbers that differ completely between architectures:

  • x86-64: write=1, exit=60
  • AArch64: write=64, exit=93

macOS x86-64 uses a class-based system where syscall numbers include a class identifier:

  • Formula: (class << 24) | syscall_number
  • BSD/UNIX class: 0x2000000 (class 2 shifted left 24 bits)
  • Mach class: 0x1000000 (class 1)
  • Examples: write = 0x2000000 + 4 = 0x2000004, exit = 0x2000000 + 1 = 0x2000001

macOS AArch64 drops the BSD class prefix entirely and uses direct syscall numbers:

  • write=4, exit=1
  • This represents a modernization of Apple’s syscall interface for ARM64.
Entry Point Symbol Naming

Linux (both x86-64 and AArch64):

  • Uses _start with underscore as the default entry point
  • GNU ld linker expects this symbol by default.

macOS (both x86-64 and AArch64):

  • Uses start without underscore as the default entry point.
  • Apple’s ld64 linker expects this symbol by default.
AArch64 Syscall Register Differences

Linux AArch64:

  • Uses x8 for the syscall number
  • Standard ARM Linux convention

macOS AArch64:

  • Uses x16 for the syscall number (critical difference!).
  • x16 (IP0) is an intra-procedure-call scratch register in the ARM calling convention.

Both platforms use x0-x5 for the first six arguments consistently.

x86-64 Fourth Argument Register

This is a subtle but important difference between syscalls and regular function calls on x86-64:

Normal function calls (System V ABI):

  • Fourth argument: rcx
  • Standard user-space function calling convention

Syscalls:

  • Fourth argument: r10
  • Required because the syscall instruction clobbers rcx by storing the return RIP there.
  • Syscall wrapper functions in libc execute mov r10, rcx to translate between the two conventions.
Supervisor Call Instruction Variations

Linux AArch64:

  • Instruction: svc #0
  • Standard ARM supervisor call with immediate value 0

macOS AArch64:

  • Instruction: svc #0x80
  • Uses immediate value 0x80, following BSD/Darwin convention

Important: this immediate value is encoded in the instruction but is ignored by the CPU at dispatch time — the kernel identifies which syscall to make via x16, not the svc immediate. The distinction between svc #0 and svc #0x80 is purely a software convention.

Both x86-64 platforms:

  • Use the syscall instruction (identical syntax)
Compilation and Execution
Linux AArch64
Bash
as -o hello.o hello_linux_aarch64.s
ld -o hello hello.o
./hello
macOS AArch64 (M-series Macs)
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 (Intel Macs)
Bash
as -o hello.o hello_macos_x86_64.s
ld -o hello hello.o -lSystem
./hello
Understanding the Syscalls
The Write Syscall

The write syscall has identical semantics across all platforms, despite different syscall numbers:

Prototype: ssize_t write(int fd, const void *buf, size_t count)

Parameters:

  • fd (file descriptor): 1 represents stdout (standard output), 2 is stderr
  • buf (buffer): Memory address containing the data to write
  • count (size): Number of bytes to write from the buffer

Return value: Number of bytes written on success, or -1 on error

The Exit Syscall

The exit syscall terminates the process immediately:

Prototype: void exit(int status)

Parameters:

  • status (exit code): 0 conventionally indicates success; non-zero values indicate various error conditions.
  • Parent processes can examine this exit code via wait() or similar calls.

Note: This is a process termination syscall, so it never returns.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top