模仿GDB的 C/C++ Mini Debugger
Windows上还没有解决相关路径的问题, 搞完相关的再弄吧 Windows上相关原理还没搞清楚
- vscode
- cmake
- clang/clangd
需要的两个库
- 在Windows上
- 目前还没有
- 在linux上
这里是待补充的,项目中的文件信息。
这里的知识点我写到了我的知乎上了,可以看一下: gdb调试的原理,收获的赞还不少捏(
调试器最重要的原理就是ptrace这个系统调用函数,
基本上依赖于这个函数ptrace,
long ptrace(request, pid_t pid, void *addr, void *data)- request: 宏定义,比如说
PTRACE_TRACEME,PTRACE_PEEKDATA等,表示一个动作 - pid: 要调试的进程号
- addr: 要写入的地址
- data: 写入地址的话 data就是数据,读出地址的话,就会赋值给data。
- request: 宏定义,比如说
可以接管一个子程序的执行,跟踪一个程序,Linux上的程序执行差不多是这个样子:父进程fork()一遍一个几乎一摸一样的自己,然后excel()一个新进程,这时候,就会将fork出来的进程替换成要执行的进程,使用ptrace可以跟踪这个新产生的进程,然后进行调试,代码如下
#include <sys/ptrace.h>
#include <unistd.h>
#include <sched.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
return -1;
}
std::string program_name { argv[1] };
pid_t pid = fork();
// 子进程
if (pid == 0) {
// 设置跟踪状态
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
execl(program_name.c_str(), program_name.c_str(), nullptr);
}
// 父进程
if (pid > 0) {
// 进行调试等操作
}
}ptrace()函数调用可以对函数的汇编代码进行一定的修改,在x86上有一条汇编指令,叫做 INT 3(这里的INT 不是 int,而是interrupt的缩写),当出现这个指令的时候,程序就会发生中断,也就是trap了,然后父进程就可以捕获这个信号,子进程就会中断在那里,等待下一步的命令,也就是说当gdb进行打断点的时候,就是将一条汇编代码修改一下,将其修改成INT 3的机器码,而INT 3的机器码是多少呢?是0xcc, 所以说当打断点的时候,就会将原来的汇编代码对应的机器码中的机器码指令换成0xcc,当程序执行到一步的时候,就会触发断点,等到调试,那如何跳出这个断点?,将要被替换的机器码指令保存下来就可以了,等到执行的时候将其重新替换上去就可以了(相当于恢复现场)。
回复中断的时候,就是要将这个0xcc取消掉,将原来的数值放上去,然后继续执行。ptrace一次是四个字节的修改,但是0xcc是一个字节,(八位),你就需要修改四字节的低位进行处理,然后替换的时候就是将其替换到低位(也就是末位),替换到低位是因为大部分机器是小端存储,等到一执行到这里的时候就会立马发生中断,等到恢复的时候,不仅要恢复现场(将修改的数值修改到原来的位置上),并且要将寄存器的PC指针 - 1(pc寄存器向上递增的,这里要将寄存器的数值 -1 就是0xcc之前的那个位置,然后继续执行,执行到下一个断点)
打断点的本质上是在机器码上进行打断点的,那么如何知道机器码和你写的代码之间的关系呢,有一个调试信息文件,也就是dwarf文件,下文中提到了。可以通过objdump这个工具反汇编得到的信息和dwarf文件上的信息对比一下,刚好是对应上的)函数名所在的机器码指令在哪,都可以找到,所以说在gdb上就可以直接进行在函数名上打断点了。
打断点的地方可以是地址,所以说你需要知道C变量中的变量的地址,所以就需要一定的调试信息 -> elf文件(编译和链接可执行文件)和Dwarf文件(调试信息文件
- 变量存放在在哪?
- 可以在全局数据区,在栈内存上,寄存器上也有可能(立即数),具有相同名称的变量在不同的词法作用域中可能有不同的值,设置编译器可以直接将常数进行优化
- 所以要全部捕捉他们呢。
- DIE中含有很多信息:
- DW_TAG_subprogram: 用来描述函数(也称为子程序,main函数,或者其他函数不都是函数么,所以从中可以知道很多信息
- DW_AT_name : (...): main, 很显然,是这个名字,
- DW_AT_low_pc: 函数的寄存器的入口
- DW_AT_location: 变量在给定时刻在内存中的位置
- .debug_line: 行号和机械码之间的映射
系统调用的时候,自动会进行压栈出栈,系统调用栈大部分组成是:
High
| ... |
+---------+
+24| Arg 1 |
+---------+
+16| Arg 2 |
+---------+
+ 8| Return |
+---------+
EBP+--> |Saved EBP|
+---------+
- 8| Var 1 |
+---------+
ESP+--> | Var 2 |
+---------+
| ... |
Low
- 通过使用链表的形式,进行上下连接,(居然先是局部变量,然后才是参数列表)
- 栈指针是保存在
%rbp指针上的
- DIE: 调试信息表项 (Debugging Information Entry),每个DIE有一个标签 ——> 包含它的类型,以及一组属性。各个DIE之间通过兄弟和孩子结点互相链接,属性值可以指向其他的DIE。
三种类型:错误异常,陷阱异常,中止异常
产生原因:硬件异常,软件异常
- 异常:
- 程序发生异常,Windows捕获异常,这时候就要转到内核态上执行
- Windows上检查是否有相关软件在调试这个异常,如果是,那么就会发送
EXCEPTION_DEBUG_EVENT调试事件给调试器,如果 没有收到这个事件,那么就会调到第四步 - 如果在调试这个事件的时候,第三个参数是
DBG_CONTINUE,那么说明调试器已经处理了这个异常,程序继续执行,异常结束。如果第三个参数是DBG_EXCEPTION_NOT_HANDLED,那么调试器没有处理此异常,第四步 - Windows转到用户态执行,寻找可以处理该异常的异常处理器,如果有,那么进入异常处理器中执行,根据执行的结果继续程序的执行,如果没有,转到第五步
- Windows又转到内核态中执行,检查异常的程序是否被调用,如果是,则再次发送
EXCEPTION_DEBUG_EVENT调试事件给调试器,如果还是寄了,那么调到第七步 - 第二次处理异常,没有处理的话,调到第七步
- 程序错误,寄了