分类 调试 下的文章

通常,您可能希望在调试器中跟踪的一类问题(除了崩溃)是某个特定函数以某种方式失败。在大多数Win32函数的情况下,通常会得到某种(希望是有意义的)最后的错误代码。有时,您可能需要知道返回错误的原因或错误的来源(在最后一个错误值通过几个函数向上传播的情况下)。
一种可能的方法是使用条件断点,但是SetLastError路径通常会被命中,因此这在性能方面经常是有问题的,即使在本地计算机上的用户模式调试中也是如此。
在Windows Vista上,NTDLL内部有一个未记录的钩子(它现在负责set last error背后的大部分逻辑),允许您配置一个程序,以便在将特定错误代码设置为最后一个错误时进入调试器。这是Vista的新功能,由于没有文档记录(至少在我能看到的任何地方都没有),它可能不会无限期地存在。

不过,现在可以设置ntdll!g_dwLastErrorToBreakOn设置为非零值(通过调试器中的ed命令),以便在NTDLL看到设置了最后一个错误值时要求它执行断点。显然,这不会直接捕获修改TEB中字段的内容,但是使用setlastererror或RtlSetLastWin32Error的任何内容都将根据该值进行检查(在debuggee的上下文中)。
例如,如果要求NTDLL在错误5(错误访问被拒绝)时中断,然后尝试打开您无权访问的文件或目录,您可能会看到类似的情况:

0:002> ed ntdll!g_dwLastErrorToBreakOn 5
0:002>g

[...] Perform an operation to cause ERROR_ACCESS_DENIED

(
1864.2774): Break instruction exception- code 80000003(first chance)
ntdll
!DbgBreakPoint:00000000`76d6fdf0 cc int 3 0:004>k
Call Site
ntdll
!DbgBreakPoint
ntdll
! ?? ::FNODOBFM::`string'+0x377b kernel32!BaseSetLastNTError+0x16kernel32!CreateFileW+0x325SHELL32!CEnumFiles::InitAndFindFirst+0x7aSHELL32!CEnumFiles::InitAndFindFirstRetry+0x3eSHELL32!CFileSysEnum::_InitFindDataEnum+0x5eSHELL32!CFileSysEnum::Init+0x135SHELL32!CFSFolder::EnumObjects+0xd3SHELL32!_GetEnumerator+0x189SHELL32!CEnumThread::_RunEnum+0x6dSHELL32!CEnumThread::s_EnumThreadProc+0x13SHLWAPI!WrapperThreadProc+0xfckernel32!BaseThreadInitThunk+0xdntdll!RtlUserThreadStart+0x1d

我们知道符号文件对我们调试是非常重要的,如果没有符号文件,我们在调试器里看到的要么是偏移地址,要么看到的是错误符号,这会导致我们定不了位或错误定位,如果有了匹配的符号文件,这一切都不是问题了。

一、调试器寻找符号文件

首先在我们编译我们的程序时,如果设置了符号选项,那么在编译连接时,除了产生我们需要的符号文件外,还会在我们的PE文件(exe,dll)里记录下对应的符号文件信息。

这里的路径其实是开发编译机上生成的路径,当我们在这台机上调试时,就会到这个目录下寻找符号文件。当在别的机器上调试时,调试器就会会拿路径里的文件名去查找。

当调试器加载一个PE模块时,第一个搜索的路径就是这个模块所在的路径,如果不在模块所在的路径,则查找模块中记录的build目录,就时上图里的路径, 如果以上两个路径都没有找到PDB,则根据symbol server的设置,在本地的symbol server的cache中查找,如果在本地的symbol server的cache中没有对应的PDB,则最后才到远程的symbol server中查找。

当在某个路径找到了文件名匹配的符号文件,就要看符合文件根模块是否匹配。根据上图,我们知道编译器在生成PE模块时,调试信息里面会生成一个GUID值,同时符号文件里也会记录这个GUID值,每一次编译连接,这个值都会发生变化。

当两个文件里的GUID值相等时,符号文件才跟模块文件匹配,这时才能进行正常的调试,获得正确的符号信息。所以我们在调试时,一定要把正确的符号文件跟模块文件放在同一目录下才行。

二、调试器定位

