20

在 Linux 中,使用 C/C++ 代码,使用 gdb,如何添加 gdb 断点来扫描传入的字符串以中断特定字符串?

我无权访问特定库的代码,但我想在该库将特定字符串发送到标准输出时立即中断,以便我可以返回堆栈并调查调用该库的代码部分。当然,我不想等到缓冲区刷新发生。这可以做到吗?也许是例行公事libstdc++

4

4 回答 4

26

这个问题可能是一个很好的起点:如何在 gdb 中的“某些内容被打印到终端”上设置断点?

因此,只要将某些内容写入标准输出,您至少可以中断。该方法主要涉及在write系统调用上设置断点,条件是第一个参数是1(即 STDOUT)。在评论中,还有关于如何检查write调用的字符串参数的提示。

x86 32 位模式

我想出了以下内容并使用 gdb 7.0.1-debian 对其进行了测试。它似乎工作得很好。$esp + 8包含指向传递给 的字符串的内存位置的指针write,因此首先将其转换为整数,然后再转换为指向 的指针char$esp + 4包含要写入的文件描述符(1 表示 STDOUT)。

$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0

x86 64 位模式

如果您的进程在 x86-64 模式下运行,则参数将通过暂存器%rdi%rsi

$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0

请注意,由于我们使用的是暂存寄存器而不是堆栈上的变量,因此删除了一级间接。

变体

strcmp上述代码段中可以使用的函数除外:

  • strncmpn如果您想匹配正在写入的字符串的前几个字符,这很有用
  • strstr 可用于在字符串中查找匹配项,因为您不能始终确定要查找的字符串位于通过函数写入的字符串的开头。write

编辑:我喜欢这个问题并找到它的后续答案。我决定写一篇关于它的博客文章

于 2011-11-22T23:59:48.153 回答
5

catch+strstr条件

这个方法很酷的一点是它不依赖于write使用的 glibc:它跟踪实际的系统调用。

此外,它对缓冲更有弹性printf(),因为它甚至可以捕获跨多个printf()调用打印的字符串。

x86_64 版本:

define stdout
    catch syscall write
    commands
        printf "rsi = %s\n", $rsi
        bt
    end
    condition $bpnum $rdi == 1 && strstr((char *)$rsi, "$arg0") != NULL
end
stdout qwer

测试程序:

#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    write(STDOUT_FILENO, "asdf1", 5);
    write(STDOUT_FILENO, "qwer1", 5);
    write(STDOUT_FILENO, "zxcv1", 5);
    write(STDOUT_FILENO, "qwer2", 5);
    printf("as");
    printf("df");
    printf("qw");
    printf("er");
    printf("zx");
    printf("cv");
    fflush(stdout);
    return EXIT_SUCCESS;
}

结果:休息时间:

  • qwer1
  • qwer2
  • fflush. 前一个printf实际上并没有打印任何东西,它们被缓冲了!sycallwrite只发生在fflush.

笔记:

  • $bpnum感谢 Tromey:https ://sourceware.org/bugzilla/show_bug.cgi?id=18727
  • rdi: 包含 x86_64 中 Linux 系统调用编号的寄存器,1用于write
  • rsi: 系统调用的第一个参数,因为write它指向缓冲区
  • strstr: 标准 C 函数调用,搜索子匹配,如果未找到则返回 NULL

在 Ubuntu 17.10、gdb 8.0.1 中测试。

跟踪

如果您感觉互动,另一种选择:

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'

样本输出:

[00007ffff7b00870] write(1, "a\nb\n", 4a

现在复制该地址并将其粘贴到:

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'

这种方法的优点是您可以使用通常的 UNIX 工具来操作strace输出,并且不需要很深的 GDB-fu。

解释:

于 2015-07-27T20:59:31.097 回答
3

安东尼的回答很棒。按照他的回答,我在Windows(x86-64 位 Windows)上尝试了另一种解决方案。我知道这里的这个问题是针对Linux 上的GDB的,但是,我认为这个解决方案是对这类问题的补充。它可能对其他人有帮助。

Windows 上的解决方案

在 Linux 中,对 的调用printf将导致对 API 的调用write。而且因为 Linux 是一个开源操作系统,我们可以在 API 中进行调试。但是,Windows 上的 API 有所不同,它提供了自己的 API WriteFile。由于 Windows 是商业非开源操作系统,因此无法在 API 中添加断点。

但是VC的一些源代码是和Visual Studio一起发布的,所以我们可以在源代码中找到最终调用WriteFileAPI的地方,并在那里设置断点。在对示例代码进行调试后,我发现该printf方法可能会导致调用_write_nolockin which WriteFileis called。该函数位于:

your_VS_folder\VC\crt\src\write.c

原型是:

/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
        int fh,
        const void *buf,
        unsigned cnt
        )

writeLinux 上的 API 相比:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count); 

它们具有完全相同的参数。所以我们可以参考上面的解决方案来设置a condition breakpoint_write_nolock只是在细节上有一些区别。

适用于 Win32 和 x64 的便携式解决方案

