Tracing shared library calls with ltrace and latrace
Tracing shared library calls with ltrace
Similar to strace, which can be used to trace system calls, ltrace can be used to trace shared library calls. This can be very useful to get insight into the program flow of applications while analyzing an issue. Consider the following simple sample: hello.c:
include <stdio.h> void helloFunc(const char* s) { printf(s); }
hello.h:
void helloFunc(const char* s);
helloMain.c:
#include "hello.h" int main() { helloFunc("Hello World, how are you?"); return 0; }
We create a shared library from hello.c:
$ gcc -fPIC --shared -o libhello.so hello.c
And we create the executable from helloMain.c which links against the library created before:
$ gcc -o hello helloMain.c -lhello -L.
We can now use ltrace to see which library calls are executed:
$ export LD_LIBRARY_PATH=. $ ltrace ./hello __libc_start_main(0x40069d, 1, 0x7fffe2f3b778, 0x4006c0 <unfinished ...> helloFunc(0x400744, 0x7fffe2f3b778, 0x7fffe2f3b788, 0) = 25 Hello World, how are you?+++ exited (status 0) +++
One drawback with ltrace is that it only traces calls from the executable to the libraries the executable is linked against – it does not trace calls between libraries! Hence, the call to the printf() function (which itself resides in the libc shared library) is not shown in the output. Also, there is no option to include the library name in the output for each of the called functions.
Tracing shared library calls with latrace
Especially for larger applications, a better alternative to ltrace is latrace which uses the LD_AUDIT feature from the libc library (available from libc 2.4 onward). On Ubuntu, it can be installed with
$ sudo apt-get install latrace
When using latrace with the sample program from above, there are two important differences:
- First, latrace also traces library calls between shared libraries, so the output includes the printf call executed from our own shared library.
- Second, the shared library name which contains the symbol is printed after each function call:
$ latrace ./hello 10180 _dl_find_dso_for_object [/lib64/ld-linux-x86-64.so.2] 10180 __libc_start_main [/lib/x86_64-linux-gnu/libc.so.6] 10180 helloFunc [./libhello.so] 10180 printf [/lib/x86_64-linux-gnu/libc.so.6] 10180 __tls_get_addr [/lib64/ld-linux-x86-64.so.2] 10180 __cxa_finalize [/lib/x86_64-linux-gnu/libc.so.6] Hello World, how are you? ./hello finished - exited, status=0
By default, function parameters are not shown, but this can be enabled with the -A option:
$ latrace -A ./hello 10190 _dl_find_dso_for_object [/lib64/ld-linux-x86-64.so.2] 10190 __libc_start_main(main = 0x40069d, argc = 1, ubp_av = 0x7fffccd35248, auxvec = 0x4006c0, init = 0x400730, fini = 0x7f3d16b21560, rtld_fini = 0x7fffccd35238) [/lib/x86_64-linux-gnu/libc.so.6] { 10190 helloFunc [./libhello.so] 10190 printf(format = "Hello World, how are you?") [/lib/x86_64-linux-gnu/libc.so.6] { Hello World, how are you?10190 } printf = 25 10190 __tls_get_addr [/lib64/ld-linux-x86-64.so.2] 10190 __cxa_finalize(ptr = 0x7f3d1650b030) [/lib/x86_64-linux-gnu/libc.so.6] { 10190 } __cxa_finalize = void
With -A, ltrace uses some configuration files at /etc/latrace.d to define the parameter format for a set of well-known functions such as printf. We can see in the output that, even though helloFunc() takes a parameter, this parameter is not shown in the output (since it is not defined in the configuration files). We can use the -a option to specify our own argument definition file. An argument definition file is essentially a header file which defines the prototypes for all functions for which the arguments should be displayed in the output. On Ubuntu, the default argument definition files are stored at /etc/latrace.d/headers/, and there is a master file /etc/latrace.d/headers/latrace.h which includes the other header files. We can use the same approach in our own definition file, by first including the master file from /etc/latrace.d/headers/latrace.h and then add the prototypes for each function we want to trace. For our ssample above, the file could look like
#include "/etc/latrace.d/headers/latrace.h" void helloFunc(const char* s);
Assumed this file is called mylatrace.h, wen can now use the -a option to pass the file to latrace:
$ latrace -a mylatrace.h -A ./hello 10801 _dl_find_dso_for_object [/lib64/ld-linux-x86-64.so.2] 10801 __libc_start_main(main = 0x40069d, argc = 1, ubp_av = 0x7fff4b89fb58, auxvec = 0x4006c0, init = 0x400730, fini = 0x7f1927ea1560, rtld_fini = 0x7fff4b89fb48) [/lib/x86_64-linux-gnu/libc.so.6] { 10801 helloFunc(s = "Hello World, how are you?") [./libhello.so] { 10801 printf(format = "Hello World, how are you?") [/lib/x86_64-linux-gnu/libc.so.6] { 10801 } printf = 25 10801 } helloFunc = void 10801 __tls_get_addr [/lib64/ld-linux-x86-64.so.2] Hello World, how are you?10801 __cxa_finalize(ptr = 0x7f192788b030) [/lib/x86_64-linux-gnu/libc.so.6] { 10801 } __cxa_finalize = void ./hello finished - exited, status=0
As you can see in the output, we now also see that helloFunc() is called with one parameter s which is set to “Hello World, how are you?”.