上一篇文章介绍了调试符号以及DbgHelp的加载和清理,这回我们使用它来实现一个显示源代码的功能。该功能的实际使用效果如下图所示:

该功能不仅仅是显示源代码,还要显示每一行代码对应的地址。实现该功能大概需要进行以下的步骤:

①获取下一条要执行的指令的地址。

②通过调试符号获取该地址对应哪个源文件的哪一行。

③对于其它的行,通过调试符号获取它对应的地址。

第一步可以通过获取EIP寄存器的值来完成,相关的内容已经在第四篇文章中进行了讲解,这里不再重复。下面讲一下如何实现第二个和第三个步骤。

获取源文件以及行号

在调试符号中,记录了每一行源代码对应的地址。通过DbgHelp的SymGetLineFromAddr64函数可以由地址获取源文件路径以及行号。该函数的声明如下:

1 BOOL WINAPI SymGetLineFromAddr64(
2     HANDLE hProcess,
3     DWORD64 dwAddr,
4     PDWORD pdwDisplacement,
5     PIMAGEHLP_LINE64 Line
6 );

hProcess参数是符号处理器的标识符,dwAddr是指令的地址。pdwDisplacement是一个输出参数,用于获取dwAddr相对于它所在行的起始地址的偏移量,以字节为单位。之所以需要这么一个参数,是因为一行代码可能对应多条汇编指令,有了它就可以知道下一条要执行的指令位于这一行代码的哪个位置。例如,int b = 3 * a + a;这行代码对应以下的汇编指令:

1 8B 45 F8    mov    eax,dword ptr [a] 
2 6B C0 03    imul   eax,eax,3 
3 03 45 F8    add    eax,dword ptr [a] 
4 89 45 EC    mov    dword ptr [b],eax 

如果分别以这四条指令的地址调用SymGetLineFromAddr64函数,那么通过pdwDisplacement返回的值分别是0,3,6和9。

第四个参数是指向IMAGEHLP_LINE64结构体的指针,该结构体用来保存有关于行的信息,其声明如下:

1 typedef struct _IMAGEHLP_LINE64 {
2     DWORD SizeOfStruct;
3     PVOID Key;
4     DWORD LineNumber;
5     PTSTR FileName;
6     DWORD64 Address;
7 } IMAGEHLP_LINE64, *PIMAGEHLP_LINE64;

SizeOfStruct字段保存结构体的大小,在调用SymGetLineFromAddr64之前需要初始化这个字段,否则函数调用会失败。Key字段是由操作系统保留的,我们不需要使用它。FileName和LineNumber字段分别是源文件的绝对路径以及行号。Address是该行的起始地址。

要注意,FileName字段是一个指向字符串的指针,而这个字符串的存储空间并不需要我们自己分配,我们也不需要释放这个指针指向的内存。实际上这个指针指向了调试符号内的某个地方,我们可以读取这些数据,但是不能修改其中的数据,一旦这些数据被修改,其它的DbgHelp函数可能会出现奇怪的问题。如果一定要修改这个字符串,要先将它复制到另一个地方再进行操作。我很奇怪为什么这个字段不是PCTSTR类型的,这样的话就不必担心这个字符串被修改了。

调用SymGetLineFromAddr64成功的条件有两个:一是dwAddr的值所在的模块已经通过SymLoadModule64函数加载到符号处理器中;二是该模块含有SymGetLineFromAddr64所需的调试符号信息。如果第一个条件没有满足,GetLastError返回126;如果第二个条件没有满足,GetLastError返回487。

下面是调用SymGetLineFromAddr64的一个例子:

 1 //获取EIP
 2 CONTEXT context;
 3 GetDebuggeeContext(&context);
 4 
 5 //获取源文件以及行信息
 6 IMAGEHLP_LINE64 lineInfo = { 0 };
 7 lineInfo.SizeOfStruct = sizeof(lineInfo);
 8 DWORD displacement = 0;
 9 
10 if (SymGetLineFromAddr64(
11     GetDebuggeeHandle(),
12     context.Eip,
13     &displacement,
14     &lineInfo) == FALSE) {
15 
16     DWORD errorCode = GetLastError();
17         
18     switch (errorCode) {
19 
20         // 126 表示还没有通过SymLoadModule64加载模块信息
21         case 126:
22             std::wcout << TEXT("Debug info in current module has not loaded.") << std::endl;
23             return;
24 
25         // 487 表示模块没有调试符号
26         case 487:
27             std::wcout << TEXT("No debug info in current module.") << std::endl;
28             return;
29 
30         default:
31             std::wcout << TEXT("SymGetLineFromAddr64 failed: ") << errorCode << std::endl;
32             return;
33     }
34 }

