system calls Linux tutorial
Linux System Programming
William  

Linux System Calls Tutorial: A Step-by-Step Guide

Have you ever wondered how a user program actually reaches the kernel to do privileged work?

I set the goal for this tutorial: show how a program crosses the CPU boundary, what the kernel does next, and how you can verify behavior with tracing tools.

I explain what you will be able to do by the end: read syscall man pages correctly, reason about wrappers versus raw entry, and recognize common entry mechanisms on x86.

Scope is clear: user space vs kernel space, syscall numbers and tables, x86 legacy and fast paths, plus the vDSO shortcut.

Guardrails I use in real work: I prefer libc wrappers for correctness; I only handcraft raw assembly for learning or controlled experiments.

Why this matters in production: when latency, permissions, or sandboxing break, low-level knowledge saves hours of debugging.

Table of Contents

Key Takeaways

  • I map how a program invokes kernel services at the CPU boundary.
  • I show which paths x86 uses and when vDSO helps.
  • I recommend using wrappers for safety; use raw calls for study.
  • I preview tracing with strace and sysdig to expose behavior.
  • This guide targets seasoned admins and devs working on Intel/AMD x86_64.

What a system call is and why Linux programs use it

Think of a system call as a formal request from your program to the kernel to perform work it cannot do itself. I use that doorway whenever code needs privileged access: open a file, talk to a device, or create a process.

A call differs from a normal C library function in one key way: a call triggers a CPU mode switch into kernel mode. A pure library function stays in user space and runs without that transition.

Some libc functions map directly to kernel entry points—chdir is a clean example. Others do extra work first: fwrite buffers data, batches writes, and updates bookkeeping before invoking write.

Common jobs handled by kernel entry

  • Files: open, read, write, sync.
  • Processes: fork, clone, execve.
  • Network I/O: socket, connect, accept.
  • Devices and control: ioctl and mmap for device memory.
LayerTypical functionDoes it enter kernel?Notes
User libraryfwriteNo, unless buffer flushBuffers and coalesces writes
WrapperchdirYesMaps directly to kernel entry
KernelopenatYesSee man 2 openat for RETURN VALUE/ERRORS

Where I look for details

I start at man 2 syscalls to get the index. Then I jump to specific pages: man 2 openat, man 2 execve, man 2 mmap. Read the RETURN VALUE and ERRORS sections first—those spell out what to check when behavior differs across machines.

User space vs kernel space: CPU privilege levels and kernel mode

I describe the split as a hard CPU fence. The processor enforces who may touch sensitive state. The fence keeps faults and crashes out of the kernel.

I run apps in Ring 3. The kernel runs in Ring 0. Ring 3 cannot execute privileged instructions or poke hardware. That restriction prevents rogue programs from corrupting device registers or kernel memory.

Ring 3 vs Ring 0 — practical view

Ring 3 runs your code and normal libraries. Ring 0 runs the scheduler, drivers, and memory manager. A privilege level change lets the kernel perform I/O or change address space on your behalf.

Privilege levelWho runs hereObservable effects
Ring 3 (user)Applications, librariesPermission errors like EPERM; limited hardware access
Ring 0 (kernel)Kernel core, driversCan program devices, change page tables, handle interrupts
TransitionInstruction + entry pointCPU switches mode; kernel code executes; return to user

What “entering the kernel” means

A dedicated instruction triggers the switch. The cpu changes privilege level and jumps to a kernel entry handler. Registers and a saved state let the kernel validate arguments and act safely.

Bad pointers or missing rights show up as EFAULT or EPERM when the kernel checks them. Observing these errors with tracing tools makes the boundary visible.

Every syscall needs two pieces: an entry mechanism (the instruction or gate) and a target kernel function (looked up in a table). I will cover those next.

How the kernel finds the right function: syscall numbers and the syscall table

At the CPU boundary, a small numeric ID points the processor to the right kernel routine. The stable mental model is simple: number + arguments. The number identifies the service; the arguments carry the data.

Loading the number and arguments into registers

I put the call number and its arguments into ABI-defined registers before entry—usually via libc. That setup is predictable: one register holds the number, others hold the argument values. The CPU instruction that triggers entry then hands control to the kernel.

Table lookup to a kernel function pointer, conceptually

Inside the kernel, the dispatcher reads the number and checks bounds. It indexes a table and loads a function pointer. The kernel validates the pointer and runs the target.

  • Why numbers exist: compact identifiers that survive the user/kernel boundary.
  • Where mappings come from: arch/x86/…/syscall_64.tbl and similar .tbl files used at build time.
  • Common debugging trap: wrong number or wrong calling convention looks like random EINVAL or EFAULT—garbage arguments confuse the kernel.
StepWhat movesWhy it matters
Usernumber, arguments in registersCompact entry; ABI agreement
Entryprocessor switches modeSafe kernel execution
Kerneltable index → function pointerFast dispatch to implementation

