2012-02-24 20 views
5

Estoy trabajando en una clase que me gustaría utilizar para registrar la Pila de llamadas actual en computadoras con Windows Vista/7. (Muy similar a "Caminando la pila de llamadas" http://www.codeproject.com/Articles/11132/Walking-the-callstack).C++ Stack Tracing problema

Primero utilicé RtlCaptureContext para obtener el registro de contexto actual, luego utilicé StackWalk64 para obtener los marcos de pila individuales. Ahora, me di cuenta de que el contador del Programa en STACKFRAME64.AddrPC realmente cambia para una línea de código específica cada vez que cierro mi programa y lo reinicio. Por alguna razón, pensé que la dirección de PC para una línea de código específica se mantendría igual siempre que no cambie el código fuente y lo vuelva a compilar.

Necesito la dirección de PC para usar SymFromAddr y SymGetLineFromAddr64 para obtener información sobre la función llamada, el archivo de código y el número de línea. Lamentablemente, esto solo funciona mientras dure la base de datos de depuración de programa (archivo PDB), pero no puedo proporcionar eso al cliente.

Mi plan era registrar las direcciones de PC de la pila de llamadas (siempre que sea necesario) y luego enviarlas desde el cliente a mí. Para poder usar mis archivos PDB para averiguar qué funciones se llamaron, pero eso solo funciona si las direcciones de PC son identificadores únicos. Como cambian cada vez que inicio el programa, no puedo usar ese enfoque.

¿Conoces una manera mejor de leer la pila de llamadas o de resolver el problema con el contador de programa cambiante?

Creo que una posible solución podría ser obtener siempre la dirección de PC de una ubicación conocida y usarla como referencia para determinar solo el desplazamiento entre diferentes direcciones de PC. Eso parece funcionar, pero no estoy seguro si ese es un método válido y siempre funcionará.

¡Muchas gracias por su ayuda! Publicaré la solución final (encapsulada) en codeproject.com y SI ME GUSTA, diré que me has ayudado.

+1

Check out my aplicación: http://www.dima.to/blog/?p=13 – Alexandru

+0

Según sus comentarios en el blog de su implementación requiere AP-s. –

Respuesta

4

Utilizando el formulario de información CONTEXT puede encontrar sección de función y el desplazamiento de imagen PE. Por ejemplo, puede usar esta información para obtener el nombre de la función del archivo .map generado por el vinculador.

  1. Obtener CONTEXT struct. Usted está interesado en el miembro del contador del programa. Como CONTEXT depende de la plataforma, debe resolverlo usted mismo. Ya lo hace cuando lo inicializa, por ejemplo STACKFRAME64.AddrPC.Offset = CONTEXT.Rip para Windows x64. Ahora comenzamos la caminata a pie y usamos STACKFRAME64.AddrPC.Offset, rellenado por StaclkWalk64 como nuestro punto de partida.

  2. Debe traducirlo a la dirección virtual relativa (RVA) utilizando la dirección base de asignación: RVA = STACKFRAME64.AddrPC.Offset - AllocationBase. Puede obtener AllocationBase usando VirtualQuery.

  3. Una vez que tenga eso, necesita encontrar en qué sección cae este RVA y restar la dirección de inicio de la sección para obtener SectionOffset: SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase. Para hacerlo, debe acceder a la estructura del encabezado de la imagen PE (IMAGE_DOS_HEADER, IMAGE_NT_HEADER, IMAGE_SECTION_HEADER) para obtener el número de secciones en PE y sus direcciones de inicio/finalización. Es bastante sencillo.

Eso es todo. Ahora tiene el número de sección y el desplazamiento en la imagen PE. El desplazamiento de función es el desplazamiento más alto más pequeño que SectionOffset en el archivo .map.

puedo publicar código más tarde, si lo desea.

EDIT: Código para imprimir function address (suponemos una CPU x64 genérico):

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

Aviso, nuestra pila de llamadas es (de adentro hacia afuera) GenerateReport()->Run()->main(). La salida del programa (en mi máquina, ruta es absoluta):

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

Ahora, pila de llamadas en términos de direcciones es (adentro hacia afuera) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521. Comparando los tres primeros desplazamientos a contenido .map archivo:

... 

0001:00002f40  [email protected]@YAXXZ  0000000140003f40 f FMain.obj 
0001:000031e0  [email protected]@YAXXZ    00000001400041e0 f FMain.obj 
0001:00003220  main      0000000140004220 f FMain.obj 

... 

donde 00002f40 es la más cercana a menores compensadas 00002F8D y así sucesivamente. Últimos tres direcciones se refieren a funciones CRT/OS esa llamada main (_tmainCRTstartup etc) - debemos ignorarlos ...

Así, podemos ver que somos capaces de recuperar seguimiento de la pila con la ayuda de .map archivo.Con el fin de generar seguimiento de la pila de excepción lanzada, todo lo que tiene que hacer es colocar el código en GenerateReport() constructor de excepción (de hecho, esta GenerateReport() fue tomada de mi código de excepción personalizada constructor de la clase (una parte de ella que)).

+0

¡Guau, suena muy poderoso y me encantaría ver cómo lo implementarías! Logré crear el archivo .map. Por favor, avíseme si desea ayudarme. – user667967

+0

@ user667967 Se publica el código. – lapk

+1

¡Muchas gracias por su ayuda! ¡Acabo de probar tu código y funciona! – user667967

1

Yo sugeriría mirar a la configuración de su proyecto de Visual Studio: Linker-> Avanzado-> Base aleatorizado Dirección para todos sus programas y archivos DLL dependientes (que puede reconstruidas) y vuelve a intentarlo. Esa es la única cosa que viene a la mente.

Espero que ayude.

+1

Circumventing ASLR no es prudente. –

+0

¡Gracias por la solución rápida y sucia! Lo usaré a corto plazo. – user667967

2

Necesita enviar la asignación de la memoria de ejecución del programa que le dice a la biblioteca/programa de la dirección de base cargado desde el cliente hacia usted.

Luego puede calcular el símbolo con la dirección base.

3

La pila en sí no es suficiente, necesita el mapa de los módulos cargados para que luego pueda asociar cualquier dirección (aleatoria, verdadera) con el módulo y localizar el símbolo de PDB. Pero en realidad está reinventar la rueda, porque hay al menos dos bien soportado fuera de la caja soluciones para resolver este problema:

  • la API de Windows DbgHlp específica minidump: MiniDumpWriteDump. Tu aplicación no debería llamar a esto directamente, sino que debes enviar con un .exe pequeño que todo lo que hace es tomar un volcado de un proceso (ID de proceso dado como argumento) y tu aplicación, cuando encuentra una condición de error, debe iniciar esto. exe y luego waitr para su finalización. La razón es que el proceso de "volcado" congelará el proceso de volcado durante el volcado, por lo que el proceso que se está volcando no puede ser el mismo proceso que el volcado. Este esquema es común con todas las aplicaciones que implementan WER. Sin mencionar que el volcado resultante es un verdadero .mdmp que puedes cargar en WinDbg (o en VisualStudio si te parece bien).

  • la solución plataforma de código abierto cruz: Breakpad. Utilizado por Chrome, Firefox, Picassa y otras aplicaciones conocidas.

Por lo tanto, principalmente, no reinventar la rueda. Como nota al margen, también hay servicios que hacen de valor agregado al reporte de errores, como la agregación, notificaciones, seguimiento y respuestas de los clientes automatizados, como la mencionada WER ofrecido por Microsoft (el código debe ser firmado digitalmente para calificar), airbreak.io, exceptioneer.com , bugcollect.com (este es creado por los tuyos) y otros, pero afaik. solo el WER funciona con aplicaciones nativas de Windows.

+0

+1 porque me gusta esa respuesta tan elaborada. –