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

什么是调试符号

我们知道,在exe、dll等可执行文件中保存的数据大部分都是二进制指令,CPU直接读取这些指令并执行。那么调试器是如何知道每条指令对应哪个源文件的哪一行代码呢?它又是如何知道每个变量和函数的名称,并显示变量的值呢?很显然,可执行文件的二进制数据中不可能包含这么多信息,这一切都是由调试符号来支持的。

所谓符号,简单来说就是源代码中每个对象的名称。例如变量、函数、类型等,它们都有一个名称,以及其它的相关信息:变量有类型、地址等信息;函数有返回值类型、参数类型、地址等信息;类型有长度等信息。编译器在编译每个源文件的时候都会收集该源文件中的符号的信息,在生成目标文件的时候将这些信息保存到符号表中。链接器使用符号表中的信息将各个目标文件链接成可执行文件,同时将多个符号表整合成一个文件,这个文件就是用于调试的符号文件,它既可以嵌入可执行文件中,也可以独立存在。

符号文件中包含的信息可多可少,这样可以避免泄露程序的信息。调试版程序的符号文件包含了所有的调试信息,而发行版程序的符号文件只包含非常少的调试信息,甚至没有符号文件。

符号文件有多种不同的格式,不同的编译器可能使用不同的格式。目前Visual Studio默认使用的是PDB格式,生成项目之后,在Debug或者Release文件夹下都可以找到与生成的文件同名的PDB文件。本文以及接下来的文章中,均使用PDB格式的符号文件来进行调试。

使用调试符号

Windows提供了两种方法让我们可以访问调试符号,分别是DbgHelp(Debug Help Library)和DIA(Debug Interface Access)。DIA是基于COM的,对于不熟悉COM的人使用起来会比较麻烦;而使用DbgHelp就像使用普通的Windows API那样,比较容易。本文以及接下来的文章中,使用的都是DbgHelp。

使用DbgHelp的程序需要加载DbgHelp.dll这个动态链接库,Windows自带这个文件,位于C:\Windows\System32。但是Windows自带的通常是较低版本的文件,所以最好是获取一个最新版本的,将其与程序的可执行文件放在同一个目录中,这样既可以使用最新的DbgHelp,又不需要改动系统文件。

获取最新DbgHelp.dll的一个方法是下载Windows Debugging Tools,地址为http://msdn.microsoft.com/en-us/windows/hardware/gg463009.aspx。不过这个工具包很大,为了这一个小小的文件可能要下载很长时间。其实在Visual Studio 2010中已包含了最新版本的DbgHelp(至少在写作本文的时候是如此),路径是C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\dbghelp.dll。(假设Visual Studio 2010安装在C:\Program Files)

为了在程序中使用DbgHelp,你需要先完成以下的事情:

打开项目属性对话框,定位到“配置属性”-“链接器”-“输入”,在右边的“附加依赖项”中添加dbghelp.lib。

有一点需要注意,DbgHelp使用DBGHELP_TRANSLATE_TCHAR这个预定义标记来决定是否使用Unicode字符串,而不是UNICODE标记。所以,如果你的程序使用Unicode字符串,那就定位到“配置属性”-“C/C++”-“预处理器”,在右边的“预处理器定义”中添加DBGHELP_TRANSLATE_TCHAR。

最后,在需要使用DbgHelp的源文件中,包含Windows.h和DbgHelp.h头文件即可。(Windows.h需要包含在DbgHelp.h的前面)

加载调试符号

一个进程会有多个模块,每个模块都有它自己的符号文件,有关符号文件的信息保存在模块的可执行文件中。DbgHelp通过符号处理器(Symbol Handler)来处理模块的符号文件。符号处理器位于调试器进程中,每个被调试的进程对应一个符号处理器。通常,调试器在被调试进程启动的时候创建符号处理器,在被调试进程结束的时候清理相应符号处理器占用的资源。

创建一个符号处理器使用SymInitialize函数,该函数声明如下:

 BOOL WINAPI SymInitialize(
     HANDLE hProcess,
     PCTSTR UserSearchPath,
     fInvadeProcess
 );

