为什么我的堆栈上会有奇怪的函数名?(关于符号的讨论)
符号可以包含有关全局变量、局部变量、函数名、参数、结构和源行号的信息。符号有三种类型:导出符号、pdb符号(公共符号)和专用pdb符号(专用符号)。
导出符号是dll本身的一部分。例如,ntdll.dll和kernel32.dll将其函数的很大一部分公开为导出符号,因此可以将它们称为API,但是在进程中发现的大多数dll都有一组非常小的导出符号。通常导出符号不包含函数的参数信息,并且由于很少有函数以这种方式公开,因此当只有导出符号时,您不能真正依赖堆栈的有效性。
公共符号包含一些基本符号,如函数名和全局变量,但同样,并非所有函数名都在公共符号中公开。dll的开发人员选择公开什么作为公共符号,因此他/她可以隐藏任何他们认为会泄露太多有关实现信息的内容。私人符号包含第一段中列出的几乎所有内容
调试时,通过将dll/exe链接到符号文件的GUID,符号与相应的dll或exe匹配。这意味着,如果符号搜索路径中有多个ntdll.pdb,调试器将知道哪个对应于特定版本的ntdll.dll。搜索路径由.sympath指定,除sympath中列出的内容外,调试器还将查找加载dll的目录以及环境变量_NT_SYMBOL_PATH中给定的路径中的任何内容。
如果符号是错误的的时候会发生什么?
让我们看看这个带有mscorsvr.dll公共符号的堆栈:
54 Id: 62c.1590 Suspend: 1 Teb: 7ffa2000 Unfrozen
ChildEBP RetAddr Args to Child
1212ef44 7c59a030 00000090 00000000 1212ef64 ntdll!NtWaitForSingleObject+0xb [i386\usrstubs.asm @ 2004]
1212ef6c 7c57b3db 00000090 00009c40 00000000 kernel32!WaitForSingleObjectEx+0x71 [D:\nt\private\windows\base\client\synch.c @ 1309]
1212ef7c 791b578b 00000090 00009c40 00000000 kernel32!WaitForSingleObject+0xf [D:\nt\private\windows\base\client\synch.c @ 1217]
1212efa0 791dbe6e 00000000 00000000 00000000 mscorsvr!ThreadpoolMgr::WorkerThreadStart+0x3a
1212ffb4 7c57b388 0d406838 00000002 00000000 mscorsvr!ThreadpoolMgr::intermediateThreadProc+0x44
1212ffec 00000000 791dbe2d 0d406838 00000000 kernel32!BaseThreadStart+0x52 [D:\nt\private\windows\base\client\support.c @ 460]
我们正在创建一个工作线程,然后坐下来等待工作。让我们看一看同一个堆栈,其中包含用于mscorsvr的导出符号:
0:054> kb ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. 1212ef6c 7c57b3db 00000090 00009c40 00000000 ntdll!NtWaitForSingleObject+0xb *** ERROR: Symbol file could not be found. Defaulted to export symbols for mscorsvr.dll - 1212efa0 791dbe6e 00000000 00000000 00000000 kernel32!WaitForSingleObject+0xf 1212ffb4 7c57b388 0d406838 00000002 00000000 mscorsvr!GetCompileInfo+0x8e99 1212ffec 00000000 791dbe2d 0d406838 00000000 kernel32!lstrcmpiW+0xb
调试器很好,告诉我们找不到mscorsvr的符号文件,但是它给了我们一个函数名(从导出符号中),所以看起来我们正在调用一个名为GetCompileInfo的函数,嗯…很奇怪,它为什么选择这个名称?
如果我们要列出符号(导出mscorsvr的符号),我们会得到一个如下所示的列:
0:054>x mscorsvr!*
791b0000 mscorsvr!Ordinal73=791b0000 mscorsvr!Ordinal76=791b0000 mscorsvr!Ordinal77=791b0000 mscorsvr!Ordinal75=791b0000 mscorsvr!Ordinal78=791b0000 mscorsvr!Ordinal71=791b0000 mscorsvr!Ordinal79=791b0000 mscorsvr!Ordinal74=791b0000 mscorsvr!Ordinal72=791d2fd5 mscorsvr!GetCompileInfo= 791e0920 mscorsvr!GetAssemblyMDImport
再谈FPO
在调试程序的过程中,可能遇到过一两次“FPO”这个词。FPO是指在x86上处理编译器如何访问本地变量和基于堆栈的参数的编译器优化的一个特定类型。对于使用局部变量(和/或基于堆栈的参数)的函数,编译器需要一种机制来引用堆栈上的这些值。通常,这是通过以下两种方式之一完成的:
- 直接从堆栈指针(esp)访问局部变量。这是启用FPO优化时的行为。虽然这不需要单独的寄存器来跟踪局部变量和参数的位置,但如果禁用了FPO优化,这会使生成的代码稍微复杂一些。特别是,由于函数调用或修改堆栈的其他指令等原因,esp中局部变量和参数的位移实际上会随着函数的执行而改变。因此,编译器必须在引用基于堆栈的值的函数中的每个位置跟踪当前esp值的实际位移。对于编译器来说,这通常不是什么大问题,但是在手工编写的汇编程序中,这可能会变得有点棘手。
- 指定一个寄存器指向堆栈上相对于局部变量和基于堆栈的参数的固定位置,并使用此寄存器访问局部变量和参数。这是禁用FPO优化时的行为。约定是使用ebp寄存器访问局部变量和堆栈参数。Ebp通常设置为第一个堆栈参数可以在[Ebp+08]中找到,而局部变量通常位于Ebp的负位移处。
禁用FPO优化的函数的典型情况可能如下所示:
push ebp ; save away old ebp (nonvolatile) mov ebp, esp ; load ebp with the stack pointer sub esp, sizeoflocals ; reserve space for locals ... ; rest of function
主要的概念是禁用FPO优化,一个函数将立即保存ebp(作为第一个接触堆栈的操作),然后用当前堆栈指针加载ebp。这时的堆栈布局:
[ebp-01] Last byte of the last local variable [ebp+00] Old ebp value [ebp+04] Return address [ebp+08] First argument...
此后,函数将始终使用ebp访问局部变量和基于堆栈的参数。(函数的汇编序列可能会有一些变化,特别是使用变化的函数设置初始SEH帧时,但相对于ebp,堆栈布局的最终结果始终相同。)
这确实(如前所述)使得ebp寄存器不可用于其他用途。但是,相对于打开FPO优化后编译的函数,此性能影响通常不足以成为一个大问题。此外,有许多情况下要求函数使用帧指针:
- 任何使用SEH的函数都必须使用帧指针,因为当发生异常时,无法从异常分派时的esp值(堆栈指针)中知道局部变量的位移(异常可能发生在任何地方,而诸如进行函数调用或为函数调用设置堆栈参数之类的操作会修改esp的值。
- 任何使用析构函数的C++对象都必须使用SEH来编译解压缩支持。这意味着大多数C++函数最终都被禁用了FPO优化。(可以改变编译器关于SEH异常和C++解卷的假设,但是默认的(和推荐的设置)是在出现SEH异常时取消对象。)
- 任何使用alloca在堆栈上动态分配内存的函数都必须使用一个帧指针(因此禁用了FPO优化),因为esp对局部变量和参数的位移可以在运行时更改,编译器在生成代码时不知道。
由于这些限制,您可能正在编写的许多函数将已经禁用FPO优化,而没有显式地将其关闭。但是,仍有可能许多不符合上述条件的函数启用了FPO优化,因此不使用ebp引用局部变量和堆栈参数。
既然您已经大致了解了FPO优化的功能,那么我将在本系列的下半部分介绍为什么在调试某些类问题时全局关闭FPO优化对您有利。(事实上,大多数微软系统代码也会关闭FPO,因此您可以放心,已经在FPO和非FPO优化代码之间进行了真正的成本效益分析,在一般情况下禁用FPO优化总体上更好。)
考虑下面的示例程序,其中有几个不做任何事情的函数,这些函数将堆栈参数乱放并相互调用。(在本文中,我禁用了全局优化和函数内联。)
__declspec(noinline) void f3( int* c, char* b, int a ) { *c = a * 3 + (int)strlen(b); __debugbreak(); } __declspec(noinline) int f2( char* b, int a ) { int c; f3( &c, b + 1, a - 3); return c; } __declspec(noinline) int f1( int a, char* b ) { int c; c = f2( b, a + 10); c ^= (int)rand(); return c + 2 * a; } int __cdecl wmain( int ac, wchar_t** av ) { int c; c = f1( (int)rand(), "test"); printf("%d\\n", c); return 0; }
将windbg与.dmp文件关联
如果您厌倦了启动调试器、加载转储文件、设置sympath、加载扩展名等,这里有一个很好的方法,可以在.dmp文件的上下文菜单上获取“调试此转储文件”,并自动加载所有您喜欢的命令。
首先创建一个包含以下内容的.reg文件(在修改注册表时一定要非常小心)
Windows Registry Editor Version 5.00[HKEY_CLASSES_ROOT\.dmp]
@="Debugger.Dump"[HKEY_CLASSES_ROOT\Debugger.Dump]
[HKEY_CLASSES_ROOT\Debugger.Dump\DefaultIcon]
@="c:\\debuggers\\cdb.exe"[HKEY_CLASSES_ROOT\Debugger.Dump\Shell]
[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_Without_Remote]
@="Debug This Dump"[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_Without_Remote\Command]
@="\"C:\\debuggers\\windbg\" -z \"%1\" -c \"$<c:\\debuggers\\commands.txt\""[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_With_Remote70]
@="Debug this Dump With Remote:70"[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_With_Remote70\Command]
@="\"C:\\debuggers\\windbg\" -server tcp:port=70 -z \"%1\" -c \"$<c:\\debuggers\\commands.txt\""
Windbg常用命令系列---!stl
!stl
!stl扩展命令显示一些已知的标准模板库(STL)的模板。
语法
!stl [Options] Template
!stl -?
参数
- Options
- 可以包含任意的下面这些选项:
- -v
- 显示详细输出。
- -V
- 显示更加详细的输出,例如包含特定函数何时被调用以及何时返回这样的扩展命令执行过程的信息。
- Template
- 指定要显示的模板的名字。
- -?
- 在调试器命令窗口中显示简要的命令帮助文本。
DLL
Windows 2000 | 不可用 |
Windows XP和之后 | Exts.dll |
注释
只有在调试器的详细输出模式打开之后,详细输出的选项才能起作用。
该扩展命令当前支持的STL模板包括:string、 wstring、vector<string>、vector<wstring>、list<string>、list<wstring>、以及任何指向前面这几个类型的指针。
在windbg调试.net时遇到的问题
调试.net应用程序时,有时会在windbg中收到错误消息。以下是我最常遇到的几个问题。
Failed to start stack walk---启动堆栈遍历失败
如果你运行sos命令!clrstack在线程上显示.net堆栈,而此线程是.net线程,但它当前未运行任何.net代码,sos将提示失败“Failed to start stack walk: 80004005.”。这并不意味着进程或调试器有任何问题。这仅仅意味着sos不能显示堆栈,因为没有堆栈。如下
Unable to walk the managed stack---无法遍历托管堆栈
如果你在本机线程(即没有对应的System.Threading.thread的线程)上运行!clrstack,sos将显示以下消息。
Following frames may be wrong---以下帧可能错误
如果windbg无法解析某个符号,它将在第一次遇到该符号时出错,告诉您找不到该符号文件,并且在随后的所有时间遇到该符号时,它都将向您发出警告,告诉您它无法正确展开堆栈。当您看到此消息时,这意味着您在警告说明下面看到的任何内容都可能不正确,因此您无法从此处信任堆栈。例如,在本例中,我们可以看到堆栈正在等待输入关键节,DataLayer.dll中的某个方法正在尝试输入关键节,但是,由于我们没有DataLayer.dll的适当符号,因此无法确定它是否是DllUnregisterServer方法。实际上,DllUnregisterServer很可能只是最后一个导出的符号名,否则我们将处于0x43fb的偏移量,这意味着此方法将非常长。
我们不仅不知道这是否是正确的方法名,而且当windbg没有正确的符号时,它甚至可能会丢失堆栈帧,所以您根本不应该信任这个堆栈。
Failed to load data access DLL, 0x80004005---加载数据访问DLL失败
此错误意味着a加载的sos.dll版本不正确,即在本例中,我在1.1转储中加载了2.0版本,或b)找不到正确的mscordacwks.dll版本。如果是选项b,请尝试运行!sym noise 和 run.cordell-ve-u-l.当然,正如错误消息中提到的,确保有一个完整的转储而不是一个小型转储。