C ++堆栈跟踪问题

我正在开发一个我想用于在Windows Vista / 7的计算机上logging当前调用堆栈的类。 (非常类似于“漫游呼叫” http://www.codeproject.com/Articles/11132/Walking-the-callstack )。

首先,我使用RtlCaptureContext来获取当前的上下文logging,然后使用StackWalk64来获取各个堆栈帧。 现在,我意识到在STACKFRAME64.AddrPC中的程序计数器实际上会随着特定的代码行而改变,只要我closures程序并重新启动。 出于某种原因,我认为只要不更改源代码并重新编译它,特定代码行的PC地址就会保持不变。

我需要PC-Address使用SymFromAddr和SymGetLineFromAddr64来获取有关被调用函数,代码文件和行号的信息。 不幸的是,只有在程序debugging数据库(PDB-File)时才有效,但我不能将其提供给客户端。

我的计划是logging调用堆栈的PC地址(无论何时需要),然后将其从客户端发送给我。 所以,我可以使用我的PDB文件来找出哪些函数被调用,但当然只有当PC地址是唯一标识符时才起作用。 由于每次启动程序都会改变,所以我不能使用这种方法。

你知道更好的方法来读取调用堆栈或改变程序计数器的问题吗?

我认为一个可能的解决scheme可能是始终获取已知位置的PC地址,并将其用作参考,以仅确定不同PC地址之间的偏移量。 这似乎工作,但我不知道,如果这是一个有效的方法,并将始终工作。

非常感谢您的帮助! 我将在codeproject.com上发布最终(封装)的解决scheme,如果你愿意,我会说你帮助了我。

Solutions Collecting From Web of "C ++堆栈跟踪问题"

使用信息形式CONTEXT你可以在PE图像中找到功能部分和偏移量。 例如,您可以使用此信息从链接器生成的.map文件获取函数名称。

  1. 获取CONTEXT结构。 您对计划会员感兴趣。 由于CONTEXT是依赖于平台的,所以你必须自己弄明白。 你在初始化的时候就已经做了,例如STACKFRAME64.AddrPC.Offset = CONTEXT.Rip for x64 Windows。 现在我们开始栈走,并使用STACKFRAME64.AddrPC.Offset ,由StaclkWalk64填充作为我们的起点。

  2. 您需要使用分配基地址将其转换为相对虚拟地址(RVA): RVA = STACKFRAME64.AddrPC.Offset - AllocationBase 。 您可以使用VirtualQuery获取AllocationBase

  3. 一旦你有了这个,你需要找到这个RVA SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase部分,并从中减去部分的开始地址来得到SectionOffset: SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase 。 为了做到这一点,你需要访问PE图像头结构(IMAGE_DOS_HEADER,IMAGE_NT_HEADER,IMAGE_SECTION_HEADER)来获得PE中的段数和它们的开始/结束地址。 这非常简单。

而已。 现在在PE图像中有段号和偏移量。 函数偏移量是在.map文件中小于SectionOffset的最高偏移量。

如果你喜欢,我可以稍后发布代码。

编辑:代码打印function address (我们假设x64通用CPU):

 #include <iostream> #include <windows.h> #include <dbghelp.h> void GenerateReport( void ) { ::CONTEXT lContext; ::ZeroMemory( &lContext, sizeof( ::CONTEXT ) ); ::RtlCaptureContext( &lContext ); ::STACKFRAME64 lFrameStack; ::ZeroMemory( &lFrameStack, sizeof( ::STACKFRAME64 ) ); lFrameStack.AddrPC.Offset = lContext.Rip; lFrameStack.AddrFrame.Offset = lContext.Rbp; lFrameStack.AddrStack.Offset = lContext.Rsp; lFrameStack.AddrPC.Mode = lFrameStack.AddrFrame.Mode = lFrameStack.AddrStack.Mode = AddrModeFlat; ::DWORD lTypeMachine = IMAGE_FILE_MACHINE_AMD64; for( auto i = ::DWORD(); i < 32; i++ ) { if( !::StackWalk64( lTypeMachine, ::GetCurrentProcess(), ::GetCurrentThread(), &lFrameStack, lTypeMachine == IMAGE_FILE_MACHINE_I386 ? 0 : &lContext, nullptr, &::SymFunctionTableAccess64, &::SymGetmoduleeBase64, nullptr ) ) { break; } if( lFrameStack.AddrPC.Offset != 0 ) { ::MEMORY_BASIC_INFORMATION lInfoMemory; ::VirtualQuery( ( ::PVOID )lFrameStack.AddrPC.Offset, &lInfoMemory, sizeof( lInfoMemory ) ); ::DWORD64 lBaseAllocation = reinterpret_cast< ::DWORD64 >( lInfoMemory.AllocationBase ); ::TCHAR lNamemodulee[ 1024 ]; ::GetmoduleeFileName( reinterpret_cast< ::HMODULE >( lBaseAllocation ), lNamemodulee, 1024 ); PIMAGE_DOS_HEADER lHeaderDOS = reinterpret_cast< PIMAGE_DOS_HEADER >( lBaseAllocation ); PIMAGE_NT_HEADERS lHeaderNT = reinterpret_cast< PIMAGE_NT_HEADERS >( lBaseAllocation + lHeaderDOS->e_lfanew ); PIMAGE_SECTION_HEADER lHeaderSection = IMAGE_FIRST_SECTION( lHeaderNT ); ::DWORD64 lRVA = lFrameStack.AddrPC.Offset - lBaseAllocation; ::DWORD64 lNumberSection = ::DWORD64(); ::DWORD64 lOffsetSection = ::DWORD64(); for( auto lCnt = ::DWORD64(); lCnt < lHeaderNT->FileHeader.NumberOfSections; lCnt++, lHeaderSection++ ) { ::DWORD64 lSectionBase = lHeaderSection->VirtualAddress; ::DWORD64 lSectionEnd = lSectionBase + max( lHeaderSection->SizeOfRawData, lHeaderSection->Misc.VirtualSize ); if( ( lRVA >= lSectionBase ) && ( lRVA <= lSectionEnd ) ) { lNumberSection = lCnt + 1; lOffsetSection = lRVA - lSectionBase; break; } } std::cout << lNamemodulee << " : 000" << lNumberSection << " : " << reinterpret_cast< void * >( lOffsetSection ) << std::endl; } else { break; } } } void Run( void ); void Run( void ) { GenerateReport(); std::cout << "------------------" << std::endl; } int main( void ) { ::SymSetOptions( SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS ); ::SymInitialize( ::GetCurrentProcess(), 0, 1 ); try { Run(); } catch( ... ) { } ::SymCleanup( ::GetCurrentProcess() ); return ( 0 ); } 

注意,我们的调用堆栈是(从里到外) GenerateReport()->Run()->main() 。 程序输出(在我的机器上,路径是绝对的):

 D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000002F8D D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 00000000000031EB D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000003253 D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000007947 C:\Windows\system32\kernel32.dll : 0001 : 000000000001552D C:\Windows\SYSTEM32\ntdll.dll : 0001 : 000000000002B521 ------------------ 

现在,调用堆栈的地址是(内向外) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521 。 将前三个偏移量与.map文件内容进行比较:

 ... 0001:00002f40 ?GenerateReport@@YAXXZ 0000000140003f40 f FMain.obj 0001:000031e0 ?Run@@YAXXZ 00000001400041e0 f FMain.obj 0001:00003220 main 0000000140004220 f FMain.obj ... 

其中00002f40最靠近00002f40偏移较小,依此类推。 最后三个地址是指调用main_tmainCRTstartup等)的CRT / OS函数 – 我们应该忽略它们…

