Input/Output & Syscalls

A program with no output is indistinguishable from a crash. Everything a process does to the outside world goes through the kernel — and on Linux, "asking the kernel" is one instruction: syscall.

Writing Output

The write Syscall

Output is one call to the kernel: write(fd, buffer, count). The count is the number of bytes, not characters or items — and for a string that means you must know its length.

section .rodata
    msg     db  "Hello, stdout!", 10
    MSG_LEN equ $ - msg              ; byte count, computed by the assembler

section .text
    mov  rax, 1                      ; syscall 1 = write
    mov  rdi, 1                      ; fd 1 = stdout
    mov  rsi, msg                    ; pointer to the bytes
    mov  rdx, MSG_LEN                ; how many bytes
    syscall

write returns the number of bytes actually written in rax. This can be fewer than you asked for — on a pipe, a socket, or a slow terminal — so robust code loops until every byte has been sent. For small console output this almost never happens, but it is the reason library functions look more complicated than the snippet above.

The kernel writes bytes; it does not format integers. To print the number 12345 you must produce the characters '1', '2', '3', '4', '5' yourself. The standard technique is repeated division by 10, filling a buffer backwards.

; print_uint — write an unsigned integer in rax to stdout
; Destroys rax, rbx, rcx, rdx, rsi, rdi
section .bss
    numbuf  resb 32                  ; digits, filled from the end

print_uint:
    mov  rbx, 10                     ; divisor
    lea  rsi, [numbuf + 31]          ; point at the LAST byte of the buffer
    xor  rcx, rcx                    ; rcx = number of digits produced

.digit_loop:
    xor  rdx, rdx                    ; clear the high half before dividing
    div  rbx                         ; rax = rax / 10, rdx = rax % 10
    add  dl, '0'                     ; convert the digit to its ASCII code
    dec  rsi                         ; move one byte towards the front
    mov  [rsi], dl                   ; store the character
    inc  rcx                         ; one more digit
    test rax, rax                    ; any quotient left?
    jnz  .digit_loop                 ; yes -> next digit

    ; write(1, rsi, rcx)
    mov  rdx, rcx                    ; length = digit count
    mov  rax, 1
    mov  rdi, 1
    syscall
    ret

Walking through 12345: the loop extracts 5, then 4, then 3, 2, 1 — least significant first — and stores each one moving backwards from the end of the buffer. When it finishes, rsi points at the first digit and the digits are already in the correct order. This is the algorithm behind every integer-to-string conversion, in every language.

Reading Input

The read Syscall

Reading is the mirror image of writing: give the kernel a buffer, a maximum size, and the file descriptor to read from.

    ; read(fd, buffer, count)   -> returns bytes actually read in rax
    mov  rax, 0              ; syscall 0 = read
    mov  rdi, 0              ; fd 0 = stdin
    mov  rsi, buffer         ; where to put the bytes
    mov  rdx, 64             ; maximum number of bytes
    syscall

    ; rax now holds the number of bytes read:
    ;   0  = end of input (EOF, e.g. Ctrl-D)
    ;  <0  = error (negative errno)

Three facts make read different from the scanf you may be used to. It returns raw bytes, not a parsed value. It reads up to the requested count, which may be fewer than you asked for. And the newline you type is part of the data — it is the last byte before the count you received.

The Linux Syscall Interface

How a Syscall Works

User code cannot touch hardware directly. To open a file or write to the screen, it asks the kernel, and on x86-64 that request travels through registers in a fixed layout:

RegisterPurpose
raxSyscall number (in), return value (out)
rdiArgument 1
rsiArgument 2
rdxArgument 3
r10Argument 4 (note: not rcx)
r8, r9Arguments 5 and 6

Then syscall switches the CPU into kernel mode, the kernel performs the work, and control returns with the result in rax — a non-negative value on success, or a negative -errno on failure. That the failure signal is a negative number rather than a flag is worth remembering when you check for errors.

The Syscalls You Will Use Most

#NameSignaturePurpose
0readread(fd, buf, count)Read bytes from a file descriptor
1writewrite(fd, buf, count)Write bytes to a file descriptor
2openopen(path, flags, mode)Open a file; returns a file descriptor
3closeclose(fd)Release a file descriptor
9mmapmmap(...)Map memory pages (allocator building block)
60exitexit(status)Terminate the process

File descriptors are small integers that name open files. Three are created for you before your first instruction runs: 0 is standard input, 1 is standard output, and 2 is standard error. That is why mov rdi, 1 means "print to the screen".

Using libc Instead

Raw syscalls are educational but verbose: you must format every number yourself. Linking against the C library gives you printf, scanf, and file handling that already work.

; echo_libc.asm — read a line and print it back, using printf/scanf
; Build:  nasm -f elf64 echo_libc.asm -o echo_libc.o
; Link:   gcc echo_libc.o -o echo_libc

section .rodata
    prompt  db  "Type something: ", 0
    fmt_in  db  "%63s", 0               ; read at most 63 chars, stop at space
    fmt_out db  "You typed: %s", 10, 0

section .bss
    buffer  resb 64                     ; space for the input

section .text
    global main
    extern printf, scanf

main:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16                     ; keep rsp 16-byte aligned

    lea     rdi, [rel prompt]           ; printf(prompt)
    xor     eax, eax
    call    printf

    lea     rdi, [rel fmt_in]           ; scanf("%63s", buffer)
    lea     rsi, [rel buffer]
    xor     eax, eax
    call    scanf

    lea     rdi, [rel fmt_out]          ; printf("You typed: %s\n", buffer)
    lea     rsi, [rel buffer]
    xor     eax, eax
    call    printf

    xor     eax, eax                    ; return 0
    leave
    ret

Compare the two approaches honestly. The syscall version has no dependencies and shows exactly what the kernel does, but you own the formatting. The libc version is shorter and richer, but you are now calling a function whose internals are hidden — and whose stack-alignment and register rules you must still respect.

ApproachLink withEntry pointBest for
Raw syscallsld_startLearning, tiny binaries, freestanding code
libc functionsgccmainFormatting, buffered I/O, portability

Summary

  • syscall is the only doorway from a program to the kernel on Linux x86-64.
  • The syscall number goes in rax; arguments go in rdi, rsi, rdx, r10, r8, r9; the result comes back in rax.
  • File descriptors 0, 1, and 2 are standard input, output, and error — already open when your program starts.
  • write needs a pointer and a byte count; a partial write returns the number of bytes actually written.
  • Printing a number means converting it to digits yourself — the kernel does not format anything.

Next: Debugging & Disassembly — what to do when the program compiles and still does the wrong thing.