之前一段时间在研究Linux下的虚拟内存分布,还有一些比较细节的内容吧。现在打算更加深入了解这个内容,刚好看到了这篇教程,所以打算花一周左右的时间跟着学习一下,顺便做个简陋的翻译吧。这里我们使用的C++
这是项目的源地址: 编写 Linux 调试器第一部分:设置 — Writing a Linux Debugger Part 1: Setup
准备
环境准备
要用到Linenoise
库和libelfin
库,也不知道配好没有,先这么开始吧
启动可执行文件
在我们实际开始调试任何程序之前,我们首先需要启动被调试的进程。我们使用最经典的fork/exec
来完成这个功能
1 |
|
我们使用fork
将进程分裂,如果我们在子进程中,fork
会向我们返回0。如果我们在父进程中,fork
会向我们返回当前的进程号
如果我们在子进程中,我们将我们想要调试的程序替换当前正在执行的程序,以下是我们要用到的函数:
1 | long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); |
其中pstrace
允许我们通过读取寄存器,读取内存,的那部执行等方式来对一个程序实现调试执行。我们介绍一下它的使用:
request:
指定对被跟踪进程执行的操作pid:
被跟踪的进程的进程号(PID)addr:
指定操作的内存地址data:
用于请求的附加数据- 返回值通常用于返回提供错误信息
我们以ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
的方式使用它,表示该进程应该允许父进程跟踪它。
而execl
则是众多exec
风格中的一种,用于替换当前进程的进程映像为一个新的程序,并执行它:
path:
要执行的程序的路径arg:
程序的第一个参数(通常是程序名)...:
程序的其他参数,以nullptr结尾
我们以execl(prog, prog, nullptr);
使用它,表示将当前子进程替换为指定的程序
这样我们就启动了我们的子进程,并将其替换为我们的被调试函数
调加调试器的循环
我们启动了子进程,但我们希望能够和它交互,所以我们需要创建一个debugger
类,并给它一个循环来监听用户的输入,并在我们的父进程中启动它。
1 |
|
同时在父进程中启动它:
1 | }else if(pid >= 1){ |
在我们的run
函数中,我们需要等待子进程的启动,然后使用linenoise获取输入直到EOF(ctrl+d)
1 | void debugger::run(){ |
当被跟踪的进程启动时,他将收到一个SIGYTAP
信号,这是一个跟踪或断点信号,我们使用waitpid
等待这个信号的发送
当我我们直到进程准备号被调试之后,我们监听用户的输入。linenoise
函数接受一个显示提示,并自行处理用户的输入。我们将输入交给handle_command
函数处理,然后将输入添加到lineniose
的历史输入中,并释放分配的空间资源
处理输入
我们的调试器支持的命令结构像GDB那样,我们用continue
或cont
或c
来告诉他们继续执行。还有下断点之类的操作,我们用空格来实现对他们的分割:
1 | void debugger::handle_command(const std::string& line){ |
接下来我们进一步完成handle_command
的辅助函数is_prefix
和split
函数:
1 | std::vector<std::string> debugger::split(const std::string& s,char delimiter){ |
1 | bool debugger::is_prefix(const std::string& s,const std::string& of){ |
最后我们再完成我们的continue_execution
函数:
1 | void debugger::continue_execuion(){ |
我们的continue_execution
将使用pstrace
告诉进程继续,然后使用waitpid
直到接受到信号
执行流程
到此为止,我们初步的框架就完成了,我们可以整理一下整个程序的执行流程
- 首先我们接受到两个参数
./minigdb test
- 程序运行到
fork
会创建一个新的进程,我们将父进程作为调试器进程,将子进程作为被调试的进程 - 子进程因为
ptrace(PTRACE_TRACEME,0,nullptr,nullptr);
会等待父进程发出调试信息,才开始执行 - 父进程开始执行
run()
,其中waitpid
会等待子进程的结束,但是由于此时子进程还在等待父进程的调试信息,还没有开始执行,所以父进程开始执行命令c
- 此时父进程调用了
ptrace(PTRACE_CONT, m_pid, nullptr, nullptr)
向子进程发出了继续执行的命令。且使用waitpid
等待子进程的结束,并重新回到读取输入的循环
整个过程使用ptrace
和waitpid
实现了控制父子进程的协同运行
我们可以看下程序的运行效果:

符合我们的设计,那么调试器的准备阶段就到此为止了