上次把断点基本的设置搞完了,接下来需要进一步的对寄存器进行操作。我们将添加读取和写入寄存器和内存的功能,这样我们就可以操作程序寄存器(PC),从而实现程序状态的观察和操作。
寄存器和内存
寄存器结构
我们需要建立x86_64架构寄存器的元数据,以便通过ptrace
系统调用和DWARF
调试信息于寄存器交互
首先我们需要明确我们需要用到的寄存器,我们主要使用一些比较常用的寄存器,忽略对于架构非必须的寄存器,我们先创建一个寄存器强类型枚举,用于确定我们要用到的寄存器
1 | enum class reg { |
接下来我们来建立一下寄存器的描述结构,方便我们存储信息:
1
2
3
4
5struct reg_descriptor {
reg r;
int dwarf_r;
std::string name;
};
然后我们要设置一个全局寄存器描述符数组,其元素顺序需要和ptrace
返回的寄存器结构的顺序一样,这样便于我们直接通过数组索引直接访问ptrace
返回的结构字段。

1 | const std::array<reg_descriptor,n_registers> g_register_descriptors{{ |
中间的各种数字是对应的DWARF
寄存器编号,你可以在这里找到他们psABI-x86_64.pdf
寄存器内容读取
现在我们可以尝试编写一些函数用来和寄存器交互。我们希望能够读取寄存器、向寄存器中写入数据、从DWARF
寄存器编号中检索值,以及按名称查找寄存器。总之我们先实现一个函数用来读取get_register_value
的值开始。
我们还是通过ptrace
的PTRACE_GETREGS
实现对寄存器数据的访问。我们用一个user_regs_struct
的类型来存储ptrace
的返回值。
1 | uint64_t get_register_value(pid_t pid,reg r) { |
这里我们之所以把传递的user_regs_struct
结构按uint64_t
的类型理解,因为这样是安全的,返回的user_regs_struct
在内存上是连续分布的,我们可以将它按uint64_t
的类型处理。
现在我们可以获取regs
中的任意的指定寄存器的值了。
接着我们编写set_register_value
的函数实现,和刚刚差不多的过程:
1 | void set_register_value(pid_t pid,reg r,uint64_t value){ |
然后再拓展一下,刚刚是根据枚举变量进行查找,现在我们还可以根据DWARF
值查找,或者根据寄存器名进行查找:
1 | uint64_t get_register_from_dwarf_register(pid_t pid,unsigned regnum){ |
这里中间有个辅助函数get_register_name
可加可不加。
现在我们开始获取我们寄存器中的信息了!
操作寄存器
首先我们向调试器加入一个dump_register
函数,用来获取我们的寄存器信息:
1 | void debugger::dump_register(){ |
我们修改handle_command
函数,将我们的读写寄存器的操作功能加入进去
1 | void debugger::handle_command(const std::string& line){ |
操作内存
不像寄存器那么复杂,我们直接使用ptrace
系统调用就可以进行对程序内存的读写,在这里我们将函数封装起来:
1 | uint64_t debugger::read_memory(uint64_t address){ |
这里我们使用ptrace
一次只能传递64位的信息,如果你想要传递更多的信息可以通过系统调用process_vm_readv
、
process_vm_writev
来实现这个功能,我们将内存操作添加到我们的handle_command
函数中
1 | }else if(is_prefix(command,"memory")){ |
OK !!!
退出断点(完善我们的continue_exection)
我们的程序在执行到断点之后,没办法执行下去了,这是因为我们原来在执行int 3
之后,pc计数器向后移动了1个字节,我们之前无法重新设置它,所以程序会停止在那里,但是现在可以了。我们可以通过获取并修改pc
的值,将保存的数据重新覆写,以实现程序的继续运行。我们在执行时可以检查我们的断点映射,看看我们是否处于断点。如果是,我们可以取消断点,并继续执行。
首先我们先添加几个辅助函数,将它们封装起来以提高清晰和简洁性:
1 | uint64_t debugger::get_pc(){ |
我们先设置一个操作来读取pc
的值,便于后面的操作,这里我们设置一个操作用来实现断点后的执行
1 | void debugger::step_over_breakpoint(){ |
首先我们判断有没有断点,如果有,我们就退回到下断点之前,禁用它,并单步执行原始指令。然后,再把断点重新启用(我一开始也很疑惑为啥呀重新启用呢,这里我忽略了循环的情况)
这里有个wait_for_signal
实际上这是我们对waitpid
的封装:
1 | void debugger::wait_for_signal(){ //等待子进程执行结束 |
现在我们可以重写我们的continue_execution
函数了:
1 | void debugger::continue_execution(){ |
这样很好呀
测试
试了一下断点和查看寄存和内存的功能,能正常使用,不错:
