• [Win32]一个调试器的实现(四)读取寄存器和内存


    在前几篇文章中,我实现的那个调试器只能被动接收调试事件并输出这些事件的信息。现在,我要将它修改成可以接收命令,并根据命令对被调试进程进行各种操作。首先从最基本的操作开始。

    获取寄存器的值

    每个线程都有一个上下文环境,它包含了有关线程的大部分信息,例如线程栈的地址,线程当前正在执行的指令地址等。上下文环境保存在寄存器中,系统进行线程调度的时候会发生上下文切换,实际上就是将一个线程的上下文环境保存到内存中,然后将另一个线程的上下文环境装入寄存器。

    获取某个线程的上下文环境需要使用GetThreadContext函数,该函数声明如下:

    1 BOOL WINAPI GetThreadContext(
    2     HANDLE hThread,
    3     LPCONTEXT lpContext
    4 );

    第一个参数是线程的句柄,第二个参数是指向CONTEXT结构的指针。要注意,调用该函数之前需要设置CONTEXT结构的ContextFlags字段,指明你想要获取哪部分寄存器的值。该字段的取值如下:

    CONTEXT_CONTROL

    获取EBPEIPCSEFLAGSESPSS寄存器的值。

    CONTEXT_INTEGER

    获取EAXEBXECXEDXESIEDI寄存器的值。

    CONTEXT_SEGMENTS

    获取DSESFSGS寄存器的值。

    CONTEXT_FLOATING_POINT

    获取有关浮点数寄存器的值。

    CONTEXT_DEBUG_REGISTERS

    获取DR0DR1DR2DR3DR6DR7寄存器的值。

    CONTEXT_FULL

    等于CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS

    调用GetThreadContext函数之后,CONTEXT结构相应的字段就会被赋值,此时就可以输出各个寄存器的值了。

    对于其它寄存器来说,直接输出它的值就可以了,但是EFLAGS寄存器的输出比较麻烦,因为它的每一位代表不同的含义,我们需要将这些含义也输出来。一般情况下我们只需要了解以下标志:

    标志

    含义

    CF

    0

    进位标志。无符号数发生溢出时,该标志为1,否则为0

    PF

    2

    奇偶标志。运算结果的最低字节中包含偶数个1时,该标志为1,否则为0

    AF

    4

    辅助进位标志。运算结果的最低字节的第三位向高位进位时,该标志为1,否则为0

    ZF

    6

    0标志。运算结果未0时,该标志为1,否则为0

    SF

    7

    符号标志。运算结果未负数时,该标志为1,否则为0

    DF

    10

    方向标志。该标志为1时,字符串指令每次操作后递减ESIEDI,为0时递增。

    OF

    11

    溢出标志。有符号数发生溢出时,该标志为1,否则为0

    用按位与操作就可以得知某个标志是否为1。例如,要检查OF是否为1

    1 if ((context.EFlags & 0x400!= 0) {
    2 
    3    std::wcout << TEXT("OF ");
    4 
    5 }

    十六进制数0x400只有第11位是1,其余位都是0。对于其它的标志也是用同样的方法进行判断。

    读取内存内容

    我对Windows自带的16位调试器DEBUGd命令印象很深刻,这个命令可以以十六进制和ASCII编码显示进程内存的内容,用于观察数据段的数据。现在我要在调试器中添加类似的功能。

    读取进程的内存使用ReadProcessMemory函数,该函数声明如下:

    1 BOOL WINAPI ReadProcessMemory(
    2     HANDLE hProcess,                  //进程句柄
    3     LPCVOID lpBaseAddress,            //要读取的地址
    4     LPVOID lpBuffer,                  //一个缓冲区的指针,保存读取到的内容
    5     SIZE_T nSize,                     //要读取的字节数
    6     SIZE_T* lpNumberOfBytesRead       //一个变量的指针,保存实际读取到的字节数
    7 );

    要想成功读取到进程的内存,需要两个条件:一是hProcess句柄具有PROCESS_VM_READ的权限;二是由lpBaseAddressnSize指定的内存范围必须位于用户模式地址空间内,而且是已分配的。

    对于调试器来说,第一个条件很容易满足,因为调试器对被调试进程具有完整的权限,可以对其进行任意操作。

    第二个条件意味着我们不能读取进程任意地址的内存,而是有一个限制。Windows将进程的虚拟地址空间分成了四个分区,如下表所示:(来自《Windows核心编程(第5版)》)

    分区

    地址范围

    空指针赋值分区

    0x00000000~0x0000FFFF

    用户模式分区

    0x00010000~0x7FFEFFFF

    64KB禁入分区

    0x7FFF0000~0x7FFFFFFF

    内核模式分区

    0x80000000~0xFFFFFFFF

    空指针赋值分区主要为了帮助程序员检测对空指针的访问,任何对这一分区的读取或写入操作都会引发异常。64KB禁入分区正如其名字所言,是禁止访问的,由Windows保留。内核模式分区由Windows的内核部分使用,运行于用户态的进程不能访问这一区域。进程只能访问用户模式分区的内存,对于其它分区的访问将会引发ACCESS_VIOLATION异常。

    另外,并不是用户模式分区的任意部分都可以访问。我们知道,在32位保护模式下,进程的4GB地址空间是虚拟的,在物理内存中不存在。如果要使用某一部分地址空间的话,必须先向操作系统提交申请,让操作系统为这部分地址空间分配物理内存。只有经过分配之后的地址空间才是可访问的,试图访问未分配的地址空间仍然会引发ACCESS_VIOLATION异常。

    下图是ReadProcessMemory调用成功的情况,灰色部分是用户模式地址空间中已分配的部分,虚线部分是由lpBaseAddressnSize指定的范围:

    只有虚线部分的长度小于等于灰色部分的长度时,ReadProcessMemory才会成功。

    以下的几幅图是ReadProcessMemory调用失败的情况:

    这引出了一个问题:如何处理ReadProcessMemory失败的情况?每个人的想法或许都不同,在这里我的处理方式是:对于导致ReadProcessMemory失败的字节,以“??”来表示。如下图所示:

    d 3FFFFFD0表示显示从地址0x3FFFFFD0开始的内存。由于0x40000000之前的地址空间是未分配的,所以前面的48个字节显示“??”。

    要注意的是这种处理方式是以字节为单位的,也就是说对于每个字节都要调用一次ReadProcessMemory。如果要显示128个字节,则要调用ReadProcessMemory128次。你肯定会认为这样做的话效率会很低,不过经过实际的使用情况来看,显示速度是可以接受的,与调用一次ReadProcessMemory读取所有内存的做法几乎没有差别。

    具体的做法可以参考示例代码。如果你有更好的做法,不妨一起分享一下。

    至于右边的字符显示,我只使用了ASCII字符集对字节进行解码,而没有用ANSI字符集。原因是中文字符使用两个字节表示,如果一个中文字符的第一个字节位于一行的末尾,而第二个字节位于下一行的头部,这种情况该如何处理呢?想不出好的解决方法,因此只好放弃ANSI字符集了。

    0x00~0x1F0x81~0xFF之间的ASCII字符不能显示,我分别以“.”和“?”来代替。

    示例代码

    这次MiniDebugger的结构作了很大的改进,主要为了支持与用户的交互以及以后功能的添加。要注意,该调试器只支持单线程程序,如果用它来调试多线程程序,会导致两者都陷入阻塞状态。当然,要改成支持多线程也不是很难,你可以尝试这么做。代码中都作了注释,这里就不再赘述了。下面是当前支持的命令:

    s path

    启动被调试进程,开始进行调试。如果路径中有空格,则应该用双引号括住路径。例如:s C:\windows\notepad.exe

    t

    结束被调试进程,停止调试。

    g [c]

    继续被调试进程的执行。如果不带参数c,则表示未处理异常;带参数c则表示已处理了异常。

    r

    显示被调试进程的寄存器的值。

    d [address] [length]

    显示被调试进程的内存。如果省略了length参数,则显示128个字节;如果两个参数都省略,则显示EIP地址处的128个字节。

    q

    结束调试并退出调试器。

     

    如果想让被调试进程运行到某处停下来,以便测试各种命令,可以在代码中加入__asm int 3;语句,或者抛出一个异常,让调试器捕捉到异常即可。

    https://files.cnblogs.com/zplutor/MiniDebugger4.rar


    作者:Zplutor
    出处:http://www.cnblogs.com/zplutor/
    本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    Java之Jacob调用COM接口DLL-----------------------------------dm。dll
    mac版idea报错:Information:java: javacTask: 源发行版 1.8 需要目标发行版 1.8
    Spring-data-jpa 学习笔记
    Mac下IntelliJ IDEA快捷键大全
    mac怎么快速回到桌面 隐藏所有窗口
    Idea下安装Lombok插件
    spring注解第05课 FactoryBean
    apt 软件安装问题
    常用软件和库安装
    openMP---第一篇
  • 原文地址:https://www.cnblogs.com/zplutor/p/1983010.html
Copyright © 2020-2023  润新知