Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 574
user667967 Avatar asked Feb 24 '12 02:02

user667967


2 Answers

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

like image 55
lapk Avatar answered Sep 22 '22 00:09

lapk


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. 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. 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. 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. This scheme is common with all apps that implement 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).

  • the cross platform open-source solution: Breakpad. Used by Chrome, Firefox, Picassa and other well known apps.

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. only the WER works with native Windows apps.

like image 20
Remus Rusanu Avatar answered Sep 21 '22 00:09

Remus Rusanu