简体   繁体   English

C ++堆栈跟踪问题

[英]C++ Stack Tracing issue

I am working on a class which I would like to use to log the current Call Stack on computers with Windows Vista/7. 我正在研究一个类,我想用它来记录Windows Vista / 7计算机上的当前Call Stack。 (Very similar to “Walking the callstack” http://www.codeproject.com/Articles/11132/Walking-the-callstack ). (非常类似于“行走书架” http://www.codeproject.com/Articles/11132/Walking-the-callstack )。

First I used RtlCaptureContext to get the current context record then I used StackWalk64 to get the individual stack frames. 首先,我使用RtlCaptureContext获取当前上下文记录,然后使用StackWalk64获取单个堆栈帧。 Now, I realized that the Program counter in STACKFRAME64.AddrPC actually changes for a specific code line whenever I close my program and start it again. 现在,我意识到每当我关闭程序并重新启动时,STACKFRAME64.AddrPC中的程序计数器实际上会针对特定代码行进行更改。 For some reason I thought that the PC-Address for a specific code line would stay the same as long as I don't change the source code and recompile it again. 出于某种原因,我认为只要我不更改源代码并重新编译它,特定代码行的PC地址就会保持不变。

I need the PC-Address to use SymFromAddr and SymGetLineFromAddr64 to get information about the called function, code file and line number. 我需要PC-Address使用SymFromAddr和SymGetLineFromAddr64来获取有关被调用函数,代码文件和行号的信息。 Unfortunately that only works as long as the Program-Debug-Database (PDB-File) is around, but I am not allowed to provide that to the client. 不幸的是,只有程序调试数据库(PDB-File)存在才能工作,但我不允许将其提供给客户端。

My plan was to record the PC-Addresses of the call stack (whenever it is needed) and then send it from the client to me. 我的计划是记录调用堆栈的PC地址(只要需要),然后从客户端发送给我。 So that I could use my PDB-Files to find out which functions were called but that of course only works if the PC-Addresses are unique identifiers. 这样我就可以使用我的PDB文件来找出调用了哪些函数,但这当然只有在PC-Addresses是唯一标识符时才有效。 Since they change every time I start the program, I cannot use that approach. 由于每次启动程序时它们都会改变,我不能使用这种方法。

Do you know a better way to read the call stack or to overcome the problem with the changing program counter? 您是否知道更好的方法来读取调用堆栈或克服更改程序计数器的问题?

I think one possible solution could be to always get the PC-Address of a known location and use that as a reference to determine only the offset between different PC-Addresses. 我认为一种可能的解决方案可能是始终获取已知位置的PC地址并将其用作参考来仅确定不同PC地址之间的偏移量。 That appears to work, but I am not sure if that is a valid method and will always work. 这似乎有效,但我不确定这是否是一个有效的方法,并将始终有效。

Thank you very much for your help! 非常感谢您的帮助! I will publish the final (encapsulated) solution in codeproject.com and IF YOU LIKE I will say that you helped me. 我将在codeproject.com上发布最终(封装的)解决方案,如果你喜欢,我会说你帮了我。

Using information form CONTEXT you can find function section and offset in PE image. 使用信息表格CONTEXT可以在PE图像中找到功能部分和偏移。 For example, you can use this info to get function name from .map file generated by linker. 例如,您可以使用此信息从链接器生成的.map文件中获取函数名称。

  1. Get CONTEXT struct. 获取CONTEXT结构。 You are interested in program counter member. 您对程序计数器成员感兴趣。 Since CONTEXT is platform-dependent, you have to figure it out for yourself. 由于CONTEXT取决于平台,因此您必须自己解决。 You do it already when you initialize, for example STACKFRAME64.AddrPC.Offset = CONTEXT.Rip for x64 Windows. 初始化时已经这样做了,例如对于x64 Windows, STACKFRAME64.AddrPC.Offset = CONTEXT.Rip Now we start stack walk and use STACKFRAME64.AddrPC.Offset , filled by StaclkWalk64 as our starting point. 现在我们开始堆栈遍历并使用STACKFRAME64.AddrPC.Offset ,由StaclkWalk64填充作为我们的起点。

  2. You need to translate it to Relative Virtual Address (RVA) using allocation base address: RVA = STACKFRAME64.AddrPC.Offset - AllocationBase . 您需要使用分配基址将其转换为相对虚拟地址(RVA): RVA = STACKFRAME64.AddrPC.Offset - AllocationBase You can get AllocationBase using VirtualQuery . 您可以使用VirtualQuery获取AllocationBase

  3. Once you have that, you need to find into which Section this RVA falls and subtract section start address from it to get SectionOffset: SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase . 一旦你有了这个,你需要找到这个RVA落入哪个部分并从中减去部分起始地址以获得SectionOffset: SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase In order to do that you need to access PE image header structure (IMAGE_DOS_HEADER, IMAGE_NT_HEADER, IMAGE_SECTION_HEADER) to get number of sections in PE and their start/end addresses. 为此,您需要访问PE映像头结构(IMAGE_DOS_HEADER,IMAGE_NT_HEADER,IMAGE_SECTION_HEADER)以获取PE中的节数及其开始/结束地址。 It's pretty straightforward. 这很简单。

That's it. 而已。 Now you have section number and offset in PE image. 现在您在PE图像中有节号和偏移量。 Function offset is the highest offset smaller than SectionOffset in .map file. 函数偏移量是小于.map文件中SectionOffset的最高偏移量。

I can post code later, if you like. 如果你愿意,我可以稍后发布代码。

EDIT: Code to print function address (we assume x64 generic CPU): 编辑:打印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 );
}

