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.
Assembly Syscall Tutorial: Table of Contents
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
// 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
// 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
# 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
# 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
| Syscall | Linux x86-64 | Linux AArch64 | macOS x86-64 | macOS AArch64 |
|---|---|---|---|---|
| write | 1 | 64 | 0x2000004 | 4 |
| exit | 60 | 93 | 0x2000001 | 1 |
Syscall Invocation
| Platform | Syscall Instruction | Syscall Number Register |
|---|---|---|
| Linux x86-64 | syscall | rax |
| Linux AArch64 | svc #0 | x8 |
| macOS x86-64 | syscall | rax |
| macOS AArch64 | svc #0x80 | x16 |
Register Calling Conventions
x86-64 (AMD64) Syscall Convention
| Purpose | Register | Description |
|---|---|---|
| Syscall Number | rax | Identifies which syscall to invoke |
| Argument 1 | rdi | First parameter (e.g., file descriptor) |
| Argument 2 | rsi | Second parameter (e.g., buffer pointer) |
| Argument 3 | rdx | Third parameter (e.g., byte count) |
| Argument 4 | r10 | Fourth parameter (syscall only, not standard function ABI) |
| Argument 5 | r8 | Fifth parameter |
| Argument 6 | r9 | Sixth parameter |
| Return Value | rax | Syscall 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
| Purpose | Linux Register | macOS Register | Description |
|---|---|---|---|
| Syscall Number | x8(orw8) | x16 | Identifies which syscall to invoke |
| Argument 1 | x0 | x0 | First parameter (e.g., file descriptor) |
| Argument 2 | x1 | x1 | Second parameter (e.g., buffer pointer) |
| Argument 3 | x2 | x2 | Third parameter (e.g., byte count) |
| Argument 4 | x3 | x3 | Fourth parameter |
| Argument 5 | x4 | x4 | Fifth parameter |
| Argument 6 | x5 | x5 | Sixth parameter |
| Return Value | x0 | x0 | Syscall 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
_startwith underscore as the default entry point - GNU ld linker expects this symbol by default.
macOS (both x86-64 and AArch64):
- Uses
startwithout underscore as the default entry point. - Apple’s ld64 linker expects this symbol by default.
AArch64 Syscall Register Differences
Linux AArch64:
- Uses
x8for the syscall number - Standard ARM Linux convention
macOS AArch64:
- Uses
x16for 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
syscallinstruction clobbersrcxby storing the return RIP there. - Syscall wrapper functions in libc execute
mov r10, rcxto 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
syscallinstruction (identical syntax)
Compilation and Execution
Linux AArch64
as -o hello.o hello_linux_aarch64.s
ld -o hello hello.o
./hellomacOS AArch64 (M-series Macs)
as -o hello.o hello_macos_aarch64.s
ld -o hello hello.o -lSystem
./helloLinux x86-64
as -o hello.o hello_linux_x86_64.s
ld -o hello hello.o
./hellomacOS x86-64 (Intel Macs)
as -o hello.o hello_macos_x86_64.s
ld -o hello hello.o -lSystem
./helloUnderstanding 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):
1represents stdout (standard output),2is 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):
0conventionally 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.

