LTRACE(1)

๐Ÿ’– LTRACE ๐Ÿ’–

okay bestie, welcome to LTRACE(1) โ€” User Commands, section 1, the whole thing, no skips โœจ


๐ŸŒธ NAME ๐ŸŒธ

ltrace โ€” a library call tracer ๐Ÿ•ต๏ธโ€โ™€๏ธ


โœจ SYNOPSIS โœจ

ltrace [-e filter|-L] [-l|--library=library_pattern] [-x filter] [-S]
[-b|--no-signals] [-i] [-w|--where=nr] [-r|-t|-tt|-ttt] [-T]
[[-F|--config] pathlist] [-A maxelts] [-s strsize] [-C|--demangle]
[-a|--align column] [-n|--indent nr] [-o|--output filename] [-D|--debug
mask] [-u username] [-f] [-p pid] [[--] command [arg ...]]

ltrace -c [-e filter|-L] [-l|--library=library_pattern] [-x filter] [-S]
[-o|--output filename] [-f] [-p pid] [[--] command [arg ...]]

ltrace -V|--version

ltrace -h|--help

๐Ÿฆ‹ DESCRIPTION ๐Ÿฆ‹

so ltrace is literally just a program that runs whatever command you hand it until that command exits. that's it. that's the job ๐Ÿ’…

while it's doing that, it intercepts and records the dynamic library calls that the executed process makes โ€” plus the signals that process receives. and if you want, it can also intercept and print the system calls your program runs. it's giving "watch your code's every move" energy and honestly? iconic ๐Ÿ”ฅ

its whole vibe is very similar to strace(1), if you know her ๐Ÿ‘‘

ltrace shows you the parameters of invoked functions and system calls. but here's the thing โ€” to figure out what arguments each function takes, it needs an external declaration of function prototypes. those live in files called prototype libraries โ€” go read ltrace.conf(5) for the tea on the syntax of those files. and to learn how ltrace finds prototype libraries in the first place, scroll down to the section PROTOTYPE LIBRARY DISCOVERY ๐ŸŒˆ


๐ŸŽ€ OPTIONS ๐ŸŽ€

  • -a, --align column โ€” line your return values up in a specific column (the default column is 5/8 of the screen width, so like, it does the math for you bestie) ๐Ÿ“
  • -A maxelts โ€” the max number of array elements to print before it shuts the rest up with an ellipsis ("..."). this ALSO caps how many recursive structure expansions you get. so it's a two-for-one limiter ๐Ÿซถ
  • -b, --no-signals โ€” turns OFF printing the signals that the traced process receives. if the signal spam is giving you a headache, this is your girl ๐Ÿ™ˆ
  • -c โ€” count the time and calls for each library call, then drop a summary when the program exits. receipts, basically ๐Ÿงพ
  • -C, --demangle โ€” decodes (demangles) those low-level symbol names into user-level names. besides stripping any initial underscore prefix the system throws on there, this is what makes C++ function names actually readable instead of a cursed glyph soup ๐Ÿ’€
  • -D, --debug mask โ€” show the debugging output of ltrace itself. mask is a number describing which debug messages should be displayed. run -Dh to see what's available, but heads up: currently the only reliable debugmask is 77, which shows all debug messages. (so like, just use 77 fr fr) ๐Ÿ”
  • -e filter โ€” a qualifying expression that changes WHICH library calls get traced (i.e. calls done through PLT slots โ€” typically calls from the main binary into a library, or inter-library calls). usage examples and the syntax breakdown are down below in FILTER SPECIFICATIONS and FILTER EXPRESSIONS. if you put more than one -e on the command line, it traces the library calls that match ANY of them. and if you give no -e at all, @MAIN is assumed as the default ๐Ÿ‘‘
  • -f โ€” trace child processes as they get created by the processes currently being traced, because of the fork(2) or clone(2) system calls. the new process gets attached immediately. no waiting around ๐Ÿผ
  • -F, --config pathlist โ€” holds a colon-separated list of paths. if a path points to a directory, that directory gets considered when prototype libraries are being searched (see the section PROTOTYPE LIBRARY DISCOVERY). if it points to a file, that file gets imported implicitly into all loaded prototype libraries ๐Ÿ“‚
  • -h, --help โ€” show a summary of ltrace's options and exit. the classic โœจ
  • -i โ€” print the instruction pointer at the time of the library call. pointer girlies rise ๐Ÿซก
  • -l, --library library_pattern โ€” only display calls to functions implemented by libraries that match library_pattern. this works as if you'd specified one -e for every single symbol implemented in a library matching library_pattern. you can stack multiple library patterns by using this option several times. usage examples and the syntax description of library_pattern are below in FILTER SPECIFICATIONS and FILTER EXPRESSIONS.
