调试器工作原理:获取和设置x86寄存器

2020-10-24 07:31:15

在本文中,我将简要描述用于在32位和64位x86CPU上转储和恢复不同类型寄存器的方法。第一部分将重点介绍通用寄存器、调试寄存器和浮点寄存器,直至SSE扩展提供的XMM寄存器。我将解释如何通过ptrace(2)接口获取它们的值。

Ptrace(2)API通常在所有现代BSD系统和Linux中使用,因为它们都是从4.3BSD中设计和实现的原始表单中派生出来的。本文主要关注FreeBSD和NetBSD系统。尽管如此,其他操作系统(如OpenBSD、蜻蜓BSD或Linux)的用户仍然可以从本文中获益,因为基本原理是相同的,并且代码示例旨在很容易地适用于其他平台。

单个CPU(在现代硬件中:如果超线程可用,则为CPU核心或CPU线程)一次只能执行一个程序线程。为了能够同时运行多个进程和线程,操作系统必须执行上下文切换,即周期性地挂起当前运行的线程,保存其状态,恢复另一个线程保存的状态并恢复它,保存和恢复处理器寄存器的值在上下文切换中起着重要的作用。重要的是,该进程对于要切换的进程是完全透明的,并且在正确实现的内核中,应该没有程序可以察觉到的副作用。

由于多种原因,调试器可能需要检查被调试程序的寄存器集。通过检查程序计数器,它能够确定源代码中将继续执行的位置,并通过更改它可以控制执行。堆栈指针是内省存储在堆栈上的变量所必需的,而其余的寄存器本身可以保存变量。

一组特殊的x86寄存器是调试寄存器。它们不能由程序本身访问;但是,它们可以由调试器读取或写入。它们允许在正在执行的代码上设置硬件辅助断点(指令执行陷阱),并在变量上设置观察点(读和/或写操作陷阱)。

术语“通用寄存器”有点含糊,从狭义上讲,它指的是少数几个(i386上8个,AMD64上16个)可用于存储任意数据(通常是整数或指针)的基线寄存器。在更广泛的意义上,它意味着处理器体系结构中的所有基线寄存器,历史上不包括浮点寄存器和特殊类型的寄存器。在x86上,这包括“狭义”通用寄存器、程序计数器(EIP/RIP)、段寄存器和标志寄存器。

可以直接复制大多数通用寄存器,例如使用MOV指令,或通过推送将其推送到堆栈上。可以使用LEA指令复制EIP/RIP寄存器,并通过JMP恢复。可以通过PUSHFD/PUSHFQ将标志寄存器推送到堆栈,然后通过POPFD/POPFQ从堆栈中弹出标志寄存器。

下面的清单演示了一个程序,该程序在执行过程中在任意点捕获allamd64GPRS的值,并在从汇编返回后打印这些值。

