Capstone: Tracing a System Call from Your Code Down to Silicon
This module follows one printf("speed = %d km/h\n", speed) through every layer of the course.
Concept
This module follows one printf("speed = %d km/h\n", speed) through every layer of the course.
Layer by layer:
Source and compiler (Modules 1, 7, 9): printf becomes a call __printf_chk@PLT; the argument 120 travels in edx per the calling convention; the dynamic linker resolves the call to glibc at load time.
User-space library: printf formats the text into the stdout buffer. When stdout is a terminal the buffer is flushed at each newline; when it is a pipe or file, when the buffer fills or the program exits (Module 23).
System call (Module 11): glibc's write wrapper loads system call number 1 into eax and executes syscall.
CPU mode switch (Modules 8, 12): the CPU switches to ring 0, jumps to the kernel entry point stored in an MSR, and the kernel saves the user registers and switches to the kernel stack.
Kernel: the dispatcher calls ksys_write; the file descriptor table maps descriptor 1 to an open file (a pipe here, a tty in a terminal); the VFS calls that object's write method, which copies the bytes from user memory (with checks that the buffer belongs to the process).
Memory (Modules 18–19): every instruction and data access along the way goes through the TLB and the cache hierarchy; touching a new page may page-fault.
Scheduling (Module 14): if the pipe were full, the thread would block and the scheduler would run something else; writing wakes the reader.
Devices (Modules 12, 23): for a real terminal or disk, the driver programs the device; completion arrives as an interrupt, possibly with DMA.
Return: sysret restores user mode; the return value 17 travels back in rax.
Hardware (Modules 2–6, 24–26): underneath all of it, the core fetches, decodes, renames and executes micro-ops in a pipeline, predicts branches, keeps caches coherent — built from gates and flip-flops.
sequenceDiagram
participant Main as main (user code)
participant Stdio as C library stdio
participant Wrap as libc write wrapper
participant CPU
participant Kernel
participant Dev as Terminal or pipe driver
Main->>Stdio: printf formats text<br/>into the stdout buffer
Stdio->>Wrap: flush: write(1, buf, 17)
Wrap->>CPU: mov eax 1, syscall
CPU->>Kernel: ring 3 to ring 0<br/>entry_SYSCALL_64
Kernel->>Kernel: ksys_write:<br/>look up fd 1, check buffer
Kernel->>Dev: copy 17 bytes into<br/>the pipe or tty buffer
Dev-->>Kernel: wake the reader
Kernel-->>CPU: rax = 17, sysret
CPU-->>Wrap: back in ring 3
Wrap-->>Main: printf returns 17Why It Matters
Being able to explain this path end to end is the classic "what happens when…" interview question, and it is the mental model you use when debugging anything from a slow logger to a hung process.
Syntax
The tools used along the way: gcc -S, objdump -d, strace -T, ltrace, /proc/<pid>/fd, /proc/<pid>/maps.
Example
30.1 Observing the path
$ strace -T -e trace=write -o w.txt ./hello > out.txt; cat w.txt
write(1, "speed = 120 km/h\n", 17) = 17 <0.000061>
+++ exited with 0 +++
$ objdump -d --no-show-raw-insn --start-address=0x$A --stop-address=$((0x$A+32)) /lib/x86_64-linux-gnu/libc.so.6 | grep -E 'mov +\$0x1,%eax|syscall|cmp|ret'
114a30: mov $0x1,%eax
114a35: syscall
114a37: cmp $0xfffffffffffff000,%rax
114a3f: ret
$ grep -m1 -o 'syscall' /proc/cpuinfo || grep -m1 -o -w 'lm' /proc/cpuinfo
syscall
$ ls -l /proc/self/fd/1 | awk '{print $9, $10, $11}'
/proc/self/fd/1 -> pipe:[5623]Expected Result
strace -T shows the single write(1, "speed = 120 km/h\n", 17) = 17 and the time spent inside the kernel for it (tens of microseconds here, including tracing overhead).
The disassembly of glibc's write wrapper shows exactly the instructions described above: mov $0x1,%eax (system call number 1), syscall, a comparison of the return value against −4095 (error range → set errno), and ret.
The CPU advertises the syscall instruction in /proc/cpuinfo.
Descriptor 1 of the traced command was a pipe (the transcript captured its output), not a terminal — which is why stdout was fully buffered and flushed only at exit.
Common Mistakes
Thinking printf itself is a system call.
Forgetting buffering: output order can change when stdout is a pipe instead of a terminal.
Believing the kernel runs "somewhere else" — it runs on the same core, in the context of the calling thread.
Best Practices
Use this layered model to locate problems quickly: compile, link, load, library, system call, kernel, device, hardware.
Keep strace, objdump and /proc in your everyday toolkit.
Interview Relevance
Very high: "what happens when you call printf?", "what happens when you type ls and press Enter?", "walk me through a system call", "what happens on a keypress from the keyboard to the screen?".
Practice Questions
Repeat the trace with stdout connected to a file and to a terminal; compare when the write happens.
Draw the same sequence diagram for read() from a keyboard.
Explain what changes on ARM64 (svc #0, x8 holds the number).
Describe what happens when you type ls in a shell and press Enter, using fork, exec, system calls and scheduling.
Quick Quiz
Q1. (Multiple choice) Which register carries the system call number on x86-64 Linux? A) rdi B) rax C) rsp D) rip
Answer: B (verified: mov $0x1,%eax in the write wrapper).
Q2. (Predict the output) How many write system calls does the program make for its one line?
Answer: One, returning 17 (verified).
Q3. (True/False) When stdout is a pipe, printf output with a newline is written immediately.
Answer: False. It is fully buffered and written when the buffer fills or at exit/fflush.
Q4. (Find the bug) A program prints progress with printf without newlines and crashes before exit; no output appears.
Answer: The text was still in the stdio buffer; call fflush(stdout) (or use stderr, which is unbuffered) for progress output.
Coding Challenge
Show that stdio buffering depends on the output type: print whether stdout is a terminal (isatty) and demonstrate with setvbuf switching stdout to line buffering before printing two lines.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int tty = isatty(STDOUT_FILENO);
setvbuf(stdout, NULL, _IOLBF, BUFSIZ); /* force line buffering even for pipes and files */
printf("stdout is %s; now line-buffered\n", tty ? "a terminal" : "not a terminal (pipe or file)");
printf("each line is written as soon as it ends\n");
return 0;
}