system calls Linux tutorial: making a call from C with glibc wrappers

I default to the C library wrappers for most kernel-facing work. They handle ABI setup, errno, and expected user-space hooks. That keeps code predictable across distros and libc versions.

Why wrappers matter

Wrappers normalize return values and set errno. They implement cancellation points and run atexit handlers. They also hide ABI quirks that vary by architecture.

In practice I call open, read, write, fork/clone, and friends through glibc. That gives me consistent output and known behavior for library-driven tooling.

When to use syscall(2) and what you give up

I use syscall(2) only for new or missing interfaces, or when I need a minimal path to reproduce a kernel bug. It calls by number and avoids wrapper logic.

  • Pros: minimal layers; direct entry for testing.
  • Cons: you lose type safety, errno helpers, and user-space hooks like atexit.
  • Risk: other libraries expect wrapper behavior—bypassing them can break cleanup and tools.
ApproachWhen to useWhat you lose
glibc wrapperDefault for app codeNothing; safer semantics
syscall(2)Unwrapped or new syscallWrapper fixes, atexit, type checks
Raw entry (assembly)Experiments, demosUser-space cleanup and portability

I recommend: start with wrappers, trace with strace to confirm behavior, then use syscall(2) only when you need the minimal path.

Legacy entry: int 0x80 on x86 and what it teaches you

I teach int $0x80 because it peels back the curtain on how user code hands privileged work to the kernel.

A close-up of a vintage computer setup, showcasing a detailed motherboard with an x86 CPU highlighted. In the foreground, a prominent terminal window is open, displaying the assembly code instruction "int 0x80" in a sleek, modern text editor with syntax highlighting. The middle layer features a softly lit workspace with a programmer’s hands typing on a mechanical keyboard, surrounded by various hardware components such as RAM sticks and a power supply. The background includes blurred network diagrams and Linux system architecture visuals, creating an atmosphere of technical sophistication and learning. The lighting is ambient and warm, evoking a sense of focus and dedication in a professional environment, perfect for a tutorial on Linux system calls.

Register convention on 32‑bit

On 32‑bit x86 the call number lives in %eax. Arguments follow in %ebx, %ecx, %edx, %esi, %edi, %ebp.

That layout forces you to think about how each register carries information across the boundary.

Following the int $0x80 path

  1. You load the number and arguments into registers.
  2. The int instruction raises a software interrupt; the CPU consults the IDT and switches mode.
  3. The kernel handler reads %eax, indexes the syscall table, and invokes the implementation.

Return to user space

The kernel uses the saved user state on the stack; an iret restores EIP/CS/EFLAGS and execution resumes in user mode.

StageKey registersRole
User prep%eax, %ebx..%ebpNumber + args
EntryIDT, CPU stateMode switch to kernel
ReturnstackSaved state restored with iret

Note: int $0x80 is a teaching tool—rarely the right choice for production but common in old binaries, compatibility paths, and certain writeups.

Fast system calls on 32-bit: sysenter and sysexit

A different entry instruction shaves cycles—but it shifts more responsibility to user space. On 32‑bit x86 the sysenter/sysexit pair is a fast path provided by the CPU. It avoids the full interrupt machinery that int $0x80 uses.

Why sysenter is different

sysenter runs faster because the CPU does less work. It does not save a user return address for you. That missing save makes return handling a contract between user and kernel—one broken contract causes crashes or wrong pointers on return.

MSRs you should know

The kernel programs three MSRs: IA32_SYSENTER_EIP (the kernel entry address), IA32_SYSENTER_ESP (the Ring 0 stack pointer), and IA32_SYSENTER_CS (the kernel code segment selector). Together they tell the hardware where to land and which stack to use.

MechanismState savedSpeed
int 0x80CPU saves return stateslower
sysenterminimal save—kernel assumes setupfaster
__kernel_vsyscallUser-mapped stub sets statefast and consistent

Why care? If the linux kernel’s MSR plumbing or the user stub misaligns, you see odd faults near entry. glibc picks the best mechanism at runtime—so your C code usually stays stable. __kernel_vsyscall is an early vDSO-style helper: the kernel maps a small stub into user space so processes can prepare for sysenter reliably.

Fast system calls on 64-bit: syscall and sysret

The syscall instruction on x86-64 is the common bridge—lean, fast, and predictable.

CPU expects the call number in a specific place and arguments in ABI registers. On x86-64 the convention is: rax = number, rdi/rsi/rdx/r10/r8/r9 = args. Executing the instruction hands control to kernel entry; sysret returns to user mode.

Here is a minimal learning plan in assembly: use write (number 1) to send bytes to fd 1 (stdout), then exit (number 60). This shows how a small program produces console output and terminates cleanly.

