终于来到了本系列的最后一篇!这一次,我将对调试中的一些更高级的概念进行高级概述:远程调试、共享库支持、表达式求值和多线程支持。这些想法实施起来很复杂,所以我不会详细说明如何实施,但如果您有任何疑问,我很乐意回答有关这些概念的问题。系列索引准备环境断点寄存器和内存ELF和DWARF源代码和信号源代码级步进源代码级断点堆栈展开处理变量高级主题远程调试远程调试对于调试嵌入式系统或不同环境非常有用。它还在高级调试器操作和与操作系统和硬件的交互之间设置了一条细线。事实上,像GDB和LLDB这样的调试器即使在调试本地程序时也可以作为远程调试器运行。总体架构是这样的:debugarch调试器是我们通过命令行与之交互的组件。也许如果您使用的是IDE,则在其之上还有另一层通过机器接口与调试器通信。在目标机器(可能与本地机器相同)上有一个调试存根,理论上它是操作系统调试库的一个非常小的包装器,它执行所有低级调试任务,如在地址上设置断点。我说“理论上”是因为现在调试存根越来越大。例如,我机器上的LLDB调试存根大小为7.6MB。调试存根通过远程协议使用一些特定于操作系统的功能(在我们的例子中是ptrace)与被调试进程和调试器通信。最常见的远程调试协议是GDB远程协议。这是一种基于文本的数据包格式,用于在调试器和调试存根之间传递命令和信息。我不会详细介绍它,但您可以在此处进一步阅读。如果您启动LLDB并执行命令logenablegdb-remotepackets,那么您将获得通过远程协议发送的所有数据包的踪迹。在GDB上,您可以使用setremotelogfile执行相同的操作。作为一个简单的例子,这里是设置了断点的数据包:$Z0,400570,1#43$标记数据包的开始。Z0是插入内存断点的命令。400570和1是参数,前者是设置断点的地址,后者是目标特定的断点类型说明符。最后,#43是校验和以确保数据未损坏。GDB远程协议很容易通过自定义包进行扩展,这对于实现特定于平台或语言的功能非常有用。共享库和动态加载支持调试器需要知道被调试的程序加载了哪些共享库,以便它可以设置断点、获取源代码级信息和符号等。除了查找动态链接库之外,调试器还必须跟踪在运行时通过dlopen加载的库。为此,动态链接器维护一个连接结构。该结构维护一个共享库描述符的链表,以及一个指向函数的指针,每当链表更新时调用该函数。该结构存储在ELF文件的.dynamic部分,并在程序执行前进行初始化。一个简单的跟踪算法:跟踪器在ELF标头中查找程序的条目(或者可以使用存储在/proc//aux中的辅助向量)。跟踪器在程序入口处设置断点并开始执行。到达断点后,通过查找ELF文件中.dynamic的加载地址,找到junction结构的地址。检查交集结构以获取当前加载的库列表。链接器在更新函数上设置断点。只要到达断点,列表就会更新。跟踪器无限循环,继续执行程序并等待信号,直到跟踪器信号退出。我为这些概念写了一个小例子,你可以在这里找到。如果有人有兴趣,我可以在将来写得更详细一些。表达式求值表达式求值是一种程序功能,允许用户在调试程序时对原始源语言中的表达式求值。例如,在LLDB或GDB中,您可以执行printfoo()来调用foo函数并打印结果。根据表达式的复杂程度,有几种不同的计算方法。如果表达式只是一个简单的标识符,那么调试器可以查看调试信息、找到变量并打印出值,就像我们在本系列的最后一部分中所做的那样。如果表达式有些复杂,代码可以编译成中间表达式(IR)并解释以获得结果。例如,对于某些表达式,LLDB会使用Clang将表达式编译成LLVMIR并进行解释。如果表达式比较复杂,或者需要调用某些函数,则可能需要将代码JIT到目标并在调试对象的地址空间中执行。这涉及调用mmap来分配一些可执行内存,然后将编译后的代码复制到该块中并执行它。LLDB通过使用LLVM的JIT特性来做到这一点。如果您想了解有关JIT编译的更多信息,我强烈推荐EliBendersky关于该主题的文章。多线程调试支持本系列中展示的调试器仅支持单线程应用程序,但为了调试大多数实际程序,非常需要多线程支持。支持这一点的最简单方法是跟踪线程创建,并解析procfs以获取您需要的信息。Linux线程库称为pthreads。当调用pthread_create时,该库使用clone系统调用创建一个新线程,我们可以使用ptrace对其进行跟踪(假设您的内核早于2.5.46)。为此,您需要在连接到调试器后设置一些ptrace选项:ptrace(PTRACE_SETOPTIONS,m_pid,nullptr,PTRACE_O_TRACECLONE);现在,当调用克隆时,该进程将收到我们的老朋友SIGTRAP信号。对于本系列的调试器,你可以在handle_sigtrap中添加一个例子来处理新线程的创建:;//handlecreation//...收到后,您可以查看/proc//task/并查看内存映射等以获取所需的所有信息。GDB使用libthread_db,它提供了一堆辅助函数,所以你不需要自己解析和处理它。设置这个库很奇怪,我不会在这里展示它是如何工作的,但如果你想使用它,你可以去阅读本教程。多线程支持最复杂的部分是调试器中线程状态的建模,特别是当您的计算涉及多个CPU时,如果您想要支持不间断模式或某种异构调试。最后!吼!这个系列写的时间比较长,但是一路上学到了很多东西,希望对大家有帮助。如果您对调试或本系列中的任何内容有疑问,请通过Twitter@TartanLlama或在评论中与我联系。如果您还想看到任何其他调试主题,请告诉我,我可能会发布另一篇文章。