Walking the callstack [译文 part2]
遍历当前线程的callstack
在x86的系统上(在XP之前),是没有一个直接的API用来获得当前线程的上下文的。当时提倡的做法是在捕获系统异常中来获得。现在在我们的代码中,我们实现了有效的获取上下文的方法。默认的情况下,我们是通过内联汇编代码来获取EIP,ESP和EBP的值。如果你想使用我刚才提到的捕获异常的方法来实现的话,那你就需要定义一个CURRENT_THREAD_VIA_EXCEPTION
这样的宏。但是我们应该意识到,其实
GET_CURRENT_CONTEXT
也是一个宏,内部也是使用了捕捉异常的原理。我们的函数都必须要能包含这些宏的声明。
从
XP
开始以及在
x64
与
IA64
平台上,目前已经有
API
来获得当前线程的上下文,就是
RtlCaptureContext
.。
演示代码7
StackWalker sw;
sw.ShowCallstack();
在同一个进程内遍历其他线程的callstack(略)
遍历另一个进程内的某线程callstack(略)
(译者注:由于时间原因,上述两部分的翻译暂时省略了,内容也比较简单,只是调用了StackWalker的不同构造函数)
重用StackWalk的实例
重用StackWalk的实例是没有任何问题的,只要你想在同一个进程内遍历callstack。如果你重复多次用到callstack的遍历,我强烈你推荐重用一个实例。原因很简单:当你创建一个新的实例的时候,symbol文件就要被重新加载一次,这个是非常耗时的。而且多个StackWalk跨线程工作也是不可靠的,因为dbghelp.dll不是线程安全的。综上,在一个进程中保持只有一个StackWalker实例是最合理的做法。
Symbol的搜索路径
通常情况下,Symbol的搜索路径(SymBuildPath 和 SymUseSymSrv)主要是用来搜索这个文件dbghelp.dll。这个路径通常包含一下目录:
1, szSymPath是否提供是可选择的,如果提供的话,那么SymBuildPath会自动生成。在szSymPath中每个路径之间要用分号“;”来分开。
2, 当前工作目录
3, 可执行文件的目录,如exel
4, _NT_SYMBOL_PATH
的环境变量
5, _NT_ALTERNATE_SYMBOL_PATH
的环境变量
6, SYSTEMROOT
的环境变量
7, SYSTEMROOT\system32
的环境变量
8, MS符号服务器SRV*%SYSTEMDRIVE%\websymbols*http://msdl.microsoft.com/download/symbols
符号服务器
如果你想使用MS的公共信号服务器,你可以选择安装windbg(这样symsrv.dll和最新dbghelp.dll会被自动查询到),你也可以选择从网络传输中获取这些最新符号,不过推荐前者,这样就不会因为网络故障而出现加载符号失败。
加载程式和符号
为了能成功遍历线程的callstack,dbghelp.dll要获得所有被加载模块的信息。所有你需要通过SymLoadModule64这个API来注册所有被加载的模块,在注册之前,第一步是枚举出所有的模块。
在win9x之后。利用ToolHelp32_API可以实现这个需求,需要用的API有,CreateToolhelp32SnapShot,Module32First和Module32Next。通常情况下这些API包含在kernel32.dll,但是在win9x的系统上,这些API包含在tlhelp32.dll中,所以在代码中要做分支判断。
如果你是在NT4上干活的话,那么使用ToolHelp32-API只是一个梦想。但是你可以使用PSAPI来取而代之。你需要使用到一下API:EnumProcessModules
, GetModuleInformation
, GetModuleBaseName
, GetModuleFileNameEx
。
遍历当前线程的callstack
在x86的系统上(在XP之前),是没有一个直接的API用来获得当前线程的上下文的。当时提倡的做法是在捕获系统异常中来获得。现在在我们的代码中,我们实现了有效的获取上下文的方法。默认的情况下,我们是通过内联汇编代码来获取EIP,ESP和EBP的值。如果你想使用我刚才提到的捕获异常的方法来实现的话,那你就需要定义一个CURRENT_THREAD_VIA_EXCEPTION
这样的宏。但是我们应该意识到,其实
GET_CURRENT_CONTEXT
也是一个宏,内部也是使用了捕捉异常的原理。我们的函数都必须要能包含这些宏的声明。
从
XP
开始以及在
x64
与
IA64
平台上,目前已经有
API
来获得当前线程的上下文,就是
RtlCaptureContext
.。
演示代码7
StackWalker sw;
sw.ShowCallstack();
在同一个进程内遍历其他线程的callstack(略)
遍历另一个进程内的某线程callstack(略)
(译者注:由于时间原因,上述两部分的翻译暂时省略了,内容也比较简单,只是调用了StackWalker的不同构造函数)
重用StackWalk的实例
重用StackWalk的实例是没有任何问题的,只要你想在同一个进程内遍历callstack。如果你重复多次用到callstack的遍历,我强烈你推荐重用一个实例。原因很简单:当你创建一个新的实例的时候,symbol文件就要被重新加载一次,这个是非常耗时的。而且多个StackWalk跨线程工作也是不可靠的,因为dbghelp.dll不是线程安全的。综上,在一个进程中保持只有一个StackWalker实例是最合理的做法。
Symbol的搜索路径
通常情况下,Symbol的搜索路径(SymBuildPath 和 SymUseSymSrv)主要是用来搜索这个文件dbghelp.dll。这个路径通常包含一下目录:
1, szSymPath是否提供是可选择的,如果提供的话,那么SymBuildPath会自动生成。在szSymPath中每个路径之间要用分号“;”来分开。
2, 当前工作目录
3, 可执行文件的目录,如exel
4, _NT_SYMBOL_PATH
的环境变量
5, _NT_ALTERNATE_SYMBOL_PATH
的环境变量
6, SYSTEMROOT
的环境变量
7, SYSTEMROOT\system32
的环境变量
8, MS符号服务器SRV*%SYSTEMDRIVE%\websymbols*http://msdl.microsoft.com/download/symbols
符号服务器
如果你想使用MS的公共信号服务器,你可以选择安装windbg(这样symsrv.dll和最新dbghelp.dll会被自动查询到),你也可以选择从网络传输中获取这些最新符号,不过推荐前者,这样就不会因为网络故障而出现加载符号失败。
加载程式和符号
为了能成功遍历线程的callstack,dbghelp.dll要获得所有被加载模块的信息。所有你需要通过SymLoadModule64这个API来注册所有被加载的模块,在注册之前,第一步是枚举出所有的模块。
在win9x之后。利用ToolHelp32_API可以实现这个需求,需要用的API有,CreateToolhelp32SnapShot,Module32First和Module32Next。通常情况下这些API包含在kernel32.dll,但是在win9x的系统上,这些API包含在tlhelp32.dll中,所以在代码中要做分支判断。
如果你是在NT4上干活的话,那么使用ToolHelp32-API只是一个梦想。但是你可以使用PSAPI来取而代之。你需要使用到一下API:EnumProcessModules
, GetModuleInformation
, GetModuleBaseName
, GetModuleFileNameEx
。
Dbghelp.dll
下面就来随便啰嗦几句dbghelp.dll
1, 首先,在MS,有两个team在负责开发dbghelp.dll,一个是os-team,另一个是debug-team。通常情况下,你会以为windbg提供的dbghelp.dll是最新的版本。但是有个问题就是这两个小组发布的dbghelp.dll的版本是不同的。举个例子来说:xp-sp1的dbghelp.dll版本是5.1.2600.1106( 2002-08-29 )。但是debug-team发布的 6.0.0017 .0版本时间却是2002-04-31。(译者注:寒,MS也会犯这种错误)。这样版本的发布就会有冲突,所以很难通过版本好来确定哪个更好,更有效。
2, 从Winme/W2k开始,system32目录下面的dbghelp.dll文件是受保护的。所以如果你想成功遍历callstack,,最好去下载个最新版本的dbghelp.dll放在你的exe目录下面。否则在W2k上会导致一个问题,就是,如果你想遍历一个用VC7+编译的工程就会出错。因为VC7+的编译器生成的PDB格式文件不能被dbghelp.dll识别,这样你就不会得到有效的callstack信息。总之,保险起见,不要使用 OS的dbghelp.dll,去下载最新的dbghelp.dll来使用。(译者注:我在论坛中看到很多人无法正确遍历栈,都是dbghelp.dll的版本较老造成的。)
3, V 6.5.3 .7版本的dbghelp.dll有个bug,或是说StackWalk64函数的文档发生了变化。文档中描述:
如果STACKFRAME64的两个成员AddrPC和AddrFrame没有被初始化就作为参数传给StackWalk64的话,那么这个函数在第一次被调用的时候就会失败。而且,只有当参数MachineType不是IAMGE_FILE_MACHINE_I386的时候,参数ContectRecord才要求被初始化。
但是这个是错误的。在x86上,当你给ContextRecord传NULL的时候,并不能获得到callstack。以我的观点,这是比较大的文档改动。现在你既可以通过初始话AddrStack,也可以通过包含EIP,EBP,ESP的ContextRecord来成功获取callstack。
Stackwalker的操作开关
你可以按照自己的需求来定义操作开关
演示代码7
typedef enum StackWalkOptions
{
// No addition info will be retrived
// (only the address is available)
RetrieveNone = 0,
// Try to get the symbol-name
RetrieveSymbol = 1,
// Try to get the line for this symbol
RetrieveLine = 2,
// Try to retrieve the module-infos
RetrieveModuleInfo = 4,
// Also retrieve the version for the DLL/EXE
RetrieveFileVersion = 8,
// Contains all the abouve
RetrieveVerbose = 0xF,
// Generate a "good" symbol-search-path
SymBuildPath = 0x10,
// Also use the public Microsoft-Symbol-Server
SymUseSymSrv = 0x20,
// Contains all the abouve "Sym"-options
SymAll = 0x30,
// Contains all options (default)
OptionsAll = 0x 3F
} StackWalkOptions;
使用须知
1, NT/Win9x:这个工程只支持StackWalk64这个API。如果你想在NT4/win9x上使用的话,你需要重新配置dbghelp.dll。
2, 当前工程在遍历过程中只支持ANSI名称符,(译者注:C++中没看到过有人用中文命名的函数名,但java大有人在),当然,如果你也可以选择以unicode的编码方式来编译工程来解决中文函数名的问题。
3, 在NT4/win9x的平台上,用“OpenThread”来打开远程线程是不支持的,如果你想实现,请参考Remote Library。
4, 遍历混合模式的callstack(包含managed和unmanaged)并不会返回unmanaged的函数。