RegisterRoleExample
raxcall number1 for write
rdifirst argumentfile descriptor (1 = stdout)
rsisecond argumentpointer to buffer
  • I verify the bytes appear on stdout.
  • I check the process exit status matches the exit call.
  • I run strace to confirm the exact syscall sequence.

Reality check: this is for controlled learning. For production I prefer libc wrappers that handle edge cases, errno, and compatibility.

vDSO and virtual system calls: faster paths without a full kernel transition

A tiny shared object mapped into each process can remove a full kernel transition for common helpers. The kernel provides that mapping so user code can run a few coordinated functions quickly.

What the vDSO is and why it is mapped

The vDSO is a small, kernel-provided shared object in a process’s address space. It exposes helpers the kernel agrees to keep up to date.

Those helpers avoid a costly mode switch for frequent operations—things like reading a clock or translating monotonic time. Faster paths reduce latency and CPU overhead for hot code.

How glibc finds and uses the vDSO

glibc detects the vDSO at runtime. If a function exists there, the library calls it instead of issuing a full kernel entry.

You do not change source code: the choice is automatic. That makes apps faster with no code changes.

Timing expectations and measurement tips

Benchmarks may show very low overhead for some “kernel” calls because they actually run in user space via the vDSO. That can mislead microbenchmarks.

  • Expect some time readings to avoid a mode switch.
  • Confirm behavior with tracing: use strace or perf to see whether the process enters kernel mode.
  • Pair microbenchmarks with a tracing tool to avoid false conclusions about intervention or memory effects.
AspectvDSOFull kernel entry
Where it runsUserKernel
Typical latencyLowHigher
Use caseTime helpers, small transformsI/O, privileged ops

Note: __kernel_vsyscall was an early variant of this idea. Modern vDSO is safer and more flexible—another way the kernel reduces overhead while keeping control.

Tracing system calls to understand what a program is doing

I start tracing when I need to know exactly which files and sockets a process touches. Tracing turns vague symptoms into concrete evidence: which file was missing, which process spawned, which network connection failed.

Using strace for a quick view of file, process, and network activity

strace is my first tool for fast answers. Run a targeted trace:

strace -f -e trace=open,connect,execve,read,write -o trace.log ./mycommand

I scan for openat/open, execve, connect, and permission errors (EACCES, ENOENT). That shows files, directories, spawned commands, and immediate failures.

Using sysdig with filters for targeted troubleshooting

When I need system-wide context, I switch to sysdig. Capture just the process I care about:

sysdig proc.name=mycommand and evt.is_io=true

Use evt.type=connect, evt.type=execve, evt.type=clone to focus output and avoid noise.

Calls worth watching and a quick checklist

  • clone — threads/process creation.
  • execve — what actually runs.
  • open/creat — file access and creation.
  • connect / accept — outbound and inbound network.
  • read/write, readv/writev — I/O patterns on descriptors.
  • brk, mmap, munmap — memory growth and mappings.
  • select, poll — blocked or waiting states.
  • kill — signals and sudden exits.
ToolScopeBest for
straceSingle processQuick debug of file and exec activity
sysdigSystem-wideFiltered, correlated traces across processes
perf/otherMicrobenchmarksLatency and high-volume I/O profiling
  1. Reproduce the issue with minimal input.
  2. Capture a short trace window with strace or sysdig.
  3. Filter for errors: ENOENT, EACCES, ECONNREFUSED.
  4. Map errors to config, file paths, or network endpoints and fix.

Where to go next: safer experiments and deeper kernel reading

Begin with a reproducible lab: boot a VM you can snapshot, keep a known-good kernel entry in GRUB, and isolate experiments from production.

Read the arch syscall table for your architecture, follow the generated headers, and trace one system call from entry to implementation. That reveals the full path: number → entry stub → handler.

For a custom syscall you must edit arch/x86/entry/syscalls/syscall_64.tbl, add the handler with SYSCALL_DEFINE, copy user data with helpers like strncpy_from_user, rebuild, and reboot into the test kernel.

Safety rules: never trust user pointers—return -EFAULT on bad input. Validate changes with dmesg for printk output and strace to confirm the call path. Note: tables and conventions shift across kernel versions—always match source to the running kernel.

Next reads: man 2 syscalls, the architecture entry code in the tree, and tool docs for strace/sysdig. Test in a VM, iterate, and keep snapshots.

FAQ

What is a system call and why do programs use it?

A system call is the mechanism user code uses to request a kernel service—access files, control processes, talk to devices, or perform network I/O. Programs use it because user code can’t access hardware or privileged CPU instructions directly; the kernel provides a controlled gateway.

How does a system call differ from a C library function?

A C library function can be pure user-space work or a thin wrapper that prepares arguments, handles errno, and invokes the kernel. When you hit the kernel—kernel mode executes—the library stops and kernel code runs with higher privileges to do the real I/O or resource change.

