在 Linux 中,使用 C/C++ 代码,使用 gdb,如何添加 gdb 断点来扫描传入的字符串以中断特定字符串?
我无权访问特定库的代码,但我想在该库将特定字符串发送到标准输出时立即中断,以便我可以返回堆栈并调查调用该库的代码部分。当然,我不想等到缓冲区刷新发生。这可以做到吗?也许是例行公事libstdc++
?
在 Linux 中,使用 C/C++ 代码,使用 gdb,如何添加 gdb 断点来扫描传入的字符串以中断特定字符串?
我无权访问特定库的代码,但我想在该库将特定字符串发送到标准输出时立即中断,以便我可以返回堆栈并调查调用该库的代码部分。当然,我不想等到缓冲区刷新发生。这可以做到吗?也许是例行公事libstdc++
?
这个问题可能是一个很好的起点:如何在 gdb 中的“某些内容被打印到终端”上设置断点?
因此,只要将某些内容写入标准输出,您至少可以中断。该方法主要涉及在write
系统调用上设置断点,条件是第一个参数是1
(即 STDOUT)。在评论中,还有关于如何检查write
调用的字符串参数的提示。
我想出了以下内容并使用 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 模式下运行,则参数将通过暂存器%rdi
和%rsi
$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0
请注意,由于我们使用的是暂存寄存器而不是堆栈上的变量,因此删除了一级间接。
strcmp
上述代码段中可以使用的函数除外:
编辑:我喜欢这个问题并找到它的后续答案。我决定写一篇关于它的博客文章。
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=18727rdi
: 包含 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。
解释:
-i
使 strace 输出 RIPsetarch -R
personality
通过系统调用为进程禁用 ASLR : How to debug with strace -i when everytime address is different GDB默认情况下已经这样做了,所以不需要再做一次。安东尼的回答很棒。按照他的回答,我在Windows(x86-64 位 Windows)上尝试了另一种解决方案。我知道这里的这个问题是针对Linux 上的GDB的,但是,我认为这个解决方案是对这类问题的补充。它可能对其他人有帮助。
在 Linux 中,对 的调用printf
将导致对 API 的调用write
。而且因为 Linux 是一个开源操作系统,我们可以在 API 中进行调试。但是,Windows 上的 API 有所不同,它提供了自己的 API WriteFile。由于 Windows 是商业非开源操作系统,因此无法在 API 中添加断点。
但是VC的一些源代码是和Visual Studio一起发布的,所以我们可以在源代码中找到最终调用WriteFile
API的地方,并在那里设置断点。在对示例代码进行调试后,我发现该printf
方法可能会导致调用_write_nolock
in which WriteFile
is 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
)
与write
Linux 上的 API 相比:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
它们具有完全相同的参数。所以我们可以参考上面的解决方案来设置a condition breakpoint
,_write_nolock
只是在细节上有一些区别。
很幸运,我们可以直接在 Visual Studio 上使用参数名称来设置 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")
次数。在这种情况下,条件不能永远激活。
当然我们可以使用提供的相同方法Anthony
:通过寄存器设置断点的条件。
GDB
对于 Win32 程序,解决方案与在 Linux 上几乎相同。您可能会注意到__cdecl
的原型中有一个装饰_write_nolock
。这个调用约定意味着:
这里有说明。并且有一个示例用于显示 Microsoft 网站上的寄存器和堆栈。结果可以在这里找到。
然后很容易设置断点的条件:
_write_nolock
。限制条件:
*(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
它与 Linux 上的方法相同。第一个条件是确保将字符串写入stdout
. 第二种是匹配指定的字符串。
从 x86 到 x64 的两个重要修改是 64 位寻址能力和一组通用的 16 个 64 位寄存器。随着寄存器的增加,x64 只__fastcall
用作调用约定。前四个整数参数在寄存器中传递。参数 5 和更高的参数在堆栈上传递。
您可以参考微软网站上的参数传递页面。四个寄存器(按从左到右的顺序)是RCX
、RDX
和。所以很容易限制条件:R8
R9
中设置断点_write_nolock
。
注意:它与上面的可移植解决方案不同,我们可以将断点的位置设置为函数而不是函数的第一行。原因是所有的寄存器都已经在入口处初始化了。
限制条件:
$rcx == 1 && strstr((char *)$rdx, "Hello") != 0
我们需要强制转换和取消引用的原因esp
是$esp
访问ESP
寄存器,并且出于所有意图和目的是一个void*
. 而这里的寄存器直接存储参数的值。因此不再需要另一个级别的间接。
我也很喜欢这个问题,所以我把安东尼的帖子翻译成中文,把我的答案放在里面作为补充。帖子可以在这里找到。感谢@anthony-arnold 的许可。
安东尼的回答非常有趣,它肯定会给出一些结果。然而,我认为它可能会错过 printf 的缓冲。事实上,关于write() 和 printf() 之间的差异,您可以读到:“printf 不一定每次都调用 write。相反,printf 缓冲它的输出。”
因此,我提出了另一种解决方案,该解决方案包括创建一个帮助程序库,您可以预先加载该程序库以包装类似 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 函数来输出内容。