Master the fundamental concepts of system calls & kernel interface through this focused micro-challenge.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksEvery libc call like write() eventually lands in a syscall instruction. Calling syscall() directly shows the register ABI the kernel expects: syscall number in rax, arguments in rdi, rsi, rdx, and friends on x86-64.
Common pattern:
cLoading…
SYS_write = 11 is stdoutrax (negative errno on error)For example, hello world needs only write and exit, no printf formatting layer.
Bypassing libc to call syscall() directly is exactly how musl and Google's Fuchsia libc bootstrap their runtimes, and it's the same register convention (RAX for the number, RDI/RSI/RDX for arguments) that strace decodes and gdb inspects when debugging a crash. Static binaries built this way are also how minimal Docker base images and Go's early runtime avoided depending on a dynamically linked glibc entirely.
Before you call the implementation done, walk failure modes on purpose. Test empty structures, single-element edge cases, maximum concurrency, and errno paths that must not crash the program. OS code usually fails in production when happy-path tests pass but invariants break under contention or memory pressure.
Keep structures small and name fields after kernel counterparts when possible. That lets you read man pages and kernel source side by side while you work. Print observable events during development; remove noisy logs once tests pass reliably.
You will print a message using only syscall() wrappers with no libc stdio. This task asks you to list each register assignment so you can read strace output without guessing.
With no libc, a program talks to Linux through the syscall instruction alone. On x86-64, the syscall number goes in rax and the arguments in rdi, rsi and rdx (then r10, r8 and r9). The result comes back in rax, and a value from -4095 to -1 means -errno. Simulate a freestanding program that makes these calls: print the register setup for each call, the kernel's effect, and the raw return value, and explain how a libc wrapper would turn it into -1/errno.
cLoading…
| Syscall | rax | Behaviour |
|---|---|---|
| write | 1 | fds 0-2 start open. A closed or out-of-range fd (0-15) gives -9 EBADF. raw has no buffer, so a raw write to an open fd gives -14 EFAULT |
| close | 3 | Closes the fd, or gives -9 EBADF |
| getpid | 39 | Returns 4242 |
| exit / exit_group | 60 / 231 | Exit status CODE & 0xff. Nothing after this runs |
| anything else | -38 ENOSYS |
cLoading…
raw loads as many as were given). A write to fd 2 is labelled stderr:, and any other fd fd:.\n, \t, \" and \\ escapes.fell off the end without exit: a real no-libc _start would crash here.Input:
cLoading…
Output:
cLoading…
strlen in a freestanding program.Hidden tests cover writes to closed and invalid fds, a double close, unknown syscall numbers, raw writes (EFAULT), exit codes above 255, lines after exit that must not run, and a program that never exits.