ok important caveat bestie: this option selects calls that might be directed to the selected libraries, but there's no actual guarantee the call won't be directed somewhere else โ€” thanks to things like LD_PRELOAD or just plain dependency ordering. if you NEED to make sure symbols in a given library are actually the ones being called, use -x @library_pattern instead. periodt ๐Ÿ’…
  • -L โ€” when no -e option is given, do NOT assume the default action of @MAIN. in practice, that means library calls just won't be traced. sometimes you want silence, and that's valid ๐Ÿคซ
  • -n, --indent nr โ€” indent the trace output by nr spaces for every level of call nesting. this makes the program flow visualization super easy to follow, which is a slay. but ngl, it also uselessly indents functions that never return โ€” like those service functions for throwing exceptions in the C++ runtime ๐Ÿ˜ญ
  • -o, --output filename โ€” write the trace output to the file filename instead of to stderr ๐Ÿ“
  • -p pid โ€” attach to the process with process ID pid and start tracing. you can use this option together with passing a command to execute. and yes, you can attach to several processes by passing more than one -p option. collect them all ๐ŸŽ€
  • -r โ€” print a relative timestamp with each line of the trace. this records the time difference between the beginning of successive lines โฑ๏ธ
  • -s strsize โ€” specify the maximum string size to print (the default is 32) โœ‚๏ธ
  • -S โ€” display system calls as well as library calls. the full menu ๐Ÿ”ฅ
  • -t โ€” prefix each line of the trace with the time of day ๐Ÿ•
  • -tt โ€” if given twice, the time printed will include the microseconds โฑ๏ธโœจ
  • -ttt โ€” if given thrice, the time printed will include the microseconds AND the leading portion will be printed as the number of seconds since the epoch ๐ŸŒŒ
  • -T โ€” show the time spent inside each call. this records the time difference between the beginning and the end of each call โณ
  • -u username โ€” run command with the userid, groupid and supplementary groups of username. this option is only useful when running as root, and it enables the correct execution of setuid and/or setgid binaries ๐Ÿ‘‘
  • -w, --where nr โ€” show a backtrace of nr stack frames for each traced function. heads up though: this option is enabled only if elfutils or libunwind support was enabled at compile time ๐Ÿง—โ€โ™€๏ธ
  • -x filter โ€” a qualifying expression that changes which symbol table entry points get traced (those are typically calls inside a library or the main binary โ€” though PLT calls, which are traced by -e, land on entry points as well). usage examples and the syntax description are below in FILTER SPECIFICATIONS and FILTER EXPRESSIONS. if more than one -x appears on the command line, the symbols matching ANY of them get traced. and if you give no -x at all, no entry points are traced ๐Ÿ™ˆ
  • -V, --version โ€” show the version number of ltrace and exit ๐Ÿ’œ

๐ŸŒˆ FILTER SPECIFICATIONS ๐ŸŒˆ

filters get specified with the -l, -e and -x options. in short, here's what they mean:

  • -x is ยดshow me what calls these symbols (including local calls)ยด
  • -e is ยดshow me what calls these symbols (inter-library calls only)ยด
  • -l is ยดshow me what calls into this libraryยด

so suppose I have a library defined with this header tstlib.h:

void func_f_lib(void);
void func_g_lib(void);

