Friday, April 1, 2016

How to Record Function Calls Using GDB Automated Script

Probably one of the best ways to analyze new program source files is by looking at the function calls. In this post, I will discuss how to record the function flow from the main function using gdb. The credit goes to Juan M. Bello Rivas, and I found this from the link here.

From the link above, download the callgraph.tar.gz file and extract it..
$ wget http://web.archive.org/web/20090317091725/http://superadditive.com/software/callgraph.tar.gz
$ tar xfz callgraph.tar.gz
$ cd callgraph

You will need gawk to be able to successfully run it, so let's install if not already installed on your system.
$ sudo apt-get install -y gawk

Sometimes, it may be the case that your system has the original version of awk, which won't work with the callgraph script. You may need to replace awk with gawk in the file.
$ sed -i 's/awk/gawk/g' callgraph

OK, you are now ready to run the script. Let's first test it with the given test.c code.
$ gcc -g test.c

You should have a.out executable file. Let's run the script file. Before that, by the way, don't forget to enable execution flag.
$ chmod u+x callgraph
$ ./callgraph a.out

You should now see the following output
main foo (n=23)
foo bar (n=23)

This is very cool! Note that this script is probably suitable to work with only a small or medium size program, but this shall be still very useful!

In case the link will become dead in the future, I will copy the callgraph script file and test.c file below:
#!/bin/sh

  # Copyright (c) 2004-2007 Juan M. Bello Rivas <jmbr@superadditive.com>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

prog_name="callgraph"

if [ $# -lt 1 ]; then
    echo "Usage: $prog_name EXECUTABLE [ARGS...]"
    echo
    echo "Example: $prog_name ~/bin/test-program foo 23"
    exit 1
fi

# Sanity checks.
FILE=$1

if [ ! -x $FILE ]; then
    echo "$prog_name: Unable to find executable '$FILE'"
    exit 1
fi

LANG="" gdb --eval-command=quit $FILE 2>&1 \
    | grep -E '(no\ debugging\ symbols\ found|not\ in\ executable\ format)' 2>&1 > /dev/null
if [ $? -eq 0 ]; then
    echo -n "$prog_name: Can't print call graph for '$FILE' because it's not a "
    echo "binary executable compiled with debugging symbols."
    exit 1;
fi

shift

# Set up temporary files.
TRACE="`mktemp -t $prog_name.XXXXXXXXXX`" || exit
GETFUNCS="`mktemp -t $prog_name.XXXXXXXXXX`" || exit
trap 'rm -f -- "$TRACE" "$GETFUNCS"' EXIT
trap 'trap - EXIT; rm -f -- "$TRACE" "$GETFUNCS"; exit 1' HUP INT QUIT TERM

# Take control of GDB and print call graph.
cat > $GETFUNCS <<EOF
set height 0
info functions
EOF

gdb --batch --command=$GETFUNCS $FILE 2>/dev/null | awk '
function get_func_name(str)
{
  split(str, part, "(");
  len = split(part[1], part, " ");
  len = split(part[len], part, "*");

  return part[len];
}

BEGIN {
  total = 0;
  print "set width 0";
  print "set height 0";
  print "set verbose off";
}

/[a-zA-Z_][a-zA-Z0-9_]*\(/ {
  fn = get_func_name($0);
  printf("break %s\n", fn);
  ++total;
}

END {
  for (i = 1; i <= total; i++) {
    print "commands", i;
    /* print "info args"; */
    print "backtrace 2";
    print "continue";
    print "end";
  }

  print "run"
}
' > $TRACE

gdb --batch --command=$TRACE --tty=/dev/null --args $FILE $@ 2>/dev/null | awk '
function get_callee(s)
{
  split(s, info, ",");
  split(info[2], fn, " ");
  callee = fn[1];

  return callee;
}

function get_params(s, n)
{
  split(s, par, n);
  split(par[2], par, " at ");
  sub(/ \(/, "(", par[1]);

  return par[1];
}

BEGIN {
  isrecord = 0;
  callee = "";
  caller = "*INITIAL*";
  params = "";
}

/^Breakpoint [0-9]+,/ {
  isrecord = 1;

  callee = get_callee($0);
  params = get_params($0, callee);
}

/^#1[ \t]+/ {
  if (isrecord)
    caller = $4;
}

/^$/ {
  if (isrecord && (caller != "*INITIAL*")) {
    printf("%s %s %s\n", caller, callee, params);
    callee = caller = params = "";
  }
}
'


Here is test.c file
#include <stdio.h>
#include <stdlib.h>

static void foo(int n);
static void bar(int n);
static void baz(int n);

int
main(int argc, char *argv[])
{
foo(23);

exit(EXIT_SUCCESS);
}

void 
foo(int n)
{
bar(n);
}

void
bar(int n)
{
baz(n);
}

void
baz(int n)
{
return;
}

No comments:

Post a Comment