Notice, our call stack is (inside out) GenerateReport()->Run()->main() . 注意,我们的调用栈是(由内向外) GenerateReport()->Run()->main() Program output (on my machine, path is absolute): 程序输出(在我的机器上,路径是绝对的):

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
------------------

Now, call stack in terms of addresses is (inside out) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521 . 现在,就地址而言,调用堆栈是(内向外) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521 Comparing first three offsets to .map file content: 将前三个偏移量与.map文件内容进行比较:

...

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

...

where 00002f40 is closest smaller offset to 00002F8D and so on. 其中00002f40最接近较小的偏移00002F8D等。 Last three addresses refer to CRT/OS functions that call main ( _tmainCRTstartup etc) - we should ignore them... 最后三个地址是指调用main_tmainCRTstartup等)的CRT / OS函数 - 我们应该忽略它们......

So, we can see that we are able to recover stack trace with help of .map file. 因此,我们可以看到我们能够借助.map文件恢复堆栈跟踪。 In order to generate stack trace for thrown exception, all you have to do is to place GenerateReport() code into exception constructor (in fact, this GenerateReport() was taken from my custom exception class constructor code (some part of it it) ) . 为了生成抛出异常的堆栈跟踪,您所要做的就是将GenerateReport()代码放入异常构造函数中(实际上,这个GenerateReport()取自我的自定义异常类构造函数代码(它的某些部分)) 。

The stack itself is not enough, you need the loaded modules map so that then you can associate any address (random, true) with the module and locate the PDB symbol. 堆栈本身是不够的,您需要加载的模块映射,以便您可以将任何地址(随机,真)与模块关联并找到PDB符号。 But you're really reinventing the wheel, because there are at least two well supported out-of-the-box solutions to solve this problem: 但是你真的在重新发明轮子,因为至少有两个很好的支持开箱即用的解决方案来解决这个问题:

  • the Windows specific DbgHlp minidump API: MiniDumpWriteDump . Windows特定的DbgHlp minidump API: MiniDumpWriteDump You app should not call this directly, but instead you should ship with a tiny .exe that all it does it take sa dump of a process (process ID given as argument) and your app, when encounters an error condition, should launch this .exe and then waitr for its completion. 你的应用程序不应该直接调用它,而是你应该附带一个小的.exe,它只需要一个进程的转储(作为参数给出的进程ID)和你的应用程序,当遇到错误条件时,应该启动它。 exe然后等待完成。 The reason is that the 'dumper' process will freeze the dumped process during the dump, so the process being dumped cannot be the same process taking the dump. 原因是'dumper'进程将在转储期间冻结转储进程,因此转储的进程不能与转储进程相同。 This scheme is common with all apps that implement WER . 此方案适用于所有实施WER的应用程序。 Not to mention that the resulted dump is a true .mdmp that you can load in WinDbg (or in VisualStudio if that's your fancy). 更不用说结果转储是一个真正的.mdmp,您可以在WinDbg中加载(或者在VisualStudio中加载,如果这是您的想象)。

  • the cross platform open-source solution: Breakpad . 跨平台开源解决方案: Breakpad Used by Chrome, Firefox, Picassa and other well known apps. 由Chrome,Firefox,Picassa和其他知名应用使用。

So, primarily, don't reinvent the wheel. 所以,主要是,不要重新发明轮子。 As a side note, there are also services that do value-add to error reporting, like aggregation, notifications, tracking and automated client responses, like the aforementioned WER offered by Microsoft (your code must be digitally signed to qualify), airbreak.io , exceptioneer.com , bugcollect.com (this one is create by yours truly) and other, but afaik. 作为旁注,还有一些服务可以对错误报告进行增值,例如聚合,通知,跟踪和自动客户端响应,例如Microsoft提供的上述WER(您的代码必须经过数字签名才能获得资格), airbreak.ioexceptioneer.combugcollect.com (这个是由你真正创造的)和其他,但afaik。 only the WER works with native Windows apps. 只有WER适用于本机Windows应用程序。

You need send the program's running memory mapping which tells your the base address library/program loaded from client to you. 您需要发送程序的运行内存映射,该映射告诉您从客户端加载的基本地址库/程序。

Then you can calculate the symbol with the base address. 然后,您可以使用基址计算符号。

I would suggest looking at your Visual Studio project's settings: Linker->Advanced->Randomized Base Address for all your programs and dependent dlls (that you can rebuilt) and try again. 我建议您查看Visual Studio项目的设置: Linker-> Advanced-> Randomized Base Address,用于所有程序和相关的dll(可以重建),然后重试。 That is the only one thing that comes to mind. 这是我想到的唯一一件事。

Hope that helps. 希望有所帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM