C ++堆栈跟踪问题 [英] C++ Stack Tracing issue

查看:165
本文介绍了C ++堆栈跟踪问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用一个类,我想用它来记录当前的呼叫堆栈在Windows Vista / 7的电脑上。 (非常类似于Walking the callstack http://www.codeproject.com/Articles/11132/Walking -the-callstack )。



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



我需要PC地址使用SymFromAddr和SymGetLineFromAddr64来获取有关被调用函数,代码文件和行号的信息。不幸的是,只有程序调试数据库(PDB文件),只有工作,但我不能提供给客户端。



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



你知道一个更好的方法来读取调用堆栈或克服变化的问题程序计数器?



我认为一个可能的解决方案可能是总是获得一个已知位置的PC地址,并使用它作为参考来确定不同PC之间的偏移地址。这似乎工作,但我不知道这是一个有效的方法,将始终工作。



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

解决方案

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


  1. 获取 CONTEXT struct。您对程序计数器成员感兴趣。由于 CONTEXT 是平台相关的,你必须自己弄明白。您在初始化时已经执行它,例如对于x64 Windows, STACKFRAME64.AddrPC.Offset = CONTEXT.Rip 。现在我们开始堆栈步,并使用 STACKFRAME64.AddrPC.Offset 作为我们的起点填充 StaclkWalk64 >


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


  3. SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase code>。为了做到这一点,你需要访问PE映像头结构(IMAGE_DOS_HEADER,IMAGE_NT_HEADER,IMAGE_SECTION_HEADER)以获取PE中的段数及其开始/结束地址。这很简单。


就是这样。现在PE图像中有截面编号和偏移。函数偏移量是最小偏移量小于.map文件中的SectionOffset。



我可以稍后发布代码。



编辑:要打印的代码函数地址(我们假设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 = 1FrameStack.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))
{
断裂;
}
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 1HeaderDOS = reinterpret_cast< PIMAGE_DOS_HEADER>(lBaseAllocation);
PIMAGE_NT_HEADERS 1HeaderNT = reinterpret_cast< PIMAGE_NT_HEADERS>(lBaseAllocation + 1HeaderDOS-> e_lfanew);
PIMAGE_SECTION_HEADER lHeaderSection = IMAGE_FIRST_SECTION(1HeaderNT);
:: DWORD64 lRVA = lFrameStack.AddrPC.Offset -lBaseAllocation;
:: DWORD64 lNumberSection = :: DWORD64();
:: DWORD64 lOffsetSection = :: DWORD64();

for(auto lCnt = :: DWORD64(); lCnt< lHeaderNT-> FileHeader.NumberOfSections; lCnt ++,1HeaderSection ++)
{
:: DWORD64 lSectionBase = 1HeaderSection- > VirtualAddress;
:: DWORD64 lSectionEnd = lSectionBase + max(1HeaderSection-> SizeOfRawData,1HeaderSection-> Misc.VirtualSize);
if((lRVA> = lSectionBase)&&(lRVA< = 1SectionEnd))
{
lNumberSection = lCnt + 1;
lOffsetSection = lRVA - lSectionBase;
break;
}
}
std :: cout<< lNameModule<< :000<< lNumberSection<< :<< reinterpret_cast< void *>(lOffsetSection) std :: endl;
}
else
{
断点;
}
}
}

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);
}



注意,我们的调用栈是(inside out) 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 \\\
tdll.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 ()取自我的自定义异常类构造函数代码(它的一部分)。)


I am working on a class which I would like to use to log the current Call Stack on computers with Windows Vista/7. (Very similar to "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. 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. 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.

I need the PC-Address to use SymFromAddr and SymGetLineFromAddr64 to get information about the called function, code file and line number. 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.

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

解决方案

Using information form CONTEXT you can find function section and offset in PE image. For example, you can use this info to get function name from .map file generated by linker.

  1. Get CONTEXT struct. You are interested in program counter member. Since CONTEXT is platform-dependent, you have to figure it out for yourself. You do it already when you initialize, for example STACKFRAME64.AddrPC.Offset = CONTEXT.Rip for x64 Windows. Now we start stack walk and use STACKFRAME64.AddrPC.Offset, filled by StaclkWalk64 as our starting point.

  2. You need to translate it to Relative Virtual Address (RVA) using allocation base address: RVA = STACKFRAME64.AddrPC.Offset - AllocationBase. You can get AllocationBase using VirtualQuery.

  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. 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. It's pretty straightforward.

That's it. Now you have section number and offset in PE image. Function offset is the highest offset smaller than SectionOffset in .map file.

I can post code later, if you like.

EDIT: Code to print function address (we assume x64 generic 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(). 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. Comparing first three offsets to .map file content:

...

 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. Last three addresses refer to CRT/OS functions that call main (_tmainCRTstartup etc) - we should ignore them...

So, we can see that we are able to recover stack trace with help of .map file. 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) ) .

这篇关于C ++堆栈跟踪问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