QUESTION
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
Structure diagram
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
Call path
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.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What 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 PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 syscall_write.c -o syscall_write01#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
Code notes
const char message[]A string literal ends with a NUL, but the length passed to write() does not include that NUL.
while (done <write() may write fewer bytes than requested, so the program accumulates the number of completed bytes separately.
ssize_t n = writeThe 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.
errno == EINTRIf a signal handler interrupts the call before any data is written, retry from the same position.
strerror(errno)Do not call another function that might change errno before printing the error. In a multithreaded program, errno is thread-local.
DETAILS
Detailed behavior
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.
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.
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
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
int fd | Created by open or inheritance; removed by close or close-on-exec during exec | FD_CLOEXEC, fd table slot |
struct file | An open file description, released when its last reference is dropped | f_pos, f_flags, f_op |
errno | A failed libc call records a value for the current thread | Read only when the return value is -1 |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| write() returns -1 | EBADF, EPIPE, EFAULT, or a filesystem error | Check the strace return value and the signal (SIGPIPE) |
| Only some bytes are written | Pipe/socket capacity, a signal, quota, or a limit | Check whether returned byte counts are accumulated |
| The program exits unexpectedly | Writing to a closed pipe/socket delivers SIGPIPE | Check the SIGPIPE disposition and EPIPE |
LAB
Verify it yourself
- Run the example in a terminal and verify that strace displays the fd, buffer, and count in a call such as write(1, ..., 17).
- Pipe stdout to head -c 1, repeatedly send a large buffer, and observe the relationship between EPIPE and SIGPIPE.
- Use objdump -d to find the executable's write@plt call, then compare the wrapper with the registers immediately before the syscall in gdb.
./syscall_writestrace -e trace=write ./syscall_writePRIMARY REFERENCES