and this implementation tstlib.c:

#include "tstlib.h"
void func_f_lib(void)
{
    func_g_lib();
}
void func_g_lib(void)
{
}

suppose I have an executable that uses this library, defined like this in tst.c:

#include "tstlib.h"
void func_f_main(void)
{
}
void main(void)
{
    func_f_main();
    func_f_lib();
}

if linking WITHOUT -Bsymbolic, the internal func_g_lib() call uses the PLT just like external calls do, and so ltrace says:

$ ltrace -x 'func*' -L ./tst
func_f_main()                             = <void>
func_f_lib@tstlib.so( <unfinished ...
func_g_lib@tstlib.so()                    = <void>
<... func_f_lib resumed> )                = <void>
+++ exited (status 163) +++

$ ltrace -e 'func*' ./tst
tst->func_f_lib( <unfinished ...
tstlib.so->func_g_lib()                   = <void>
<... func_f_lib resumed> )                = <void>
+++ exited (status 163) +++

$ ltrace -l tstlib.so ./tst
tst->func_f_lib( <unfinished ...
tstlib.so->func_g_lib()                   = <void>
<... func_f_lib resumed> )                = <void>
+++ exited (status 163) +++

by contrast, if linking WITH -Bsymbolic, then that internal func_g_lib() call bypasses the PLT, and ltrace says:

$ ltrace -x 'func*' -L ./tst
func_f_main() = <void>
func_f_lib@tstlib.so( <unfinished ...
func_g_lib@tstlib.so()                    = <void>
<... func_f_lib resumed> )                = <void>
+++ exited (status 163) +++

$ ltrace -e 'func*' ./tst
tst->func_f_lib()                         = <void>
+++ exited (status 163) +++

$ ltrace -l tstlib.so ./tst
tst->func_f_lib()                         = <void>
+++ exited (status 163) +++

and that's where part 1 leaves you, bestie โ€” more is coming, don't you dare close the tab ๐Ÿซถ๐Ÿ’–

๐Ÿงš FILTER EXPRESSIONS ๐Ÿงš

Okay bestie, so a filter expression is literally just a chain of glob- or regexp-based rules that pick which symbols get traced out of the libraries your process is using. It's giving "guest list at the door" energy. Most of it is super intuitive, so let's start with the moment:

-e malloc+free-@libc.so*

This reads as: trace malloc and free, but do not trace anything that comes from libc. So if libc itself calls malloc, we're not interested, periodt. Semi-formally, the syntax of the above example looks approximately like this:

{[+-][symbol_pattern][@library_pattern]}

symbol_pattern is used to match symbol names, and library_pattern matches library SONAMEs. Both are implicitly globs, but they can be regular expressions too (more on that below). The glob syntax supports the meta-characters * and ? and character classes, similarly to what basic bash globs support. And ^ and $ are recognized to mean, respectively, start and end of the given name.

Both symbol_pattern and library_pattern have to match the whole name โ€” no half-matching, no shortcuts, full commit only. If you want to match just part of the name, surround it with one or two *'s as appropriate. The exception is if the pattern is not mentioned at all, in which case it's as if the corresponding pattern were *. (So malloc is really malloc@*, and @libc.* is really *@libc.*.) See? Sneaky but consistent ๐Ÿ’…

