Syscall / ELF · Linux userspace / kernel ABI

From the libc wrapper to the syscall

Traces how a single write() call passes through the calling convention, syscall number, registers, and kernel entry code before reaching a file object.

Series
01 / 38
Build
cc -std=c17 -Wall -Wextra -O2 syscall_write.c -o syscall_write
Run
./syscall_write
Kernel
Linux 6.18.37 LTS

Is write() an ordinary C function or a kernel entry point?

Applications normally call glibc's write(). The function loads its arguments into registers according to the architecture ABI and executes the syscall instruction. The kernel selects an implementation by syscall number and returns a negative errno value.

Treating the libc wrapper and the kernel syscall implementation as the same function leads you to debug in the wrong place. In userspace you see -1 and errno; at the kernel boundary you see a negative error code and the actual number of bytes returned.

Structure diagram

Figure 1. Execution domains and value representations across a write() call
Applicationfd=1 · buffer pointer · count
glibc write()C ABI → syscall register ABI · errno conversion
CPU entrysyscall instruction · pt_regs · syscall number
VFSfd table → struct file → file_operations
Returnkernel: byte/-errno · libc: byte/-1 + errno

Userspace -1/errno and a negative kernel error code do not exist at the same point. The diagram shows what each layer passes to the layer below.

Call path

Figure 2. From userspace code to observable results
main() prepare buffer and count
write() glibc wrapper
syscall x86-64 instruction
ksys_write convert fd to struct file
return byte count or errno

A userspace function return value and a kernel-internal return value use different representations. Keep separate the point where libc converts a negative kernel error into -1 and errno.

Figure 3. Major points along the kernel-internal path
entry_SYSCALL_64 save pt_regs
__x64_sys_write argument decode
ksys_write fdget_pos
vfs_write file_operations
f_pos update offset

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
arch/x86/entry/entry_64.S entry_SYSCALL_64 The first point where the CPU turns userspace registers into pt_regs
fs/read_write.c ksys_write(), vfs_write() How an integer fd becomes a struct file and file_operations
include/linux/syscalls.h SYSCALL_DEFINE3(write) The syscall prototype and argument types

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 syscall_write.c -o syscall_write
01#include <errno.h>
02#include <stdio.h>
03#include <string.h>
04#include <unistd.h>
05
06int main(void)
07{
08    const char message[] = "syscall boundary\n";
09    size_t done = 0;
10
11    while (done < sizeof(message) - 1) {
12        ssize_t n = write(STDOUT_FILENO, message + done,
13                          sizeof(message) - 1 - done);
14        if (n > 0) {
15            done += (size_t)n;
16            continue;
17        }
18        if (n < 0 && errno == EINTR)
19            continue;
20        fprintf(stderr, "write: %s\n", strerror(errno));
21        return 1;
22    }
23    return 0;
24}

Code notes

Source line 8const char message[]

A string literal ends with a NUL, but the length passed to write() does not include that NUL.

Source line 11while (done <

write() may write fewer bytes than requested, so the program accumulates the number of completed bytes separately.

Source line 12ssize_t n = write

The result is a byte count on success and -1 on failure, so it is stored in ssize_t, which can represent negative values, rather than size_t.

Source line 18errno == EINTR

If a signal handler interrupts the call before any data is written, retry from the same position.

Source line 20strerror(errno)

Do not call another function that might change errno before printing the error. In a multithreaded program, errno is thread-local.

Detailed behavior

01

The function-call convention and syscall convention are distinct

C function calls follow the compiler ABI, while syscall entry uses a separate register layout defined by the architecture. On x86-64 the syscall number is in rax, and the fourth argument is moved to r10 rather than remaining in rcx as it would for a function call.

Bypassing the wrapper with inline assembly also makes you responsible for libc policies such as cancellation points, errno conversion, and vDSO selection. This is not an optimization that merely removes one instruction.

02

An fd is not a kernel pointer

The value 1 in STDOUT_FILENO is a small integer used to look up the process's file-descriptor table. The open file description is reached only after ksys_write() obtains a struct file reference through fdget_pos().

After dup() or fork(), different fds can refer to the same struct file. They then share the file offset and status flags.

03

The return value is part of the data flow

Even a regular file can produce a short write because of a signal, resource limit, or filesystem error. Short writes are more common with pipes, sockets, and nonblocking fds.

To avoid retrying a write result of n == 0 forever, a real program must also define the target type and the conditions under which progress is guaranteed.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
int fdCreated by open or inheritance; removed by close or close-on-exec during execFD_CLOEXEC, fd table slot
struct fileAn open file description, released when its last reference is droppedf_pos, f_flags, f_op
errnoA failed libc call records a value for the current threadRead only when the return value is -1

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
write() returns -1EBADF, EPIPE, EFAULT, or a filesystem errorCheck the strace return value and the signal (SIGPIPE)
Only some bytes are writtenPipe/socket capacity, a signal, quota, or a limitCheck whether returned byte counts are accumulated
The program exits unexpectedlyWriting to a closed pipe/socket delivers SIGPIPECheck the SIGPIPE disposition and EPIPE

Verify it yourself

  1. Run the example in a terminal and verify that strace displays the fd, buffer, and count in a call such as write(1, ..., 17).
  2. Pipe stdout to head -c 1, repeatedly send a large buffer, and observe the relationship between EPIPE and SIGPIPE.
  3. Use objdump -d to find the executable's write@plt call, then compare the wrapper with the registers immediately before the syscall in gdb.
Run./syscall_write
Tracestrace -e trace=write ./syscall_write

Primary sources