如何监控放入标准输出缓冲区的内容,并在管道中存放特定字符串时中断?



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

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

这个问题可能是一个很好的起点:我如何在"打印到终端的内容"上设置断点?广东发展银行吗?

因此,无论何时向stdout写入内容,您至少可以中断。该方法基本上涉及在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以外的函数:

  • strncmp是有用的,如果你想匹配字符串的第一个n字符数
  • strstr可用于查找字符串中的匹配,因为您不能总是确定您正在查找的字符串位于开头通过write函数写入的字符串。

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

catch+strstrcondition

这个方法的酷之处在于它不依赖于使用的glibcwrite:它跟踪实际的系统调用。

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

x86_64版本:

define stdout
catch syscall write
commands
printf "rsi = %sn", $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;
}

结果:break at:

  • qwer1
  • qwer2
  • fflush。之前的printf实际上没有打印任何东西,它们被缓冲了!write同步调用只发生在fflush上。

指出:

  • $bpnumthanks to Tromey at: 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中测试。

strace

另一个选项,如果你想要互动:

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

样本输出:

[00007ffff7b00870] write(1, "anbn", 4a

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

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

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

解释:

  • -i使条带输出RIP
  • setarch -R为具有personality系统调用的进程禁用ASLR:如何在每次地址不同时使用strace -i进行调试GDB已经默认这样做了,所以不需要再这样做。

Anthony的回答很棒。根据他的回答,我在Windows上尝试了另一种解决方案(x86-64位Windows)。我知道这个问题是针对GDB的然而,在Linux上,我认为这个解决方案是对这类问题的补充。这可能对别人有帮助。

Windows解决方案

在Linux中,调用printf将导致调用APIwrite。因为Linux是一个开源的操作系统,我们可以在API中进行调试。然而,Windows上的API是不同的,它提供了自己的API WriteFile。由于Windows是一个商业的非开源操作系统,断点不能添加到api中。

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

your_VS_folderVCcrtsrcwrite.c

原型为:

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

与Linux上的writeAPI相比:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count); 

它们有完全相同的参数。因此,我们可以在_write_nolock中设置condition breakpoint,只是参考上面的解决方案,只有一些细节上的差异。

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

很幸运,我们可以直接使用参数名在Win32和x64上为断点设置条件时。所以写条件就变得很容易了:

  1. _write_nolock中添加断点

    注意:在Win32和x64上差别不大。我们可以只使用函数名来设置Win32上断点的位置。但是,它不能在x64上工作,因为在函数的入口,参数没有初始化。因此,我们不能使用参数名来设置断点的条件。

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

  2. 限制条件:

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

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

<

Win32解决方案/h3>

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

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

  • 参数传递顺序为从右到左。
  • 调用函数从栈中弹出参数。
  • 名称修饰约定:在名称前加下划线(_)。
  • 未执行病例翻译。

这里有一个描述。在微软的网站上有一个用来展示寄存器和堆栈的例子。结果可以在这里找到。

那么设置断点的条件就很容易了:

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

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

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

<

x64解决方案/h3>

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

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

  1. 设置_write_nolock的断点

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

  2. 限制条件:

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

我们需要对esp进行强制转换和解引用的原因是$esp访问的是ESP寄存器,而实际上是void*寄存器。而这里的寄存器直接存储参数的值。因此,不再需要另一层间接性。

后我也非常喜欢这个问题,所以我把安东尼的帖子翻译成中文,并把我的答案作为补充。这篇文章可以在这里找到。谢谢@anthony-arnold的允许。

Anthony的回答非常有趣,它确实给出了一些结果。然而,我认为它可能会错过printf的缓冲。实际上,在write()和printf()的区别上,您可以读到:"printf不一定每次都调用write。相反,printf缓冲它的输出。"

STDIO WRAPPER SOLUTION

因此,我提出了另一个解决方案,即创建一个可以预加载的辅助库来包装类似printf的函数。然后,您可以在此库源代码上设置一些断点并进行反向跟踪,以获取有关正在调试的程序的信息。

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

下面是劫持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("%sn",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)。做这个工作很容易,所以我们可以这样做,把真正的工作留给v函数。要获得printf的可变参数,我们只需要使用va_start和va_end。

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

最新更新