记录调试器的学习,既然是自己的读书笔记,我就记录自己不会的和想弄懂的部分,大牛勿喷
原文地址:https://blog.tartanllama.xyz/writing-a-linux-debugger-setup/
前提准备
两个github项目的导入
Linenoise 用来处理命令行输入相关的部分,包括自动补全,历史记录等等
libelfin 处理调试符号部分所需头文件
正式开始
一个调试器的框架:
1 | int main(int argc, char* argv[]) { |
先说题外,关键字auto用于声明变量的生存期为自动,所有的变量默认就是auto的。没有任何意义
进入正题,先是一个fork函数。
先看fork函数,其原型pid_t fork( void)
大概就是,进程通过fork函数获得一个新进程,返回两次,一次返回到父进程,还有一次子进程
两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
接下来的if-else很明显是分别处理子父进程的
既然写的是调试器,那么父进程是属于调试器,那么子进程就必须加载被调试进程
这个时候
1 | ptrace(PTRACE_TRACEME, 0, nullptr, nullptr); |
我们查看一下ptrace函数的原型,
1 | long ptrace(int request, pid_t pid, void * addr, void * data) |
ptrace 百度百科的简单解释为进程跟踪,可以用来跟踪进程,可以用来跟踪子进程的运行,包括读取子进程寄存器的值,对子进程执行单步功能;
具体执行行为取决于ptrace函数的第一个参数request,经常用的有PTRACE_ME 这个参数通常由子进程调用,用来告诉父进程:来跟踪我吧。。。,这个时候其他参数是无所谓的了
再有PTRACE_PEEKUSER参数,可以用来读取寄存器的值,因为发生系统调用后,计算机会将系统调用号保留到寄存器eax中;而这个参数可以用来读取这个系统调用号的。等等
更多参见
ptrace运行原理及使用详解
Linux源码分析之Ptrace
pid参数,则是指被跟踪进程的进程ID,addr参数则是一个内存地址,data参数则是指某些特殊request的resources
而execl函数则是用来运行指定程序的,由第一个参数和第二个参数指定具体程序
这样就实现了进程附载
这样就实现了子进程的部分,接下来来看父进程部分
如作者所写,为了与子进程进行通信交互;作者建了一个debugger类,并且添加了一个类函数run用来循环监听用户输入,以便由用户进行单步、下断点等操作。并且通过父进程在main函数中执行这个类函数。debugger类的大体代码如下
1 | class debugger { |
首先,当被调试的进程执行后,它将发出一个SIGTRAP
信号,用来表示被跟踪或遇到断点,而waitpid
函数就是用来等待这个信号的,waitpid
具体分析见深入浅出—unix多进程编程之wait()和waitpid()函数
我们主要关注的是这个循环,首先这个循环是一直等待用户输入直到输入EOF,方才退出循环的。
再看循环内部,首先一个handle_command函数,毫无疑问是根据我们约定的符号进入对应的命令处理的,包括单步、断点等等,这个函数具体代码如下
1 | void debugger::handle_command(const std::string& line) { |
而后就用到了我们之前用的两个支持库之一用来将命令加入历史,并释放资源
处理输入
最后,这一部分,处理问题,实质上就是处理输入的字符串,不同的字符串对应不同命令处理流程,作者为了接近与gdb和lldb用了类似的字符串, 其处理函数handle_command 上面已给出
我们来看看这个部分函数,split函数和is_prefix函数就不具体分析,代码如下
1 | std::vector<std::string> split(const std::string &s, char delimiter) { |
我们具体看看continue_execution
函数,先放代码
1 | void debugger::continue_execution() { |
由于是对子进程的操作,那么有用到了我们的ptrace函数,这里第一个request参数用的是PTRACE_CONT
,用来继续子进程,接下来的waitpid函数又是用来等待信号的.
至此,第一部分分析完毕,在下分析浅薄,如有不对或者不够好的地方,望指摘。