0%

一个简单的linux调试器-part-1

记录调试器的学习,既然是自己的读书笔记,我就记录自己不会的和想弄懂的部分,大牛勿喷

原文地址:https://blog.tartanllama.xyz/writing-a-linux-debugger-setup/

前提准备

两个github项目的导入
Linenoise 用来处理命令行输入相关的部分,包括自动补全,历史记录等等
libelfin 处理调试符号部分所需头文件

正式开始

一个调试器的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Program name not specified";
return -1;
}

auto prog = argv[1];

auto pid = fork();
if (pid == 0) {
//we're in the child process
//execute debugee

}
else if (pid >= 1) {
//we're in the parent process
//execute debugger
}
}

先说题外,关键字auto用于声明变量的生存期为自动,所有的变量默认就是auto的。没有任何意义
进入正题,先是一个fork函数。
先看fork函数,其原型pid_t fork( void)
大概就是,进程通过fork函数获得一个新进程,返回两次,一次返回到父进程,还有一次子进程
两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
接下来的if-else很明显是分别处理子父进程的

既然写的是调试器,那么父进程是属于调试器,那么子进程就必须加载被调试进程
这个时候

1
2
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
execl(prog, prog, 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class debugger {
public:
debugger (std::string prog_name, pid_t pid)
: m_prog_name{std::move(prog_name)}, m_pid{pid} {}

void run(){
int wait_status;
auto options = 0;
waitpid(m_pid, &wait_status, options);

char* line = nullptr;
while((line = linenoise("minidbg> ")) != nullptr) {
handle_command(line);
linenoiseHistoryAdd(line);
linenoiseFree(line);
}
}

private:
std::string m_prog_name;
pid_t m_pid;
};

首先,当被调试的进程执行后,它将发出一个SIGTRAP信号,用来表示被跟踪或遇到断点,而waitpid函数就是用来等待这个信号的,waitpid具体分析见深入浅出—unix多进程编程之wait()和waitpid()函数
我们主要关注的是这个循环,首先这个循环是一直等待用户输入直到输入EOF,方才退出循环的。
再看循环内部,首先一个handle_command函数,毫无疑问是根据我们约定的符号进入对应的命令处理的,包括单步、断点等等,这个函数具体代码如下

1
2
3
4
5
6
7
8
9
10
void debugger::handle_command(const std::string& line) {
auto args = split(line,' ');
auto command = args[0];
if (is_prefix(command, "continue")) {
continue_execution();
}
else {
std::cerr << "Unknown command\n";
}
}

而后就用到了我们之前用的两个支持库之一用来将命令加入历史,并释放资源

处理输入

最后,这一部分,处理问题,实质上就是处理输入的字符串,不同的字符串对应不同命令处理流程,作者为了接近与gdb和lldb用了类似的字符串, 其处理函数handle_command 上面已给出
我们来看看这个部分函数,split函数和is_prefix函数就不具体分析,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::vector<std::string> split(const std::string &s, char delimiter) {
std::vector<std::string> out{};
std::stringstream ss {s};
std::string item;

while (std::getline(ss,item,delimiter)) {
out.push_back(item);
}

return out;
}

bool is_prefix(const std::string& s, const std::string& of) {
if (s.size() > of.size()) return false;
return std::equal(s.begin(), s.end(), of.begin());
}

我们具体看看continue_execution函数,先放代码

1
2
3
4
5
6
7
void debugger::continue_execution() {
ptrace(PTRACE_CONT, m_pid, nullptr, nullptr);

int wait_status;
auto options = 0;
waitpid(m_pid, &wait_status, options);
}

由于是对子进程的操作,那么有用到了我们的ptrace函数,这里第一个request参数用的是PTRACE_CONT,用来继续子进程,接下来的waitpid函数又是用来等待信号的.

至此,第一部分分析完毕,在下分析浅薄,如有不对或者不够好的地方,望指摘。