C调试器是如何工作的?

2020-10-18 11:21:10

当您使用gdb时,您可以看到它完全控制您的 申请流程。在按住Ctrl-C组合键的同时 应用程序正在运行,进程执行停止,并且gdb显示 其当前位置、堆栈跟踪等。

让我们先从它的工作原理说起。它不能模拟 通过读取和解释二进制指令来执行。它 可以,这将会起作用(valgrind内存调试器的方式 工作),但那太慢了。Valgrind会降低应用程序的速度 降低1000倍,gdb就不会了。这也是虚拟机喜欢的方式。 QEMU工作。

另一种猜测?...?黑客攻击!是的,那里有很多这样的东西,再加上 来自操作系统内核的帮助。

首先,关于Linux进程,有一件事需要知道: 父进程可以获取有关其 儿童,特别是追踪他们的能力。而且,您可以 我猜,调试器是被调试进程的父进程(或它 成为,进程可以在Linux中领养子进程:-)。

Linux ptrace API 允许(调试器)进程访问有关 另一个进程(被调试进程)。具体地说,调试器可以:

收到系统事件通知:PTRACE_O_TRACEEXEC, Ptrace_O_TRACECLONE、ptrace_O_EXITKILL、ptrace_syscall(您 可以识别exec syscall、克隆、退出和所有其他操作 系统调用)。

Ptrace实现不在本文的讨论范围内,但是我 我不想把黑匣子移到上面一步,所以让我解释一下 快速了解它的工作方式(我不是内核专家,如果我不是内核专家,请纠正我 错了,如果我简化得太多,请原谅:-)。

Ptrace 是Linux内核的一部分, 因此它可以访问所有关于 过程:

