1 of 4

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.

  • strace
  • strace -f
  • strace -e trace=
  • strace -c
  • strace -p
  • strace -o
  • ltrace
  • ltrace -S
  • ldd
  • file

45 min3 exercises

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)
PartMeaning
openatThe system call
AT_FDCWD, "/etc/...", O_RDONLY|O_CLOEXECThe arguments: directory reference, path, flags
= -1The 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:

errnoMeaningLook at
ENOENTNo such file or directoryPath, working directory, typos, a missing mount
EACCESPermission deniedMode, owner, ACLs, SELinux, ProtectSystem= in a unit
EPERMOperation not permittedNeeds root or a capability
EEXISTAlready existsStale lock or pid file
EADDRINUSEAddress already in useAnother process owns the port
ECONNREFUSEDNobody listeningServer down, wrong port, firewall REJECT
ETIMEDOUT, EHOSTUNREACHNo answer, no routeFirewall DROP, routing, remote host
EMFILEToo many open files (this process)ulimit -n, LimitNOFILE=
ENOSPCNo space leftdf -h, df -i

Options you will use

OptionUse
-fFollow forked children and threads (essential for shell scripts and servers)
-e trace=openat,statShow only those calls; classes: file, network, process, memory, signal
-o FILEWrite the trace to a file (stderr stays free for the program)
-p PIDAttach to a running process (needs root, or the same user with ptrace allowed)
-cSummary: time, calls and errors per call
-tt, -TTimestamps, and time spent in each call (find where a program waits)
-s 200Show longer strings (default is truncated to 32 characters)
-yShow 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.

  1. 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.py

    The program says only FileNotFoundError with a traceback. Use strace to 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.py prints config: port=8080.
    Hint

    strace -f -e trace=openat -o /tmp/st.txt /tmp/app.py, then grep myapp /tmp/st.txt. -f follows child processes and threads, -o writes 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
  2. Practice

    ENOENT or EACCES?

    Write a one-line program (shell, Python or C) that opens /etc/shadow as 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 -1 is the key. ENOENT means "not found", EACCES means "found, not allowed", ECONNREFUSED means "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
  3. 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.sh

    Without reading the script, use strace -f to 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 with strace -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.sh ends 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 and curl are child processes, so -f is 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