开发一个Linux调试器就必须要知道寄存器和内存!

我们在调试器中加入了简单的地址断点。这一次,我们将给调试器加入读写寄存器和内存的功能,这样就可以在控制rip,观察程序的状态,以及改变程序的行为了。
注册我们的寄存器
在我们正真的读取寄存器前,调试器需要知道一些关于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寄存器号是根据system v x86_64 abi这个规范来设置的。
现在,就可以写一大堆函数来与寄存器交互了。我们希望能够通过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 case结构,但是由于我们已经构建了g_register_descriptors这个表,表中的寄存器顺序和user_regs_struct完全一致,于是就可以通过索引来查找寄存器描述符,并且以uint64_t数组的方式来访问user_regs_struct。
autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors),[r](auto&&rd){returnrd.r==r;});//译注:此处是lambda表达式return*(reinterpret_cast(®s)+(it-begin(g_register_descriptors)));
转换到uint_64_t是安全的,因为user_regs_struct是标准的布局类型,但是我认为指针在算数运算上是unsigned byte(译注:实际上是signed byte,参考内核地址高20(intel架构)位全被置1)。现有编译器甚至对此没有警告,我比较懒,也不想多花心思了,但是如果你想保持最大可能的正确性就需要一个大的switch case了。
set_register_value也是一样的,我仅仅是写到相应位置,然后在最后写回寄存器:
voidset_register_value(pid_tpid,regr,uint64_tvalue){user_regs_structregs;ptrace(ptrace_getregs,pid,nullptr,®s);autoit=std::find_if(begin(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(begin(g_register_descriptors),end(g_register_descriptors),[name](auto&&rd){returnrd.name==name;});returnit->r;}
最后,加一些简单的辅助函数来dump寄存器的内容:
voiddebugger::dump_registers(){for(constauto&rd:g_register_descriptors){std::cout< 如你所见,iostreams有一个非常简洁的接口,可以很好地输出十六进制数据。如果你喜欢,可以封装一些io操作来避免混乱。
这些就足够支持我们在调试器其它部分处理寄存器了,现在,可以将其添加到ui中去了。
操作寄存器
我们需要做的就是将一个新的命令加入到handle_command函数中。在下边的代码示意中,用户可以通过输入register read rax或者register write rax 0x42以及其他的命令来操纵寄存器。
elseif(is_prefix(command,register)){if(is_prefix(args[1],dump)){dump_registers();}elseif(is_prefix(args[1],read)){std::cout< 思路
在设置断点时,我们已经读取和写入内存,所以只需要添加一些函数来封装一下ptrace调用。
uint64_tdebugger::read_memory(uint64_taddress){returnptrace(ptrace_peekdata,m_pid,address,nullptr);}voiddebugger::write_memory(uint64_taddress,uint64_tvalue){ptrace(ptrace_pokedata,m_pid,address,value);}
你可能希望一次添加对读取和写入大于word(16位)型数据的支持,只需通过在每次要读取另一个word时递增地址即可。同时也可以使用process_vm_readv和process_vm_writev或者使用/proc//mem来替代ptrace。
现在,为我们的ui加入相关命令:
elseif(is_prefix(command,memory)){std::stringaddr{args[2],2};//assume0xaddressif(is_prefix(args[1],read)){std::cout< 修复continue_execution
110/5000
您是不是要找:before we test out our changes, we’re now in a position to implement a more sane version ofcontinue execution)
在测试更改之前,我们现在可以执行一个更加正确的版本的continue_execution。因为可以获取rip,所以只需检查我们的断点保存结构来确定是否运行到了一个断点的位置。如果是,先禁止断点然后在继续运行前步过一次。
首先,为了清晰简洁,先添加几个辅助函数:
uint64_tdebugger::get_pc(){returnget_register_value(m_pid,reg::rip);}voiddebugger::set_pc(uint64_tpc){set_register_value(m_pid,reg::rip,pc);}
然后,可以写一个步过断点的函数:
voiddebugger::step_over_breakpoint(){//-1becauseexecutionwillgopastthebreakpointautopossible_breakpoint_location=get_pc()-1;if(m_breakpoints.count(possible_breakpoint_location)){auto&bp=m_breakpoints[possible_breakpoint_location];if(bp.is_enabled()){autoprevious_instruction_address=possible_breakpoint_location;set_pc(previous_instruction_address);bp.disable();ptrace(ptrace_singlestep,m_pid,nullptr,nullptr);wait_for_signal();bp.enable();}}}
首先,检查此刻rip所处的位置是不是被设置了断点,如果是,将rip后退一个字节(译注:0xcc断点触发时0xcc本身已经被执行过了,所以停下的位置和下断点的位置差了一个字节,需要将rip回拨一个字节),禁用断点(译注:将原始的指令数据写回来),单步步过此处原来的指令,然后重新设置断点(译注:再将0xcc写回去)r
wait_for_signal函数将封装一些常用的waitpid模式:
voiddebugger::wait_for_signal(){intwait_status;autooptions=0;waitpid(m_pid,&wait_status,options);}
最后,重新写的continue_execution就像这样:
voiddebugger::continue_execution(){step_over_breakpoint();ptrace(ptrace_cont,m_pid,nullptr,nullptr);wait_for_signal();}
测试
现在我们可以读取和修改寄存器,hello world程序于是就可以有一些乐子了。首先来测试一下在call指令上下断点,然后从断点处继续运行吧。应该可以看见hello world已经被输出。乐子来了,在输出的那个call后边下一个断点,继续运行,然后将设置调用参数的代码的地址写入rip并继续。你应该可以看见由于rip被改变hello world被输出了两次。以防你不知道在哪里设置断点,下边我给出我的objdump:
0000000000400936
:400936:55pushrbp400937:4889e5movrbp,rsp40093a:be350a4000movesi,0x400a3540093f:bf60106000movedi,0x601060400944:e8d7feffffcall400820400949:b800000000moveax,0x040094e:5dpoprbp40094f:c3
你需要将rip移回到0x40093a,以便对esi和edi进行正确的赋值。
在下一篇文章中,我们将会首次探索一下dwarf信息,以及向调试器加入几种单步操作。之后,我们将有一个具备大部分功能的工具,可以通过代码来单步,设置断点到想要的地方去,修改数据以及更多功能。有问题,尽管在回复区提问!

LwIP中TCP协议是如何实现的
罗镇球:4nm芯片也在路上,预计明年正式批量生产4nm
土壤肥料检测仪的功能特点
探讨中国地面数字电视的现状与发展
Waymo或将在密歇根州建造自动驾驶汽车工厂 计划在2021年底前投入运营
开发一个Linux调试器就必须要知道寄存器和内存!
选择智能门锁需要擦亮眼,如果选错了,过两年你还得换
GB/T 4802.4乱翻式起毛起球测试仪的特性是怎样的
贴片电阻的主要优点
SafeRobot CRR模式设置
Avago展示QSFP+ AOC与收发器解决方案
国产品牌16TB SSD杀来!1899元?
微软15英寸Surface Laptop 3曝光,将搭载i5处理器
两款低压DC/DC升压转换器应用电路
Mini LED背光正式开启“暴增”模式
无线充电技术为何能像Wi-Fi一样变得无处不在
智能电网的三阶段
为车载网络保驾护航
BTL功率放大器的特点
应持续关注超千亿规模的北斗导航产业