当前位置: 首页 > 科技观察

开发一个Linux调试器(三):寄存器和内存

时间:2023-03-19 16:35:30 科技观察

在上一篇博文中,我们在调试器中添加了一个简单的地址断点。这一次,我们将添加读写寄存器和内存的能力,这将允许我们使用我们的程序计数器、观察状态和改变程序的行为。系列文章索引这些链接会随着后续文章的发布而逐渐生效。准备环境断点寄存器和内存Elves和dwarves源和信号源级别步进源级别断点调用堆栈展开读取变量下一步注册我们的寄存器在我们实际读取任何寄存器之前,我们需要告诉调试器一些关于我们的信息目标平台信息,这里是x8664平台。除了多组通用和专用寄存器外,x8664还提供了浮点和向量寄存器。为简单起见,我将跳过后两个寄存器,但您可以根据需要选择支持它们。x86_64还允许您像访问32、16或8位寄存器一样访问某些64位寄存器,但我只介绍64位寄存器。由于这些简化,对于每个寄存器,我们只需要它的名称、它的DWARF寄存器编号和ptrace返回结构中的内存地址。我使用范围枚举引用这些寄存器,然后我列出了一个全局寄存器描述符数组,其元素的顺序与ptrace中的寄存器结构相同。enumclassreg{rax,rbx,rcx,rdx,rdi,rsi,rbp,rsp,r8,r9,r10,r11,r12,r13,r14,r15,rip,rflags,cs,orig_rax,fs_base,gs_base,fs,gs,ss,ds,es};constexprstd::size_tn_registers=27;structreg_descriptor{regr;intdwarf_r;std::stringname;};conststd::arrayg_register_descriptors{{{reg::r15,15,"r15"},{reg::r14,14,"r14"},{reg::r13,13,"r13"},{reg::r12,12,"r12"},{reg::rbp,6,"rbp"},{reg::rbx,3,"rbx"},{reg::r11,11,"r11"},{reg::r10,10,"r10"},{reg::r9,9,"r9"},{reg::r8,8,"r8"},{reg::rax,0,"rax"},{reg::rcx,2,"rcx"},{reg::rdx,1,"rdx"},{reg::rsi,4,"rsi"},{reg::rdi,5,"rdi"},{reg::orig_rax,-1,"orig_rax"},{reg::rip,-1,"rip"},{reg::cs,51,"cs"},{reg::rflags,49,"eflags"},{reg::rsp,7,"rsp"},{reg::ss,52,"ss"},{reg::fs_base,58,"fs_base"},{reg::gs_base,59,"gs_base"},{reg::ds,53,"ds"},{reg::es,50,"es"},{reg::fs,54,"fs"},{reg::gs,55,"gs"},}};如果您想自己查看,通常可以在/usr/include/sys/user.h中找到寄存器数据结构,而DWARF寄存器编号取自SystemVx86_64ABI现在我们可以编写一堆函数来与寄存器交互。我们希望能够读取寄存器、写入数据、通过DWARF寄存器编号获取值,以及通过名称查找寄存器,反之亦然。让我们从实现get_register_value开始:uint64_tget_register_value(pid_tpid,regr){user_regs_structregs;ptrace(PTRACE_GETREGS,pid,nullptr,®s);//...}ptrace让我们很容易得到我们想要的数据。我们只需要构造一个user_regs_struct的实例并将其与PTRACE_GETREGS请求一起传递给ptrace。现在根据要请求的寄存器,我们要读取regs。我们可以写一个大的switch语句,但由于我们的g_register_descriptors表的布局顺序与user_regs_struct相同,我们只需要搜索寄存器描述符的索引,然后将user_regs_struct作为uint64_t的数组访问。(您也可以重新排序reg枚举变量并使用索引将它们转换为基础类型,但我第一次以这种方式编写它时,它起作用了,我没有费心去改变它。)autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors),[r](auto&&rd){returnrd.r==r;});return*(reinterpret_cast(®s)+(it-开始(g_register_descriptors)));到uint64_t是安全的,因为user_regs_struct是标准布局类型,但我认为指针算法在技术上是未定义的行为。目前没有编译器对此发出警告,我懒得修复它,但如果你想保持最严格的正确性,写一个大的switch语句。set_register_value非常相似,我们只是写入位置并在最后写回寄存器:(g_register_descriptors),end(g_register_descriptors),[r](auto&&rd){returnrd.r==r;});*(reinterpret_cast(®s)+(it-begin(g_register_descriptors)))=value;ptrace(PTRACE_SETREGS,pid,nullptr,®s);}下一步是通过DWARF寄存器号查找。这次我将实际检查错误情况,以防我们收到一些奇怪的DWARF消息。uint64_tget_register_value_from_dwarf_register(pid_tpid,unsignedregnum){autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors),[regnum](auto&&rd){returnrd.dwarf_r==regnum;});if(it==end(g_register_descriptors)){throwstd::out_of_range{"Unknowndwarfregister"};}returnget_register_value(pid,it->r);}差不多完成了,现在我们有寄存器名称查找:std::stringget_register_name(regr){autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors),[r](auto&&rd){returnrd.r==r;});returnit->name;}regget_register_from_name(conststd::string&name){autoit=std::find_if(开始(g_register_descriptors)),end(g_register_descriptors),[name](auto&&rd){returnrd.name==name;});returnit->r;}最后我们将添加一个简单的辅助函数来导出所有寄存器的内容:voiddebugger::dump_registers(){for(constauto&rd:g_register_descriptors){std::cout</mem而不是ptrace。现在我们将向我们的UI添加命令:elseif(is_prefix(command,"memory")){std::stringaddr{args[2],2};//assume0xADDRESSif(is_prefix(args[1],"read")){std::cout<:400936:55pushrbp400937:4889e5movrbp,rsp:40093be350a4000movesi,0x400a3540093f:bf60106000movedi,0x601060400944:e8d7feffffcall400820<_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>400949:b800000000moveax,0x040094e:5dpoprbp40094f:c3retYouwanttomovetheprogramcounterbackto0x40093asothattheesiandediregistersaresetcorrectly.在下一篇博客中,我们将首先了解DWARF信息并向我们的调试器添加一系列逐步功能。之后,我们就有了一个功能工具,可以单步执行代码、在需要的地方设置断点、修改数据等等。一如既往,如果您有任何问题,请发表评论!您可以在此处找到这篇博文的代码。