Mastering Strace on Linux: System Call Tracing to Debug Hanging Processes and Missing Files

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

Real-World Problem: When Applications Fail Without Leaving Logs

We have all stared at a dead terminal. You restart a systemd unit, run a deployment script, or launch a proprietary vendor binary, and nothing happens. CPU usage drops to 0%, the cursor blinks indefinitely, and your shell prints no error code.

Silent exits are just as frustrating. The binary terminates instantly, leaving only exit status 1 behind. You check journalctl -u api-worker.service and scrape /var/log/, but find nothing. If the crash happened before the application could bootstrap its logging framework, standard logs will never tell you what broke.

Decompiling closed-source binaries is rarely an option. Attaching GDB to a live production daemon is often too risky. You need a fast, non-invasive way to see what the process requested from the Linux kernel right before it choked.

Root Cause Analysis: The Userspace-Kernel Boundary and System Calls

Except for local register arithmetic and internal memory operations, user applications cannot touch hardware directly. Whenever a program reads a file, binds to a socket, or spins up a thread, it must transition into kernel space by issuing a system call (syscall).

Silent failures almost always map to one of three kernel-level events:

  • Unreachable Network Targets: The process calls connect() against a remote endpoint. If an upstream firewall silently drops packets without sending a TCP RST, the kernel retransmits SYN packets. Under default Linux settings, the socket blocks for up to 127 seconds before timing out.
  • Missing Files and Permission Barriers: The binary calls openat() or access() on a configuration file or TLS certificate. If the target path is absent, the kernel returns -1 ENOENT. If permissions are wrong, it returns -1 EACCES. Poorly written binaries often swallow these errors and exit quietly.
  • Lock Contention and Deadlocks: Threaded runtimes implement mutual exclusion via the futex() syscall. If a worker thread acquires a lock and crashes, or if two workers deadlock, dependent threads sit suspended in kernel wait queues indefinitely.

Because the Linux kernel handles every single I/O transaction, tracing this boundary exposes the failure immediately. The runtime language—whether Go, Python, Rust, or C—does not matter.

Tool Comparison: Choosing the Right Diagnostics Tool

Engineers usually pick between four diagnostic layers when troubleshooting stubborn Linux workloads. Each serves a specific purpose.

1. Verbosity Flags and Environment Variables

Setting DEBUG=*, RUST_LOG=trace, or passing -vvv works well for logic errors. It relies on the developer having instrumented that specific code path. If initialization aborts before parsing environment variables, debug flags will not help.

2. Interactive Debuggers (GDB)

The GNU Debugger lets you inspect stack frames, registers, and memory allocations. It excels at diagnosing segmentation faults. However, GDB pauses execution threads and typically requires unstripped binaries with DWARF debug symbols. Using it in live production environments carries significant operational risk.

3. eBPF and bpftrace

Modern Linux kernels support Extended Berkeley Packet Filter tools like bpftrace. They introduce negligible overhead (often under 2%) and offer system-wide visibility. The barrier is complexity. Writing custom BPF probes requires CAP_BPF or root privileges, kernel headers, and specialized scripting.

4. Strace (System Call Tracer)

strace leverages the ptrace() interface to intercept every syscall a target process issues. It prints arguments, return codes, and elapsed execution time. You need neither source code nor debug symbols.

Keep in mind that strace induces non-trivial overhead. Because ptrace() triggers two context switches per system call, high-throughput I/O services may run 10x to 40x slower while attached. Use targeted filtering to keep overhead manageable.

Tool Setup Overhead Needs Debug Symbols? Runtime Slowdown Ideal Use Case
Verbosity Flags None No Negligible Known application-level bugs
GDB High Yes High (stops execution) Core dumps, memory corruption, crashes
eBPF / bpftrace Medium No Very Low (<2%) Continuous production profiling
strace Zero No Moderate to High Fast triage of hangs, missing files, permission issues

Step-by-Step: Targeted Syscall Tracing with Strace

Running unfiltered strace against an active process can dump hundreds of megabytes of raw text into your terminal in seconds. You will drown in noise. Always scope your trace to the relevant syscall family.

1. Pinpointing Missing Files and Permission Denials

When an application terminates on launch without output, restrict capture to filesystem calls with -e trace=openat,access,stat:

strace -e trace=openat,access ./my-application

Sample trace from a stalled initialization:

openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/app/config.yaml", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/appuser/.config/app.json", O_RDONLY) = -1 EACCES (Permission denied)
exit_group(1)                           = ?

The issue becomes obvious immediately. The binary tried to load /etc/app/config.yaml, received ENOENT, and attempted a fallback to /home/appuser/.config/app.json. The process owner lacked read permissions (EACCES), forcing an unhandled abort.

2. Attaching to an Unresponsive or Hung Process

When a background daemon stops servicing traffic, grab its PID using pgrep and attach via -p. Add -f so strace follows any spawned worker threads:

sudo strace -f -p 18492

If the output halts on an uncompleted call, look closely at that final line:

[pid 18495] futex(0x7f3d540009c0, FUTEX_WAIT_PRIVATE, 2, NULL

Because the call has no closing bracket or return value, thread 18495 is stuck waiting on a mutex held by another thread. If instead you see an unclosed socket call:

[pid 18493] connect(7, {sa_family=AF_INET, sin_port=htons(5432), sin_addr=inet_addr("10.0.12.44")}, 16

The worker is blocked trying to reach PostgreSQL on 10.0.12.44:5432. Verify your security groups and firewall drop rules before touching a line of application code.

3. Pinpointing Bottlenecks with Summary Profiling

If an application works but runs noticeably slow, skip full tracing and run strace in statistical mode with -c:

strace -c python3 batch_processor.py

When the command finishes, strace prints a profiling table:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 71.30    3.842100          45     84210           newfstatat
 18.25    0.983410          24     41000           openat
  6.40    0.344890           8     41000           close
  4.05    0.218200          43      5000           read
------ ----------- ----------- --------- --------- ----------------
100.00    5.388600                171210           total

In this real audit on an Ubuntu 22.04 LTS instance, batch_processor.py took 5.4 seconds to process 1,000 records. Over 89% of that time was spent on 125,000 redundant newfstatat and openat lookups against unchanged assets. Adding a memory-backed path cache reduced execution time to 0.42 seconds—a 12x speedup.

Production Flags Reference

To safely trace sensitive services without locking up your shell or dropping stdout buffers, use this battle-tested invocation:

sudo strace -tt -T -s 256 -f -o /tmp/trace_$(date +%s).log -p <PID>
  • -o /tmp/trace.log: Writes data directly to disk, avoiding standard output pollution and terminal rendering lag.
  • -tt: Prints microsecond timestamps (11:42:09.841023) at the start of each line for easy cross-referencing with application metrics.
  • -T: Appends the time spent inside each system call (for instance, <0.012410>) to isolate high-latency kernel calls.
  • -s 256: Expands printed string length from 32 bytes to 256 bytes so you can inspect complete file paths and connection strings.
  • -f: Follows child forks and POSIX threads. Without this, you only trace the supervisor process.
Share: