strace and ltrace are both debugging tools that let you see what’s happening inside a running program, but they look at different kinds of events.
Let’s see strace in action. Imagine you have a simple C program that just tries to open a file that doesn’t exist:
#include <stdio.h>
#include <fcntl.h>
int main() {
int fd = open("nonexistent_file.txt", O_RDONLY);
if (fd == -1) {
perror("Error opening file");
} else {
printf("File opened successfully (this shouldn't happen).\n");
close(fd);
}
return 0;
}
If you compile this (gcc -o open_fail open_fail.c) and then run it with strace, you’ll see something like this:
$ strace ./open_fail
execve("./open_fail", ["./open_fail"], 0x7ffd7b7d57b0 /* 58 vars */) = 0
openat(AT_FDCWD, "nonexistent_file.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
write(2, "Error opening file: No such file"..., 48Error opening file: No such file or directory
) = 48
close(3) = 0
exit(0) = ?
+++ exited with 0 +++
The key here is openat(AT_FDCWD, "nonexistent_file.txt", O_RDONLY) = -1 ENOENT (No such file or directory). strace shows you the openat system call that the C library eventually makes to the Linux kernel. It tells you the filename, the flags used, and importantly, the return value (-1) and the error code (ENOENT).
Now, let’s look at ltrace with the same program.
$ ltrace ./open_fail
__libc_start_main(0x55f189a01139, 1, 0x7ffc703f15c8, 0x55f189a01200 <unfinished ...>
open("nonexistent_file.txt", O_RDONLY) = -1.
perror("Error opening file") = 0
puts("File opened successfully (this shouldn't happen).") = 48
close(3) = 0
__libc_exit(0) = ?
+++ exited with 0 +++
Here, ltrace shows you open("nonexistent_file.txt", O_RDONLY) = -1. This is the library call to the open function within the C standard library (libc). ltrace intercepts calls to functions in shared libraries. Notice that ltrace doesn’t show the ENOENT directly; it shows the return value of the open function, which is -1. To get the reason for the failure (the ENOENT), you’d typically still need strace or check errno after the call.
The fundamental difference is the layer of abstraction. strace talks to the kernel about system calls – the fundamental operations your program asks the operating system to perform (like reading files, creating processes, network communication). ltrace talks to the dynamic linker about library calls – functions provided by shared libraries (like printf, malloc, open, read from libc).
strace is invaluable for understanding how your program interacts with the OS. It’s your window into I/O, process management, signals, and more at the kernel level. You’d use strace to debug issues like:
- "Why is my program hanging when it tries to read from this device?" (You’d see
readcalls blocking or returning errors). - "Where is my program trying to write its configuration file?" (You’d see
openoropenatcalls withO_WRONLYorO_RDWR). - "Why is my process not being created correctly?" (You’d see
forkorclonecalls failing).
ltrace is useful for debugging issues related to the C library or other dynamically linked libraries. You’d use ltrace for problems like:
- "Why is
printfnot producing output?" (You’d seeprintfcalls, and if they are returning errors, you’d investigate). - "Is my program allocating memory correctly?" (You’d trace
mallocandfreecalls). - "Why is
getenvreturning NULL unexpectedly?" (You’d see thegetenvcall and its return value).
It’s important to note that many standard library functions are wrappers around system calls. For example, libc’s open is often a thin wrapper around the openat system call. ltrace shows you the library function, while strace shows you the underlying system call.
Here’s a more complex example. If you want to see how a program opens and reads from a file, using both tools can be illustrative. Consider this program:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("my_test_file.txt", O_RDONLY);
if (fd != -1) {
char buffer[10];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Read: %s\n", buffer);
}
close(fd);
} else {
perror("Failed to open file");
}
return 0;
}
First, create my_test_file.txt with some content: echo "hello world" > my_test_file.txt.
Now, run it with strace:
$ strace ./read_file
execve("./read_file", ["./read_file"], 0x7ffc751717b0 /* 58 vars */) = 0
openat(AT_FDCWD, "my_test_file.txt", O_RDONLY) = 3
read(3, "hello worl", 9) = 10
write(1, "Read: hello worl", 16Read: hello worl) = 16
close(3) = 0
exit(0) = ?
+++ exited with 0 +++
And with ltrace:
$ ltrace ./read_file
__libc_start_main(0x560a207e2139, 1, 0x7ffc1b7849c8, 0x560a207e2200 <unfinished ...>
open("my_test_file.txt", O_RDONLY) = 3
read(3, "hello worl", 9) = 10
printf("Read: %s\n", "hello worl") = 16
close(3) = 0
__libc_exit(0) = ?
+++ exited with 0 +++
Notice how strace shows openat and read as system calls, while ltrace shows open, read (which is also a system call but ltrace can show it if it’s directly called from user code or if the library wrapper is simple), and printf as library calls. The read call in ltrace shows read(3, "hello worl", 9). This is interesting because the read system call itself takes a count of bytes to read. ltrace shows the arguments passed to the read library function, which in this case, might be a direct pass-through to the system call. strace shows the actual kernel arguments.
The most powerful way to use these tools is often in combination. If ltrace shows a library function returning an error, strace can often reveal the underlying system call that failed and why.
When you’re trying to understand a program’s behavior at its lowest level, strace is your go-to. When you’re focused on the logic of shared libraries and how they’re being invoked, ltrace is more direct. They offer complementary views of your program’s execution.
One subtle point is that ltrace can sometimes be fooled by inlined functions or functions that are not exported by the library. strace, by interacting directly with the kernel’s ptrace mechanism, is generally more reliable for system calls, as the kernel is the ultimate arbiter of these operations. If a library function is implemented entirely in user-space without making a system call, strace won’t see it, but ltrace might (if it’s in a shared library). Conversely, if a program uses syscall() directly to invoke a kernel function, strace will show that syscall instruction, while ltrace won’t see it as a library call.
Ultimately, understanding the distinction between system calls and library calls, and knowing when to deploy strace versus ltrace, is a crucial skill for any serious debugger.