Examining the Call Stack with GDB

This will be the second Blog entry I’ve made. I need to come up with a fancier welcome and introduction to each of these things. I started the forth semester of CS Masters program at Wash U. last week. I’ve got a lot of reading to do.

This week I wanted to get a short tutorial about the gdb debugger that I made a long time ago. Now, there are plenty of comprehensive debugger tutorials out there already. In this one I wanted to go into more detail than most to talk about Call Stacks, Frames, and how to examine them with GDB. We’re going to be examining the memory used to pass arguments in a C function call.

To make things easy, we’re going to use a very simple program that I have provided the source code for below. I originally tried to do this with a common program like the telnet client provided with Solaris or Linux and show what IP address and port number a socket was connecting to, but the Solaris telnet client seems to not use Frame Buffers for library calls and the Linux telnet client is stripped. This makes it difficult to do what I had set out to do with it. So, we’ll start with something much more basic.

This tutorial will show you how to examine the arguments passed to a function by examining the memory address space directly. This can be useful when debugging an executable that was not compiled with debug information. What I’m showing below does require that symbol information be intact (ie, not stripped). The example I’m presenting examines a function defined in the executable, not a library–though, in theory, the same should apply to a function in a shared library.

test.c:

#include<stdio.h>

#include<stdlib.h>

int func1(int arg1, char *arg2, char arg3)

{

printf("in func1: %d %s %c\n",arg1,arg2,arg3);

return 1;

}

int func2()

{

printf("in func2\n");

return 2;

}

int main(int argc, char **argv)

{

int val1 = func1(5, "This is a test", ‘a’);

int val2 = func2();

printf("in main\n");

return 0;

}

To give you an idea of what this program is doing, I traced its execution with ltrace. This will show all of its C Runtime Library calls and System Calls.

Here is the ltrace output for this executable and some inline comments to explain what is happening:

//Get basic information about the system we are running on.

5724 SYS_uname(0xbffff64c) = 0 <0.000017>

5724 SYS_brk(NULL) = 0x804a000 <0.000011>

5724 SYS_access(0xa9e8d8, 4, 0xaa2fb4, 0xa9e8d8, 4) = -2 <0.000033>

//Load the ld-linux.so.2 library from the cache.

5724 SYS_open("/etc/ld.so.cache", 0, 00) = 3 <0.000049>

5724 SYS_fstat64(3, 0xbfffed68, 0xaa2fb4, 0xaa34d8, 0) = 0 <0.000019>

5724 SYS_mmap(0xbfffed48, 2, 0xaa2fb4, 3, 0) = 0xb7fdf000 <0.000026>

5724 SYS_close(3) = 0 <0.000012>

//Load the C runtime library

5724 SYS_open("/lib/tls/libc.so.6", 0, 00) = 3 <0.000032>

5724 SYS_read(3, "\177ELF\001\001\001", 512) = 512 <0.000018>

5724 SYS_fstat64(3, 0xbfffedfc, 0xaa2fb4, 0xaa34d8, 0) = 0 <0.000016>

//Map the C runtime library into memory address space.

5724 SYS_mmap(0xbfffec48, 5, 0xaa2fb4, 0xbfffec80, 0xbfffecb0) = 0xaaa000 <0.000033>

5724 SYS_mprotect(0xbcd000, 27804, 0, 0xbfffec80, 0) = 0 <0.000025>

5724 SYS_mmap(0xbfffec48, 0, 0xaa2fb4, 0xbfffec98, 0xbce000) = 0xbce000 <0.000038>

5724 SYS_mmap(0xbfffec48, 7324, 0xaa2fb4, 0xbfffec98, 50) = 0xbd2000 <0.000025>

5724 SYS_close(3) = 0 <0.000012>

5724 SYS_mmap(0xbffff1cc, 34, 0xaa2fb4, 4096, 96) = 0xb7fde000 <0.000019>

5724 SYS_mprotect(0xbce000, 8192, 1, 0xaa3b88, 0xaa3b88) = 0 <0.000028>

5724 SYS_mprotect(0xaa2000, 4096, 1, 0xaa31c0, 0xaa31c0) = 0 <0.000025>

5724 SYS_set_thread_area(0xbffff574, 81, 0xb7fde940, 0xaa2fb4, -1) = 0 <0.000014>

5724 SYS_munmap(0xb7fdf000, 132591) = 0 <0.000034>

//Hook into C Run-time that calls our main()

