roadmap
week 4 · day 25

Talking to the Kernel: syscalls

write() with no libc at all

Hard 34 min 180 xp
After this you can
  • 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.

RegisterHolds
raxthe syscall NUMBER (e.g. 1 = write, 60 = exit, 0 = read)
rdi1st argument
rsi2nd argument
rdx3rd argument
r104th argument (not rcx — syscall clobbers rcx)
r8, r95th, 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:

hello.asm — pure syscalls (Intel/NASM)asm
1section .data
2msg: db "hello, kernel", 10 ; the text, plus 10 (newline '\n')
3len: equ $ - msg ; length = current address - msg
4
5section .text
6global _start
7_start:
8 mov rax, 1 ; syscall number: write
9 mov rdi, 1 ; fd 1 = stdout
10 mov rsi, msg ; buf = address of the text
11 mov rdx, len ; count = number of bytes
12 syscall ; ask the kernel to do the write
13
14 mov rax, 60 ; syscall number: exit
15 mov rdi, 0 ; exit code 0
16 syscall ; ask the kernel to end the process

Read 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.

Key idea
Everything you've ever called for I/O bottoms out here. 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.
Predict first
You want to read up to 64 bytes from standard input (fd 0) into a buffer. read is syscall number 0 with signature read(fd, buf, count). Which values go in rax, rdi, rsi, rdx before the syscall?
Note
A syscall's return in rax also encodes errors: the kernel returns a small negative value (like -2 for "no such file") on failure, which libc wrappers translate into a −1 return plus 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.
finished reading?
Your task, you write the code

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.

deliverable: hello.asm (+ the strace output showing your write and exit)
build & run
$ nasm -f elf64 hello.asm -o hello.o && ld hello.o -o hello
$ ./hello
$ strace ./hello # watch the write(1,...) and exit syscalls
self-review before running
  • 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
stretchAdd a read syscall (number 0) that reads a line from stdin into a buffer, then write it back out (a tiny echo) — all in syscalls, no libc. Then strace a normal `printf("hi\n")` C program and find the write syscall printf ultimately performed.

Self-check

01Why must a program use a syscall to write to the screen?
02On Linux x86-64, where does the syscall NUMBER go?
03How does the syscall argument convention differ from the normal function ABI?
04What is libc's write() function, underneath?
0/4 correct · 0/4 checked