第一个参数是被调试进程的句柄,它是符号管理器的标识符,其它的DbgHelp函数都需要这样一个参数值指明使用哪个符号管理器。实际上这个参数不一定是句柄:当fInvadeProcess参数为TRUE时,它必须是一个有效的进程句柄;当fInvadeProcess为FALSE时,它可以是任意一个唯一的数值。

fInvadeProcess的作用是指示是否加载进程所有模块的调试符号,如果该参数为FALSE,那么SymInitialize只是创建一个符号处理器,不加载任何模块的调试符号,此时需要我们自己调用SymLoadModule64函数来加载模块;如果为TRUE,SymInitialize会遍历进程的所有模块,并加载其调试符号,所以在这种情况下hProcess必须是一个有效的进程句柄。

当fInvadeProcess为TRUE时,第二个参数UserSearchPath指示SymInitialize函数去哪里寻找符号文件。使用PDB符号文件的可执行文件中已包含有符号文件的绝对路径,如果符号文件不存在,SymInitialize就会使用UserSearchPath指定的路径去寻找符号文件。该参数可指定多个路径,以分号(;)分割。如果该参数为NULL,那么SymInitialize会按照以下的顺序寻找符号文件:

调试器进程的工作目录;

_NT_SYMBOL_PATH环境变量指定的路径;

_NT_ALTERNATE_SYMBOL_PATH环境变量指定的路径。

如果在以上路径中仍然找不到符号文件,SymInitialize并不会返回FALSE,而是返回TRUE。也就是说,它成功创建了符号处理器,并且加载了模块的信息,但是没有加载调试符号(关于如何判断某个模块是否加载了调试符号,下文会有讲解)。实际上,SymInitialize几乎不会返回FALSE,然而在某种情况下它会这么做,下面会有关于这方面的说明。

根据对SymInitialize的描述,有两种方法可以加载调试符号。第一种方法是在调用SymInitialize的时候第三个参数传入TRUE,由它负责加载每个模块的调试符号。这种方法的好处是方便,但是有一个前提:被调试进程必须初始化完毕。我曾经尝试在处理CREATE_PROCESS_DEBUG_EVENT事件的时候使用这种方法加载调试符号,但SymInitialize总是返回FALSE,GetLastError返回-1。这是因为在处理CREATE_PROCESS_DEBUG_EVENT事件时,被调试进程需要的模块还未加载完成,处于一个不完整的状态。所以,应该等到被调试进程初始化之后才使用这种方法。由于每个进程在初始化完毕之后都会引发一个断点异常,所以加载调试符号的最好的时机就是在处理这个初始断点的时候。关于初始断点的内容在讲解断点的时候会提及。

第二种方法是在调用SymInitialize的时候第三个参数传入FALSE,然后对每个模块调用SymLoadModule64函数加载调试符号。我们可以在处理CREATE_PROCESS_DEBUG_EVENT和LOAD_DLL_DEBUG_EVENT事件时分别加载exe文件和dll文件的调试符号。SymLoadModule64函数的声明如下:


 DWORD64 WINAPI SymLoadModule64(
     HANDLE hProcess,
     HANDLE hFile,
     PCSTR ImageName,
     PCSTR ModuleName,
     DWORD64 BaseOfDll,
     DWORD SizeOfDll
 );

第一个参数是符号处理器的标识符,也就是在调用SymInitialize时第一个参数的值。第二个参数是模块文件的句柄,该函数通过这个文件句柄来获取有关符号文件的信息。你可能记得在CREATE_PROCESS_DEBUG_INFO和LOAD_DLL_DEBUG_INFO结构体中都有一个hFile的字段,这个字段刚好可以用在SymLoadModule64函数上。

第三个参数ImageName用于指定模块文件的路径和名称,当第二个参数为NULL时,SymLoadModule64会通过这里指定的路径和名称去寻找模块文件。一般情况下都不会使用这个参数,因为我们可以使用更可靠的hFile参数。