很幸运,我们可以直接在 Visual Studio 上使用参数名称来设置 Win32 和 x64 上的断点条件。因此编写条件变得非常容易:

  1. 在中添加断点_write_nolock

    注意:在 Win32 和 x64 上几乎没有区别。我们可以只使用函数名来设置 Win32 上的断点位置。但是在x64上就不行了,因为在函数的入口处,参数没有初始化。因此,我们不能使用参数名称来设置断点的条件。

    但幸运的是,我们有一些解决方法:使用函数中的位置而不是函数名来设置断点,例如,函数的第一行。参数已经在那里初始化。(我的意思是用filename+line number设置断点,或者直接打开文件并在函数中设置断点,不是入口而是第一行。)

  2. 限制条件:

    fh == 1 && strstr((char *)buf, "Hello World") != 0
    

注意:这里仍然存在问题,我测试了两种不同的方式将内容写入标准输出:printfstd::cout. printf将一次将所有字符串写入_write_nolock函数。但是std::cout只会逐个字符地传递给_write_nolock,这意味着 API 将被调用strlen("your string")次数。在这种情况下,条件不能永远激活。

Win32解决方案

当然我们可以使用提供的相同方法Anthony:通过寄存器设置断点的条件。

GDB对于 Win32 程序,解决方案与在 Linux 上几乎相同。您可能会注意到__cdecl的原型中有一个装饰_write_nolock。这个调用约定意味着:

  • 参数传递顺序是从右到左。
  • 调用函数从堆栈中弹出参数。
  • 名称装饰约定:名称前加下划线字符 (_)。
  • 没有进行案例翻译。

这里有说明。并且有一个示例用于显示 Microsoft 网站上的寄存器和堆栈。结果可以在这里找到。

然后很容易设置断点的条件:

  1. 中设置断点_write_nolock
  2. 限制条件:

    *(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
    

它与 Linux 上的方法相同。第一个条件是确保将字符串写入stdout. 第二种是匹配指定的字符串。

x64 解决方案

从 x86 到 x64 的两个重要修改是 64 位寻址能力和一组通用的 16 个 64 位寄存器。随着寄存器的增加,x64 只__fastcall用作调用约定。前四个整数参数在寄存器中传递。参数 5 和更高的参数在堆栈上传递。

您可以参考微软网站上的参数传递页面。四个寄存器(按从左到右的顺序)是RCXRDX和。所以很容易限制条件:R8R9

  1. 中设置断点_write_nolock

    注意:它与上面的可移植解决方案不同,我们可以将断点的位置设置为函数而不是函数的第一行。原因是所有的寄存器都已经在入口处初始化了。

  2. 限制条件:

    $rcx == 1 && strstr((char *)$rdx, "Hello") != 0
    

我们需要强制转换和取消引用的原因esp$esp访问ESP寄存器,并且出于所有意图和目的是一个void*. 而这里的寄存器直接存储参数的值。因此不再需要另一个级别的间接。

邮政

我也很喜欢这个问题,所以我把安东尼的帖子翻译成中文,把我的答案放在里面作为补充。帖子可以在这里找到。感谢@anthony-arnold 的许可。

于 2014-01-17T06:48:03.403 回答
3

安东尼的回答非常有趣,它肯定会给出一些结果。然而,我认为它可能会错过 printf 的缓冲。事实上,关于write() 和 printf() 之间的差异,您可以读到:“printf 不一定每次都调用 write。相反,printf 缓冲它的输出。”

STDIO 包装解决方案

因此,我提出了另一种解决方案,该解决方案包括创建一个帮助程序库,您可以预先加载该程序库以包装类似 printf 的函数。然后,您可以在此库源和回溯中设置一些断点,以获取有关您正在调试的程序的信息。

它适用于Linux并针对libc,我不知道c ++ IOSTREAM,如果程序直接使用write,它会错过它。

这是劫持 printf (io_helper.c) 的包装器。

#include<string.h>
#include<stdio.h>
#include<stdarg.h>

#define MAX_SIZE 0xFFFF

int printf(const char *format, ...){
    char target_str[MAX_SIZE];
    int i=0;

    va_list args1, args2;

    /* RESOLVE THE STRING FORMATING */
    va_start(args1, format);
    vsprintf(target_str,format, args1);
    va_end(args1);

    if (strstr(target_str, "Hello World")){ /* SEARCH FOR YOUR STRING */
       i++; /* BREAK HERE */
    }   

    /* OUTPUT THE STRING AS THE PROGRAM INTENTED TO */
    va_start(args2, format);
    vprintf(format, args2);
    va_end(args2);
    return 0;
}

int puts(const char *s) 
{   
   return printf("%s\n",s);
}

我添加了 puts,因为 gcc 倾向于在可能的情况下用 puts 替换 printf。所以我强迫它回到printf。

接下来,您只需将其编译为共享库。

gcc -shared -fPIC io_helper.c -o libio_helper.so -g

你在运行 gdb 之前加载它。

LD_PRELOAD=$PWD/libio_helper.so; gdb test

其中 test 是您正在调试的程序。

然后你可以中断,break io_helper.c:19因为你用 -g 编译了库。

解释

我们的运气是 printf 和其他 fprintf、sprintf... 只是在这里解决可变参数并调用它们的 'v' 等价物。(在我们的例子中是 vprintf)。做这个工作很容易,所以我们可以做,把真正的工作留给 libc 用 'v' 函数。要获得 printf 的可变参数,我们只需要使用 va_start 和 va_end。

这种方法的主要优点是您可以确定当您中断时,您处于程序中输出目标字符串的部分,并且这不是缓冲区中的剩余部分。此外,您不对硬件做出任何假设。缺点是您假设程序使用 libc stdio 函数来输出内容。

于 2016-05-19T01:51:03.337 回答