Which common jobs are handled via these kernel requests?

Typical jobs include file operations (open/read/write/close), process management (fork/exec/exit), device I/O, and network operations (socket/connect/accept/send/recv). Memory mapping and allocation are also performed through those interfaces.

Where can I browse the official list of calls and details?

Consult the man pages—syscalls(2) for an index and each call’s man page (e.g., open(2), mmap(2)). Those pages show arguments, return values, errno behavior, and architecture notes.

What’s the difference between user space and kernel space?

User space runs unprivileged code (Ring 3 on x86) and can’t touch hardware or protected memory. Kernel space runs privileged code (Ring 0) and has full access. The switch enforces security and stability.

What does “entering the kernel” mean during a syscall?

It means the CPU switches privilege level and context: registers may be saved, a controlled entry path runs kernel code, and the kernel services the request before returning control to user code.

How does the kernel dispatch the right function for a call?

User code loads a syscall number and arguments into registers. The kernel reads that number, indexes a syscall table, and invokes the corresponding function pointer to handle the request.

How are syscall numbers and arguments passed?

The calling convention uses specific registers: put the syscall number in the designated register and arguments in the other registers. The kernel reads them directly—no stack copy needed for the common path.

How do I invoke a call from C using the standard library?

Use glibc wrappers like read(), write(), open(). The wrappers handle argument normalization, 64-bit compatibility, and errno. They then perform the actual kernel transition for you.

When should I use syscall(2) directly?

Use syscall(2) when you need an unwrapped interface or a call glibc doesn’t expose. You give up portability, wrapper conveniences, and sometimes safety—so know the ABI and targeting architecture first.

What did int 0x80 teach us about legacy entry on x86?

int 0x80 used a software interrupt on 32-bit to switch to kernel mode; the syscall number lived in eax and args in registers. It’s a clear, simple model that shows the basic save/restore and dispatch flow.

How did return-to-user work with int 0x80?

The kernel restored saved CPU state—flags, instruction pointer, stack pointer—and used iret to resume user code. That stack-saved state ensured a faithful return to the user context.

What are sysenter and sysexit on 32-bit systems?

sysenter/sysexit are faster entry/exit instructions that avoid the interrupt machinery. They require explicit setup (no automatic return address), so the kernel uses model-specific registers (MSRs) for fast entry points.

Which MSRs matter for fast entry on x86?

IA32_SYSENTER_EIP, IA32_SYSENTER_ESP, and IA32_SYSENTER_CS are the important MSRs. They tell the CPU where to jump in the kernel and what stack to use for the fast path.

What is __kernel_vsyscall and why did it exist?

__kernel_vsyscall provided a user-mapped trampoline for certain time-related helpers before newer fast paths. It let some calls avoid a full kernel transition—an early optimization that evolved into the vDSO approach.

How do syscall and sysret work on 64-bit?

On x86-64 the syscall instruction provides a fast, controlled switch into kernel mode; sysret returns. The convention maps syscall number and args to registers different from the 32-bit layout and streamlines the entry/exit.

Can you show a minimal assembly example for write then exit?

A minimal example uses the syscall instruction: load the write syscall number and args into registers, invoke syscall; then load the exit number and code and invoke syscall again. Keep register ordering and ABI rules correct.

What is the vDSO and why is it mapped into processes?

The vDSO is a small kernel-provided library mapped into every process address space. It implements common helpers—like clock reading—so some operations avoid a full kernel transition and run faster in user space.

How does glibc find and use the vDSO?

At runtime glibc locates the vDSO by reading ELF auxiliary vectors and then calls its exported helpers when appropriate. That gives transparent speedups for time-related functions and similar helpers.

How should I interpret timing calls that might hit vDSO helpers?

Expect lower latency and no kernel entry for many clock reads. Use careful benchmarking—pin threads and avoid preemption—because the vDSO fast path changes measurable timing behavior.

Which tools help trace kernel activity to understand a program?

strace gives a quick syscall-level trace of file, process, and network activity. sysdig provides richer filtering and event context. Use both depending on depth and performance needs.

Which calls are worth watching during debugging?

watch clone and execve for process creation, open/creat and read/write for file I/O, connect/accept for sockets. Also monitor mmap/brk for memory growth and select/poll for wait states.

What I/O and memory patterns should I expect in traces?

I/O usually appears as open/read/write/close and socket activity; memory behavior shows up as brk, mmap, and munmap. Patterns reveal buffering, lazy allocation, and resource leaks.

Where should I go next for safer experiments and deeper reading?

Start with per-call man pages and a small test program that calls the interfaces you want to inspect. Use strace/sysdig and run under a VM or container to avoid harming your host. Then read kernel source and ABI docs for architecture specifics.