第四个参数ModuleName为该模块赋予一个名称,在使用其它DbgHelp函数的时候可以通过这个名称来引用模块。如果该参数为NULL,SymLoadModule64会使用符号文件的文件名作为模块名称。

第五个参数BaseOfDll是模块加载到进程地址空间之后的基地址。这个参数很重要,因为符号文件中每个符号的地址都是相对于模块基地址的偏移地址,而不是绝对地址,这样的话,不论模块被加载到哪个地址,它的符号文件都是可用的。当然,这一切的前提是你将正确的模块基地址传给了SymLoadModule64函数。幸运的是,CREATE_PROCESS_DEBUG_INFO和LOAD_DLL_DEBUG_INFO结构体中已包含了一个lpBaseOfImage字段,我们直接使用即可,不必为了获取模块基地址而大动干戈。

至于最后一个参数SizeOfDll,表示模块文件的大小。我还不知道这个参数的作用,也不知道应该传一个什么样的值给它。我一直都给它传一个0,即使如此SymLoadModule64也能正常工作。所以我们还是暂且将它放在一旁,将注意力转移到别的地方吧。

添加了加载调试符号的代码之后,处理CREATE_PROCESS_DEBUG_EVENT事件的代码大概像下面这样子:


 1 BOOL OnProcessCreated(const CREATE_PROCESS_DEBUG_INFO* pInfo) {
 2 
 3     //初始化符号处理器
 4     //注意,这里不能使用pInfo->hProcess,因为g_hProcess和pInfo->hProcess
 5     //的值并不相同,而其它DbgHelp函数使用的是g_hProcess。
 6     if (SymInitialize(g_hProcess, NULL, FALSE) == TRUE) {
 7     
 8         //加载模块的调试信息
 9         DWORD64 moduleAddress = SymLoadModule64(
             g_hProcess,
             pInfo->hFile, 
             NULL,
             NULL,
             (DWORD64)pInfo->lpBaseOfImage,
             );
 
         if (moduleAddress == ) {
 
             std::wcout << TEXT("SymLoadModule64 failed: ") << GetLastError() << std::endl;
         }
     }
     else {
 
         std::wcout << TEXT("SymInitialize failed: ") << GetLastError() << std::endl;
     }
 
     CloseHandle(pInfo->hFile);
     CloseHandle(pInfo->hThread);
     CloseHandle(pInfo->hProcess);
 
     return TRUE;
 }

处理LOAD_DLL_DEBUG_EVENT事件的代码:


 1 BOOL OnDllLoaded(const LOAD_DLL_DEBUG_INFO* pInfo) {
 2 
 3     //加载模块的调试信息
 4     DWORD64 moduleAddress = SymLoadModule64(
 5         g_hProcess,
 6         pInfo->hFile, 
 7         NULL,
 8         NULL,
 9         (DWORD64)pInfo->lpBaseOfDll,
         );
 
     if (moduleAddress == ) {
 
         std::wcout << TEXT("SymLoadModule64 failed: ") << GetLastError() << std::endl;
     }
 
     CloseHandle(pInfo->hFile);
 
     return TRUE;
 }

判断符号文件的格式

前面说过,SymInitialize在找不到符号文件的情况下仍然会返回TRUE,此时它只加载了模块的信息,而没有加载调试符号。SymLoadModule64函数同样如此。那么,如何知道某个模块是否含有调试信息呢?或者,如何知道某个模块的符号文件使用哪种格式呢?可以通过调用SymGetModuleInfo64函数来获取这些信息。该函数的声明如下:

 BOOL WINAPI SymGetModuleInfo64(
     HANDLE hProcess,
     DWORD64 dwAddr,
     PIMAGEHLP_MODULE64 ModuleInfo
 );

第一个参数是符号处理器的标识符,现在你应该对它很熟悉了。第二个参数是模块的基地址,也就是在调用SymLoadModule64时传给BaseOfDll参数的值。第三个参数是指向IMAGEHLP_MODULE64结构体的指针,调用函数完成之后模块的信息将会保存到这个结构体中。

IMAGEHLP_MODULE64结构体含有非常多的字段,不过我们一般只关心其中的一个:SymType。这个字段指示模块使用的是哪种格式的符号文件,其可能的取值如下:

SymCoff

COFF格式。

SymCv

CodeView 格式。

SymDeferred

调试符号是延迟加载的。下文会提及。

SymDia

DIA 格式。

SymExport

符号是从DLL文件的导出表中生成的。

SymNone

没有调试符号。

SymPdb

PDB格式。

SymSym

使用.sym类型的符号文件。

SymVirtual

与SymLoadModuleEx函数的最后一个参数有关,还未知道什么意思。

在调用SymGetModuleInfo64之前需要将IMAGEHLP_MODULE64结构体的SizeOfStruct字段设置为sizeof(IMAGEHLP_MODULE64);

延迟加载调试符号

在上面SymType的取值列表中有一个SymDeferred的值,它表示什么意思呢?DbgHelp支持延迟加载调试符号,意思是说在调用SymLoadModule64时,只加载模块信息,不加载调试符号,等到真正使用的时候才加载。这样做的好处是可以节省内存,避免加载了符号而不使用的情况。

如果要开启这个特性,可以使用SymSetOptions函数:

 SymSetOptions(SYMOPT_DEFERRED_LOADS);

该函数需要在调用SymInitialize之前调用。

所谓“真正使用的时候”究竟是什么时候,我也搞不清楚。我在开启了延迟加载调试符号的情况下调用SymGetLineFromAddr64获取源文件路径和行号信息时总是失败,而关闭了这个特性之后却成功了,这说明并不是所有需要访问调试符号的DbgHelp函数都会使调试符号加载进来。所以,为了确保DbgHelp函数可以正确执行,我建议不要开启这项特性。

清理调试符号

在被调试进程结束的时候必须删除与之对应的符号处理器,以及清理它占用的资源。只要在处理EXIT_PROCESS_DEBUG_EVENT事件的时候调用SymCleanup函数就可以完成这个操作,该函数接受一个符号处理器的标识符。

另外,在dll文件卸载的时候也应该清理与之相关的调试符号,避免占用内存。这要在处理UNLOAD_DLL_DEBUG_EVENT事件时调用SymUnloadModule64函数。该函数接受一个符号处理器的标识符,以及模块的基地址,我们可以直接使用UNLOAD_DLL_DEBUG_INFO结构体中唯一的字段lpBaseOfDll。

示例代码

示例代码按照本文的描述添加了对调试符号的加载和清理代码,改动不是很大。

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

jpg改rar 

[Win32]一个调试器的实现(五)调试符号的更多相关文章

  1. 嵌入式调试器原理和各类调试器集锦(JLINK、STLINK、CCDEBUG)

    工欲善其事,必先善其器.调试器在嵌入式开发调试中的重要性不言而喻,单步.断点和监察的效率远高于串口打印.但是,调试器对于一般开发人员往往是一个黑匣子.今天我们就来谈谈调试器的原理,顺便把自己的几类调试 ...

  2. Qt 4.x调试器问题,缺失调试助手。

    之前项目开发需要用到4.x环境,固定多少版本避免团队开发不协调,然后拿了同事的开发包安装[注:我本子上原来就有4.x版本跟5.x版本,只是对应的4.x跟需求的不一样] creator是2.4.1的,同 ...

  3. 第二章排错的工具:调试器Windbg(上)

    感谢博主 http://book.51cto.com/art/200711/59731.htm <Windows用户态程序高效排错>第二章主要介绍用户态调试相关的知识和工具.本文主要讲了排 ...

  4. 第二章排错的工具:调试器Windbg(下)

    感谢博主 http://book.51cto.com/art/200711/59874.htm 2.2  读懂机器的语言:汇编,CPU执行指令的最小单元2.2.1  需要用汇编来排错的常见情况 汇编是 ...

  5. 痞子衡嵌入式:飞思卡尔Kinetis开发板OpenSDA调试器那些事(上)- 背景与架构

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔Kinetis MCU开发板板载OpenSDA调试器(上篇). 众所周知,嵌入式软件开发几乎离不开调试器,因为写一个稍有代码规模 ...

  6. 在 Visual Studio 调试器中指定符号 (.pdb) 和源文件

    查找并指定符号文件和源文件:指定符号加载行为.使用符号和源服务器上:加载符号自动或在要求.   内容 查找符号 (.pdb) 文件 查找源文件   查找符号 (.pdb) 文件 说明 在之前的 Vis ...

  7. 手把手教你写Windows 64位平台调试器

    本文网页排版有些差,已上传了doc,可以下载阅读.本文中的所有代码已打包,下载地址在此. ------------------------------------------------------- ...

  8. 痞子衡嵌入式:飞思卡尔i.MX RTyyyy系列MCU硬件那些事(2.1)- 玩转板载OpenSDA,Freelink调试器

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔i.MX RTyyyy系列EVK上板载调试器的用法. 本文是i.MXRT硬件那些事系列第二篇,第一篇痞子衡给大家整体介绍了i.M ...

  9. 使用 Visual Studio 调试器附加到运行的进程

    为什么调试附加进程? Visual Studio 调试器可以附加到在 Visual Studio 外运行的进程. 可以使用此附加功能执行以下操作: 调试并非在 Visual Studio 中创建的应用 ...

  10. lldb调试器知多少

    lldb调试器简介   lldb 是一个有着 REPL 的特性和 C++ .Python 插件的开源调试器.lldb调试器的由来是伴随着Xcode的版本升级而来. Xcode4.3之前使用的默认调试器 ...

