Walking the callstack [译文 part2]

Walking the callstack [译文 part2]

遍历当前线程的callstack

x86的系统上(XP之前),是没有一个直接的API用来获得当前线程的上下文的。当时提倡的做法是在捕获系统异常中来获得。现在在我们的代码中,我们实现了有效的获取上下文的方法。默认的情况下,我们是通过内联汇编代码来获取EIP,ESPEBP的值。如果你想使用我刚才提到的捕获异常的方法来实现的话,那你就需要定义一个CURRENT_THREAD_VIA_EXCEPTION这样的宏。但是我们应该意识到,其实GET_CURRENT_CONTEXT也是一个宏,内部也是使用了捕捉异常的原理。我们的函数都必须要能包含这些宏的声明。

XP开始以及在x64IA64平台上,目前已经有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会被自动查询到),你也可以选择从网络传输中获取这些最新符号,不过推荐前者,这样就不会因为网络故障而出现加载符号失败。

加载程式和符号

为了能成功遍历线程的callstackdbghelp.dll要获得所有被加载模块的信息。所有你需要通过SymLoadModule64这个API来注册所有被加载的模块,在注册之前,第一步是枚举出所有的模块。

win9x之后。利用ToolHelp32_API可以实现这个需求,需要用的API有,CreateToolhelp32SnapShotModule32FirstModule32Next。通常情况下这些API包含在kernel32.dll,但是在win9x的系统上,这些API包含在tlhelp32.dll中,所以在代码中要做分支判断。

如果你是在NT4上干活的话,那么使用ToolHelp32-API只是一个梦想。但是你可以使用PSAPI来取而代之。你需要使用到一下APIEnumProcessModules, GetModuleInformation, GetModuleBaseName, GetModuleFileNameEx


遍历当前线程的callstack

x86的系统上(XP之前),是没有一个直接的API用来获得当前线程的上下文的。当时提倡的做法是在捕获系统异常中来获得。现在在我们的代码中,我们实现了有效的获取上下文的方法。默认的情况下,我们是通过内联汇编代码来获取EIP,ESPEBP的值。如果你想使用我刚才提到的捕获异常的方法来实现的话,那你就需要定义一个CURRENT_THREAD_VIA_EXCEPTION这样的宏。但是我们应该意识到,其实GET_CURRENT_CONTEXT也是一个宏,内部也是使用了捕捉异常的原理。我们的函数都必须要能包含这些宏的声明。

XP开始以及在x64IA64平台上,目前已经有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会被自动查询到),你也可以选择从网络传输中获取这些最新符号,不过推荐前者,这样就不会因为网络故障而出现加载符号失败。

加载程式和符号

为了能成功遍历线程的callstackdbghelp.dll要获得所有被加载模块的信息。所有你需要通过SymLoadModule64这个API来注册所有被加载的模块,在注册之前,第一步是枚举出所有的模块。

win9x之后。利用ToolHelp32_API可以实现这个需求,需要用的API有,CreateToolhelp32SnapShotModule32FirstModule32Next。通常情况下这些API包含在kernel32.dll,但是在win9x的系统上,这些API包含在tlhelp32.dll中,所以在代码中要做分支判断。

如果你是在NT4上干活的话,那么使用ToolHelp32-API只是一个梦想。但是你可以使用PSAPI来取而代之。你需要使用到一下APIEnumProcessModules, GetModuleInformation, GetModuleBaseName, GetModuleFileNameEx


Dbghelp.dll

下面就来随便啰嗦几句dbghelp.dll

1, 首先,在MS,有两个team在负责开发dbghelp.dll,一个是os-team,另一个是debug-team。通常情况下,你会以为windbg提供的dbghelp.dll是最新的版本。但是有个问题就是这两个小组发布的dbghelp.dll的版本是不同的。举个例子来说:xp-sp1dbghelp.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信息。总之,保险起见,不要使用 OSdbghelp.dll,去下载最新的dbghelp.dll来使用。(译者注:我在论坛中看到很多人无法正确遍历栈,都是dbghelp.dll的版本较老造成的。)

3, V 6.5.3 .7版本的dbghelp.dll有个bug,或是说StackWalk64函数的文档发生了变化。文档中描述:

如果STACKFRAME64的两个成员AddrPCAddrFrame没有被初始化就作为参数传给StackWalk64的话,那么这个函数在第一次被调用的时候就会失败。而且,只有当参数MachineType不是IAMGE_FILE_MACHINE_I386的时候,参数ContectRecord才要求被初始化。

但是这个是错误的。在x86上,当你给ContextRecordNULL的时候,并不能获得到callstack。以我的观点,这是比较大的文档改动。现在你既可以通过初始话AddrStack,也可以通过包含EIP,EBP,ESPContextRecord来成功获取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(包含managedunmanaged)并不会返回unmanaged的函数。

你可能感兴趣的:(Walking the callstack [译文 part2])