2026-07-15
Everyone reaches for strace when a program misbehaves. But strace only shows the kernel boundary — the moment your process finally begs the OS for something. The interesting bugs usually live one layer up: in libc, OpenSSL, libcurl, or that in-process plugin loader. That's where ltrace earns its keep. It intercepts every dynamic library call by hooking the PLT/GOT, so you see malloc, fopen, SSL_read, and dlopen the way strace shows read and mmap.
Install it: apt install ltrace or the equivalent. Then start where you'd normally start with strace:
$ ltrace ls /tmp
__libc_start_main(0x402a10, 2, 0x7ffc..., 0x40b1c0 <unfinished ...>
strrchr("ls", '/') = nil
setlocale(LC_ALL, "") = "en_US.UTF-8"
bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"
malloc(120) = 0x55c1e2a3d2a0
opendir("/tmp") = 0x55c1e2a3d3f0
readdir(0x55c1e2a3d3f0) = 0x55c1e2a3d420
...
Summarize instead of firehose. Just like strace -c, ltrace -c gives a call/time histogram. Perfect for hunting malloc storms or catching an accidental O(n²) strlen:
$ ltrace -c -f ./myapp
% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
41.22 0.180432 14 12683 malloc
30.05 0.131522 13 9812 free
12.78 0.055930 203 275 SSL_read
8.11 0.035497 9 3719 __ctype_b_loc
Filter by function or library. The full trace is noisy — narrow it to what you care about. Discover where an opaque binary reads its config:
$ ltrace -e 'fopen+open+openat+access' ./mystery-daemon 2>&1 | head
fopen("/etc/mystery/config.yml", "r") = 0x55...
access("/var/lib/mystery/license", F_OK) = 0
openat(AT_FDCWD, "/etc/hosts", O_RDONLY) = 5
Or watch every call into a specific shared library — invaluable for figuring out which crypto stack a stripped binary is really using:
$ ltrace -l 'libssl*' -l 'libcrypto*' ./client https://example.com
Catch plugin loading in the act. Watching dlopen/dlsym tells you exactly which shared objects a process resolves at runtime, and which symbols it grabs — often the fastest way to reverse-engineer a plugin ABI:
$ ltrace -e 'dlopen+dlsym+dlerror' -f -p 12345
[pid 12345] dlopen("plugins/auth_ldap.so", RTLD_NOW) = 0x7fa...
[pid 12345] dlsym(0x7fa..., "auth_init") = 0x7fa...b210
Attach to something already running. Same -p PID semantics as strace. Add -f to follow forks. Add -S and you get syscalls interleaved with library calls in one unified stream — the closest thing Linux has to a poor-man's userland dtrace -F:
$ ltrace -S -f -p $(pgrep nginx | head -1)
Caveats to know before you swear by it. ltrace only sees dynamically-linked calls — statically-linked binaries (hi, Go) show nothing useful. It's slower than strace because it single-steps through the PLT. And on distros with full RELRO the PLT is sometimes read-only in a way that breaks tracing; run against a debug or non-hardened build if you hit weirdness. None of this makes it less brilliant when the bug is above the syscall layer.
strace shows what your program asks the kernel; ltrace shows what it asks its libraries — and most bugs live in that second, chattier layer.
