ltrace: strace's Sibling That Shows You Every Library Call, Not Just Syscalls

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.

Key Takeaway: strace shows what your program asks the kernel; ltrace shows what it asks its libraries — and most bugs live in that second, chattier layer.

All newsletters