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 |
允许从内存的一个绝对地址处读取符号 |