Advanced Linux Debugging: Hunting Silent Bugs with strace and ltrace

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

When Logs Go Silent: The Black Box Problem

We’ve all been there. You have a Go binary or a Python script that runs perfectly on your laptop, but the second it hits the staging server, it just hangs. There are no error logs and no stack traces. CPU usage sits at a flat 0%. Even tail -f /var/log/syslog stays frustratingly empty. This is the “black box” scenario, where an application stops talking to the user and starts struggling with the operating system in secret.

Standard debuggers like GDB are powerful, but they can be overkill. If you lack debug symbols or the issue is rooted in how the app interacts with the kernel, GDB won’t help much. Usually, when an app “does nothing,” it’s actually stuck waiting for a resource. It might be a file descriptor that won’t open, a network socket timing out, or a thread deadlock. To find the culprit, you need to see what the application is asking the kernel to do.

The Interface: System Calls vs. Library Calls

To fix a silent failure, you have to monitor the two ways a process interacts with the world. First, there are System Calls (syscalls). These are hardware requests, like reading a file from an NVMe drive or sending a packet over Ethernet. Second, there are Library Calls. These target shared libraries like libc or openssl to handle complex logic like string formatting or encryption.

Most hangs happen at these boundaries. If you can see the exact request sent to the kernel right before the process froze, you’ve found your smoking gun. This is where strace and ltrace become your most valuable tools.

Exposing the Kernel Interface with strace

strace intercepts the conversation between your program and the Linux kernel. It doesn’t just tell you that a file failed to open; it shows you the exact path attempted and the specific error code, such as ENOENT (File not found) or EACCES (Permission denied).

Practical strace Usage

If a program named api_engine is acting up, start by running it directly under strace:

strace ./api_engine

This dumps every syscall to your terminal. On a busy app, this can fly by at 500 lines per second. To stay sane, filter for specific activity. For example, if you suspect a configuration issue or a network timeout, use these filters:

# Only show file-related calls (open, stat, etc.)
strace -e trace=file ./api_engine

# Focus on network activity like connect and sendto
strace -e trace=network ./api_engine

Fixing a Live Process

You don’t have to restart your app to debug it. If a service is currently hanging in production, find its Process ID (PID) and attach strace instantly:

ps aux | grep api_engine
sudo strace -p 1234

I recently used this on an Ubuntu 22.04 server to solve a “zombie” process issue. The app was stuck in a 100ms loop calling fstat on a missing .env file. By attaching to the live PID, I saw the infinite loop immediately. Adding the missing file dropped the CPU load from 15% to near zero.

Peering into Shared Libraries with ltrace

Sometimes strace shows nothing, yet the CPU is pegged at 100%. This usually means the app is trapped in a library function, like an infinite while loop or a heavy malloc operation. ltrace tracks these user-space library calls.

The syntax is nearly identical to strace, but the insight is different:

ltrace ./api_engine

If you want to find performance bottlenecks, use the summary flag. It generates a histogram showing which library functions are hogging the execution time:

ltrace -c ./api_engine

You might find that your app spends 40% of its time in strlen(). That is a clear sign you should be caching string lengths instead of recalculating them in a loop.

Tool Comparison: strace vs. ltrace

Feature strace ltrace
Target Kernel System Calls User-space Library Calls
Performance Hit High (10x – 50x slowdown) Very High
Best For Permissions, I/O, Network issues Logic errors, library bottlenecks
Key Metric Return codes (e.g., -1 EPERM) Function arguments and time spent

A Pro Debugging Workflow

Don’t just stare at the scrolling text. Follow this systematic approach to isolate bugs in minutes rather than hours.

1. Start with the Big Picture

Run a summary first. This highlights the most frequent syscalls and those taking the longest to complete:

strace -c ./api_engine

If futex is the top time-sink, you have a thread locking issue. If read or select is at the top, your app is likely waiting on a slow database or a blocked network socket.

2. Follow the Children

Modern apps use multiple threads or child processes. By default, strace only follows the main process. Use the -f flag to track every thread the app spawns:

strace -f -o trace_logs.txt ./api_engine

The -o flag saves the output to a file. Trying to read interleaved output from 10 different threads in a terminal is a recipe for a headache.

3. Use Precise Timestamps

To find out why an app hangs for exactly 30 seconds every hour, you need timing data. The -ttt flag adds microsecond timestamps, while -T shows the duration of every single call:

strace -ttt -T ./api_engine

This makes it easy to spot a connect() call that takes exactly 5.000 seconds to fail, pointing directly to a firewall timeout.

Production Warning

strace and ltrace rely on ptrace, which pauses the process for every event. This adds massive overhead. In a high-traffic production environment, strace can slow a process down by 10x or more. Always attach for the shortest time possible to capture the data, then detach. If you need to monitor high-performance systems constantly, look into eBPF tools like bpftrace, which have much lower overhead.

Share: