AlgoMaster Logo

System Calls: The Kernel's API

15 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

Applications need the kernel to perform protected operations.

A text editor needs to read and write files. A web server needs to accept network connections. A process may need more memory or may want to start another program.

Applications cannot perform these operations by directly modifying kernel data or controlling hardware. Instead, they make system calls.

A system call is a controlled request from a user-space program to the kernel.

The application issues a system-call request, the kernel performs or rejects the operation, and a result returns to the application.

System calls form the kernel's public interface to running programs.

Why the System-Call Boundary Exists

User mode prevents an application from executing privileged instructions or freely accessing kernel memory. This protects the kernel and other processes from buggy or malicious code.

However, complete isolation would make applications useless. They still need controlled access to files, devices, memory, processes, and networks.

The system-call interface provides that controlled access.

When an application makes a system call, it identifies an operation and supplies arguments. The kernel checks the request and either performs the operation or returns an error.

An error is a normal outcome here, not a malfunction. The checks exist precisely because the request arrived from code the kernel does not trust.

The kernel does not trust a request simply because it arrived through the correct entry point. It still checks addresses, sizes, object identifiers, and permissions before using them.

This arrangement provides both usability and protection:

Without a kernel interfaceWith system calls
Applications would need direct hardware accessApplications request high-level operations
A program could modify any kernel stateThe kernel controls which state may change
Programs could bypass access checksThe kernel enforces permissions
Hardware details would leak into applicationsThe kernel presents stable abstractions

The Kernel's API

An API, or application programming interface, describes the operations a software component makes available to other software.

The system-call API exposes kernel operations such as:

AreaCommon Linux operations
Filesopenat, read, write, close
Processesclone, execve, wait4, exit_group
Memorymmap, mprotect, munmap
Networkingsocket, connect, accept4, sendto, recvfrom
System informationgetpid, uname, clock_gettime

These names represent requests understood by the Linux kernel. Other operating systems expose different interfaces, even when they provide similar application-level functionality.

At the source-code level, developers usually work with C function declarations. At the machine-code level, a compiled program must follow the system-call conventions of its operating system and processor architecture.

This lower-level binary contract is part of the platform's ABI, or application binary interface. ABI details will be discussed later in this module. For now, remember that the kernel and the program must agree on how a request is represented.

System Calls vs. Normal Function Calls

A normal function call transfers control to another function in the same process.

The called function uses the process's existing privileges and address space. It can access memory that the calling code can access, and it returns using the language and processor's ordinary calling convention.

A system call crosses a protection boundary.

The request begins in user mode, but the requested operation is performed by kernel code with kernel privilege. The processor uses a special controlled-entry mechanism rather than an ordinary jump to an arbitrary kernel address.

The function-call path stays entirely inside one process at one privilege level. The system-call path passes through a boundary the program cannot step over on its own, and that extra stage is the whole difference. The distinction is about where the operation runs, not how the source code looks. In C, a system-call wrapper can look exactly like an ordinary function:

The expression uses normal C function-call syntax. Behind that function, however, the program may request the kernel's read operation.

The full control-transfer path is the subject of the next chapter.

Loading simulation...

Library Functions and System Calls

Applications usually do not construct raw system-call requests themselves.

They call functions provided by a system library. On Linux, this is commonly the GNU C Library, called glibc, or another C library such as musl.

The library provides wrapper functions such as read, write, mmap, and getpid.

A call travels through four stages: the application source code calls the wrapper, the C library wrapper prepares the request, the Linux system call carries it across the boundary, and the kernel performs the work.

A wrapper gives the application a normal typed C interface. Internally, it arranges the system-call number and arguments according to the platform's ABI, enters the kernel, and converts the kernel's result into the convention expected by C code.

This layer also improves source portability. Application code can call read without containing x86-64 or AArch64 register names.

The mapping is not always one to one

It is tempting to assume that every library function corresponds to one system call with the same name. That is not true.

A library function may require no system call, one system call, or several system calls.

Library operationPossible relationship to system calls
strlenWorks entirely in user space
writeUsually wraps one kernel write operation
printfFormats and buffers data in user space, then may call write
mallocUsually manages an existing user-space heap and only occasionally requests more memory
openMay use a newer kernel operation such as openat internally

The reverse can also occur: a system call may exist before a C library adds a dedicated wrapper for it.

The C library is therefore an adapter between application-facing functions and the lower-level kernel interface, not a simple list of identically named pass-through functions.

System-Call Numbers

Inside the kernel, a system call is identified by a number.

The number acts like an index into the set of operations supported by a particular system-call ABI.

Conceptually, number 0 selects one operation, number 1 selects another, number 2 a third, and so on through the table the kernel maintains.

Programs should not assume that a number has the same meaning on every processor architecture.

For example, Linux uses different numbers for the same operations on x86-64 and AArch64:

System callx86-64 numberAArch64 number
write164
getpid39172

These numbers are ABI details, not facts application developers should memorize.

C headers provide symbolic constants such as SYS_write and SYS_getpid when code genuinely needs to refer to a system call by number. The correct value is selected for the target platform.

Within a supported ABI, existing numbers are generally preserved for binary compatibility. New kernel versions can add operations, but changing the meaning of an established number would break already compiled programs.

If a program requests an operation that the running kernel does not implement, Linux can report the ENOSYS error, meaning “function not implemented.”

Passing Arguments to the Kernel

A system-call request needs more than an operation number. It also needs arguments.

Consider the application-facing write function:

The request contains three pieces of information:

ArgumentMeaning
fdWhich open resource to write to
bufferWhere the bytes are stored in the process's memory
countHow many bytes the process wants to write

