Input/Output & Syscalls
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.
Printing a Number: Converting Digits Yourself
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:
| Register | Purpose |
|---|---|
rax | Syscall number (in), return value (out) |
rdi | Argument 1 |
rsi | Argument 2 |
rdx | Argument 3 |
r10 | Argument 4 (note: not rcx) |
r8, r9 | Arguments 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
| # | Name | Signature | Purpose |
|---|---|---|---|
| 0 | read | read(fd, buf, count) | Read bytes from a file descriptor |
| 1 | write | write(fd, buf, count) | Write bytes to a file descriptor |
| 2 | open | open(path, flags, mode) | Open a file; returns a file descriptor |
| 3 | close | close(fd) | Release a file descriptor |
| 9 | mmap | mmap(...) | Map memory pages (allocator building block) |
| 60 | exit | exit(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.
| Approach | Link with | Entry point | Best for |
|---|---|---|---|
| Raw syscalls | ld | _start | Learning, tiny binaries, freestanding code |
| libc functions | gcc | main | Formatting, buffered I/O, portability |
Summary
syscallis the only doorway from a program to the kernel on Linux x86-64.- The syscall number goes in
rax; arguments go inrdi,rsi,rdx,r10,r8,r9; the result comes back inrax. - File descriptors 0, 1, and 2 are standard input, output, and error — already open when your program starts.
writeneeds 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.