所以,我们可以看到,我们能够借助.map文件恢复堆栈跟踪。 为了生成引发异常的堆栈跟踪,你所要做的就是将GenerateReport()代码放入异常构造函数(事实上,这个GenerateReport()是从我自定义的异常类构造函数代码中取得的(它的一部分)) 。

堆栈本身是不够的,你需要加载模块映射,这样你就可以将任何地址(随机,真)与模块相关联,并找到PDB符号。 但是你真的在重新发明轮子,因为至少有两个很好的支持即用型解决方案来解决这个问题:

  • Windows特定的DbgHlp小型转储API: MiniDumpWriteDump 。 你的应用程序不应该直接调用这个,而是你应该附带一个很小的.exe文件,只要遇到一个错误情况,它就会把一个进程的转储(作为参数给出的进程ID)和你的应用程序发送出去。然后等待其完成。 原因是“倾销”过程会在转储过程中冻结转储过程,所以转储过程不可能是转储的过程。 这个方案在所有实现WER的应用程序中都很常见。 更不用说,结果转储是一个真正的.mdmp,你可以加载在WinDbg(或VisualStudio,如果这是你的幻想)。

  • 跨平台的开源解决方案: Breakpad 。 由Chrome,Firefox,Picassa和其他知名应用程序使用。

所以,主要是不要重新发明轮子。 另外还有一些服务可以为错误报告提供增值服务,例如汇总,通知,跟踪和自动化客户端响应(例如Microsoft提供的WER(您的代码必须经过数字签名以符合资格), airbreak.io , exceptioneer.com , bugcollect.com (这个是由你自己创建的)和其他的,但是afaik。 只有WER与本地Windows应用程序一起工作。

你需要发送程序的运行内存映射,告诉你从客户端加载到你的基地址库/程序。

然后你可以用基地址来计算符号。

我建议看看你的Visual Studio项目的设置:链接器 – >高级 – >随机基地址为所有程序和依赖dll(你可以重建),然后再试一次。 这是唯一想到的一件事情。

希望有所帮助。