libfeihu Blog

libfeihu Blog

马上订阅 libfeihu Blog RSS 更新: http://feihu.me/blog/feed.atom

谁打印了这个字符串

2014年1月15日 00:00

前段时间在调试时遇到一个问题,运行程序出现错误,但并没有足够的信息来定位错误所在。可喜的是控制台上输出了一些可疑信息,只要找到了哪里打印了这些信息便有可能推断错误的原因。然而由于程序过于庞大,不可能一步一步跟踪调试去查找哪条语句执行后输出了这段字符串。尝试在所有的代码中搜索这段字符串也无功而返。后来突发奇想,能否在输出字符串时设置一个条件断点,只要输出的这段信息就中断,这样就可以在中断后找到何处打印了这些可疑信息,进而解决程序的问题了。

经过大量的搜索之后,在Stack Overflow上找到了答案,并且成功的解决了我的问题。被采用答案的作者Anthony Arnold由于十分喜欢这个问题,所以写了一篇关于它的博文。我也特别喜欢这个问题,之前遇到过多次,但都采用别的方式解决,而只有这个答案最完美。同时它综合运用了很多知识,能够给我们的调试带来不少启发。因为网上也没有再找到其它的解决方案,所以我决定翻译此文,后面对原文再进行其它平台的补充。离开学校之后第一次翻译,不好之处欢迎指正。

目录


调试STDOUT

前几天我遇到一个很有趣的Stack Overflow 问题,提问者希望GDB能够在一个特定的字符串写到stdout时中断程序。

除非你至少掌握了一些下面的知识,否则并不容易得到这个问题的答案:

  • 汇编语言
  • 操作系统
  • C语言标准库
  • GDB命令

我想出了一种可行的但是不可移植的解决方案,它依赖于x86指令集和Linux的系统调用write(2),所以本文的讨论限制在特定操作系统内核上的特定架构。

例子

我们使用下面的代码(定义在hello.c中)来演示如何用GDB在"Hello World!\n"写入stdout时中断。(feihu注:代码作了一定的修改,增加了另外一个输出字符串的函数,以更好的演示捕获特定字符串)

#include <stdio.h>


void test()
{
    printf("Test\n");
}

int main()
{
    printf("Hello World!\n");
    test();
    return 0;
}

用下面的命令编译链接:

# gcc -g -o hello hello.c

用下面的命令调试:

# gdb hello

在write中设置断点

第一步,我们需要找出如何在有数据被写到stdout时中断程序。我们假设你调试代码的作者没有疯,他们采用了所选语言的标准用法来向stdout写数据(比如C语言中的printf(3)),或者他们直接调用系统调用write(2)

实际上最终printf(3)也调用的是write(2),所以不管采用上面哪种方式都可以。

因此你可以在write(2)系统调用中设置一个断点:

$gdb break write

GDB也许会抱怨它并不知道write函数,但是它可以在将来这个函数有效时再在其中设置这一断点:

Function "write" not defined.
Make breakpoint pending on future shared library load? (y or [n])

这完全没问题,直接输入y即可。

在write中写到STDOUT时设置断点

一旦你能够在write函数中设置断点后,你需要设置一个条件,只有在写到stdout时才中断,这里有一点复杂。看看write(2)帮助页面:第一个参数fd是要写的文件描述符,在Linux中,stdout的文件描述符是1(也许你使用的平台有所不同)。

于是你的第一反应是这样:

$gdb condition 1 fd == 1

但是很遗憾,这不起作用。(feihu注:会出现这样的错误No symbol "fd" in current context.)除非你非常幸运,否则不可能已经加载了write(2)系统调用的调试symbols,这意味着你不能以参数名来访问传给系统调用的参数。

获取系统调用参数

当然,还有其它的方法可以获取传给系统调用的参数,GDB提供了非常完善的手段让你来访问各种汇编寄存器,在这个问题中,我们感兴趣的是Extended Stack Pointer,也就是esp寄存器。(feihu注:这里仅适用于x86 32位系统,x86 64位系统的解决方案请向下看。)

当一个函数调用发生时,栈中储存了:

  • 函数的返回地址
  • 指向函数参数的指针

这意味着当调用write函数时,栈的结构可能像下面一样:

系统调用参数

这是假设地址占4个字节,根据你自己的机器作相应的调整

所以现在你可以像这样设置条件断点:

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

再一次声明,假设地址占4个字节

注意,$esp能够访问ESP寄存器,它将所有的数据都看成是void *。因为GDB不允许直接将void *转换成int型(这么做是对的),所以你需要先将...

剩余内容已隐藏

查看完整文章以阅读更多