What's This Do?

Programming etc.

Debugging Standard Output

Update: Ross Rogers has provided a great solution for x86-64.

Debugging Standard Output

I came across an interesting Stack Overflow question the other day. The user wanted to make GDB break whenever a specific string was written to stdout.

The answer to this question is not immediately obvious unless you’ve at least some knowledge of:

  • Assembly Language
  • Operating Systems
  • The C standard library
  • GDB commands

I came up with an answer that is works, but isn’t very portable. It relies on the x86 instruction set and the write(2) Linux system call. So already we are restricted to a specific architecture on a specific OS kernel.

An Example

Using the following code (defined in hello.c), we will setup GDB to break when the string “Hello, World!\n” is written to standard output:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
}

Compile and link with:

# gcc -g -o hello hello.c

and debug with:

# gdb hello

Breaking on Write

The first step is figuring out how to break when something is being printed to stdout. We’ll assume that the author of the code you’re debugging is not utterly insane and that they’ve used one their chosen language’s standard ways of printing to stdout (like printf(3) in C), or they’re calling write(2) directly.

Either way is fine, in fact a call to printf(3) will result in a call to write(2).

So you can go ahead and put a breakpoint on the write(2) system call:

$gdb break write

GDB may complain that it doesn’t know the write function, but it can set the breakpoint if such a function becomes available in the future. This is totally fine, so just say yes to it.

Breaking on Write to STDOUT

Once you can break on write, you need to set a condition so that you’ll only break when writing to stdout; this is where it can get tricky. Take a look at the man page for write(2): the first argument, fd, is the file descriptor to write to. Under Linux, the integer file descriptor for stdout is 1 (might be different on your platform).
The first thing you might try is:

$gdb condition 1 fd == 1

This doesn’t work. Unless you’re lucky, you won’t have debugging symbols loaded for the write(2) system call. This means, you can’t access the parameters sent to the system calls by name.

Getting at the System Call Arguments

There’s another way that you can get at the arguments passed to the system call. GDB is kind enough to give you access to various assembly registers. We are interested in the Extended Stack Pointer, or esp register.

When a function call is made, the following is stored on the stack:

  • The address to continue executing from when the function returns, and
  • Pointers to the arguments to the function.

This means that the stack frame for the write call would look something like this:

ESP Offset (bytes) Points to Argument Value
0 N/A Return address
4 fd File descriptor
8 buf Pointer to the buffer to write
12 count The number of bytes to write

This is assuming that the address size is 4 bytes. Adjust for your own machine

So now you can set the conditional breakpoint:

$gdb break write if *(int*)($esp + 4) == 1

Once again, a 4-byte address size is assumed.

Note that $esp accesses the ESP register, and for all intents and purposes is a void*. GDB won’t allow you to cast a void* to an int (and rightly so). First you must cast ($esp + 4) to an int*, then dereference that pointer to get the real value of the file descriptor argument.

Restricting to Specific Text

The next step is an extension of the previous one, with the complexity ramped up a little bit. Moreover, this step may not work in all cases. The string which is passed to a call to printf(3) may not be passed on in its entirety to write(2). Instead, it may be broken into smaller chunks and written one at a time. Keep this in mind while trying to debug STDOUT calls. Stick to small strings and you should be fine.

You now must restrict the breakpoint so that breaks only when a specific string is passed to write(2). To achieve this, you can call strcmp(3) on the buf argument of write. From the table above, you know that ( $esp + 8 ) is a pointer to buf. So, it’s fairly simple to add a conditional statement to the breakpoint:

$gdb break write if *(int*)($esp + 4) == 1 && strcmp("Hello, World!\n", *(char**)($esp + 8)) == 0

Remember that $esp + n / sizeof(void*) evaluates to a pointer to the nth function argument. This means that $esp + 8 is a pointer to a pointer to a buffer. You need to do a little casting- and deferencing-fu to get it to work.

The 64-bit Solution

Thanks to Ross and Mr. Potti

Processes running in 64-bit mode make use of the RDI and RSI registers.

$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "Hello, World!\n") == 0

Note that a level of indirection is negated, because instead of the registers holding pointers to stack elements, they contain the values themselves.

Wrapping Up

So after all of that, you get a fairly non-portable solution. You can get GDB to break whenever a specific string is written to STDOUT, but you’re limitied by platform <and architecture.

If anybody has a better solution, or even just an alternative one, please post it in the comments and/or submit it as an answer in the relevant SO question. Also, if you have a solution which will work on other platforms/architectures then be sure to post it here.

About these ads

12 responses to “Debugging Standard Output

  1. Ross 23/12/2011 at 04:12

    Thanks again for this one, Anthony.

  2. Michael Enke 25/09/2013 at 01:53

    Why can not show the content of buf?
    I would expect p (char*)($esp + 8) shows “Hello, World!\n”
    but it shows empty (for printf(“Hello, World!\n”)) string or
    garbage (for printf(“%s”, s) where s declared before).

  3. biluncloud 17/01/2014 at 16:44

    Hi Anthony, I found the solution on Windows. And I’ve already posted it on the original question on stackoverflow. In addition, I like this post very much, can I translate it into Chinese on my post and append my solution in it? Thanks very much.

  4. biluncloud 17/01/2014 at 16:45

    Hi Anthony, I found the solution on Windows. And I’ve already posted it on the original question on stackoverflow. In addition, I like this post very much so can I translate it into Chinese on my post and append my solution in it? Thanks very much.

  5. biluncloud 03/04/2014 at 19:46

    Hi Anthony, I found a problem in the article. “$esp + n / sizeof(void*) evaluates to a pointer to the nth function argument” in the “Restricting to Specific Text” paragraph should be “$esp + n * sizeof(void*)”

  6. Dmitry Larchenko 21/04/2014 at 23:45

    Everything is much simpler on ARM:
    break write if $r0 == 2 && strcmp(“Hello, World!\n”, (char *) $r1) == 0

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 189 other followers