The system-call ABI defines where the program places these arguments before entering the kernel.

Linux normally passes the system-call number and a limited number of arguments in CPU registers. The chosen registers depend on the architecture.

ArchitectureSystem-call numberArgumentsResult
x86-64raxrdi, rsi, rdx, r10, r8, r9rax
AArch64x8x0 through x5x0

Linux system-call ABIs generally support up to six direct arguments.

Large values are not placed entirely in registers. Instead, an argument can be a pointer to data in the application's memory.

The registers carry only small values. The bytes themselves never move into a register, which is why one of those values is an address rather than data.

A user-space pointer is only an address in the calling process. The kernel must treat it as untrusted and verify that the requested memory range is valid for that operation.

Structures allow one pointer argument to describe several related values. Length arguments tell the kernel how much data a buffer or structure contains and can also support future extensions.

The next chapter follows these values through the complete execution path and explains how the kernel handles them safely.

Return Values

A system call returns a result to the calling program.

On success, the meaning depends on the operation:

OperationTypical successful result
open or openatA nonnegative file descriptor
readNumber of bytes read
writeNumber of bytes written
getpidA process identifier
mmapAn address
closeZero

The return value is part of the operation's contract. For example, a successful read can return fewer bytes than the application requested, and a return value of zero can indicate the end of a file.

When the kernel cannot perform an operation, it reports an error code.

At the raw Linux system-call boundary, errors are represented using negative numbers. Conceptually, the kernel might return the negative form of an error such as EACCES or ENOENT.

Most applications do not see that raw representation. For many wrappers, the C library converts it into the following conventional interface:

  1. The raw kernel result is a negative error code.
  2. The C library wrapper inspects it.
  3. The function returns -1 and sets errno.

Some functions use a different documented failure value. For example, mmap returns MAP_FAILED. Code must check the failure convention specified for the function it calls.

This translation is one of the useful jobs performed by the wrapper layer.

Understanding errno

errno provides additional information when a library function reports failure.

Common values include:

ErrorMeaning
ENOENTA requested file or path does not exist
EACCESThe operation is not permitted by access rules
EBADFA file descriptor is invalid for the operation
ENOMEMThe requested memory could not be provided
EINTRThe operation was interrupted before completing
ENOSYSThe requested system call is not implemented

The names are constants defined by system headers. An application should use names such as ENOENT, not hard-coded numeric values.

Here is the standard error-checking pattern for opening a file:

If config.txt does not exist, the output may resemble:

perror prints the supplied context, followed by a description of the current errno value.

There are three important rules for using errno:

  1. Check the function's documented failure return first.
  2. Read errno only after the function reports an error.
  3. Do not expect a successful call to reset errno to zero.

In multithreaded programs, errno behaves like thread-local state, so one thread's error does not overwrite another thread's value.

An error is a normal result of a request the kernel could not complete. It does not automatically crash the process. The application decides whether to retry, choose another action, report the problem, or exit.

The Different Meanings of syscall

The word syscall appears in several related contexts.

A system call is the abstract operation requested from the kernel.

On x86-64, syscall is also the name of the CPU instruction commonly used to enter the kernel.

Linux C libraries additionally provide a function named syscall() that lets a program request an operation by its symbolic number:

The syscall() function is still a C library helper. It provides a generic way to invoke the numbered kernel interface and applies the library's normal error convention.

Applications should usually prefer a dedicated wrapper such as getpid():

Dedicated wrappers provide appropriate types, hide architecture-specific details, and allow the library to select the best available implementation.

The generic syscall() function is most useful for low-level experiments or for using a newer kernel operation before the C library provides a dedicated wrapper.

Observing System Calls with strace

On Linux, strace shows the system calls made by a process.

Create syscall_demo.c:

Compile it:

Run it normally:

Then trace only the operations relevant to this example:

A simplified result looks like this:

Exact spacing and the way program output is interleaved with the trace can vary.

The source code explicitly calls getpid, which appears in the trace.

The source code calls printf, but printf does not appear because it is a C library function rather than a system call. It formats the text in user space and eventually uses the write system call to send the output to standard output.

The trace also displays the kernel-facing arguments and results. In the write line:

Trace fieldMeaning
1The file descriptor for standard output
"PID: 12345\n"A readable representation of the buffer
11 before )The requested byte count
= 11The number of bytes actually written

strace is showing the logical system-call interface. It does not mean that the kernel received a quoted string directly; the program passed a memory address and a length, and strace inspected the referenced bytes for display.

Later chapters will use strace to investigate files, processes, and network activity in more depth.

A Practical Mental Model

When reading C code, keep three layers separate:

  1. The application-facing function.
  2. The C library implementation.
  3. The kernel system-call interface.

The application-facing function expresses what the program wants to do.

The C library adapts that request to the current platform and translates the result into C conventions.

The kernel validates the request and operates on protected system state.

The layers often have similar names, which makes them easy to confuse. Tools such as strace help reveal where the user-space library ends and the kernel interface begins.

Summary

A system call is a controlled request from a user-space program to the kernel. It allows applications to use protected resources without receiving unrestricted kernel or hardware access.

Each request identifies an operation with an architecture-specific system-call number and passes arguments according to the platform's ABI. Results return through the same interface.

Applications normally use C library wrappers rather than constructing requests directly. These wrappers provide typed functions, hide register and number differences, and translate raw Linux error results into a failure return plus errno.

The essential mental model is:

A library call is code running in your process; a system call is a request for the kernel to perform a protected operation.

Quiz

System Calls: The Kernel's API Quiz

5 quizzes