获取行的地址

通过SymGetLineFromAddr64可以获取指令对应的源文件以及行号,那么能不能根据源文件路径以及行号获取行的地址呢?当然可以,SymGetLineFromName64函数就是用作此目的的。该函数的声明如下:

1 BOOL WINAPI SymGetLineFromName64(
2     HANDLE hProcess,
3     PCTSTR ModuleName,
4     PCTSTR FileName,
5     DWORD dwLineNumber,
6     PLONG lpDisplacement,
7     PIMAGEHLP_LINE64 Line
8 );

该函数与SymGetLineFromAddr64很相似,都是通过IMAGEHLP_LINE64结构体来返回行的信息,并且都有一个displacement输出参数,不过这个参数在两个函数中的意义大不相同,下面将会详述。首先来看一下其它参数的含义。

ModuleName用于指定模块的名称,上一篇文章讲解SymLoadModule64函数时提到的ModuleName参数就可以用在这个地方(奇怪的是SymLoadModule64的ModuleName参数是PCSTR类型,而SymGetLineFromName64的ModuleName参数却是PCTSTR类型)。当FileName参数只指定了文件名,而多个模块中含有同名的源文件时,SymGetLineFromName64就使用这个参数确定使用哪个模块的源文件。如果各个模块都没有同名的源文件,或者FileName指定的是绝对路径时,这个参数就没有必要了,指定为NULL即可。

FileName和dwLineNumber 参数分别指定源文件和行号。FileName可以是文件名,也可以是绝对路径,正如上面的描述那样。dwLineNumber是任意非零值,即使行号在源文件中不存在,甚至是负数,SymGetLineFromName64也会返回TRUE!那么我们如何知道指定的行号是否有效呢?只要检查displacement的值即可。大多数情况下,displacement表示指定行与最接近该行的有效行的行号之差,而且有效行的行号要小于等于指定行的行号。可以用下面的式子表示(式中的变量均使用函数参数的名字):

*lpDisplacement = dwLine - Line->LineNumber  (dwLine >= Line->LineNumber)

所谓有效行即能够产生汇编指令的行(能产生汇编指令才会有对应的地址),例如int a = 1 + 1;是有效行,而int a;和空白行则不属于有效行。用以下的代码为例进行说明:

 1 int wmain(int argc, wchar_t** argv) {
 2 
 3     int a = 1 + 1;
 4 
 5 
 6 
 7     int b = 2 + 2;
 8 
 9     return 0;
10 }

①dwLine = 2时,Line->LineNumber = 1,*lpDisplacement = 1。

②dwLine = 4, 5, 6时,Line->LineNumber = 3,*lpDisplacement = 1, 2 , 3。

③dwLine = 7时,Line->LineNumber = 7,*lpDisplacement = 0。

④dwLine = 12时,Line->LineNumber = 10,*lpDisplacement = 2。

由第四个例子可以看出,如果指定的行号大于源文件的行数,则函数返回最后一行有效行的信息,displacement为指定行号与该有效行行号的差,同样符合上面的式子。

如果dwLine为0,那么SymGetLineFromName64返回FALSE,GetLastError返回1168。奇怪的是,dwLine为负数竟然也可以调用成功,此时函数返回最后一行有效行的信息,displacement为INT_MAX + dwLine。

综上所述,要判断指定的行是否为有效行,只要检查displacement是否为0即可。

示例代码

好了,知道了如何获取行号以及行的地址之后就可以实现显示源代码的功能了,详细的方法请参考示例代码。使用这个功能时要注意源文件必须与被调试程序和调试符号同步,如果修改了源代码而没有重新编译链接的话,显示的代码肯定是错误的。

现在MiniDebugger中增加了一个命令:

l [after] [before]

显示当前正在执行的那一行以及附近的代码。after指定显示当前那一行代码的后面多少行,before指定显示当前那一行代码的前面多少行。如果省略的话,默认取值为10。

如果在执行s命令启动了被调试进程之后立即执行l命令,会得到“SymGetLineFromAddr64 failed: 6”的错误信息,这是因为此时还没有创建符号处理器。要至少执行一次g命令之后才可以使用l命令。

http://files.cnblogs.com/zplutor/MiniDebugger6.rar

转载于:https://www.cnblogs.com/zplutor/archive/2011/03/27/1997198.html

