上一篇文章介绍了调试符号以及DbgHelp的加载和清理,这回我们使用它来实现一个显示源代码的功能。该功能的实际使用效果如下图所示:
该功能不仅仅是显示源代码,还要显示每一行代码对应的地址。实现该功能大概需要进行以下的步骤:
①获取下一条要执行的指令的地址。
②通过调试符号获取该地址对应哪个源文件的哪一行。
③对于其它的行,通过调试符号获取它对应的地址。
第一步可以通过获取EIP寄存器的值来完成,相关的内容已经在第四篇文章中进行了讲解,这里不再重复。下面讲一下如何实现第二个和第三个步骤。
获取源文件以及行号
在调试符号中,记录了每一行源代码对应的地址。通过DbgHelp的SymGetLineFromAddr64函数可以由地址获取源文件路径以及行号。该函数的声明如下:
2 HANDLE hProcess,
3 DWORD64 dwAddr,
4 PDWORD pdwDisplacement,
5 PIMAGEHLP_LINE64 Line
6 );
hProcess参数是符号处理器的标识符,dwAddr是指令的地址。pdwDisplacement是一个输出参数,用于获取dwAddr相对于它所在行的起始地址的偏移量,以字节为单位。之所以需要这么一个参数,是因为一行代码可能对应多条汇编指令,有了它就可以知道下一条要执行的指令位于这一行代码的哪个位置。例如,int b = 3 * a + 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结构体的指针,该结构体用来保存有关于行的信息,其声明如下:
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的一个例子:
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函数就是用作此目的的。该函数的声明如下:
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表示指定行与最接近该行的有效行的行号之差,而且有效行的行号要小于等于指定行的行号。可以用下面的式子表示(式中的变量均使用函数参数的名字):
所谓有效行即能够产生汇编指令的行(能产生汇编指令才会有对应的地址),例如int a = 1 + 1;是有效行,而int a;和空白行则不属于有效行。用以下的代码为例进行说明:
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命令。