Finding out where Syscalls are Called From: Stack Traces with Strace

One of the great strengths of strace as a debugging tool is that it shows you what a program is doing regardless of whether it was compiled with debug info or not. The downside of this is that you only see the program’s syscall. You can use this information to deduce what is happening in the program but you don’t see from where in the program those syscalls originate.

The good news is that if your program was compiled with debug info strace can actually show a stack trace for every syscall in your binary.

Example

To demonstrate this we just save the following program to hello.c:

#include <stdio.h>

void print_info()
{
    int num = 5;
    printf("== Info ==\n");
    printf("Hello World!\n");
    printf("Num: %d\n", num);
}

int main(int argc, char **argv)
{
    print_info();
    return 0;
}

This program uses the printf function to print three lines of output. This function is part of the C standard library. To actually output the text it has to call the operating system by using the syscall write. We will look for this later in the strace output.

To compile the program we enter the following command:

$ gcc -g -o hello hello.c

Now we can run it with strace using the --stack-trace option (alternatively you can use the equivalent short-form option-k):

$ strace --stack-trace ./hello

This will give us an output similar to this:

...
write(1, "Num: 5\n", 7Num: 5
)                 = 7
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__write+0x14) [0x11c574]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_file_write+0x35) [0x93965]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_do_write+0xb1) [0x92561]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_file_xsputn+0xcf) [0x93a9f]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(parse_printf_format+0xaf9) [0x60cb9]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_vfprintf+0x48e3) [0x6b733]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_printf+0xb3) [0x601a3]
 > /home/user/abstractexpr/strace-stacktrace/hello(print_info+0x4a) [0x11b3]
 > /home/user/abstractexpr/strace-stacktrace/hello(main+0x1d) [0x11d3]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_init_first+0x8a) [0x2a1ca]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [0x2a28b]
 > /home/user/abstractexpr/strace-stacktrace/hello(_start+0x25) [0x10a5]
exit_group(0)                           = ?
+++ exited with 0 +++
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_exit+0x1d) [0xee21d]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(erand48_r+0x196) [0x47a26]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(exit+0x1e) [0x47bae]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_init_first+0x91) [0x2a1d1]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [0x2a28b]
 > /home/user/abstractexpr/strace-stacktrace/hello(_start+0x25) [0x10a5]

What we see here is that after every syscall (in this example only write and exit_group are shown) strace prints a stack trace that led to the call of the syscall. The stack trace has to be read from the bottom to the top with the top line being the function that made the actual syscall.

Most of the lines in the stack trace are function calls in the dynamically linked standard library (libc.so.6). But if you look closely you will see two lines that refer to functions in our hello program:

...
 > /home/user/abstractexpr/strace-stacktrace/hello(print_info+0x4a) [0x11b3]
 > /home/user/abstractexpr/strace-stacktrace/hello(main+0x1d) [0x11d3]
...

We can see here that main has called print_info before the flow of control went to printf and from there deeper into the standard library.

At the end of each line, we see an address in square brackets. This is the address from where the function call occurred that ultimately led to the syscall. Unfortunately, strace doesn’t show us the source code file and the line number that this address relates to. But we can easily get this information with the addr2line tool:

$ addr2line -e hello 0x11b3
/home/user/abstractexpr/strace-stacktrace/hello.c:9

Prettifying the Stack Trace

Now that we know how to resolve a single address we could write a Python script to replace every single address in the output of strace with the output of addr2line.

I’ve already written such a script:

#!/usr/bin/env python3

import os
import sys

import subprocess

def usage():
    print("Usage: pretty_print_strace.py FILENAME")
    sys.exit(2)

if len(sys.argv) != 2:
    usage()

filename = sys.argv[1]

def handle_line(line):
    line = line.rstrip()
    if not line.startswith(" > ") or not line.endswith("]"):
        print(line)
        return
    enrich_line(line)

def enrich_line(line):
    line = line[3:]
    components = line.split(" ")
    binary_funcoffset = components[0]
    address = components[1]
    binary = binary_funcoffset.split("(")[0]
    address = address[1:]
    address = address[:-1]

    result = subprocess.run(["addr2line", "-e", binary, address], shell=False, capture_output=True, text=True)
    line_num_info = ""
    if result.returncode != 0:
        line_num_info = address
    else:
        line_num_info = result.stdout.rstrip()

    if line_num_info == "??:?":
        line_num_info = address

    print(f" > {binary_funcoffset} [{line_num_info}]")

with open(filename) as f:
    for line in f:
        handle_line(line)

Save it as pretty_print_strace.py and make it executable with:

$ chmod a+x pretty_print_strace.py

You can then execute strace again but use the -o option to tell it to write its output to a file:

$ strace -o strace.out –stack-trace ./hello

This output file can then be prettified with the following command:

$ ./pretty_print_strace.py strace.out > strace-pretty.out

The stack traces in the newly created file strace-pretty.out will then have a filename and a line number instead of an address for every stack trace entry where addr2line could resolve the address:

...
write(1, "Num: 5\n", 7)                 = 7
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__write+0x14) [./io/../sysdeps/unix/sysv/linux/write.c:26 (discriminator 1)]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_file_write+0x35) [./libio/./libio/fileops.c:1182]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_do_write+0xb1) [./libio/./libio/fileops.c:449 (discriminator 1)]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_file_xsputn+0xcf) [./libio/./libio/fileops.c:1244 (discriminator 2)]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(parse_printf_format+0xaf9) [./stdio-common/./stdio-common/printf_buffer_to_file.c:59 (discriminator 2)]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_vfprintf+0x48e3) [./stdio-common/./stdio-common/vfprintf-internal.c:1545]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_IO_printf+0xb3) [./stdio-common/./stdio-common/printf.c:37]
 > /home/user/abstractexpr/strace-stacktrace/hello(print_info+0x4a) [/home/user/abstractexpr/strace-stacktrace/hello.c:9]
 > /home/user/abstractexpr/strace-stacktrace/hello(main+0x1d) [/home/user/abstractexpr/strace-stacktrace/hello.c:14]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_init_first+0x8a) [./csu/../sysdeps/x86/libc-start.c:74]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [./csu/../csu/libc-start.c:128]
 > /home/user/abstractexpr/strace-stacktrace/hello(_start+0x25) [0x10a5]
exit_group(0)                           = ?
+++ exited with 0 +++
 > /usr/lib/x86_64-linux-gnu/libc.so.6(_exit+0x1d) [./posix/../sysdeps/unix/sysv/linux/_exit.c:30 (discriminator 1)]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(erand48_r+0x196) [./stdlib/./stdlib/exit.c:131]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(exit+0x1e) [:?]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_init_first+0x91) [./csu/../sysdeps/x86/libc-start.c:63]
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [./csu/../csu/libc-start.c:128]
 > /home/user/abstractexpr/strace-stacktrace/hello(_start+0x25) [0x10a5]

Interestingly, many locations in the standard library could be resolved here as well.

The two lines from our program look like this now:

...
 > /home/user/abstractexpr/strace-stacktrace/hello(print_info+0x4a) [/home/user/abstractexpr/strace-stacktrace/hello.c:9]
 > /home/user/abstractexpr/strace-stacktrace/hello(main+0x1d) [/home/user/abstractexpr/strace-stacktrace/hello.c:14]
...

So here we have it! A stack trace for every syscall recorded by strace and the name of the C file and the line number are present in every entry of the stack trace.

Leave a comment