2. 符号与源码
符号与源码是调试过程中的重要因素,它们使得枯燥生硬的调试内容更容易地调试人员读懂。在可能的情况下,应该尽量地为模块加载符号和源码。大部分情况下源码难以得到,但符号却总能以符号文件的形式易于得到。
什么是符号文件呢?编译器和链接器在创建二进制镜像文件(诸如exe、dll、sys)时,伴生的后缀名为.dbg、.sym或.pdb的包含镜像文件编译、链接过程中生成的符号信息的文件称为符号文件。具体来说,符号信息包括如下内容:
- 全局变量(类型、名称、地址);
- 局部变量(类型、名称、地址);
- 函数(名称、原型、地址);
- 变量、结构体类型定义;
源文件路径以及每个符号对应于源文件中的行号,这是进行源码级别调试的基础。
有这么多的信息包含在符号文件中,使得符号文件通常要比二进制文件(PE格式文件)本身要大很多。调试过程中,符号之重要性不言而喻。只有正确设置了符号路径,使得调试器能够将调试目标、符号文件以及源码文件一一对应起来,才能够最好地发挥调试器的强大功用。
符号信息隶属于指定的模块,所以只有调试器需要用到某个模块时,他的符号信息才有被加载和分析的必要。所以我们在讲符号内容之前,先讲和模块相关的命令。
2.1 模块列表
每个可执行程序都是由若干个模块构成,有些模块静态加载,有些模块以动态方式进行加载。所以对于有些模块,可能在A时刻运行时被加载,而在B时刻运行时,自始至终都未被加载。调试过程中,调试器根据模块的加载情况加载符号。有几个命令可以用来列举模块列表,分别是:lm、!dlls、.reload /l、!imgreloc。下面分别来看。
- lm [选项] [a Address] [m Pattern | M Pattern]
lm是list loaded modules的缩写,他还有一个DML版本:
- lmD [选项] [a Address] [m Pattern | M Pattern]
使用/v选项能列出模块的详细信息,包括:模块名、模块地址、模块大小、镜像名、时间戳、以及对应的符号文件信息(包括类型、路径、类型、编译器、符号加载状态)。
如使用参数a,后面跟地址(address),则只有指定地址所在的模块能够被列出;
如使用参数m,后面跟一个表示模块名的字符串通配符,如lm m *o*将显示所有名称中包含字母o的模块,下图所示:
||0:0:001> lm m *o* start end module name f3380000 f3512000 dwmcore (private pdb symbols) f92d0000 f9327000 d3d10_1core (deferred) fa890000 fa9f1000 WindowsCodecs (deferred) faa50000 fac44000 comctl32 (deferred) fbf70000 fbf7c000 version (deferred) fce20000 fce2f000 profapi (deferred) fd970000 fdb73000 ole32 (deferred) fee60000 fee7f000 sechost (deferred)
下面介绍另一个命令:
- !dlls [选项] [LoaderEntryAddress]
首先看他的可选参数:
-i/-l/-m:排序方式,分别按照初始化顺序、加载顺序、内存起始地址顺序排列。
-a:列出镜像文件PE结构的文件头、Section头等详细信息,是分析PE结构的好帮手(更好的帮手是利用自如PEView或Stud_PE等UI工具)。
-c:指定函数所在的模块。这个选项非常实用,比如我想知道NtCreateFile函数是哪个模块暴露出来的接口,如下:
0:000> !dlls -c ntcreatefile Dump dll containing 0x7c92d0ae: 0x00251f48: C:\WINDOWS\system32\ntdll.dll Base 0x7c920000 EntryPoint 0x7c932c48 Size 0x00096000 Flags 0x00085004 LoadCount 0x0000ffff TlsIndex 0x00000000 LDRP_IMAGE_DLL LDRP_LOAD_IN_PROGRESS LDRP_ENTRY_PROCESSED LDRP_PROCESS_ATTACH_CALLED
除了lm和!dlls外,下文将讲到的.reload命令在加入 /l选项后,也能列举模块,其命令格式如下:
- .reload /l
最后再来看一个!imgreloc命令,它也能够列出模块列表并显示各模块地址。但其主要作用尚不在此,它用来判断各个模块是否处于preferred地址范围。所谓Preferred地址是这么一回事:二进制文件在编译的时候,编译器都会为其设置一个理想地址(Preferred Address),这样二进制文件被加载时,系统会尽可能将他映射到这个理想地址。当然,所谓“理想”往往是会受到“现实”的挑战的,当存在地址竞争的时候,需要适当调整二进制文件的加载地址,选择另一个合适的地方加载之。!imgreloc命令就是用来查看这种情况的,命令如下:
- !imgreloc [模块地址]
命令!imgReloc是Image Relocate的缩写,字面已能够反映其含义:镜像文件重定位信息。下面是一个例子。
上例中,大部分系统模块(上图下部方框所示)其地址由于事先经过统筹分配,所以一般都能被加载到preferred地址处。只有少数模块(如最上面的Normaliz模块)由于地址冲突而受到了调整。
2.2 模块信息
上一节我们了解了如何枚举模块列表,这一节我们研究针对单个模块,如何获取详细信息。有多个命令可以查看指定模块的详细模块信息,这包括:lm、!dh、lmi等,下面来一一介绍。
首先看lm,这个命令上面我们已经介绍过,现在利用它来获取指定模块信息。其命令格式如下:
- lm v a 模块地址
这里使用了v选项,以显示详细(verbose)信息;并使用a参数以指定模块地址。通过此命令显示的信息,和我们在explorer资源管理器中通过鼠标右键查看一个文件的属性所看到的信息差不多。请看下面的清单:
0:000> lm v a 00400000 start end module name 00400000 00752000 UsbKitApp C (private pdb symbols) C:\Trunk\CY001\UsbKitApp\Debug\UsbKitApp.pdb Loaded symbol image file: UsbKitApp.exe Image path: UsbKitApp.exe Image name: UsbKitApp.exe Timestamp: Tue Mar 16 22:07:02 2010 (4B9F9086) CheckSum: 00000000 ImageSize: 00352000 File version: 1.0.0.1 Product version: 1.0.0.1 File flags: 1 (Mask 3F) Debug File OS: 4 Unknown Win32 File type: 1.0 App File date: 00000000.00000000 Translations: 0804.03a8 CompanyName: TODO: <公司名> ProductName: TODO: <产品名> InternalName: UsbKitApp.exe OriginalFilename: UsbKitApp.exe ProductVersion: 1.0.0.1 FileVersion: 1.0.0.1 FileDescription: TODO: <文件说明> LegalCopyright: TODO: (C) <公司名>。保留所有权利。
下面看!lmi命令,此命令通过指定模块地址查找模块并获取其信息,其命令格式如下
- !lmi 模块地址
此命令侧重获取对调试器有用的信息,请看下面的列表:
0:000> !lmi 0x400000 Loaded Module Info: [0x400000] Module: UsbKitApp Base Address: 00400000 Image Name: UsbKitApp.exe Machine Type: 332 (I386) Time Stamp: 4b9f9086 Tue Mar 16 22:07:02 2010 Size: 352000 CheckSum: 0 Characteristics: 103 Debug Data Dirs: Type Size VA Pointer CODEVIEW - GUID: {5DB12DF1-71CA-43F7-AD85-0977FB3629A4} Age: 3, Pdb: C:\Trunk\CY001\UsbKitApp\Debug\UsbKitApp.pdb Image Type: FILE - Image read successfully from debugger. UsbKitApp.exe Symbol Type: PDB - Symbols loaded successfully from image header. C:\Trunk\CY001\UsbKitApp\Debug\UsbKitApp.pdb Compiler: Resource - front end [0.0 bld 0] - back end [9.0 bld 21022] Load Report: private symbols & lines, not source indexed C:\Trunk\CY001\UsbKitApp\Debug\UsbKitApp.pdb
如果还要查看更详细、丰富的模块信息,可以使用!dh命令,命令格式如下:
- !dh [标志] 模块地址
模块相关的知识点讲完了,下面讲符号有关命令。和符号相关的知识点包括:符号路径、符号服务器、符号缓存、符号加载以及符号的使用等。
2.3 符号路径
什么是符号路径呢?就是调试器寻找符号文件的方向,它可以是本地文件夹路径、可访问的UNC路径、或者是符号服务器路径。什么是符号服务器呢?如果调试过程中,需要涉及到成千上万个符号文件,以及同一个符号文件存在不同平台下的不同符号文件版本的时候,那么一一手动设置符号路径肯定是不现实的,于是引入符号服务器的概念。符号服务器有一套命名规则,使得调试软件能够正确找到需要的符号文件。一般来说,符号服务器比较大,都是共用的,放在远程主机上。为了降低网络访问的成本,又引入了符号缓存的概念,即将从服务器上下载到的符号文件,保存在本地缓存中,以后调试器需要符号文件的时候,先从缓存中寻找,找不到的时候再到服务器上下载。下面分几部分一一来看。
设置符号路径:
设置符号路径的语法如下:
- .sympath [+] [路径]
如果不加入任何参数执行.sympath命令,将显示当前的路径设置:
- .sympath
如要覆盖原来的路径设置,使用新路径即可:
- .sympath <新路径>
要在原有路径的基础上添加一个新路径,可使用:
- .sympath+ <新增路径>
要注意的是,使用.sympath改变或新增符号路径后,符号文件并不会自动更新,应再执行.reload命令以更新之。
这里要谈一谈延迟加载的知识点。延迟加载使得模块的符号表,只在第一次真正使用的时候才被加载。这加快了程序启动,不用在一开始耗费大量时间加载全部的符号文件。
使用.symopt +4和.symopt -4来开启或关闭延迟加载设置。
在已经启动了延迟加载的情况下,如想临时改变策略,立刻将指定模块的符号加载到调试器中,可以使用ld或者.reload /f命令。
符号服务器与符号缓存:
设置符号服务器的基本语法是:
- SRV*[符号缓存]*服务器地址
语法由SRV引导,符号缓存和服务器地址的前面各有一个星号引导。符号缓存一般也叫做下游符号库。如某公司有一台专门的符号服务器,地址为\\symsrv\\symbols,则他们公司的所有开发人员都应该在他们的调试器中使用类似下面的命令:
- .sympath+ srv*c:\symbols*\\symsrv\symbols
此外,我们总是应该把微软的公用符号库加入到我们的符号路径中:
- .sympath+ srv*<缓存地址>*http://msdl.microsoft.com/download/symbols
这是一台微软对外公开的服务器,使用http地址访问,不是所有人都能牢记这个网址,所以最好的办法就是使用.symfix命令,语法如下:
- .symfix [+] [符号缓存地址]
这个命令等价于上面的.sympath命令,而不用输入长长的http地址。
0:000> .symfix c:\windows\symbols 0:000> .sympath Symbol search path is: SRV*c:\windows\symbols*http://msdl.microsoft.com/download/symbols
符号选项:
命令格式如下:
- 显示当前设置:.symopt
- 增加选项:.symopt+ Flags
- 删除选项:.symopt- Flags
第一个命令没有任何参数,显示当前设置。后面两个,第二个命令含有“+”代表添加一个选项,第三个命令含有“-”代表去除一个选项。
001> .symopt Symbol options are 0x30337: 0x00000001 - SYMOPT_CASE_INSENSITIVE 0x00000002 - SYMOPT_UNDNAME 0x00000004 - SYMOPT_DEFERRED_LOADS 0x00000010 - SYMOPT_LOAD_LINES 0x00000020 - SYMOPT_OMAP_FIND_NEAREST 0x00000100 - SYMOPT_NO_UNQUALIFIED_LOADS 0x00000200 - SYMOPT_FAIL_CRITICAL_ERRORS 0x00010000 - SYMOPT_AUTO_PUBLICS 0x00020000 - SYMOPT_NO_IMAGE_SEARCH
可用的符号选项请见下表:
值 | 可读名称 | 描述 |
0×1 | SYMOPT_CASE_INSENSITIVE | 符号名称不区分大小写 |
0×2 | SYMOPT_UNDNAME | 符号名称未修饰 |
0×4 | SYMOPT_DEFERRED_LOADS | 延迟加载 |
0×8 | SYMOPT_NO_CPP | 关闭C++转换,C++中的::符号将以__显示 |
0×10 | SYMOPT_LOAD_LINES | 从源文件中加载行号 |
0×20 | SYMOPT_OMAP_FIND_ NEAREST | 如果由于编译器优化导致找不到对应的符号,就以最近的一个符号代替之 |
0×40 | SYMOPT_LOAD_ANYTHING | 使得符号匹配的时候,匹配原则较松散,不那么严格。 |
0×80 | SYMOPT_IGNORE_CVREC | 忽略镜像文件头中的CV记录 |
0×100 | SYMOPT_NO_UNQUALIFIED_ LOADS | 只在已加载模块中搜索符号,如果搜索符号失败,不会自动加载新模块。 |
0×200 | SYMOPT_FAIL_CRITICAL_ ERRORS | 不显示文件访问错误对话框。 |
0×400 | SYMOPT_EXACT_SYMBOLS | 进行最严格的符号文件检查,只要有微小的差异,符号文件都不会被加载。 |
0×800 | SYMOPT_ALLOW_ABSOLUTE_ SYMBOLS | 允许从内存的一个绝对地址处读取符号信息。 |
0×1000 | SYMOPT_IGNORE_NT_ SYMPATH | 忽视在环境变量中设置的符号路径,也忽视被调试进程的执行路径。也就是说,当搜索符号文件的时候,不会从这些路径中搜索。 |
0×2000 | SYMOPT_INCLUDE_32BIT_MODULES | 让运行在安腾系统上的调试器,也枚举32位模块。 |
0×4000 | SYMOPT_PUBLICS_ONLY | 仅搜索符号文件的公共(PUBLIC)符号表,忽略私有符号表。 |
0×8000 | SYMOPT_NO_PUBLICS | 不搜索符号文件的公共(PUBLIC)符号表 |
0×10000 | SYMOPT_AUTO_PUBLICS | 先搜索pdb文件的私有符号表,如果在其中找到对应的符号,就不再搜索公共(PUBLIC)符号表,这可以加快搜索速度。 |
0×20000 | SYMOPT_NO_IMAGE_SEARCH | 不搜索镜像拷贝 |
0×40000 | SYMOPT_SECURE | 安全模式,让调试器尽量不影响到主机。 |
0×80000 | SYMOPT_NO_PROMPTS | 不显示符号代理服务器的认证对话框,将导致某些时候无法访问符号服务器 |
0×80000000 | SYMOPT_DEBUG | 显示符号搜索的详细过程和信息 |
表8-1 符号选项
2.4 符号加载
本节分下面几个子题目分别讲解。
立刻加载:
其命令格式如下:
- ld 模块名 [/f 符号文件名]
加载指定模块的符号。调试器默认采用延迟模式加载符号,也就是直到符号被使用的时候,才将符号文件加载到调试器中并进行解析。ld使得延迟模式被打破,让指定模块的符号文件立刻加载到调试器中。此指令可为模块的符号文件设置自定义的匹配名称,比如:
- ld 123 /f abc
这样一来,abc.pdb将成为123.exe的符号文件。正常情况下,这是不可能的,只能是abc.pdb对应abc.exe。
重新加载:
如果对自己正在使用的符号文件感到疑惑,比如源代码和行号明显不匹配,最好的做法就是重新加载一下符号文件。此命令语法如下:
- .reload /f /v [模块名]
.reload命令的作用是删除指定或所有已加载的符号文件,默认情况下,调试器不会立刻根据符号路径重新搜索并加载新的符号文件,而是推迟到调试器下一次使用到此文件时。
使用/f参数(force),将迫使调试器立刻搜索并重新加载新的符号文件。
其它参数解释如下:
- /v:将搜索过程中的详细信息都显示出来。
- /i:不检查pdb文件的版本信息;
- /l:只显示模块信息,内核模式下,和“lm n t”命令类似,但显示内容比后者更多,因为包含了用户模块信息;
- /n:仅重载内核符号,不重载用户符号;
- /o:强制覆盖符号库中的符号文件,即使版本相同;
- /d:用户层模式下使用Windbg时的默认选项,重载调试器模块列表中的所有模块;
- /s:内核模式下使用Windbg时的默认选项,重载系统模块列表中的所有模块,另外,如果调试器在用户模式下运行,要加载内核模块,也必须使用/s选项,否则调试器将只会在调试器模块列表中搜索而导致找不到内核模块;
- /u:卸载指定模块。如发现当前符号版本不对,使用/u开关先卸载之再重新加载。
符号验证:
上面讲到.reload的时候,我们说过,符号文件会出现不匹配的情况。这是很有可能的,程序员在后期测试的时候可能会将工程多次编译,为了维护多个版本而使得自己也被搞混。可以使用下面的命令验证一个模块的符号文件:
- !chksym <模块名> [符号名]
加载选项:!sym
有两类符号加载选项。第一类是Noisy/Quiet,Noisy选项将打印符号加载的详细信息,Quiet选项则忽略这些信息。第二类是Prompts/Prompts off,即是否允许执行提示(Prompts)对话框。
一般都是在调用.reload 命令之前,执行加载选项命令,以见立竿见影之效。
所谓Noisy是吵闹的意思,调试器在搜索、加载符号的时候,会显示更多与搜索有关的信息。而安静模式下,则不会显示这些信息。不管吵闹与否,都不会影响到最终的搜索、加载结果。
当从网络上下载符号文件的时候,可能会碰到网络服务器要求客户进行安全认证的情况,如果开启Prompts选项,则弹出认证对话框,让用户输入认证信息;否则,不弹出对话框,并且不会下载符号文件。
不加任何参数的情况下,显示当前加载选项设置,下面的清单表明当前的设置为Quite及Prompts模式:
lkd> !sym !sym <noisy/quiet - prompts/prompts off> - quiet mode - symbol prompts on
2.5 符号搜索
符号搜索包括全局搜索和就近搜索两种。
全局搜索:
命令“x”被用来进行符号的全局搜索,你可以把它直接就理解为search。格式如下:
- x [参数] [模块!符号]
如果什么参数都没有的话,它将列出当前调试环境下的所有局部变量,前提是要在有局部变量存在的情况下,显示局部变量的另一个命令是dv,后文也会讲到。
- x kernel32!a*,
上面命令搜索并打印出kernel32模块中所有a开头的符号。x命令支持DML,使用/D选项即以DML格式显示。
如果你不知道ntcreatefile这个函数是在哪个模块中定义的,可以试着使用下面的命令:
- x *!*NtCreateFile* (注:亦请参照!dlls –c命令)
同名函数在多个系统模块中并定义,这可能出乎你的意料,但却给你带来真正的知识。
此外,x命令有多个可选参数。建议总是带上/t和/v,可显示更多符号、类型信息。
- /f:将只显示函数符号;并且会显示函数的详细定义。
- /d:显示更多的变量类型相关信息。
就近搜索
如果知道了符号的大概地址,但不能确定确切的符号名称,该怎么处理呢?就近查找命令ln能发挥作用,ln是List Nearest的缩写。它的作用是:(根据给定的地址)列出附近一定范围内的所有符号。下表中,指定地址0x7c8179f0的前后各有一个符号被找到:
0:000> ln 7c8179f0 (7c8179c3) kernel32!NlsServerInitialize+0x29 | (7c8179fe) kernel32!AllocTables
2.6 源码命令
如果含有源码信息,可使得调试过程能够以源码模式逐行进行。和源码相关的命令包括下面几个:
源码路径:
和符号路径类似,要设置源码路径,使用如下语法格式:
- .srcpath[+] [路径1;路径2]
不含任何参数的情况下,显示当前设置的源码路径。
下面命令将覆盖原设置,设置新的源码搜索路径:
- .srcpath <路径信息>
使用“+”可以将新的路径添加到原设置中,而不会把原设置覆盖掉:
- .srcpath+ <路径信息>
源码选项
这里列出的源码选项有三个,下面分别来讲。第一个是源码的Noisy选项,语法如下:
- .srcnoisy [1|0]
此命令乃source noisy缩写,可以理解成“嘈杂的源码”,类似于符号设置中也存在的noisy选项。他的三种运用如下所示:
- 状态:.srcnoisy
- 开启:.srcnoisy 1
- 关闭:.srcnoisy 0
开始“吵闹的源码”选项后,在源码加载、卸载,甚至单步的时候,都会显示丰富的源码信息。下图显示了一个含有Noisy信息的单步命令:
第二个命令是行号选项,即在符号文件加载过程中,是否将行号也一并加载进来。因为Windbg支持源码级调试,所以它在Windbg中是默认开启(Enable)的,我们一般也不应该去禁止他。语法如下:
- .lines [-d|-e|-t]
参数d是disable的意思;e是enable的意思;t表示切换的意思,即自动在disable和enable两者之间切换。
最后看第三个命令,是代码行选项,包括行号和内容。语法如下:
- 打开:l+ [选项]
- 关闭:l- [选项]
命令l是line的缩写,和上面的.lines命令不同的是,.lines是加载时选项,l是调试时选项。我建议读者总是调用“l+*”指令,打开所有的行选项,效果会很不错。这样在单步调试的时候,每一步的代码和行号都会显示出来。显得很醒目!
值得注意的是,进入源码模式和进入汇编模式的命令分别为:
- 源码模式:l+t
- 汇编模式:l-t
运行这两个命令和在Windbg的Debug菜单下点击source mode选项其效果是一样的。