[Win32]一个调试器的实现(六)显示源代码相关推荐

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

    [Win32]一个调试器的实现(四)读取寄存器和内存 作者:Zplutor  出处:http://www.cnblogs.com/zplutor/  本文版权归作者和博客园共有,欢迎转载.但未经作者同 ...

  2. [Win32]一个调试器的实现(五)调试符号

    一个调试器应该可以跟踪被调试程序执行到了什么地方,显示下一条将要执行的语句,显示各个变量的值,设置断点,进行单步执行等等,这些功能都需要一个基础设施的支持,那就是调试符号. 什么是调试符号 我们知道, ...

  3. [Win32]一个调试器的实现(九)符号模型

    在接下来的文章中会讲解如何在调试器中显示局部变量和全局变量的类型和值.实现这个功能一定要有调试符号的支持,因为调试符号记录了每个变量的名称,类型,地址,长度等信息.这不是一件轻松的事情,因为你首先要对 ...

  4. [Win32]一个调试器的实现(二)调试事件的处理

    上一篇文章说到了调试循环的写法,这回讲一下调试器应该如何处理各种调试事件. RIP_EVENT 关于这种调试事件的文档资料非常少,即使提到也只是用"系统错误"或者"内部错 ...

  5. 微信开发者工具 微信小程序中调试器console界面不显示跑出的代码结果的问题解决

    最初的console 界面可能以下这样的, 当ctrl + s 保存并跑了一段代码,但是不显示结果. 解决办法就是把旁边的设置按钮 (蓝色的小齿轮) 点击一下就可以切换界面并看到跑代码的结果了 点击一 ...

  6. eac 反调试_自己动手制作一个过保护调试器

    一.起因 本人是新手第一次接触驱动开发的小白,事情是这样的,一个星期前突发奇想想做一个调试器保护程序用于调试游戏,既然要调试驱动保护的程序,自然也要深入驱动底层.做调试器必须要hook api去隐藏调 ...

  7. gdb 的用法(Linux调试器)

    在Linux应用程序开发中,最常用的调试器是gdb,它可以在程序中设置断点.查看变量值.一步一步跟踪程序的执行过程. GDB(GNU symbolic debugger)简单地说就是一个调试工具.它是 ...

  8. 如何一起破解图形化Python调试器

    15分钟内从零调试 (Zero-to-Debugging in 15 mins) You don't realize the value of a debugger until you're stuc ...

  9. 开源项目-基于Intel VT技术的Linux内核调试器

    本开源项目将硬件虚拟化技术应用在内核调试器上,使内核调试器成为VMM,将操作系统置于虚拟机中运行,即操作系统成为GuestOS,以这样的一种形式进行调试,最主要的好处就是调试器对操作系统完全透明.如下 ...

最新文章

  1. 配置网口相机(大恒水星相机)
  2. 开放linux下mysql数据库3306端口
  3. 计算机桌面颜色如何设置标准,电脑调整桌面颜色设置_电脑桌面颜色设置
  4. 《Python快速入门》6大数据类型详解
  5. 谢宝友:会说话的Linux内核
  6. Python——杂记
  7. 右下角使用css,CSS3 屏幕右下角的径向菜单
  8. libevent c++高并发网络编程_高并发-网络I/O
  9. webstorm中 scss或sass配置自动编译
  10. Unity URP/SRP 渲染管线浅入深出【匠】
  11. python数据分组聚合案例_《利用Python进行数据分析》十章·数据聚合与分组运算·学习笔记(二)...
  12. 网络编程学习路线计划
  13. Linux fflush 函数
  14. GBase 8c应用场景分析
  15. 什么是MapReduce(入门篇)
  16. 怎样在matlab中写技术,rect矩形函数 matlab中怎样编写矩形函数
  17. 通信电子电路(一)通电课程背景 以及选频网络概念
  18. 微信小程序开发流程指南
  19. nvme固态硬盘开机慢_装上固态SSD的电脑几个月后就卡慢?是因为你忽略了这一个细节...
  20. 多多情报通:拼多多怎么样快速引流?

热门文章

  1. [译]GPU加持,TensorFlow Lite更快了
  2. spring boot分环境导出自定义xml配置
  3. OC和JS互相调用小框架
  4. iMX8方案服务-辰汉
  5. cwRsync 同步时报错 STATUS_ACCESS_VIOLATION
  6. git 分支合并到当前
  7. Android 通过反射让SQlite建表
  8. 细节:以为字符串不为空
  9. oracle 条件查询,比较运算符,逻辑运算符,特殊运算符,判断空值,大小写敏感,多行,多列子查询...
  10. Ubuntu 12.04 一键安装lnmp环境