#include<;stdio.h>;#include<;stdint.h>;enum{R_Rax,R_RBX,R_RCX,R_RDX,R_RSI,R_RDI,R_RBP,R_RSP,R_R8,R_R9,R_R10,R_R11,R_R12,R_R13,R_R14,R_R15,R_RIP,R_RFLAGS,R_Length};enum{S_CS,S_DS,S_ES,S_FS,S_GS,S_SS,S_Length};Int main(){uint64_t GPR[R_LENGTH];uint16_t seg[S_LENGTH];ASM易失性(/*用随机数据填充寄存器*/";mov$0x0102030405060708,%%rax\n\t";";mov$0x1112131415161718,%%rbx\n\t";";mov$0x2122232425262728,%%rcx\n\t";";mov$0x3132333435373738,%%rdx\n\t";";mov$0x2122232425262728,%%rcx\n\t";";mov$0x3132333435373738,%rdx\n\t";MOV$0x4142434445464748,%%RSI\n\t";";mov$0x5152535455565758,%%RDI\n\t";/*RBP用于帧指针,RSP是堆栈指针*/";mov$0x8182838485868788,%%R8\n\t";";mov$0x9192939495969798,%%R9\n\t";";mov$0xa1a1a2a3a4a5a6a7a8,%%Rb1b2b3b4b5b6b7b8,%%R11\t";";mov$0x9192939495969798,%%R9\n\t";";mov$0xa1a2a2a4a5a6a7a8,%%Rb1b2b3b4b5b6b7b8,%r11\t";";mov$0x9192939495969798,%%R9\n\t";"。";mov$0xc1c2c3c4c5c6c7c8,%%r12\n\t";";mov$0xd1d2d3d4d5d5d6d7d8,%%r13\n\t";";mov$0xe1e2e3e4e5e6e7e8,%r14\n\t";";mov$0xf1f2f3f4f5f6f7f8,%%R15\n\t";/*GPRS*/&34;mov%%rax,%[rax]\n\t&34;";MOV%%RBX,%[RBX]\n\t";";mov%%RCX,%[RCX]\n\t";";mov%%RDX,%[RDX]\n\t";";mov%%RSI,%[RSI]\n\t";";mov%RDI,%[RDI]\n\t";";MOV%%RBP,%[RBP]\n\t";";mov%%RSP,%[RSP]\n\t";";mov%%R8,%[R8]\n\t";";mov%%R9,%[R9]\n\t";";mov%R10,%[R10]\n\t";";MOV%%R11,%[R11]\n\t";";mov%%R12,%[R12]\n\t";";mov%%R13,%[R13]\n\t";";mov%%R14,%[R14]\n\t";";mov%R15,%[R15]\n\t";/*转储RIP*/#34;LEA(%%rip),%%rbx\n\t";";mov%%rbx,%[rip]\n\t";";mov%[rbx],%%rbx\n\t";/*转储段寄存器*/";mov%%cs,%[cs]\n\t";";mov%%ds,%[ds]\n\t";";Mov%%es,%[es]\n\t";";mov%%fs,%[fs]\n\t";";mov%%gs,%[gs]\n\t";";mov%%ss,%[ss]\n\t";/*dump RFLAGS*/";push fq\n\t";";popq%[rflag]\n\t";:[RAX]";=m";(GPR[R_RAX]),[RBX]";=m";(GPR[R_RBX]),[RCX]";=m";(GPR[R_RCX]),[RDX]";=m";(GPR[R_RDX]),[RSI]";=m";(GPR[R_RSI]),[RDI]";=m";(GPR[R_RDI]),[RBP]";=m";(GPR[R_RBP]),[RSP]";=m";(GPR[R_RSP]),[R8]";=m";(GPR[R_R8]),[R9]";=m";(GPR[R_R9]),[R10]";=m";(GPR[R_R10]),[R11]";=m";(GPR[R_R11]),[R12]";=m";(GPR[R_R12]),[R13]";=m";(GPR[R_R13]),[R14]";=m";(GPR[R_R14]),[R15]";=m";(GPR[R_R15]),[RIP]";=m";(GPR[R_RIP]),[rflag]";=m";(GPR[R_RFLAGS]),[cs]";=m";(seg[S_CS]),[ds]";=m";(seg[S_DS]),[ES]";=m";(seg[S_ES]),[fs]";=m";(seg[S_FS]),[gs]";=m";(seg[S_GS]),[ss]";=m";(seg[S_SS])::";%rax";,";%rbx";,";%rcx";,";%rdx";;,";%RSI";,";%RDI";,";%R8";,";%R9";,";%R10";,";%R11";,";%R12";,";%R13";,";%R14";,";%R15";,";Memory";);printf(";rax=0x%016lx\n";,GPR[R_RAX]);printf(";rbx=0x%016lx\n";,GPR[R_RBX]);printf(";rcx=0x%016lx\n";,GPR[R_RCX]);printf(";RDX=0x%016lx\n&34;,GPR[R_RDX]);printf(";RSI=0x%016lx\n&34;,GPR[R_RSI]);printf(";RDI=0x%016lx\n";,GPR[R_RDI]);printf(";RBP=0x%016lx\n";,GPR[R_RBP]);printf(";RSP=0x%016lx\n";,GPR[R_RSP]);printf(";R8=0x%016lx\n";GPR[R_R8]);printf(#34;R9=0x%016lx\n&34;#34;R8=0x%016lx\n&34;);printf(";R8=0x%016lx\n";);printf(#&34;R9=0x%016lx\n&34;,GPR[R_R9]);printf(";R10=0x%016lx\n";,GPR[R_R10]);printf(";R11=0x%016lx\n";,GPR[R_R11]);printf(";r12=0x%016lx\n";,GPR[R_R12]);printf(";R13=0x%016lx\n";GPR[R_R13]);printf(#34;R14=0x%016lx\n&34;#34;R13=0x%016lx\n";);printf(";R13=0x%016lx\n");printf(#&34;R14=0x%016lx\n&34;,GPR[R_R14]);printf(";R15=0x%016lx\n";,GPR[R_R15]);printf(";RIP=0x%016lx\n";,GPR[R_RIP]);printf(";cs=0x%04x\n";,seg[S_CS]);printf(";ds=0x%04x\n";,seg[S_DS]);printf(";es=0x%04x\n";,seg[S_ES]);printf(";fs=0x%04x\n";,seg[S_FS]);printf(";gs=0x%04x\n";,seg[S_GS]);printf(";ss=0x%04x\n";,seg[S_SS]);printf(";rflag=0x%016lx\n&34;,gpr[R_RFLAGS]);Return 0;}。

