C ++堆栈跟踪问题

时间:2012-02-24 02:36:43

标签: c++ windows debugging stack-trace callstack

我正在研究一个类,我想用它来记录Windows Vista / 7计算机上的当前Call Stack。 (非常类似于“行走书架”http://www.codeproject.com/Articles/11132/Walking-the-callstack)。

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

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

我的计划是记录调用堆栈的PC地址(只要需要),然后从客户端发送给我。这样我就可以使用我的PDB文件来找出调用了哪些函数,但这当然只有在PC-Addresses是唯一标识符时才有效。因为每次我启动程序时它们都会改变,所以我不能使用这种方法。

您是否知道更好的方法来读取调用堆栈或克服更改程序计数器的问题?

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

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

4 个答案:

答案 0 :(得分:5)

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

  1. 获取CONTEXT结构。您对程序计数器成员感兴趣。由于CONTEXT取决于平台,因此您必须自己解决问题。您在初始化时已经这样做了,例如x64 Windows的STACKFRAME64.AddrPC.Offset = CONTEXT.Rip。现在我们开始堆栈漫步并使用STACKFRAME64.AddrPC.Offset填充StaclkWalk64作为起点。

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

  3. 完成后,您需要找到此RVA落入哪个部分并从中减去部分起始地址以获得SectionOffset:SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase。为此,您需要访问PE映像头结构(IMAGE_DOS_HEADER,IMAGE_NT_HEADER,IMAGE_SECTION_HEADER)以获取PE中的节数及其开始/结束地址。这很简单。

  4. 就是这样。现在您在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()取自我的自定义异常类构造函数代码(某些部分)它)))。

答案 1 :(得分:4)

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

  • Windows特定的DbgHlp minidump API:MiniDumpWriteDump。你的应用程序不应该直接调用它,而是你应该附带一个小的.exe,它只需要一个进程的转储(作为参数给出的进程ID)和你的应用程序,当遇到错误条件时,应该启动它。 exe然后等待完成。原因是'dumper'进程将在转储期间冻结转储进程,因此转储的进程不能与转储进程相同。此方案适用于实施WER的所有应用。更不用说结果转储是一个真正的.mdmp,您可以在WinDbg中加载(或者在VisualStudio中加载,如果这是您的想象)。

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

所以,主要是,不要重新发明轮子。作为旁注,还有一些服务可以对错误报告进行增值,例如聚合,通知,跟踪和自动客户端响应,例如Microsoft提供的上述WER(您的代码必须经过数字签名以符合条件),{{3 }},airbreak.ioexceptioneer.com(这个是真正由你创造的)和其他,但是afaik。只有WER适用于原生Windows应用程序。

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

我建议您查看Visual Studio项目的设置: Linker-&gt; Advanced-&gt; Randomized Base Address ,用于所有程序和相关dll (你可以重建)然后再试一次。这是我想到的唯一一件事。

希望有所帮助。