Tuesday, September 29, 2009

c/c++: call stack v.2

In previous c/c++: call stack article I wrote about obtaining function call stack. The method described in the article is good enough but it is linux-specific. There's no similar solutions out-of-box in other *nix-like operation systems as far as I know.
This time I would like to discuss more generic way. At list it is available for the code compiled with gcc.

gcc provides two built-in functions which could be used to obtain function call stack: __builtin_return_address and __builtin_frame_address. With __builtin_return_address it's possible to obtain return address of the current function, or of one of its callers. __builtin_frame_address returns the address of the function frame.

Both functions require constant argument - the number of frames to scan up.

To store function call struct call structure is used defined as following:

struct call {
 const void *address;
 const char *function;
 const char *object;
};

With __builtin_frame_address it is checked if the top of the stack has been reached - in this case aforementioned built-in returns zero. In the loop the return value of this function is compared with zero and the loop is terminated if this expression in compare statement turns into true - the top of the call stack has been reached.
Finally __builtin_return_address is used to get the return address of the function.

The resulting function to get the backtrace looks like:
#define _GNU_SOURCE
#include <dlfcn.h>

int backtrace(struct call trace[], int maxlen)
{
 Dl_info dlinfo;
 unsigned int i;

 for (i=0;i<maxlen;++i) {
  switch (i) {
   case 0:
    if(!__builtin_frame_address(0))
     return i;
    trace[i].address = __builtin_return_address(0);
    break;
   case 1:
    if(!__builtin_frame_address(1))
     return i;
    trace[i].address = __builtin_return_address(1);
    break;
   case 2:
    if(!__builtin_frame_address(2))
     return i;
    trace[i].address = __builtin_return_address(2);
    break;
 /* SNIP */
 /* .... */
 /* SNIP */
   case 63:
    if(!__builtin_frame_address(63))
     return i;
    trace[i].address = __builtin_return_address(63);
    break;
   default:
    return i;
  }

  if (dladdr(trace[i].address, &dlinfo) != 0) {
   trace[i].function = dlinfo.dli_sname;
   trace[i].object = dlinfo.dli_fname;
  }
 }

 return i;
}
backtrace routine fills trace array with the call stack information and returns the depth of the function calls.

Following small example shows backtrace in action:
#include <stdio.h>

#define CALLSTACK_MAXLEN 64

void f0()
{
 struct call trace[CALLSTACK_MAXLEN];
 int i;
 int depth;

 depth = backtrace(trace, CALLSTACK_MAXLEN);

 for (i=0;i<depth;++i)
  printf("%s: %s(%p)\n", trace[i].object, trace[i].function, trace[i].address);
}

void f1()
{
 f0();
}

void f2()
{
 f1();
}

void f3()
{
 f2();
}

void f4()
{
 f3();
}


int main(int argc, char **argv)
{
 f4();

 return 0;
}
Again the application should be compiled with -rdynamic gcc flag needed for dladdr function and linked with dl library for the same purpose:
gcc backtrace.c -o backtrace -ldl -rdynamic
After execution the program should provide the following output:
./backtrace 
./backtrace: f0(0x804b11c)
./backtrace: f1(0x804b1ac)
./backtrace: f2(0x804b1b9)
./backtrace: f3(0x804b1c6)
./backtrace: f4(0x804b1d3)
./backtrace: main(0x804b1e0)
/lib/libc.so.6: __libc_start_main(0x6ff58a9e)
Though it's gcc-specific method this compiler is available for most platforms and operation systems and is used almost everywhere as a default one.

No comments: