咱们在调试器中参加了简略的地址断点。这一次,咱们将给调试器参加读写寄存器和内存的功用,这样就能够在操控RIP,调查程序的状况,以及改动程序的行为了。
注册咱们的寄存器
在咱们正真的读取寄存器前,调试器需求知道一些关于x8664架构的相关常识。包含通用寄存器,专用寄存器以及浮点寄存器和向量寄存器。为了简略期间,我将省掉后两者(浮点以及向量寄存器),当然假如你喜爱的话你能够挑选去参加相关支撑。x86_64架构也答应你用32,16或许8位的方法来拜访64位寄存器,可是我将会一向运用64位的。因为简化了一些东西,所以对寄存器来说,咱们只需求知道它的姓名以及它在DWARF中的寄存器号,以及它被存储在ptrace回来的结构中什么方位就能够了。我挑选用一个枚举来引证寄存器,然后来构建一个和ptrace中的寄存器结构次序相同的大局寄存器描述符数组。
enum class reg { 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};constexpr std::size_t n_registers = 27;struct reg_descriptor { reg r; int dwarf_r; std::string name;};const std::array g_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_t get_register_value(pid_t pid, reg r) { user_regs_struct regs; 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。
auto it = std::find_if(begin(g_register_descriptors), end(g_register_descriptors), [r](auto&& rd) { return rd.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也是相同的,我仅仅是写到相应方位,然后在最终写回寄存器:
void set_register_value(pid_t pid, reg r, uint64_t value) { user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, nullptr, ®s); auto it = std::find_if(begin(g_register_descriptors), end(g_register_descriptors), [r](auto&& rd) { return rd.r == r; }); *(reinterpret_cast(®s) + (it – begin(g_register_descriptors))) = value; ptrace(PTRACE_SETREGS, pid, nullptr, ®s);}
接下来便是经过DWARF寄存器号来查找相应的值了。这一次我会查看一个过错条件,以防万得到一些古怪的DWARF信息:
uint64_t get_register_value_from_dwarf_register (pid_t pid, unsigned regnum) { auto it = std::find_if(begin(g_register_descriptors), end(g_register_descriptors), [regnum](auto&& rd) { return rd.dwarf_r == regnum; }); if (it == end(g_register_descriptors)) { throw std::out_of_range{“Unknown dwarf register”}; } return get_register_value(pid, it->r);}
差不多完成了,现在咱们就有了下边看起来这样的寄存器值了:
std::string get_register_name(reg r) { auto it = std::find_if(begin(g_register_descriptors), end(g_register_descriptors), [r](auto&& rd) { return rd.r == r; }); return it->name;}reg get_register_from_name(const std::string& name) { auto it = std::find_if(begin(g_register_descriptors), end(g_register_descriptors), [name](auto&& rd) { return rd.name == name; }); return it->r;}
最终,加一些简略的辅佐函数来dump寄存器的内容:
void debugger::dump_registers() { for (const auto& rd : g_register_descriptors) { std::cout << rd.name << " 0x" << std::setfill('0') << std::setw(16) << std::hex << get_register_value(m_pid, rd.r) << std::endl; }}
如你所见,iostreams有一个十分简练的接口,能够很好地输出十六进制数据。假如你喜爱,能够封装一些IO操作来防止紊乱。
这些就满足支撑咱们在调试器其它部分处理寄存器了,现在,能够将其增加到UI中去了。
操作寄存器
咱们需求做的便是将一个新的指令参加到handle_command函数中。鄙人边的代码示意中,用户能够经过输入register read rax或许register write rax 0x42以及其他的指令来操作寄存器。
else if (is_prefix(command, “register”)) { if (is_prefix(args[1], “dump”)) { dump_registers(); } else if (is_prefix(args[1], “read”)) { std::cout << get_register_value(m_pid, get_register_from_name(args[2])) << std::endl; } else if (is_prefix(args[1], "write")) { std::string val {args[3], 2}; //assume 0xVAL set_register_value(m_pid, get_register_from_name(args[2]), std::stol(val, 0, 16)); } }
思路
在设置断点时,咱们现已读取和写入内存,所以只需求增加一些函数来封装一下ptrace调用。
uint64_t debugger::read_memory(uint64_t address) { return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr);}void debugger::write_memory(uint64_t address, uint64_t value) { ptrace(PTRACE_POKEDATA, m_pid, address, value);}
你或许期望一次增加对读取和写入大于WORD(16位)型数据的支撑,只需经过在每非必须读取另一个WORD时递加地址即可。一起也能够运用process_vm_readv和process_vm_writev或许运用/proc//mem来代替ptrace。
现在,为咱们的UI参加相关指令:
else if(is_prefix(command, “memory”)) { std::string addr {args[2], 2}; //assume 0xADDRESS if (is_prefix(args[1], “read”)) { std::cout << std::hex << read_memory(std::stol(addr, 0, 16)) << std::endl; } if (is_prefix(args[1], "write")) { std::string val {args[3], 2}; //assume 0xVAL write_memory(std::stol(addr, 0, 16), std::stol(val, 0, 16)); } }
修正continue_execuTIon
110/5000
您是不是要找: Before we test out our changes, we’re now in a posiTIon to implement a more sane version of conTInue execuTIon)
在测验更改之前,咱们现在能够履行一个愈加正确的版别的continue_execution。因为能够获取RIP,所以只需查看咱们的断点保存结构来确认是否运转到了一个断点的方位。假如是,先制止断点然后在持续运转前步过一次。
首要,为了明晰简练,先增加几个辅佐函数:
uint64_t debugger::get_pc() { return get_register_value(m_pid, reg::rip);}void debugger::set_pc(uint64_t pc) { set_register_value(m_pid, reg::rip, pc);}
然后,能够写一个步过断点的函数:
void debugger::step_over_breakpoint() { // – 1 because execution will go past the breakpoint auto possible_breakpoint_location = get_pc() – 1; if (m_breakpoints.count(possible_breakpoint_location)) { auto& bp = m_breakpoints[possible_breakpoint_location]; if (bp.is_enabled()) { auto previous_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形式:
void debugger::wait_for_signal() { int wait_status; auto options = 0; waitpid(m_pid, &wait_status, options);}
最终,从头写的continue_execution就像这样:
void debugger::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: 55 push rbp 400937: 48 89 e5 mov rbp,rsp 40093a: be 35 0a 40 00 mov esi,0x400a35 40093f: bf 60 10 60 00 mov edi,0x601060 400944: e8 d7 fe ff ff call 400820 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 400949: b8 00 00 00 00 mov eax,0x0 40094e: 5d pop rbp 40094f: c3
你需求将RIP移回到0x40093a,以便对esi和edi进行正确的赋值。
鄙人一篇文章中,咱们将会初次探究一下DWARF信息,以及向调试器参加几种单步操作。之后,咱们将有一个具有大部分功用的东西,能够经过代码来单步,设置断点到想要的当地去,修正数据以及更多功用。有问题,虽然在回复区发问!