AArch64 normal function calls and Linux system calls both pass arguments starting from x0, but they do not use the same convention. Normal functions use bl for branching and allocate registers according to AAPCS64; system calls place the call number in w8, execute svc #0, and the kernel fulfills the request. The immediate value of svc is not the Linux system call number.
This article discusses native Linux user-space for AArch64 and does not cover the AArch32 compatibility ABI. Applications should use SYS_* or __NR_* from the target platform headers; do not carry over x86-64 call numbers to ARM64.
Master the Application-Visible Conventions First
Purpose
AArch64 Linux Convention
Entering the kernel
svc #0
System call number
w8, i.e., the lower 32 bits of x8
Up to six arguments
x0, x1, x2, x3, x4, x5
Primary return value
x0
Raw error
-errno in x0, error range -4095 to -1
libc public wrapper error
Usually returns -1 and sets the thread-local errno
The same x0 represents argument 1 upon entry and the result upon exit. For example, in write, the x0 at entry is the file descriptor, and the x0 at successful exit is the number of bytes written in this call. The C function syscall() already handles errno; it is not equivalent to a snippet of assembly that directly returns raw registers. syscall(2)
w and x are different width views of the same register
Writing to a w register clears the upper 32 bits of the corresponding x register. Therefore, the full x0 value obtained from mov w0, #-1 is not 64-bit -1. If the kernel argument type is int, the all-1s lower 32 bits can be interpreted as -1 according to that type; if the interface requires a signed 64-bit number, sign extension cannot be omitted. Pointer, size_t, and offset arguments must also be prepared according to the target ABI and interface definitions; they cannot be uniformly loaded using w registers. Arm AArch64 Registers
The following bitwise operation model only illustrates register bit patterns and does not invoke the kernel. It also demonstrates that truncating the return value to 32 bits masks the full 64-bit result; actual raw error checking must use the full width of x0.
Here, x_after_w_write is not in the 64-bit error range, so you cannot judge it as a system call failure just by seeing 0xffffffff in the lower 32 bits. On the other hand, you cannot declare it a valid address just because the same number is not in the error range: whether it is an address and whether it is accessible are still determined by the specific call result and mapping conditions.
Same low bits, different return value?
At -1, switching extension changes only the upper half but changes the 64-bit meaning from 4294967295 to -1. At -4096, even the sign-extended negative value lies outside the reserved raw-error interval.
Preparing the visual
Same low bits, different return value?
Writing w clears the upper half; sign extension replicates the sign bit. Raw errors are recognized using the full 64-bit reserved range.
Taking entry from EL0 to EL1 as an example, the hardware saves the return address and processor state, selects the stack for the target exception level, and enters the exception vector. Synchronous exception slots from a lower exception level in AArch64 state are located at VBAR_EL1 + 0x400; the entry code also needs to distinguish between SVC, instruction exceptions, etc., based on the exception reason.
The hardware does not push all x0–x30 into memory at once. The Linux software entry is responsible for saving the required general-purpose registers, user stack pointer, etc., forming a context available for tracing, signal delivery, and return path operations. Do not mistakenly write "hardware does not save any state" because "hardware does not automatically save all general-purpose registers." Arm Exception Model
When reading Linux v6.18, you can trace from kernel_entry/kernel_exit in entry.S to el0_svc_common and invoke_syscall in syscall.c. Entry and exit also involve tracing, signals, rescheduling, and security-related work; you cannot represent the complete kernel implementation with a few spliced pseudo-assemblies. Entry Assembly, System Call Dispatch
When Comparing with x86-64, Compare at the Same Level
Question
Native x86-64 Linux
Native AArch64 Linux
Argument 4
r10, avoiding rcx used by hardware
x3
Where is the return location temporarily stored by hardware
rcx
ELR_EL1 (using EL1 as an example)
Where is the processor state temporarily stored by hardware
r11
SPSR_EL1 (using EL1 as an example)
Establishing entry memory frame
Linux software saves registers
Linux software saves registers
Exception return
SYSRET if conditions met, otherwise IRET
ERET
AArch64 does not have the same-named general-purpose register corruption of x86 SYSCALL on rcx/r11, but this still does not allow you to say "system calls preserve all register states." Especially for extended vector states with independent ABIs: the SVE system call boundary has clear conventions for high vector registers, predicate registers, etc., and you cannot assume the entire SVE/SME state remains unchanged. Linux SVE ABI
Performance cannot be deduced from the table either. Whether page table switches are triggered, kernel mitigation measures, CPU model, frequency, and virtualization environment all affect latency; neater register arrangement does not necessarily mean faster execution.
A Complete, Assemblable Example
The following relies only on AArch64 Linux's write=64 and exit=93. It handles short writes and EINTR, and exits on other failures; it is suitable for observing the ABI, not a general asynchronous I/O loop.
Generating an object file does not mean it has been executed and verified on ARM64. Running it also requires AArch64 Linux or a correctly configured emulation environment. This example has no libc startup code, and exit does not flush stdio buffers.
Making an Assembly Leaf Function Satisfy Both C ABI and Syscall ABI
The previous _start has no normal callers. If you want C to call an assembly wrapper, the function must receive arguments and return according to AAPCS64, and also place the registers required by Linux before svc. The following raw-write-arm64.S uses the three C arguments directly for write and returns the raw result directly.
The corresponding C caller is saved as raw-write-main.c:
externlongraw_write_arm64(intfd,constvoid*buffer,unsignedlongsize);intmain(void){staticconstchar message[]="hello\n";constchar*next = message;unsignedlong left =sizeof message -1;while(left !=0){long result =raw_write_arm64(1, next, left);if(result ==-4)continue;/* Native Linux EINTR */if(result <=0)return1;
next += result;
left -=(unsignedlong)result;}return0;}
Here, the C call places fd, buffer, and size in the argument positions corresponding to x0–x2; the assembly only needs to set w8. The return address established by bl is in x30; the exception return state of svc is saved in the exception level registers. After Linux returns to user mode, this leaf wrapper can ret back to the C caller.
If you later add a bl helper in the wrapper, you must save and restore your own return address and comply with the callee-saved register rules. AAPCS64 public interface requires SP to be aligned to 16 bytes; do not imitate the style of pushing only one 8-byte value, causing subsequent calls to execute with incorrect stack alignment. AAPCS64 Subroutine Call
The above two compilations do not require C standard library headers, making them suitable for checking symbols and call relocations on other architectures. They do not complete C runtime linking. For actual runtime, use the native toolchain on AArch64 Linux to execute cc raw-write-main.c raw-write-arm64.S -o raw-write-demo, then run ./raw-write-demo; the expected output is hello.
Check Level
What It Can Show
What It Cannot Replace
Cross-assembly
Instruction and relocation syntax are acceptable
Whether the kernel accepts the current arguments
ELF header / symbol / relocation
Target is AArch64, call references have been generated
Correct runtime register contents
Native or emulation environment run
Output and exit status on the current path
All error paths, real hardware performance
Target machine stress and fault testing
Behavior under signals, short writes, etc.
Universal guarantees for other kernel configurations
CPU Features and System Call Numbers Are Different Issues
getcpu and mlock2 are not ARM64-unique system calls. PR_PAC_RESET_KEYS is an operation of prctl, not an independent syscall number. Whether PAC, BTI, SVE, and LSE are available requires judgment based on processor capabilities and the ABI exposed by the kernel.
In AArch64 Linux C programs, use the bit definitions in system headers; do not hand-copy HWCAP constants:
HWCAP describes capabilities that the kernel allows user-space to use, which is more reliable than guessing based on CPU model strings; it is not a checklist of whether system calls are implemented. Linux ELF HWCAP
LDXR/STXR are instructions that user-space atomic algorithms might use, and do not require entering the kernel every time. They must be distinguished from LDAXR/STLXR with acquire/release semantics; LSE provides another set of atomic instructions. Atomicity, memory ordering, and wait/wake are three separate issues; you cannot deduce NUMA scalability from "no x86 lock prefix." For how actual synchronization enters the kernel, continue reading futex.