windows - C ++ Stack Tracing problem

Indlæg af Hanne Mølgaard Plasc

Problem



Jeg arbejder på en klasse, som jeg gerne vil bruge til at logge den nuværende Call Stack på computere med Windows Vista/7. (Meget ligner 'Walking the callstack' http://www.codeproject.com/Articles/11132/Walking-the-callstack).[26]


Først brugte jeg RtlCaptureContext til at få den nuværende kontekstpost, så brugte jeg StackWalk64 til at få de enkelte stakrammer. Nu indså jeg, at programtælleren i STACKFRAME64.AddrPC faktisk ændrer sig for en bestemt kode linje, når jeg lukker mit program og starter det igen. Af en eller anden grund troede jeg, at PC-adressen til en bestemt kode linje ville forblive den samme, så længe jeg ikke ændrer kildekoden og genkompilerer den igen.


Jeg har brug for PC-adressen til at bruge SymFromAddr og SymGetLineFromAddr64 for at få oplysninger om den kaldte funktion, kodefil og linjenummer. Desværre fungerer det kun, så længe Program-Debug-Database (PDB-File) er omkring, men jeg må ikke give det til kunden.


Min plan var at registrere pc-adresserne på opkaldsstakken (når det er nødvendigt) og derefter sende det fra klienten til mig. Så jeg kunne bruge mine PDB-filer til at finde ud af hvilke funktioner der blev kaldt, men det fungerer selvfølgelig kun, hvis PC-adresserne er unikke identifikatorer. Da de ændres hver gang jeg starter programmet, kan jeg ikke bruge den tilgang.


Kender du en bedre måde at læse opkaldsstakken på eller for at overvinde problemet med den skiftende programtæller?


Jeg tror, ​​at en mulig løsning kunne være at altid få PC-adressen til et kendt sted og bruge det som reference til kun at bestemme forskydningen mellem forskellige pc-adresser. Det ser ud til at fungere, men jeg er ikke sikker på, om det er en gyldig metode og altid vil fungere.


Mange tak for din hjælp! Jeg vil offentliggøre den endelige (indkapslede) løsning i codeproject.com, og hvis du vil sige, at du hjalp mig.

Bedste reference


Brug af informationsformular CONTEXT kan du finde funktionssektion og offset i PE billede. For eksempel kan du bruge denne info til at få funktionsnavn fra .map-fil genereret af linker.



  1. CONTEXT struct. Du er interesseret i programtæller. Da CONTEXT er platformafhængig, skal du finde ud af det selv. Du gør det allerede, når du initialiserer, for eksempel STACKFRAME64.AddrPC.Offset = CONTEXT.Rip til x64 Windows. Nu begynder vi at gå og bruge STACKFRAME64.AddrPC.Offset, fyldt med StaclkWalk64 som udgangspunkt.

  2. Du skal oversætte den til Relative Virtual Address (RVA) ved hjælp af tildeling base adresse: RVA = STACKFRAME64.AddrPC.Offset - AllocationBase. Du kan få AllocationBase ved hjælp af VirtualQuery.

  3. Når du har det, skal du finde ud af, hvilken sektion denne RVA falder og trække sektions startadresse fra den for at få SectionOffset: SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase. For at gøre det skal du få adgang til PE image header struktur (IMAGE\_DOS\_HEADER, IMAGE\_NT\_HEADER, IMAGE\_SECTION\_HEADER) for at få antallet af sektioner i PE og deres start/slut adresser. Det er ret ligetil.



Det er det. Nu har du sektionsnummer og offset i PE-billede. Funktionsforskydning er den højeste offset mindre end SectionOffset i .map-fil.


Jeg kan post kode senere, hvis du kan lide det.


EDIT: Kode til udskrivning function address (vi antager x64 generisk 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 );
}


Bemærk, vores opkaldsstak er (indvendig ud) GenerateReport()->Run()->main().
Programudgang (på min maskine er stien absolut):


D:WorkC++SourceApplicationPrototype.ConsolePrototype.Console.exe : 0001 : 0000000000002F8D
D:WorkC++SourceApplicationPrototype.ConsolePrototype.Console.exe : 0001 : 00000000000031EB
D:WorkC++SourceApplicationPrototype.ConsolePrototype.Console.exe : 0001 : 0000000000003253
D:WorkC++SourceApplicationPrototype.ConsolePrototype.Console.exe : 0001 : 0000000000007947
C:Windowssystem32kernel32.dll : 0001 : 000000000001552D
C:WindowsSYSTEM32
tdll.dll : 0001 : 000000000002B521
------------------


Nu er call stack i form af adresser (inde ude) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521.
Sammenligning af de første tre forskydninger til .map filindhold:


...

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

...


hvor 00002f40 er nærmest mindre kompenseret til 00002F8D og så videre. De sidste tre adresser henviser til CRT/OS-funktioner, der kalder main (\_tmainCRTstartup osv.) - vi bør ignorere dem ...


Så vi kan se, at vi er i stand til at genoprette stakkespor ved hjælp af .map fil. For at generere staksporing for kastet undtagelse er alt du skal gøre, at placere GenerateReport() kode i undtagelseskonstruktor (faktisk blev denne GenerateReport() taget fra min brugerdefinerede undtagelsesklasse konstruktørkode (en del af det det)).

Andre referencer 1


Stakken selv er ikke nok, du har brug for det indlæste modulkort, så du kan derefter tilknytte enhver adresse (tilfældig, sand) med modulet og finde PDB symbolet. Men du 'genopfinder virkelig hjulet, fordi der er mindst to velunderbyggede løsninger uden for pakken for at løse dette problem:



  • Windows-specifik DbgHlp minidump API: MiniDumpWriteDump. Din app skal ikke kalde dette direkte, men i stedet skal du sende med en lille .exe, at alt det gør, at det tager en dumpning af en proces (proces-ID angivet som argument), og din app skal lancere dette, når der opstår en fejltilstand. exe og derefter waitr for dens færdiggørelse. Årsagen er, at 'dumper' -processen vil fryse den dumpede proces under dumpningen, så processen der dumpes, kan ikke være den samme proces, der tager dumpningen. Denne ordning er fælles med alle apps, der implementerer WER. For ikke at nævne, at det resulterede dump er en sand .mdmp, som du kan indlæse i WinDbg (eller i VisualStudio, hvis det er din fancy). [27] [28]

  • Kryds platform open source løsning: Breakpad. Bruges af Chrome, Firefox, Picassa og andre velkendte apps. [29]



Så primært må du ikke genopfinde hjulet. Som en sidebesked er der også tjenester, der giver værdifuld til fejlrapportering, f.eks. Aggregering, underretninger, sporing og automatiserede klientresponser, som den førnævnte WER, der tilbydes af Microsoft (din kode skal være digitalt underskrevet for at kvalificere), airbreak.io, exceptioneer.com, bugcollect.com (dette er oprettet af dig selv) og andre, men afaik. Kun WER fungerer sammen med native Windows apps. [30] [31] [32]

Andre referencer 2


Du skal sende programmets løbende hukommelseskortlægning, som fortæller dit baseadressebibliotek/program, der er indlæst fra klienten til dig.


Derefter kan du beregne symbolet med basisadressen.

Andre referencer 3


Jeg foreslår at se på dine Visual Studio-projektets indstillinger: Linker-> Advanced-> Randomized Base Address for alle dine programmer og afhængige dlls
(som du kan genopbygge) og prøv igen. Det er den eneste ting, der kommer til at tænke på.


Håber det hjælper.