5724 __libc_start_main(0x80483a2, 1, 0xbffff8a4, 0x80483e8, 0x804843c <unfinished …>

//Call to printf() function in libc

5724 printf("in func1\n" <unfinished …>

//Check status of standar dout

5724 SYS_fstat64(1, 0xbffff0ac, 0xbcfff4, 0xbd05c0, 8192) = 0 <0.000021>

5724 SYS_mmap2(0, 4096, 3, 34, -1) = 0xb7fff000 <0.000023>

//Write to standard out via write() system call

5724 SYS_write(1, "in func1\n", 9) = 9 <0.000173>

5724 <… printf resumed> ) = 9 <0.000835>

//Another printf() library call.

5724 printf("in func2\n" <unfinished …>

5724 SYS_write(1, "in func2\n", 9) = 9 <0.000128>

5724 <… printf resumed> ) = 9 <0.000229>

//Another printf() library call.

5724 printf("in main\n" <unfinished …>

5724 SYS_write(1, "in main\n", 8) = 8 <0.000120>

5724 <… printf resumed> ) = 8 <0.000218>

5724 SYS_munmap(0xb7fff000, 4096) = 0 <0.000023>

//Process exit.

5724 SYS_exit_group(0 <unfinished …>

5724 +++ exited (status 0) +++

If you got lost in the last part, that’s okay. I’ll go into more detail about ltrace output in the future.

Anyway, to compile the program:

gcc -o test test.c

We’ve created a fairly straightforward ELF executable that does not contain any debug information. But, it has not been stripped, so symbols are still in there. If an executable such as our test program has been stripped, then the program has no concept of symbols such as "main", "func1", and "func2" that could be used to set break points. At this point, you would have to know the functions memory address in order to set the break point as a pointer. If we needed it, we could have gotten that information from objdump before it is stripped. Otherwise, you can get the starting address of every function in the text section and start narrowing the choices. This is not the ideal way to use a debugger.

Moving along, load the program into gdb as follows:

gdb ./test

You should see the following:

[broeckel@yakko test3]$ gdb test

GNU gdb Red Hat Linux (6.1post-1.20040607.43rh)

Copyright 2004 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux-gnu"…(no debugging symbols found)…Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb)

Set a break point for func1():

(gdb) break func1

You should see the following:

Breakpoint 1 at 0x804836e

Run the program:

(gdb) run

You should see the following:

Starting program: /share/home/broeckel/work/buffer_overflows/test3/test

(no debugging symbols found)…(no debugging symbols found)…

A moment later, you should see the following:

Breakpoint 1, 0x0804836e in func1 ()

Run the following command:

(gdb) backtrace

#0 0x0804836e in func1 ()

#1 0x080483dd in main ()

At this point, your program has proceeded to the point where it has called the func1() function (ie, a stack frame has been created for it), but the first instruction has not yet been executed in that function.

In order to find out where the arguments for func1() are being stored, we need to look at Stack Frame 0. Run the following command:

(gdb) info frame 0

Stack frame at 0xbffff7c0:

eip = 0x804836e in func1; saved eip 0x80483dd

called by frame at 0xbffff7f0

Arglist at 0xbffff7b8, args:

Locals at 0xbffff7b8, Previous frame’s sp is 0xbffff7c0

Saved registers:

ebp at 0xbffff7b8, eip at 0xbffff7bc

This tell us that the argument list for this Stack Frame is stored at memory address 0xbffff7b8.

Looking directly at this memory address shows:

(gdb) x/8x 0xbffff7b8

0xbffff7b8: 0xbffff7e8 0x080483dd 0x00000005 0x08048502

0xbffff7c8: 0x00000061 0x0804841e 0x00bcfff4 0x00bcfff4

                                      (+4 bytes)    (+8 bytes)  (+12 bytes)

Note, arguments to C function are pushed into the stack in reverse order. 0xbffff7c8 is the character ‘a’ that was passed in for the third argument. The value located at 0xbffff7b8 + <12>, 0x08048502 is a pointer to the string "This is a test". The value located at 0xbffff7b8 + <8> is the first argument, an integer of value five.

arg 1:

The value stored at 0xbffff7b8 + <8> is the integer with value of five. There is not much more to show.

arg 2:

Argument 2 is a string, we can examine the string with the following command:

(gdb) x/1s 0x08048502

0x8048502 <_IO_stdin_used+34>: "This is a test"

arg3:

Argument 3 is a single character, we can examine this character the same we looked at the string in argument 2:

(gdb) x/1s 0xbffff7c8

0xbffff7c8: "a"

The value at 0xbffff7b8 + <4> is a pointer to the place in the text segment that called func1(). This is needed so the stack can be properly unwound later. To be exact, this is a pointer to the very next assembly instruction that should be executed after func1() returns.

The value stored at 0xbffff7b8, 0xbffff7e8 is a pointer to the argument list for the previous stack frame. If you will recall:

(gdb) info frame 1

Stack frame at 0xbffff7f0:

eip = 0x80483dd in main; saved eip 0xabee23

caller of frame at 0xbffff7c0

Arglist at 0xbffff7e8, args:

Locals at 0xbffff7e8, Previous frame’s sp is 0xbffff7f0

Saved registers:

ebp at 0xbffff7e8, eip at 0xbffff7ec

(gdb)

These last two pieces of information allow the program to return to the caller at the completion of func1()’s execution.

From here, you can type continue, and the program will execute to completion.

Next time, we’re going to disassemble main() and func1().  Then, walk through what happens to the Registers and Call Stack after each instruction is executed.

Reference