skip to content

On Linux, what does ltrace show that strace does not, and why does ltrace often print nothing useful for a Go binary or a statically linked program?

level: middleimportance: nice to knowfreq 25%

answer

  1. one layer above the kernel boundary
  2. library calls, not system calls
  3. breakpoints on linker stubs
  4. nothing to hook when statically linked
  5. Go does not go through libc

basics

~20 s

ltrace traces calls into shared libraries — malloc, strcmp, OpenSSL functions — which never appear in a system-call trace. It works by breakpointing dynamic-linking stubs, so a statically linked or Go binary that makes no such calls gives an empty trace.

solid answer

~50 s

strace shows only the boundary between the process and the kernel. ltrace sits one layer higher: it intercepts calls a dynamically linked program makes into shared libraries, so you see `malloc`, `strcmp`, `getenv` or an OpenSSL function with its arguments, none of which are system calls. It does that by placing breakpoints on the procedure linkage table entries the dynamic linker uses to reach those libraries. That mechanism explains its blind spots. A statically linked binary has no such entries, so there is nothing to intercept. A Go program does not go through libc for most work and issues its syscalls directly, so the same applies. Calls inside a library to its own functions never traverse the table either, so they are invisible. In practice I reach for strace first and treat ltrace as a niche tool for a dynamically linked C or C++ binary I have no source for.

code

bash · 2 lines
bash
ltrace -S -l libcrypto.so.3 -o /tmp/tls.ltrace /usr/local/bin/client
ltrace -c /bin/ls /tmp

go deeper

for a junior

Know the one-line distinction: strace shows calls into the kernel, ltrace shows calls into shared libraries such as malloc or strcmp. Knowing that ltrace needs a dynamically linked program is enough.

for a middle

Be ready to explain the PLT-breakpoint mechanism and derive the blind spots from it — static binaries, Go programs, and calls internal to a library. Mention -S for interleaving syscalls with library calls.

for a senior

Show judgement about when ltrace is worth it at all: closed-source dynamically linked binaries and allocation-churn questions, in a debugging environment rather than production, with scepticism about decoded arguments for unknown symbols.

for a principal

Frame the layer choice as an architectural question — what a team should be able to answer from its own instrumentation rather than by breakpointing a shipped binary, and why a tracing approach that depends on how something was linked is a fragile foundation to standardise on.

## Two different boundaries A running program crosses two interesting interfaces. The lower one is the system call: the program asks the kernel to open a file, send bytes, or map memory. That is strace's territory. The higher one is the library call: the program calls `malloc`, `strlen`, `getaddrinfo`, `SSL_read` — functions that live in shared objects loaded into its own address space. Many of those never reach the kernel at all. `malloc` usually satisfies an allocation from a free list the allocator already owns, and only occasionally calls `brk` or `mmap`. So a strace of a memory-hungry program shows a handful of `mmap` calls; an ltrace shows the thousands of `malloc` calls behind them. ## How ltrace intercepts When a dynamically linked program calls a function in a shared library, the call does not jump straight to the function. It goes through a stub in the *procedure linkage table* (PLT), a small piece of generated code whose corresponding data slot is filled in by the dynamic linker with the function's real address. ltrace attaches with ptrace and puts breakpoints on those stubs. When one is hit, ltrace reads the registers and stack to decode the arguments, prints the call, and lets it proceed. That design gives ltrace its reach and every one of its limits. ## Why it goes silent - **Statically linked binaries.** There is no dynamic linking, no PLT, no interception point. The library code was pasted into the executable and its calls are ordinary local jumps. - **Go binaries.** Go's runtime issues system calls itself rather than routing through libc, and Go executables are commonly built statically. Nothing goes through a PLT for ltrace to catch, so the trace is empty even though the program is plainly busy. strace still works perfectly on the same binary, because syscalls are syscalls regardless of language. - **Intra-library calls.** When a function inside libc calls another libc function directly, the call is resolved internally and never crosses the table. So you see the entry point your program used, not the library's internal call graph. - **Anything not a function call.** Inlined code, static functions, and work the compiler optimised away are invisible by construction. ## Argument decoding is heuristic strace knows the kernel's system-call interface exactly — the argument types are a fixed, documented contract, so decoding is reliable. ltrace has no such contract: it consults prototype definitions shipped in its configuration files and falls back to printing integers and pointer values when it does not know the signature. That means a third-party library's calls often come out as raw hex, and misdecoded arguments are a real hazard. Combined with ltrace being less actively maintained than strace, this is why it appears far less often in production triage. ## Useful options `-S` adds system calls to the output, giving you both layers in one interleaved stream — the clearest way to see that a burst of `malloc` calls resulted in one `mmap`. `-l <library>` restricts tracing to symbols from a named shared object, which is the way to keep an OpenSSL or libcurl trace readable. `-c` produces a per-function summary table analogous to strace's, useful for spotting a function called an absurd number of times. `-p <pid>` attaches to a running process, `-f` follows children, and `-e` filters symbols — the same shape of interface as strace. ## When to actually reach for it ltrace earns its place in a narrow set of cases: a closed-source, dynamically linked C or C++ binary whose behaviour between syscalls you need to understand; a suspicion about allocation churn where the syscall trace is uninformative; or reverse-engineering how a program builds an argument it eventually passes to the kernel. Its overhead is at least as bad as strace's — a breakpoint trap per intercepted call, on functions that may be called far more often than syscalls are — so it is a debugging-environment tool rather than a production one. For everything else, the first question is still "what did the program ask the kernel for", and strace answers that on any binary, in any language, without depending on how it was linked.

  • A program allocates heavily but its strace shows only a few mmap calls. Why?
    Because `malloc` is a library function, not a system call. The allocator serves most requests from memory it already holds, and only grows the heap occasionally with `brk` or `mmap`. The syscall trace therefore shows the growth events, not the allocation traffic. ltrace on a dynamically linked binary shows the individual `malloc` and `free` calls behind them.
  • Why is strace's argument decoding trustworthy while ltrace's is not always?
    The kernel's system-call interface is a fixed, documented contract, so strace can decode each argument's type exactly and render flags symbolically. ltrace has no equivalent contract for arbitrary shared libraries; it relies on prototype definitions in its configuration and falls back to raw integers or pointers for unknown symbols, which is why third-party library calls often come out as hex and can be misinterpreted.

saying these in an interview costs you the question

  • Calling ltrace "strace for user space" without knowing the PLT mechanism
  • Concluding a Go binary does nothing because ltrace is empty
  • Expecting ltrace to show a library's internal function calls
  • Assuming ltrace argument decoding is as reliable as strace's
  • Reaching for ltrace on a production host as a first instrument

context