Application DebuggingLesson 1 of 4
Seeing what a program does with strace and ltrace
Trace system calls and library calls to find the exact file, permission, connection or library behind a failing program.
stracestrace -fstrace -e trace=strace -cstrace -pstrace -oltraceltrace -Slddfile
After this lesson you can
- Run a program under strace, filter by call or class, and read the return value and errno.
- Find a missing file, a permission problem or a refused connection from a trace.
- Attach to a running process and use ltrace for library calls and ldd for missing libraries.
The idea
A program that fails with a vague message (“cannot start”, “permission denied”, “error 2”) has usually told the kernel exactly what it was trying to do, and what happened, a moment before. Programs talk to the operating system through system calls (openat, read, connect, execve, bind…). strace shows those calls with their arguments and results. It turns “it fails” into “it fails to open /etc/myapp/app.conf with ENOENT”.
ltrace does the same for calls into shared libraries (malloc, strlen, printf, getaddrinfo), which sit between the program and the kernel.
program --(library calls: ltrace)--> shared libraries (glibc, libssl, ...)
|
(system calls: strace)
v
kernel
Reading one line of strace
openat(AT_FDCWD, "/etc/myapp/app.conf", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
| Part | Meaning |
|---|---|
openat | The system call |
AT_FDCWD, "/etc/...", O_RDONLY|O_CLOEXEC | The arguments: directory reference, path, flags |
= -1 | The return value; -1 means failure for most calls (a file descriptor number such as = 3 means success for open) |
ENOENT (No such file or directory) | The errno: why it failed |
Errors worth recognising immediately:
| errno | Meaning | Look at |
|---|---|---|
ENOENT | No such file or directory | Path, working directory, typos, a missing mount |
EACCES | Permission denied | Mode, owner, ACLs, SELinux, ProtectSystem= in a unit |
EPERM | Operation not permitted | Needs root or a capability |
EEXIST | Already exists | Stale lock or pid file |
EADDRINUSE | Address already in use | Another process owns the port |
ECONNREFUSED | Nobody listening | Server down, wrong port, firewall REJECT |
ETIMEDOUT, EHOSTUNREACH | No answer, no route | Firewall DROP, routing, remote host |
EMFILE | Too many open files (this process) | ulimit -n, LimitNOFILE= |
ENOSPC | No space left | df -h, df -i |
Options you will use
| Option | Use |
|---|---|
-f | Follow forked children and threads (essential for shell scripts and servers) |
-e trace=openat,stat | Show only those calls; classes: file, network, process, memory, signal |
-o FILE | Write the trace to a file (stderr stays free for the program) |
-p PID | Attach to a running process (needs root, or the same user with ptrace allowed) |
-c | Summary: time, calls and errors per call |
-tt, -T | Timestamps, and time spent in each call (find where a program waits) |
-s 200 | Show longer strings (default is truncated to 32 characters) |
-y | Show paths for file descriptors |
Try it
All outputs are real, from strace and ltrace on a test machine.
A missing file
strace -f -e trace=openat,open,stat -o st.txt ./app.py
grep myapp st.txt
FileNotFoundError: [Errno 2] No such file or directory: '/etc/myapp/app.conf'
309 openat(AT_FDCWD, "/etc/myapp/app.conf", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
The Python traceback already named the file; the value of strace is for programs that do not. Notice the number at the front: it is the process id (shown because of -f).
Where is the time going? A summary
strace -c -f ./app.py 2>&1 | head -6
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
36.34 0.002933 17 171 30 stat
(The top line of the table; the full output lists every call.) A large number in the errors column is suspicious: the program is probing many paths that do not exist. For slowness, -T and -tt tell you which individual call waited.
A refused connection
strace -f -e trace=connect ./net.sh 2>&1 | grep -E 'connect\(3|error'
connect(3, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused)
error: [Errno 111] Connection refused
The trace shows the address and the port in numeric form and names the cause. If it were a timeout, the line would end ETIMEDOUT, or the call would simply take long (-T).
Library calls with ltrace
For a small C program that allocates a string and prints it:
ltrace ./hello
malloc(32) = 0x555e430162a0
strlen("labapp") = 6
printf("name=%s len=%zu\n", "labapp", 6) = 18
free(0x555e430162a0) = <void>
name=labapp len=6
+++ exited (status 0) +++
ltrace shows the library functions with arguments, such as which DNS name getaddrinfo was asked to resolve or which file fopen was asked to open. ltrace -S adds system calls to the same output. It works on dynamically linked programs only; a static binary or most Go programs have no library calls to see.
Libraries that will not load
If a program fails with error while loading shared libraries: libfoo.so.1: cannot open shared object file, ldd lists what it needs and which ones are missing:
ldd /usr/local/bin/myapp | grep "not found"
rpm -qf /usr/lib64/libfoo.so.1 || dnf provides '*/libfoo.so.1'
Install the package that provides the library. If the library is in a non-standard directory, add it with a file in /etc/ld.so.conf.d/ and run ldconfig, or set LD_LIBRARY_PATH for a test (never as the permanent fix).
Attaching to a running process
sudo strace -f -p $(pidof httpd | cut -d' ' -f1) -e trace=network -o /tmp/httpd.trace
# press Ctrl+C to stop; the process continues normally
Attaching is safe, but slows the process while tracing; avoid it on a server doing heavy work except briefly. Some hardened systems restrict ptrace (/proc/sys/kernel/yama/ptrace_scope, SELinux deny_ptrace), in which case the attach fails with Operation not permitted even for root.
Going further
Reading outside the program. The common conclusion of a trace is not in the program but in its environment: the file is missing, the permission or label is wrong, the service is not listening. The next lesson covers finding what is open and listening; the one after shows why the environment under systemd differs from your shell. Beyond the exam: perf trace is a lower-overhead alternative to strace on busy systems, ltrace and strace can alter timing enough to hide a race condition, and bpftrace/BCC tools give system-wide views of the same events.
Practice
0 of 3 done
Do these on your own system. They get harder. Check each result, then tick it off.
- Warm-up
Which file is it looking for?
Setup:
cat > /tmp/app.py <<'EOT' #!/usr/bin/env python3 cfg = open('/etc/myapp/app.conf').read() print("config:", cfg.strip()) EOT chmod +x /tmp/app.pyThe program says only
FileNotFoundErrorwith a traceback. Usestraceto find the exact path it tried, and the error number it got, then create the file and show the program works.Check your result
- The trace shows
openat(AT_FDCWD, "/etc/myapp/app.conf", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory). - After
sudo mkdir -p /etc/myapp && echo port=8080 | sudo tee /etc/myapp/app.conf,/tmp/app.pyprintsconfig: port=8080.
Hint
strace -f -e trace=openat -o /tmp/st.txt /tmp/app.py, thengrep myapp /tmp/st.txt.-ffollows child processes and threads,-owrites the trace to a file so the program's own output stays readable.Solution
strace -f -e trace=openat,open,stat -o /tmp/st.txt /tmp/app.py grep myapp /tmp/st.txt sudo mkdir -p /etc/myapp && echo port=8080 | sudo tee /etc/myapp/app.conf /tmp/app.py - The trace shows
- Practice
ENOENT or EACCES?
Write a one-line program (shell, Python or C) that opens
/etc/shadowas an ordinary user, and another that opens/etc/nonexistent. Trace both. For each, record the system call, the return value and the errno name, and explain what each tells you about where to look next (a path problem or a permissions/SELinux problem).Check your result
- Shadow, as a normal user:
= -1 EACCES (Permission denied): the path exists, access is refused: check mode, owner, ACLs and SELinux (ls -lZ,ausearch -m avc). - Nonexistent:
= -1 ENOENT (No such file or directory): the path is wrong or the file is not there.
Hint
Run as a normal user:
strace -e trace=openat cat /etc/shadow. The errno name after the-1is the key.ENOENTmeans "not found",EACCESmeans "found, not allowed",ECONNREFUSEDmeans "reached the host, nobody is listening".Solution
strace -e trace=openat cat /etc/shadow 2>&1 | tail -3 strace -e trace=openat cat /etc/nonexistent 2>&1 | tail -3 ls -lZ /etc/shadow - Shadow, as a normal user:
- Challenge
Unknown failure, three tools
Setup: a helper script silently exits with status 1 and prints nothing.
cat > /tmp/helper.sh <<'EOT' #!/bin/bash source /etc/helper/env.conf || exit 1 curl -sf http://127.0.0.1:9/health > /dev/null || exit 1 echo ok EOT chmod +x /tmp/helper.shWithout reading the script, use
strace -fto answer: which file does it try first, which network address and port does it connect to, and which of the two failures stops it first? Fix the first (create the file) and then identify the second from the trace alone. Summarise the syscall counts withstrace -c.Check your result
- The first failure:
openat(... "/etc/helper/env.conf" ...) = -1 ENOENT. - After you create it, the next failure:
connect(... sin_port=htons(9), sin_addr=inet_addr("127.0.0.1") ...) = -1 ECONNREFUSED. strace -c -f /tmp/helper.shends with a summary table of calls and errors.
Hint
strace -f -e trace=openat,connect -o /tmp/h.txt /tmp/helper.sh; grep -E 'helper|connect' /tmp/h.txt. A shell andcurlare child processes, so-fis required.Solution
strace -f -e trace=openat,connect -o /tmp/h.txt /tmp/helper.sh; echo rc=$? grep -E 'env.conf|connect\(' /tmp/h.txt | head sudo mkdir -p /etc/helper && sudo touch /etc/helper/env.conf strace -f -e trace=connect -o /tmp/h2.txt /tmp/helper.sh; echo rc=$? grep ECONNREFUSED /tmp/h2.txt strace -c -f /tmp/helper.sh 2>&1 | head -12 - The first failure: