Talking to the Kernel: syscalls
write() with no libc at all
- › Explain the user/kernel boundary and why a program can't do I/O itself
- › Describe the Linux x86-64 syscall convention: number in rax, args in rdi, rsi, rdx…
- › Invoke write and exit directly in assembly, with no C library
- › See that libc functions like write() and printf() are wrappers around syscalls
The wall between you and the hardware
Your program cannot touch the disk, the network, or the screen directly. It runs in user mode, walled off from the hardware for safety — one buggy program can't corrupt another or the OS. Only the kernel, running in kernel mode, may do those things. So when your program needs the outside world — write bytes, open a file, allocate pages, exit — it must ask the kernel. That request is a system call (syscall). It's the one doorway between your code and everything real.
The convention (Linux, x86-64)
A syscall is almost like a function call, with its own register convention — closely related to the
ABI from day 13, but with two differences: the syscall number goes in rax, the arguments use
rdi, rsi, rdx, r10, r8, r9 (note: r10, not rcx — the syscall instruction clobbers rcx), and you
trigger it with the `syscall` instruction. The kernel does the work and returns a result in
rax.
| Register | Holds |
|---|---|
| rax | the syscall NUMBER (e.g. 1 = write, 60 = exit, 0 = read) |
| rdi | 1st argument |
| rsi | 2nd argument |
| rdx | 3rd argument |
| r10 | 4th argument (not rcx — syscall clobbers rcx) |
| r8, r9 | 5th, 6th arguments |
| rax (after) | the return value / error code |
Hello, kernel — with no libc
write is syscall number 1. Its signature is write(fd, buf, count): write count bytes from
buf to file descriptor fd (1 = standard output). exit is number 60, taking the exit code.
Here's a complete program that prints a line and exits — no C library involved at all:
1section .data2msg: db "hello, kernel", 10 ; the text, plus 10 (newline '\n')3len: equ $ - msg ; length = current address - msg4 5section .text6global _start7_start:8 mov rax, 1 ; syscall number: write9 mov rdi, 1 ; fd 1 = stdout10 mov rsi, msg ; buf = address of the text11 mov rdx, len ; count = number of bytes12 syscall ; ask the kernel to do the write13 14 mov rax, 60 ; syscall number: exit15 mov rdi, 0 ; exit code 016 syscall ; ask the kernel to end the processRead it against the table: for the write, rax=1 (which syscall), rdi=1 (fd), rsi=address of the
bytes, rdx=how many; then syscall hands control to the kernel, which writes the bytes and returns.
Then rax=60, rdi=0, syscall and the process ends. No main, no libc, no runtime — just the raw
requests. This is the bedrock every higher-level I/O sits on.
write(2) in C is a thin wrapper that
puts your arguments in the right registers, sets rax=1, and executes syscall. printf formats a
string in a buffer and then calls write. malloc ultimately asks the kernel for memory pages via
mmap/brk syscalls. Peel any library function and you eventually find a syscall. Knowing them
means nothing about how a program touches the world is a mystery — and it's why shellcode (weeks 40+)
is written straight in syscalls: no library needed, just ask the kernel directly.read is syscall number
0 with signature read(fd, buf, count). Which values go in rax, rdi, rsi, rdx before the syscall?errno. So
"check the return value" is not optional — a write can be short, a read can hit end-of-input, an open
can fail. The full number-to-name table is one lookup away, and worth bookmarking.Speak to the kernel with no libc
In hello.asm, write a program that uses ONLY the write and exit syscalls to print a message of your choice to stdout and exit with code 0 — no C library, entry point _start. Assemble with nasm, link with ld (no libc), and run it. Then: use `strace ./hello` to watch the exact syscalls your program makes (you should see your write(1, ...) and exit(0)). Finally, write the same 'print a line' in C using write() directly and strace it too, confirming it makes the same syscall. Uses only this lesson plus day 6.
$ nasm -f elf64 hello.asm -o hello.o && ld hello.o -o hello$ ./hello$ strace ./hello # watch the write(1,...) and exit syscalls- › Your program prints correctly using only write, and exits with code 0 via exit
- › It links with ld alone (no libc)
- › strace shows your write(1, "...", len) and the exit call
- › You can name which register held the syscall number, fd, buffer, and count