存取 CPU寄存器?轻松使用 Copy_regset_to/from_user。(在那里 这里并不复杂,因为CPU寄存器保存在 Linux';s Struct task_struct* 调度器结构,当进程未调度时。

一步一个脚印? 设置正确的标志 (手臂, X86) 在任务结构上,并且在触发执行之前,在 处理器。

Ptrace也是挂钩的(搜索函数 Ptrace_event) 在许多调度操作中,以便它可以发送 SIGTRAP信号 如果需要,将其添加到调试器(ptrace_O_TRACEEXEC选项及其 家庭)。

上面的解释针对的是Linux本机调试,但它是有效的 对于大多数其他环境。要获得GDB要求的线索,请执行以下操作 它的目标不同,你可以看看它的运作 目标堆栈。

在此目标界面中,您可以看到所有高级操作 C调试所需:

结构target_ops { 将target_ops*struct;/*构造到此函数下的目标。*/ Const char*TO_SHORTNAME;/*命名该目标类型*/ Const char*to_long name;/*打印名称*/ Const char*to_doc;/*文档。不包括尾随 换行符,并以一行描述开始- (可能类似于TO_LONGNAME)。*/ Void(*to_Attach)(struct target_ops*ops,const char*,int); Void(*to_fetch_registers)(struct target_ops*,struct regcache*,int); Void(*to_store_registers)(struct target_ops*,struct regcache*,int); INT(*TO_INSERT_BREAKPOINT)(struct target_ops*,struct gdbarch*, Struct BP_target_info*); INT(*TO_INSERT_WATCHPOINT)(struct target_ops*, CORE_ADDR,int,int,struct表达式*); ..。 }。

Gdb的泛型部分调用这些函数,而特定于目标的 部件实现它们。它(在概念上)形状为堆栈,或 金字塔:堆栈的顶部非常通用,例如:

这个 远距 Target很有趣,因为它将执行堆栈拆分为两个 ";计算机";,通过通信协议(TCP/IP、串行端口)。

远程部分可以是在另一个Linux机器上运行的gdbserver。但 它还可以是硬件调试端口(JTAG)的接口或 虚拟机管理程序(例如 QEMU),这将起到 内核+ptrace。不是查询操作系统内核结构,而是 远程调试器存根将查询虚拟机管理程序结构,或直接查询 处理器的硬件寄存器。

为了进一步了解这个远程协议,Embecosm写了一个 有关不同消息的详细指南。Gdbserver事件处理循环 有没有,还有 QEMU gdb-服务器存根 也在网上。

我们在这里可以看到,所有需要的低级机制 实现一个调试器,由此ptrace API提供:

但这就是调试器所做的全部工作吗?不,那只是非常低的水平 零件..。它还处理符号处理。这是两者之间的联系 二进制代码和程序源代码。还有一样东西还没有找到, 也许最重要的一点是:断点!我将首先解释一下如何 断点的工作原理是非常有趣和棘手的,然后我会来的。 回到符号管理上来。

正如我们在上面看到的,断点不是ptrace API的一部分 服务。但我们可以更改内存,并接收调试对象的 信号。你看不到链接吗?那是因为断点 实现起来相当棘手和繁琐!让我们研究一下如何设置一个 给定地址处的断点:

调试器读取(Ptrace Peek)存储在 该地址,并将其保存在其数据结构中。

它在此位置写入无效指令。这到底是什么 指令,它只需要是无效的。

当被调试者达到此无效指令时(或,放入更多 正确地说,处理器、利用被调试存储器上下文设置)、 它将无法执行它(因为它无效)。

在现代多任务操作系统中,无效指令不会使 整个系统,但它通过以下方式将控制权交还给操作系统内核 引起中断(或故障)。

该中断由Linux翻译成SIGTRAP信号, 并传送到过程中..。或作为调试器添加到它的父级 我要的是。

调试器获取有关信号的信息,并检查 被调试对象的指令指针的值(即,陷阱 发生)。如果IP地址在其断点列表中,这意味着 它是调试器断点(否则,它是进程中的错误, 只需传递信号并让它崩溃)。

既然被调试对象在断点处停止,调试器 可以让它的用户做任何他/她想做的事情,直到到了继续的时候 行刑。

要继续,调试器需要1/写入正确的指令 回到被调试者的内存中,2/单步执行(继续 一条CPU指令的执行,带有ptrace单步)和3/ 将无效指令写回(以便可以停止执行 下次再来一次)。和4/,让执行正常进行。

很整洁,不是吗?顺便说一句,您可以注意到这个算法 如果不是所有线程都同时停止,则不会工作 (因为当有效的 说明已就位)。我不会详细说明GDB的人是如何解决这个问题的, 但这篇论文对此进行了详细的讨论: GDB中的不间断多线程调试。放 简而言之,它们将指令写入内存中的其他位置,将 指向该位置指令指针,并单步执行 处理器。但问题是,有些指令是 与地址相关,例如跳转和条件跳转...。

现在,让我们回到符号和调试信息处理上来 纵横比。我没有详细研究这一部分,所以我将只介绍一个 概述。

首先,我们可以在没有调试信息和符号的情况下进行调试吗 地址呢?答案是肯定的,正如我们在上面看到的,所有的 低级命令处理CPU寄存器和内存地址,以及 不是源代码级别的信息。因此,与源代码的链接是 这只是为了方便用户。如果没有调试信息,您将看到 您的应用程序以处理器(和内核)的方式看待它: 二进制(汇编)指令和内存位。GDB不需要任何 将二进制数据转换为CPU指令的更多信息:

(Gdb)x/10x$pc#十六进制表示法 0x402c60:0x56415741 0x54415541 0x55f48949 0x4853fd89 0x402c70:0x03a8ec81 0x8b480000 0x8b48643e 0x00282504 0x402c80:0x89480000 0x03982484 (Gdb)x/10i$pc#指令表示法 =>;0x402c60:推送%r15 0x402c62:推送%r14 0x402c64:推送%r13 0x402c66:推送%r12 0x402c68:MOV%RSI,%r12 0x402c6b:推送%RBP 0x402c6c:MOV%edi,%ebp 0x402c6e:推送%rbx 0x402c6f:SUB$0x3a8,%rsp 0x402c76:MOV(%RSI),%RDI。

Gdb还将能够显示堆栈跟踪(稍后将详细介绍), 但兴趣有限:

(Gdb)其中 #0写入() #1 0x0000003d492769e3 in_IO_new_file_write() NEW_DO_WRITE()中的#2 0x0000003d49277e4c #3_IO_NEW_DO_WRITE() #4 0x0000003d49278223 in_IO_new_file_overflow() PRINT_CURRENT_FILES()中的#5 0x00000000004085bb Main()中的#6 0x000000000040431b。

我们已经有了PC地址和相应的函数,但那是 它。在函数内部,您需要在汇编语言中进行调试!

现在让我们添加调试信息:这就是侏儒标准,GCC 选择。我对这个标准不是很熟悉,但我知道 提供:

尝试使用dwarfdump查看嵌入您的信息 二进制文件。Addr2line还使用以下信息:

$dwarfdump/usr/lib/debug/usr/bin/ls.debug|grep 402ce4 0x00402ce4[1289,0]ns $addr2line-e/usr/lib/debug/usr/bin/ls.debug 0x00402ce4 /usr/src/debug/coreutils-8.21/src/ls.c:1289。

许多源代码级调试命令将依赖于这些信息, 与命令NEXT一样,该命令在 下一行是打印命令,它依赖于类型来显示 正确类型的变量(char、int、浮点数,而不是 二进制/十六进制!)。

我们已经看到了调试器内部的许多方面,所以我只想说一个 最后几点的几句话:

堆栈跟踪是从当前帧($sp和 $BP/#FP)向上,一次一帧。函数名称, 在调试信息中可以找到参数和局部变量。

观察点在(如果可用)的帮助下实现 处理器:在其寄存器中写入哪些地址应该 被监视,并且它将在读取内存或读取内存时引发异常 写好了。如果此支持不可用,或者如果您请求更多支持 超过处理器支持的观察点...。然后调试器将失败 回到手工制作的观察点:执行应用程序指令 指令,并检查当前操作是否触及 有手表的地址。是的,那真是太慢了!

也可以通过这种方式进行反向调试,记录 每条指令,并将其向后应用以进行反向执行。

条件断点是正常断点,除了 在内部,调试器在给出 控件交给用户。如果条件不匹配,则执行 默默地继续着。

并使用gdb gdb,或者更好(实际上要好得多)gdb--pid$(Pidof Gdb),因为同一终端中的两个调试器太疯狂了:-)。另一件值得学习的事情是系统调试: