当使用windbg时,最常用的命令就是'K', 栈回溯。那么是怎么实现栈回溯的呢,下面简单介绍一下。
首先要了解我们所编译出来的EXE或者DLL的调试信息都是包含在PDB文件中的,PDB文件可由编译器来产生。在栈回溯中使用的API都是来自动态库DbgHelp.dll中的,当然你可以显式或者隐式的调用这个DLL,但是这个DLL有着不同的版本,当隐式调用时需要注意。因为我在使用时出现了问题,所以我这里详细介绍一下。当我直接把DbgHelp.dll, DbgHlep.lib和DbgHelp.h直接从windbg目录拷贝到我的工程目录下时,直接编译会出现问题,原因是有些宏和变量没有被定义。当我从前辈的源码中拷贝的这些文件却可以顺利的编译过去。对比一下这些文件,发现很多的不同,是版本的原因造成的。而显式调用却不会存在这些问题,所以我建议显式调用DbgHelp.dll中的文件。
DbgHelp.dll中函数都是以Sym*开头的,为了获得这些调试信息,我们首先要为当前进程初始化符号句柄,利用函数
bool bSuccess = SymInitialize(hProcess, csPath, false)
其中第一个参数就是指当前运行进程的句柄了,第二个参数用分号分隔的几个路径,用来指定那些路径用来搜索符号文件。第三个参数用来表示符号句柄是否为所有的模块自动导入符号文件,这里一般设为false。
接下需要对导入的符号文件进行配置,能够使用的函数是DWORD SymGetOptions()和 DWORD SymSetOptions(DWORD SymOptions), 具体的option可以参考http://msdn.microsoft.com/zh-cn/library/ff558827.aspx,但我们一般包括SYMOPT_LOAD_LINES,里面要包含代码的行的信息。
接下来可以通过
BOOL WINAPI SymGetSearchPath( __in HANDLE hProcess, __out PTSTR SearchPath, __in DWORD SearchPathLength);来得到符号文件的搜索路径。在这里我也有个疑问,因为在使用函数SymInitialize()时的第二个参数是用来设置符号文件的搜索路径的,这两个路径是否是一回事呢?从我目前所观察到的是一样的。
下面介绍一下一个重要的结构体 STACKFRAME64,它的定义如下
typedef struct _tagSTACKFRAME64 { ADDRESS64 AddrPC; ADDRESS64 AddrReturn; ADDRESS64 AddrFrame; ADDRESS64 AddrStack; ADDRESS64 AddrBStore; PVOID FuncTableEntry; DWORD64 Params[4]; BOOL Far; BOOL Virtual; DWORD64 Reserved[3]; KDHELP64 KdHelp; } STACKFRAME64, *LPSTACKFRAME64; 它代表的是一个栈桢, 能得到获得栈信息的地址。在使用时需要进行初始化,过程如下
STACKFRAME64 stack={0};
CONTEXT context; context.ContextFlags = CONTEXT_FULL; GetThreadContext(hThread, &context); // Must be like this stack.AddrPC.Offset = context.Eip; // EIP - Instruction Pointer stack.AddrPC.Mode = AddrModeFlat; stack.AddrFrame.Offset = context.Ebp; // EBP stack.AddrFrame.Mode = AddrModeFlat; stack.AddrStack.Offset = context.Esp; // ESP - Stack Pointer stack.AddrStack.Mode = AddrModeFlat;
初始化这个结构主要用于函数StackWalk64. StackWalk64 就是获得栈回溯的,它的定义如下
StackWalk64
BOOL WINAPI StackWalk64(
__in DWORD MachineType,
__in HANDLE hProcess,
__in HANDLE hThread,
__inout LPSTACKFRAME64 StackFrame,
__inout PVOID ContextRecord,
__in_opt PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine,
__in_opt PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,
__in_opt PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine,
__in_opt PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress
);
倒数第二个和倒数第三个函数指针可以直接调用Dbghelp.dll中的函数SymGetModuleBase64与SymFunctionTableAccess64。 倒数第四个ReadMemoryRoutine ,可以自己定义一个函数,主要调用ReadProcessMemory。这个函数主要得到STACKFRAME64这个栈帧。
然后通过栈帧的成员AddrPC的offset,可由函数SymGetSymFromAddr64得到符号信息,再通过UnDecorateSymbolName(),SymGetLineFromAddr64()()等函数得到具体的函数名,代码行等信息。
大致的栈回溯就是这个过程。