In libraries that don't have an explicit SONAME, basename is taken for SONAME. That holds for the main binary as well: /bin/echo has an implicit SONAME of echo. On top of that, there's a special library pattern MAIN that always matches symbols in the main binary and never a library with an actual SONAME MAIN (use e.g. ^MAIN or [M]AIN if that's the one you want).

If the symbol or library pattern is surrounded in slashes (/like this/), then it is considered a regular expression instead. And as a little shorthand, instead of writing /x/@/y/, you can write /x@y/. Cute, right? ๐ŸŽ€

If the library pattern starts with a slash, it is not a SONAME expression, but a path expression, and it is matched against the library path name.

The first rule may lack a sign, in which case + is assumed. If, on the other hand, the first rule has a - sign, it is as if there was another rule @ in front of it, which has the effect of tracing the complement of the given rule. The way that works... iconic.

The above rules are used to construct the set of traced symbols. Each candidate symbol is passed through the chain of above rules. Initially, the symbol is unmarked. If it matches a + rule, it becomes marked; if it matches a - rule, it becomes unmarked again. If, after applying all the rules, the symbol is marked, it will be traced. Marked = invited. Unmarked = not on the list. ๐Ÿซถ


๐Ÿ” PROTOTYPE LIBRARY DISCOVERY ๐Ÿ”

When a library is mapped into the address space of a traced process, ltrace needs to know what the prototypes are of the functions that this library implements. And for purposes of ltrace, a prototype really is a bit more than just a type signature: it's also the formatting of individual parameters and of the return value. These prototypes are stored in files called prototype libraries.

After a library is mapped, ltrace finds out what its SONAME is. It then looks for a file named SONAME.conf โ€” e.g. the protolib for libc.so.6 would be in a file called libc.so.6.conf. When such a file is found (more about where ltrace looks for these files is below), ltrace reads all prototypes stored therein. When a symbol table entry point (such as those traced by -x) is hit, the prototype is looked up in a prototype library corresponding to the library where the hit occurred. When a library call (such as those traced by -e and -l) is hit, the prototype is looked up in all prototype libraries loaded for the given process. That is necessary, because a library call is traced in a PLT table of a caller library, but the prototype is described at the callee library. Caller, callee, it's a whole relationship ๐Ÿ˜ญ

If a library has no SONAME, basename of the library file is considered instead. For the main program binary, basename is considered as well (e.g. the protolib for /bin/echo would be called echo.conf). If a name corresponding to soname (e.g. libc.so.6.conf) is not found, and the module under consideration is a shared library, ltrace also tries partial matches. ltrace snips one period after another, retrying the search, until either a protolib is found, or X.so is all that's left. Thus libc.so.conf would be considered, but libc.conf not. Draw the line, bestie โœ‹

When looking for a prototype library, ltrace potentially looks into several directories. On Linux, those are $XDG_CONFIG_HOME/ltrace, $HOME/.ltrace, X/ltrace for each X in $XDG_CONFIG_DIRS and /usr/share/ltrace. If the environment variable XDG_CONFIG_HOME is not defined, ltrace looks into $HOME/.config/ltrace instead.

There's also a mechanism for loading legacy config files. If $HOME/.ltrace.conf exists it is imported to every loaded prototype library. Similarly for /etc/ltrace.conf. If both exist, both are imported, and $HOME/.ltrace.conf is consulted before /etc/ltrace.conf.

If -F contains any directories, those are searched in precedence to the above system directories, in the same order in which they are mentioned in -F. Any files passed in -F are imported similarly to the above legacy config files, before them.

See ltrace.conf(5) for details on the syntax of ltrace prototype library files. Yes, there's a whole other manpage, go read it ๐ŸŒธ

๐Ÿ’€ BUGS ๐Ÿ’€

Okay so not everything is slay:

  • It has most of the bugs stated in strace(1).
  • It only works on Linux and in some architectures.
  • If you would like to report a bug, send a message to the mailing list (ltrace-devel@lists.alioth.debian.org), or use the reportbug(1) program if you are under the Debian GNU/Linux distribution. The devs want to hear from you, fr fr ๐Ÿซถ

๐Ÿ“ FILES ๐Ÿ“

  • /etc/ltrace.conf โ€” System configuration file
  • ~/.ltrace.conf โ€” Personal config file, overrides /etc/ltrace.conf

๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

  • Juan Cespedes <cespedes@debian.org>
  • Petr Machata <pmachata@redhat.com>

๐ŸŒˆ SEE ALSO ๐ŸŒˆ


And that's the whole thing, bestie! This page came from the ltrace source, version-flavored January 2013, and it's the LTRACE(1) manual โ€” you've officially read the entire thing. Go trace something pretty ๐Ÿ’–โœจ๐Ÿฆ‹