调试器如何工作

Posted wuhui_gdnt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了调试器如何工作相关的知识,希望对你有一定的参考价值。

调试器如何工作:第三部分——调试信息

原作者:Eli Bendersky

http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information

这是关于调试器如何工作系列文章的第三部分。在这之前确保你读过第一、第二部分。

在这部分

我将解释调试器如何明白,在它跋涉机器代码里,在哪里找到C函数与变量,以及它用来在C源代码与机器语言内存字间进行映射的数据。

调试信息

现代编译器在翻译高级语言代码方面做得相当好。其良好缩进及嵌套的控制结构以及任意类型的变量被翻译为一大堆称为机器码的比特,主要目的是在目标CPU上运行得尽可能地快。大多数C代码行被翻译为多条机器代码指令。变量则被到处乱塞——进入栈,进入寄存器,或完全被优化掉。结构体与对象在结果代码里甚至不存在——它们只是一个抽象,被翻译为到内存缓存的写死的偏移。

因此在你要求调试器在某些函数的入口暂停时,它如何知道哪里暂停?在你向它要求一个变量值时,它如何设法确定给你显示什么?答案是——调试信息。

调试信息连同机器代码一起由编译器产生。它代表了可执行程序与源代码的联系。这个信息以一个预定义的格式编码并连同机器码一起保存。多年来为不同的平台及可执行文件发展了许多这样的格式。因为本文的目标不是调查这些格式的历史,而是展示它们如何工作,我们必须解决一些问题。这个问题将是DWARF,在今天它几乎一统在Linux及其他类Unix平台上ELF可执行文件的调试信息。

ELF中的DWARF


根据其维基网页,DWARF连同ELF一起设计,虽然在理论上它也可以嵌入到其他目标文件格式[1]。DWARF是一个复杂的格式,基于之前用于各种架构及操作系统格式的多年经验。它不得不复杂,因为它解决了一个非常棘手的问题——向调试器展示来自高级语言的调试信息,提供对任意平台及ABI的支持。这不是这篇粗陋的文章能完全解释的,并且说实话我对它角角落落的理解不足以支撑这样的尝试[2]。在本文里我将采取更多动手实践的做法,只显示足够的DWARF以解释实践中调试信息如何工作。

ELF文件里的调试节

首先看一下ELF文件中DWARF信息放在哪里。在每个目标文件里ELF可以任意定义节。节头表定义了存在哪些节以及它们的名字。不同的工具以特殊的方式处理各种节——例如链接器查找某些节,调试器查找另一些。

我们将使用从这个C源代码构建的可执行程序作为本文中的例子,它被编译为tracedprog2:

#include <stdio.h>

 

voiddo_stuff(int my_arg)

{

    int my_local = my_arg + 2;

    int i;

 

    for (i = 0; i < my_local;++i)

        printf("i = %d\\n",i);

}

 

intmain()

{

    do_stuff(2);

    return0;

}

以上是关于调试器如何工作的主要内容,如果未能解决你的问题,请参考以下文章

函数指针:*(void**) (&fun) = dlsym(lib, "fun") 它是如何工作的?

调试器如何工作

调试器如何工作

调试器如何工作?

在 dart 中调试时如何访问 Future 值?

从 unique_ptr<T> 的 void* 转换为 T** 是如何工作的?