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

    1234563为此,您需要访问 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, &::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()-&gt;Run()-&gt;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-&gt;000031EB-&gt;00003253-&gt;00007947-&gt;0001552D-&gt;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++ 堆栈跟踪问题的主要内容,如果未能解决你的问题,请参考以下文章

如何使用带有行号信息的 gcc 获取 C++ 的堆栈跟踪?

如何打印 C++ 中捕获的异常的堆栈跟踪和 C++ 中的代码注入

C++ 将堆栈跟踪转储到 *.exe.stackdump

从堆栈跟踪中查找共享库中的源代码行

找不到 android NDK 堆栈跟踪

从核心转储中获取堆栈跟踪