当有了正确的符号文件后,调试器是如何定位源代码、行号等信息的呢?我们以VS来大概讲一下,默认情况下,在pdb文件中,保存了可执行文件中所有的符号(函数名、变量名等)所在源文件、行号、OFFSET(文件中的偏移)等信息。但是这些信息,是在编译阶段得到的,编译器在编译每个cpp的过程中,就可以把这些符号的相关信息收集起来,存放在各个cpp所生成的obj文件中,然后在链接的时候,提取每个obj中的这些信息,生成一个单独的pdb文件。这样,以后调试程序的时候,调试器只要找得到这个pdb,就可以知道可执行文件中,所有符号所在的源文件、行号和OFFSET了。反过来说,当给出一个源文件和行号,就可以拿到对应的OFFSET了,所以在还没有启动调试的时候,我们下的断点,实际上调试器是知道这个断点应该在哪个OFFSET上了,等启动调试的时候,用这个OFFSET加上这个模块所加载到的基地址值,就可以得到这个断点所在的VA(程序加载到内存后的一个虚拟地址)了,然后在这个VA处强行写上int 3指令,并继续执行,当执行到这里,便中断下来给我们一个调试机会了。当我们在VS里鼠标放在某个变量上时,调试器可以拿到这个变量的名称,根据我们前面说的,用这个名称去pdb中查找,自然就可以找到pdb文件中保存的OFFSET了,加上这个模块的基地址,就找到了这个变量所在内存的VA,剩下的就是读一下这个VA内存中的内容了。这样也就实现了观察变量值得功能。

我发现自己在工作中和工作之外不时支持的一件事是使用程序的发布版本(无论如何,对于Windows应用程序)进行调试的价值。乍一看,这可能与某些人的想法相矛盾,因为人们往往认为调试构建实际上更适合调试(毕竟它被命名为“调试构建”)。

一、什么是符号文件

符号文件(Symbol Files)是一个数据信息文件,它包含了应用程序二进制文件(比如:EXE、DLL等)调试信息,专门用来作调试之用,最终生成的可执行文件在运行时并不需要这个符号文件,但你的程序中所有的变量信息都记录在这个文件中。所以调试应用程序时,这个文件是非常重要的。用 Visual C++ 和 WinDbg 调试程序时都要用到这个文件。  在 Windows 系统中,符号文件以 .pdb 为扩展名,比如:每个 Windows 操作系统下有一个 GDI32.dll 文件,编译器在编译该 DLL 的时候会产生一个 GDI32.pdb 文件,一旦你拥有了这个 PDB 文件,那么便可以用它来调试并跟踪到 GDI32.dll 内部。该文件和二进制文件的编译版本密切相关,比如修改了 DLL 的输出函数,再编译该 DLL,那么原先的 PDB 文件就过时了,不能再用老的 PDB 文件来做调试工作,而必须使用最新的 PDB 文件版本。

二、符号文件的生成和获取

  • 自己编写的代码
    在链接配置里做如下配置

  • Windows系统相关程序的符号
    从微软服务器下载http://msdl.microsoft.com/download/symbols下载,在vs里配置

 三、符号文件包含哪些信息

3.1、Native C++ PDB包含了如下的信息:

  • public,private 和static函数地址;
  • 全局变量的名字和地址;
  • 参数和局部变量的名字和在堆栈的偏移量;
  • class,structure 和数据的类型定义;
  • Frame Pointer Omission 数据,用来在x86上的native堆栈的遍历;
  • 源代码文件的名字和行数;


3.2、.NET PDB只包含了2部分信息:

  •  源代码文件名字和行数;
  • 和局部变量的名字;
  • 所有的其他的数据都已经包含在了.NET Metadata中了; 

四、符号文件分类

  •  私有符号文件
    包含所有程序调试信息,如第三节讲到的信息,VC默认生成的就是私有符号文件

  • 公有符号文件
    在编译链接时,设置/PDBSTRIPPED 选项将创建第二个程序数据库 (PDB) 文件。 此 PDB 文件省略您不希望交付给客户的符号。 第二个 PDB 文件仅包含公共符号:对象文件的列表和可执行文件中由它们参与构成的部分;用于遍历堆栈的帧指针优化 (FPO) 调试记录。去除了如下信息:类型信息,行号信息、基于对象文件的 CodeView 符号,如函数、局部变量和静态数据的符号。

五、符号文件系统

有两个常见的符号文件系统。 在本文档中,这些将称为当前系统旧系统

  • 当前符号文件系统
    在当前系统中,始终有两个文件: 可执行文件和.pdb 文件。 .Pdb 文件包含所有符号。 可执行文件包含.pdb 文件的指针。

  • 旧的符号文件系统
    在旧系统中,可执行文件和符号文件可以排列两个不同的方式:
    1、可执行文件和.pdb 文件。 在这种方案,大多数符号信息是.pdb 文件中。 可执行文件中包含的符号信息的其余部分。 可执行文件还包含指向.pdb 文件的指针。
    2、可执行文件、.pdb 文件和.dbg 文件。 .Pdb 文件是两个文件排列方式相同: 它包含的符号的大多数。 符号信息的其余部分是.dbg 文件中。 在可执行文件是没有对应符号信息。 可执行文件包含一个指向.dbg 文件,并且.dbg 文件包含.pdb 文件的指针。