Skip to content
Courses

Programs Asking Linux for Help

How Programs WorkLesson 7 of 775 minutesLessonBuilds on: The Heap and Data LifetimeNo lab next

Virtual memory lets Linux control which memory each process can access. We also need rules for shared resources such as files and network devices. Two programs may need the same resource at once, and each request has to respect permissions and the work already in progress.

If programs could issue unrestricted disk operations independently, they could overwrite each other’s data or bypass file permissions. We need one part of the system to coordinate those operations and check whether a request is allowed.

Ordinary application code runs in userspace, with the processor enforcing restrictions on privileged instructions and memory access. Kernel code runs with privileges that let it manage those resources. When a program needs to open a file, create a mapping, write output, or exit, it can make a system call: a request that transfers execution into the kernel.

The kernel checks the request and any applicable permissions, performs the operation if it can, and returns a result. The Linux system-call reference describes this interface and the library wrappers programs commonly use to reach it.

A C library function isn’t automatically a system call (this trips up a lot of people). printf does its formatting entirely in userspace, and only eventually leans on a system call like write to push bytes at an output stream. malloc manages allocations itself, and asks Linux for more memory only when its existing pool isn’t enough.

Library code crosses the system-call boundary when it needs a protected Linux service
Userspace
Your C codeprintf("hello")
C libraryformat + buffer
write(1, "hello", 5)system call · return value crosses back
Protected kernel spaceLinux kernelfile descriptor 1 · output device or file

Check for the tracing tool:

Terminal window
strace --version

If it is missing:

Terminal window
sudo apt update
sudo apt install strace

The supported course environment reports strace 6.8.

Trace a simple command, then open the trace to see its system calls:

Terminal window
strace -o echo.trace /usr/bin/echo hello
less echo.trace

That command prints one friendly line. The trace, meanwhile, contains loader activity, memory mappings, locale files, the output itself, and process termination. Very simple source behavior can require a genuinely startling amount of runtime preparation before a single byte reaches your terminal.

Search for execve, openat, mmap, write, and exit_group. A typical trace line shows the system call name, its arguments, and a return value after the =. A negative return with an error name like ENOENT is evidence of a failed attempt, and programs will try several possible files during perfectly normal startup.

A file descriptor is a small non-negative integer a process uses to refer to an open kernel-managed resource. By convention 0 is standard input, 1 is standard output, and 2 is standard error. openat returns a new descriptor on success, and the read, write, and close calls that follow all use it.

openat(..., "notes.txt", O_RDONLY) = 3
read(3, "hello\n", 4096) = 6
close(3) = 0

That trace supports three separate claims: descriptor 3 referred to the opened resource for this interval, six bytes reached userspace, and the close succeeded.

At the C library boundary, a failing wrapper usually returns -1 and sets errno, and perror or strerror(errno) is what formats that for a human. strace shows the same failure as = -1 ENOENT (...). Capture errno immediately, because a later library call may well overwrite it before you look.

write(fd, buffer, count) returns the number of bytes accepted, or -1. A positive result smaller than count is a partial write, and robust code loops until all the intended bytes are written or some policy stops it:

size_t written = 0;
while (written < length) {
ssize_t result = write(fd, buffer + written, length - written);
if (result < 0) {
if (errno == EINTR) continue;
return -1;
}
if (result == 0) {
errno = EIO;
return -1;
}
written += (size_t)result;
}

That example treats a zero-byte result as an I/O failure, because otherwise the loop can make no progress at all. Real code needs an appropriate error report and a policy for nonblocking descriptors on top of all this. The immediate lesson is simply that the requested count and the returned count are two different pieces of evidence.

The C library may combine several printf calls into a single write, or flush on a newline, an explicit fflush, a buffer limit, or a normal exit. Redirection changes the buffering too (which is why the same program behaves differently through a pipe). One print never guarantees one system call, and one write never identifies the source function that produced it.

Filter the trace to selected calls:

Terminal window
strace -e trace=execve,openat,mmap,write,exit_group /usr/bin/echo hello

Filtering helps you answer a focused question, and it also hides absolutely everything outside the filter. So write “the filtered trace showed no additional writes”, and not “the program made no other system calls”. Being precise about the limits of your own observation is part of the job.

strace also can’t show you every userspace instruction, displayed strings may be truncated, concurrent processes can interleave, and attaching requires permission. Check the active filters and string limit before interpreting missing or truncated output.

Capture strings written by a program:

Terminal window
strace -e trace=write -s 200 /usr/bin/echo hello

-s 200 raises the displayed string limit. That changes how much of a string is displayed; binary data still needs to be interpreted according to its format.

The strace manual describes trace-selection and output options. Keep the selected filter beside your evidence so an omitted event isn’t mistaken for an event that never happened.

Terminal window
strace -o lifetime.trace ./lifetime
grep -E 'brk|mmap|munmap|write|exit_group' lifetime.trace

You probably won’t see one kernel call for each malloc and free, because the allocator reuses and manages much larger regions on its own. That’s useful evidence that the C allocation interface and the kernel memory interface are related layers, rather than identical operations.

  • strace: command not found: install the pinned strace package, and check strace --version before you continue.
  • Operation not permitted while attaching: trace a new process you start directly with strace. Don’t go changing system-wide security settings for this lesson.
  • The trace output seems incomplete: check the active -e filter and the -s string limit, and then widen one filter at a time.
  • Old rows are mixed in with new ones: remove the named trace file before you rerun, or just use a fresh filename.
  • The program output and the trace are tangled together: use -o trace-file so the trace evidence gets its own destination.

You can now watch where a program asks Linux to act, and tell that boundary apart from the source-level function that led it there. The Program Investigation Capstone pulls source, ELF metadata, process mappings, named-function instructions, memory lifetime, and system calls together into one reproducible investigation.

Knowledge check

Question 1 of 2

write requests ten bytes and returns four. What does that return establish?