I/O#

Assembly’s I/O surface is the syscall instruction (or svc #0 on ARM64). The kernel exposes a numbered table of syscalls; the operator loads the number plus the arguments into the right registers and invokes the trap. Userland libraries (libc) wrap these for convenience, but the operator who writes shellcode or freestanding binaries calls them directly.

For errno handling, see Errors. For socket syscalls, see Networking.

Linux x86-64 syscall ABI#

  • Syscall number: rax.

  • Args 1-6: rdi rsi rdx r10 r8 r9. (Note: r10, not rcx; the syscall instruction clobbers rcx.)

  • Result: rax (negative errno on failure; see Errors).

  • Clobbered: rcx (return address shadow), r11 (RFLAGS shadow).

; write(1, msg, len)
mov     rax, 1                  ; syscall number
mov     rdi, 1                  ; fd = stdout
lea     rsi, [rel msg]
mov     rdx, len
syscall

The syscall numbers use daily.

Num

Name

Args

0

read

fd, buf, count

1

write

fd, buf, count

2

open

pathname, flags, mode

3

close

fd

9

mmap

addr, length, prot, flags, fd, offset

10

mprotect

addr, length, prot

11

munmap

addr, length

12

brk

addr

13

rt_sigaction

sig, act, oldact, sigsetsize

39

getpid

41

socket

domain, type, protocol

42

connect

sockfd, addr, addrlen

59

execve

filename, argv, envp

60

exit

status

231

exit_group

status

Full table at /usr/include/asm/unistd_64.h or in the kernel’s arch/x86/entry/syscalls/syscall_64.tbl.

ARM64 Linux syscall ABI#

  • Syscall number: x8.

  • Args 1-6: x0..x5.

  • Result: x0 (negative errno on failure).

  • Trap instruction: svc #0.

// write(1, msg, len)
mov     x0, #1                  // fd
adr     x1, msg
mov     x2, #len
mov     x8, #64                 // syscall number for write on arm64
svc     #0

ARM64 Linux uses a different numbering than x86-64; see arch/arm64/include/asm/unistd.h for the canonical table.

hello world (Linux x86-64)#

section .rodata
msg:    db "hello, operator", 0x0a
len     equ $ - msg

section .text
global  _start
_start:
    mov     rax, 1              ; write
    mov     rdi, 1              ; stdout
    lea     rsi, [rel msg]
    mov     rdx, len
    syscall

    mov     rax, 60             ; exit
    xor     rdi, rdi            ; status 0
    syscall

Build and run.

$ nasm -felf64 hello.asm -o hello.o
$ ld -o hello hello.o
$ ./hello
hello, operator

Reading from stdin#

section .bss
buf:    resb 4096

section .text
; read(0, buf, 4096)
mov     rax, 0
mov     rdi, 0                  ; stdin
lea     rsi, [rel buf]
mov     rdx, 4096
syscall
; rax now holds bytes read (or negative errno)

The operator handles short reads by looping until the desired length is read or EOF (rax == 0).

File open / read / close#

; fd = open(path, O_RDONLY)
mov     rax, 2
lea     rdi, [rel path]
xor     rsi, rsi                ; O_RDONLY (0)
xor     rdx, rdx                ; mode unused
syscall
mov     r12, rax                ; save fd

; n = read(fd, buf, 4096)
mov     rax, 0
mov     rdi, r12
lea     rsi, [rel buf]
mov     rdx, 4096
syscall

; close(fd)
mov     rax, 3
mov     rdi, r12
syscall

O_RDONLY = 0, O_WRONLY = 1, O_RDWR = 2, O_CREAT = 0x40, O_APPEND = 0x400. The operator combines with or.

Calling libc#

When the operator targets a hosted binary (the C runtime is linked), going through libc beats raw syscalls for portability.

extern  printf, exit

section .rodata
fmt:    db "n=%d", 10, 0

section .text
global  main
main:
    sub     rsp, 8              ; align to 16
    lea     rdi, [rel fmt]
    mov     rsi, 42
    xor     rax, rax            ; no vector args
    call    printf
    xor     edi, edi
    call    exit

Exiting#

exit_group (231) terminates every thread in the process; exit (60) terminates only the calling thread. For single-threaded binaries either works.

; exit_group(0)
mov     rax, 231
xor     rdi, rdi
syscall

Formatting and parsing#

The kernel does no formatting; read and write move raw bytes. The operator either calls into printf / sprintf via libc, or hand-rolls integer-to-string conversion.

A minimal itoa for unsigned 64-bit values.

; itoa: rdi = value, rsi = out buffer (>= 21 bytes); rax = length
global  itoa
itoa:
    mov     rax, rdi
    mov     rcx, 10
    lea     r8, [rsi + 21]      ; write from end
    mov     rsi, r8

.loop:
    xor     rdx, rdx
    div     rcx                 ; rdx = digit
    add     dl, '0'
    dec     rsi
    mov     [rsi], dl
    test    rax, rax
    jnz     .loop

    mov     rax, r8
    sub     rax, rsi            ; length
    ret

References#