随机推荐

  1. (原创)Linux下的floating point exception错误解析

    很多人也许都碰到过这样的错误:linux下程序刚一运行就报错:Floating point exception. 其实这个问题很容易排查,绝大多数情况情况都是逻辑的问题,如:c = a/b;或 c = ...

  2. 域名映射ip

    windows: 修改文件hosts文件 地址是C:\WINDOWS\system32\drivers\etc\hosts 加进你自己的如: Linux: hosts 文件目录: sudo vi /e ...

  3. css3 -- 自动生成序号(不使用JS,可任意排序)

    需求:一个table 需要在第一列生成序号:1.2.3.4.5......  并且自适应行数 不使用后台程序,开始考虑使用JS,但是一旦前台排序后,序号就乱了,最后采用CSS的一个计数器方法实现! & ...

  4. 使用openstackclient调用Keystone v3 API

    本文内容属于个人原创,转载务必注明出处:  http://www.cnblogs.com/Security-Darren/p/4138945.html 考虑到Keystone社区逐渐弃用第二版身份AP ...

  5. openssl创建自己的CA certificate

    Create a Certificate Authority private key (this is your most important key): $ openssl req -new -ne ...

  6. Oracle触发器给表自身的字段重新赋值出现ORA-04091异常

    业务描述如下: 在插入一个表的时候,需要根据一个字段的值更新另一个字段的值.当然也可以通过程序就能很简单得实现,只是这个字段只是数据交换用,和系统主业务没关系,不想修改程序,所以才用触发器的方式实现. ...

  7. linux静止ping的方法

    ping是一个通信协议,是ip协议的一部分,tcp/ip 协议的一部分.利用它可以检查网络是否能够连通,用好它可以很好地帮助我们分析判定网络故障.应用格式为:Ping IP地址.但服务启用ping有时 ...

  8. 【转】在64位windows下使用instsrv.exe和srvany.exe创建windows服务

    本文转自:https://www.iflym.com/index.php/computer-use/201205020001.html 在32位的windows下,包括windows7,windows ...

  9. 使用w查看系统负载 vmstat命令 top命令 sar命令 nload命令

    w/uptime 查看系统负载 w查看系统负载,uptime跟w一样. [root@centos7 ~]# w 22:34:10 up 6 days, 23:10,  4 users,  load a ...

  10. 设置回车的默认按钮detectEnter

    场景: 页面有一个搜索文本框和搜索按钮.正常情况下,当我在搜索文本框输入关键字后按回车键就可以触发搜索按钮进行内容搜索,但由于页面上还有其它按钮,而且默认不是搜索按钮,怎样才能实现回车就触发我们的搜索 ...