FreeBSD和NetBSD都使用PT_GETREGS请求从程序中获取GPRS的值,并使用PT_SETREGS更新它们。请求将指向struct reg的指针作为参数。

在FreeBSD上,i386和AMD64都将单独的寄存器列表作为结构的字段。在NetBSD上,i386使用常规结构,而AMD64将所有值放入一个数组中,该数组的索引在头文件中定义为常量。

下面的清单比较了FreeBSD和NetBSD上使用的结构。请注意,NetBSD/AMD64使用特殊的宏。例如,Greg(RDI RDI,0)定义_REG_RDI。

/*FreeBSD/i386*//*NetBSD/i386*/struct__reg32{struct reg{__uint32_t r_fs;int r_eax;__uint32_t r_es;int r_ecx;__uint32_t r_ds;int r_edX;__uint32_t r_edi;int r_ebx;__uint32_t r_esi;int r_esp;_uint32_t r_ebp;int r_ep;__uint32_t r_isp;int r_ESI;__uint32_t r_ebx;int r_edi;__uint32_t

可以使用FSAVE指令转储这些寄存器的内容,并使用FRSTOR指令恢复这些寄存器的内容。该指令指向108字节的内存缓冲区,存储控制寄存器和ST(I)寄存器的当前值,并重置FPU。

FSAVE助记符隐式插入额外的FWAIT指令,以确保FPU完成处理之前的操作。如果您希望在异常处理过程中捕获FPU状态,则应改为使用FNSAVE,因为它无需等待即可捕获直接的FPU状态。(=。

作为MMX指令集的一部分引入的64位MM i寄存器与ST(I)寄存器重叠。因此,不需要新的转储指令,如果使用MM i寄存器,它们将作为FSAVE中ST(I)的一部分转储。

SSE寄存器组引入了8个新的128位寄存器XMM i和一个控制MXCSR寄存器。同时介绍了一种新的倾倒功能FXSAVE及其恢复功能FXRSTOR。它们使用512字节的内存缓冲区对齐在16字节边界上,布局与FSAVE不同。

FSAVE和FXSAVE之间的明显区别在于后者保存SSE寄存器。在i386上,存储寄存器XMM0..XMM7,缓冲区的剩余部分保留/未使用。在AMD64上,保留空间的大部分用于存储XMM8..XMM15。

另一个经常被忽略的区别是,FTW状态寄存器由FSAVE以其完整形式存储,而由FXSAVE以其简化形式存储。前者指示每个ST(I)寄存器包含的值类型-空、零、归一化数字和特殊,后者仅指示寄存器是否为空。

要访问进一步的处理器扩展(如AVX)引入的寄存器,需要使用XSAVE指令。与前面描述的这些不同,它被设计为可扩展的。XSAVE是一个广泛的话题,它将是本文第二部分的主题。

FXSAVE/FXRSTOR指令的传统变体将FIP(导致异常的指令)和FDP(其操作数)指针存储为成对的16位段寄存器(分别为FCS、FD)和32位地址寄存器(FIP、FDP)。这对于AMD64程序来说是一个问题,因为原始的64位指针被截断为32位。

为解决此问题,提供了附加助记符FXSAVE64/FXRSTOR64。它们在相应的指令前面加上REX.W=1前缀,将FIP和FDP字段改为使用64位指针。它们的缺点是不再报告该段;但是,较新的AMD64处理器无论如何都不再支持FCS/FD。