C++ 堆栈跟踪问题
Posted
技术标签:
【中文标题】C++ 堆栈跟踪问题【英文标题】:C++ Stack Tracing issue 【发布时间】:2012-02-24 02:36:42 【问题描述】:我正在研究一个类,我想用它来记录装有 Windows Vista/7 的计算机上的当前调用堆栈。 (非常类似于“Walking the callstack”http://www.codeproject.com/Articles/11132/Walking-the-callstack)。
首先我使用 RtlCaptureContext 获取当前上下文记录,然后使用 StackWalk64 获取各个堆栈帧。现在,我意识到每当我关闭程序并重新启动它时,STACKFRAME64.AddrPC 中的程序计数器实际上会针对特定的代码行发生变化。出于某种原因,我认为只要我不更改源代码并重新编译它,特定代码行的 PC-Address 将保持不变。
我需要 PC-Address 以使用 SymFromAddr 和 SymGetLineFromAddr64 来获取有关被调用函数、代码文件和行号的信息。不幸的是,只有在存在 Program-Debug-Database (PDB-File) 的情况下才有效,但我不能将其提供给客户端。
我的计划是记录调用堆栈的 PC 地址(在需要时),然后将其从客户端发送给我。这样我就可以使用我的 PDB 文件来找出调用了哪些函数,但当然只有当 PC 地址是唯一标识符时才有效。由于每次启动程序时它们都会更改,因此我无法使用这种方法。
您知道读取调用堆栈或克服程序计数器变化问题的更好方法吗?
我认为一种可能的解决方案是始终获取已知位置的 PC 地址,并将其用作参考来确定不同 PC 地址之间的偏移量。这似乎可行,但我不确定这是否是一种有效的方法并且会一直有效。
非常感谢您的帮助!我将在 codeproject.com 上发布最终(封装)解决方案,如果您愿意,我会说您帮助了我。
【问题讨论】:
查看我的实现:dima.to/blog/?p=13 根据博客中的 cmets,您的实现需要 pdb-s。 【参考方案1】:使用CONTEXT
的信息表格,您可以在PE 图像中找到函数部分和偏移量。例如,您可以使用此信息从链接器生成的 .map 文件中获取函数名称。
获取CONTEXT
结构。您对节目柜台会员感兴趣。由于CONTEXT
依赖于平台,因此您必须自己弄清楚。您在初始化时已经这样做了,例如 STACKFRAME64.AddrPC.Offset = CONTEXT.Rip
用于 x64 Windows。现在我们开始堆栈遍历,并使用STACKFRAME64.AddrPC.Offset
,由StaclkWalk64
填充作为我们的起点。
您需要使用分配基地址将其转换为相对虚拟地址 (RVA):RVA = STACKFRAME64.AddrPC.Offset - AllocationBase
。您可以使用VirtualQuery
获取AllocationBase
。
就是这样。现在你在 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, &::SymGetModuleBase64, 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 lNameModule[ 1024 ];
::GetModuleFileName( reinterpret_cast< ::HMODULE >( lBaseAllocation ), lNameModule, 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 << lNameModule << " : 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
是最接近00002F8D
的较小偏移量,依此类推。最后三个地址是指调用main
(_tmainCRTstartup
等)的 CRT/OS 函数 - 我们应该忽略它们...
所以,我们可以看到我们能够借助.map
文件恢复堆栈跟踪。为了生成抛出异常的堆栈跟踪,您所要做的就是将GenerateReport()
代码放入异常构造函数中(实际上,这个GenerateReport()
是从我的自定义异常类构造函数代码中获取的(它的一部分)) .
【讨论】:
哇,这听起来很强大,我很想看看你会如何实现它!我设法创建了 .map 文件。如果您愿意帮助我,请告诉我。 非常感谢您的帮助!我刚刚测试了你的代码,它可以工作! 对不起,但这似乎对我不起作用。我刚刚将 lTypeMachine 更改为 IMAGE_FILE_MACHINE_I386 并使用 Eip、Ebp、Esp 而不是 Rip、Rbp 和 Rsp,它显示不正确的输出(将地址映射到函数会导致不正确的结果)。你能帮我解决这个问题吗? 看起来这种方法对班级成员不起作用,即使他们被导出。在简单函数的情况下。优化会减少 StackWalk64 检索的堆栈帧数量,因此我得到不正确的堆栈跟踪。 @GrigorAleksanyan,当然,如果一个函数是内联的,它将没有堆栈帧。否则,任何其他函数(无论是否为类成员)都将具有堆栈帧。【参考方案2】:堆栈本身是不够的,您需要加载的模块映射,以便您可以将任何地址(随机,真)与模块相关联并定位 PDB 符号。但是你真的是在重新发明***,因为至少有两种支持良好的开箱即用解决方案可以解决这个问题:
Windows 特定的 DbgHlp 小型转储 API:MiniDumpWriteDump
。你的应用程序不应该直接调用它,而是你应该附带一个很小的 .exe,它所做的一切都需要一个进程的转储(作为参数给出的进程 ID),并且你的应用程序在遇到错误情况时应该启动这个 .exe。 exe 然后等待它的完成。原因是“转储”进程会在转储过程中冻结转储的进程,因此被转储的进程不能与进行转储的进程相同。此方案在所有实现WER 的应用程序中都很常见。更不用说生成的转储是一个真正的 .mdmp,您可以将其加载到 WinDbg(或 VisualStudio,如果您愿意的话)。
跨平台开源解决方案:Breakpad。由 Chrome、Firefox、Picassa 和其他知名应用程序使用。
因此,首先,不要重新发明***。附带说明一下,还有一些服务可以为错误报告增值,例如聚合、通知、跟踪和自动客户端响应,例如微软提供的上述 WER(您的代码必须经过数字签名才能获得资格)airbreak.io ,exceptioneer.com,bugcollect.com(这个是你真正创造的)和其他,但是afaik。只有 WER 可用于原生 Windows 应用程序。
【讨论】:
【参考方案3】:您需要发送程序的运行内存映射,它告诉您从客户端加载的基地址库/程序给您。
然后就可以用基址计算符号了。
【讨论】:
【参考方案4】:我建议查看您的 Visual Studio 项目的设置:Linker->Advanced->Randomized Base Address 用于所有程序和相关 dll (你可以重建),然后再试一次。这是唯一想到的一件事。
希望对您有所帮助。
【讨论】:
绕过 ASLR 是不明智的。 感谢快速而肮脏的解决方案!我会在短期内使用它。以上是关于C++ 堆栈跟踪问题的主要内容,如果未能解决你的问题,请参考以下文章