在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
+strstr
condition
这个方法的酷之处在于它不依赖于使用的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
上。
指出:
$bpnum
thanks to Tromey at: https://sourceware.org/bugzilla/show_bug.cgi?id=18727rdi
:包含x86_64 Linux系统调用号的寄存器,1
用于write
strstr
:标准C函数调用,搜索子匹配项,如果未找到返回NULL
rsi
:系统调用的第一个参数,对于write
,它指向缓冲区在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
使条带输出RIPsetarch -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一起发布的,所以我们可以在源代码中找到最终调用WriteFile
API并在那里设置断点的地方。在对样例代码进行调试后,我发现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上的write
API相比:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
它们有完全相同的参数。因此,我们可以在_write_nolock
中设置condition breakpoint
,只是参考上面的解决方案,只有一些细节上的差异。
适用于Win32和x64的便携式解决方案
很幸运,我们可以直接使用参数名在Win32和x64上为断点设置条件时。所以写条件就变得很容易了:
在
_write_nolock
中添加断点注意:在Win32和x64上差别不大。我们可以只使用函数名来设置Win32上断点的位置。但是,它不能在x64上工作,因为在函数的入口,参数没有初始化。因此,我们不能使用参数名来设置断点的条件。
但幸运的是,我们有一些解决办法:使用函数中的位置而不是函数名来设置断点,例如,函数的第一行。这里的参数已经初始化了。(我的意思是使用filename+line number
来设置断点,或者直接打开文件并在函数中设置断点,而不是入口,而是第一行。)限制条件:
fh == 1 && strstr((char *)buf, "Hello World") != 0
注意这里仍然有一个问题,我测试了两种不同的方式来写入标准输出:printf
和std::cout
。printf
会一次将所有字符串写入_write_nolock
函数。然而,std::cout
只逐个字符地传递给_write_nolock
,这意味着API将被调用strlen("your string")
次。在这种情况下,条件不可能永远被激活。
Win32解决方案/h3>
当然,我们可以使用Anthony
提供的相同方法:通过寄存器设置断点的条件。
对于Win32程序,解决方案与Linux上的GDB
几乎相同。您可能会注意到在_write_nolock
的原型中有一个装饰__cdecl
。这个调用约定意味着:
- 参数传递顺序为从右到左。
- 调用函数从栈中弹出参数。
- 名称修饰约定:在名称前加下划线(_)。
- 未执行病例翻译。
这里有一个描述。在微软的网站上有一个用来展示寄存器和堆栈的例子。结果可以在这里找到。
那么设置断点的条件就很容易了:
- 设置
_write_nolock
的断点 限制条件:
*(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
方法与Linux相同。第一个条件是确保将字符串写入stdout
。第二个是匹配指定的字符串。
x64解决方案/h3>
从x86到x64的两个重要修改是64位寻址能力和一组通用的16位寄存器。随着寄存器的增加,x64只使用__fastcall
作为调用约定。前四个整数参数在寄存器中传递。参数5及以上在堆栈上传递。
你可以参考微软网站上的参数传递页面。四个寄存器(从左到右顺序)分别是RCX
、RDX
、R8
和R9
。所以很容易限制条件:
设置
_write_nolock
的断点注意它与上面的可移植解决方案不同,我们可以将断点的位置设置为函数而不是函数的第一行。原因是所有的寄存器已经在入口处初始化了。
限制条件:
$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函数来输出内容。