Win32-Backtrace desde código C


Actualmente estoy buscando una manera de obtener información de backtrace bajo Windows, desde código C (sin C++).

Estoy construyendo una biblioteca C multiplataforma, con gestión de memoria de recuento de referencias. También tiene un depurador de memoria integrado que proporciona información sobre errores de memoria ( XEOS C Foundation Library ).

Cuando se produce un fallo, se inicia el depurador, proporcionando información sobre el fallo y el registro de memoria involucrado.

introduzca la descripción de la imagen aquí

El Linux o Mac OS X, puedo buscar execinfo.h para usar la función backtrace, por lo que puedo mostrar información adicional sobre el fallo de memoria.

Estoy buscando lo mismo en Windows.

He visto ¿Cómo se puede agarrar un rastro de pila en C? en el Desbordamiento de la Pila. No quiero usar una biblioteca de terceros, por lo que las funciones CaptureStackBackTrace o StackWalk se ven bien.

El único problema es que simplemente no entiendo cómo usarlos, incluso con Microsoft documentación.

No estoy acostumbrado a la programación de Windows, ya que normalmente trabajo en sistemas compatibles con POSIX.

¿Cuáles son algunas explicaciones para esas funciones, y tal vez algunos ejemplos?

EDITAR

Ahora estoy considerando usar la función CaptureStackBackTrace de DbgHelp.lib, ya que parece que hay un poco menos de sobrecarga...

Esto es lo que he intentado hasta ahora:

unsigned int   i;
void         * stack[ 100 ];
unsigned short frames;
SYMBOL_INFO    symbol;
HANDLE         process;

process = GetCurrentProcess();

SymInitialize( process, NULL, TRUE );

frames = CaptureStackBackTrace( 0, 100, stack, NULL );

for( i = 0; i < frames; i++ )
{
    SymFromAddr( process, ( DWORD64 )( stack[ i ] ), 0, &symbol );

    printf( "%s\n", symbol.Name );
}

Solo estoy recibiendo basura. Supongo que debería usar algo más que SymFromAddr.

Author: Community, 2011-04-17

3 answers

Bien, ahora lo tengo. : )

El problema estaba en la estructura SYMBOL_INFO. Necesita ser asignado en el montón, reservando espacio para el nombre del símbolo, e inicializado correctamente.

Aquí está el código final:

void printStack( void );
void printStack( void )
{
     unsigned int   i;
     void         * stack[ 100 ];
     unsigned short frames;
     SYMBOL_INFO  * symbol;
     HANDLE         process;

     process = GetCurrentProcess();

     SymInitialize( process, NULL, TRUE );

     frames               = CaptureStackBackTrace( 0, 100, stack, NULL );
     symbol               = ( SYMBOL_INFO * )calloc( sizeof( SYMBOL_INFO ) + 256 * sizeof( char ), 1 );
     symbol->MaxNameLen   = 255;
     symbol->SizeOfStruct = sizeof( SYMBOL_INFO );

     for( i = 0; i < frames; i++ )
     {
         SymFromAddr( process, ( DWORD64 )( stack[ i ] ), 0, symbol );

         printf( "%i: %s - 0x%0X\n", frames - i - 1, symbol->Name, symbol->Address );
     }

     free( symbol );
}

La salida es:

6: printStack - 0xD2430
5: wmain - 0xD28F0
4: __tmainCRTStartup - 0xE5010
3: wmainCRTStartup - 0xE4FF0
2: BaseThreadInitThunk - 0x75BE3665
1: RtlInitializeExceptionChain - 0x770F9D0F
0: RtlInitializeExceptionChain - 0x770F9D0F
 44
Author: Macmade,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-19 12:15:26

Aquí está mi alternativa de super-low-fi, como se usa para leer pilas de una aplicación de C++ Builder. Este código se ejecuta dentro del propio proceso cuando se bloquea y obtiene una pila en la matriz cs.

    int cslev = 0;
    void* cs[300];
    void* it = <ebp at time of crash>;
    void* rm[2];
    while(it && cslev<300)
    {
            /* Could just memcpy instead of ReadProcessMemory, but who knows if 
               the stack's valid? If  it's invalid, memcpy could cause an AV, which is
               pretty much exactly what we don't want
            */
            err=ReadProcessMemory(GetCurrentProcess(),it,(LPVOID)rm,sizeof(rm),NULL);
            if(!err)
                    break;
            it=rm[0];
            cs[cslev++]=(void*)rm[1];
    }

ACTUALIZAR

Una vez que tengo la pila, voy a traducirla a nombres. Hago esto haciendo referencias cruzadas con el archivo .map que C++Builder genera. Lo mismo se podría hacer con un archivo de mapa de otro compilador, aunque el formato sería algo diferente. El siguiente código funciona para mapas de C++Builder. Esto es de nuevo bastante baja fi y probablemente no la forma canónica MS de hacer las cosas, pero funciona en mi situación. El siguiente código no se entrega a los usuarios finales.

char linbuf[300];
char *pars;
unsigned long coff,lngth,csect;
unsigned long thisa,sect;
char *fns[300];
unsigned int maxs[300];
FILE *map;

map = fopen(mapname, "r");
if (!map)
{
    ...Add error handling for missing map...
}

do
{
    fgets(linbuf,300,map);
} while (!strstr(linbuf,"CODE"));
csect=strtoul(linbuf,&pars,16); /* Find out code segment number */
pars++; /* Skip colon */
coff=strtoul(pars,&pars,16); /* Find out code offset */
lngth=strtoul(pars,NULL,16); /* Find out code length */
do
{
    fgets(linbuf,300,map);
} while (!strstr(linbuf,"Publics by Name"));

for(lop=0;lop!=cslev;lop++)
{
    fns[lop] = NULL;
    maxs[lop] = 0;
}
do
{
    fgets(linbuf,300,map);
    sect=strtoul(linbuf,&pars,16);
    if(sect!=csect)
        continue;
    pars++;
    thisa=strtoul(pars,&pars,16);
    for(lop=0;lop!=cslev;lop++)
    {
        if(cs[lop]<coff || cs[lop]>coff+lngth)
            continue;
        if(thisa<cs[lop]-coff && thisa>maxs[lop])
        {
            maxs[lop]=thisa;
            while(*pars==' ')
                pars++;
            fns[lop] = fnsbuf+(100*lop);
            fnlen = strlen(pars);
            if (fnlen>100)
                fnlen = 100;
            strncpy(fns[lop], pars, 99);
            fns[lop][fnlen-1]='\0';
        }
    }
} while (!feof(map));
fclose(map);

Después de ejecutar este código, el array fns contiene la función de mejor coincidencia del .archivo de mapa.

En mi situación, en realidad tengo la pila de llamadas como se produce por la primera pieza de código que se somete a un script PHP-Hago el equivalente del código C anterior utilizando un pedazo de PHP. Este primer bit analiza el archivo de mapa (De nuevo, esto funciona con mapas de C++Builder, pero podría adaptarse fácilmente a otros formatos de archivo de mapa):

            $file = fopen($mapdir.$app."-".$appversion.".map","r");
            if (!$file)
                    ... Error handling for missing map ...
            do
            {
                    $mapline = fgets($file);
            } while (!strstr($mapline,"CODE"));
            $tokens = split("[[:space:]\:]", $mapline);
            $codeseg = $tokens[1];
            $codestart = intval($tokens[2],16);
            $codelen = intval($tokens[3],16);
            do
            {
                    $mapline = fgets($file);
            } while (!strstr($mapline,"Publics by Value"));
            fgets($file); // Blank
            $addrnum = 0;
            $lastaddr = 0;
            while (1)
            {
                    if (feof($file))
                            break;
                    $mapline = fgets($file);
                    $tokens = split("[[:space:]\:]", $mapline);
                    $thisseg = $tokens[1];
                    if ($thisseg!=$codeseg)
                            break;
                    $addrs[$addrnum] = intval($tokens[2],16);
                    if ($addrs[$addrnum]==$lastaddr)
                            continue;
                    $lastaddr = $addrs[$addrnum];
                    $funcs[$addrnum] = trim(substr($mapline, 16));
                    $addrnum++;
            }
            fclose($file);

Entonces este bit traduce una dirección (en $rowaddr) en una función dada (así como el desplazamiento después de la función):

                    $thisaddr = intval($rowaddr,16);
                    $thisaddr -= $codestart;
                    if ($thisaddr>=0 && $thisaddr<=$codelen)
                    {
                            for ($lop=0; $lop!=$addrnum; $lop++)
                                    if ($thisaddr<$addrs[$lop])
                                            break;
                    }
                    else
                            $lop = $addrnum;
                    if ($lop!=$addrnum)
                    {
                            $lop--;
                            $lines[$ix] = substr($line,0,13).$rowaddr." : ".$funcs[$lop]." (+".sprintf("%04X",$thisaddr-$addrs[$lop]).")";
                            $stack .= $rowaddr;
                    }
                    else
                    {
                            $lines[$ix] = substr($line,0,13).$rowaddr." : external";
                    }
 3
Author: Jon Bright,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-18 10:38:56

@Jon Bright: Dices "quién sabe si la pila es válida...": Bueno, hay una manera de averiguarlo, ya que se conocen las direcciones de la pila. Suponiendo que necesita un seguimiento en el hilo actual, por supuesto:

    NT_TIB*     pTEB = GetTEB();
    UINT_PTR    ebp = GetEBPForStackTrace();
    HANDLE      hCurProc = ::GetCurrentProcess();

    while (
        ((ebp & 3) == 0) &&
        ebp + 2*sizeof(VOID*) < (UINT_PTR)pTEB->StackBase &&
        ebp >= (UINT_PTR)pTEB->StackLimit &&
        nAddresses < nTraceBuffers)
        {
        pTraces[nAddresses++]._EIP = ((UINT_PTR*)ebp)[1];
        ebp = ((UINT_PTR*)ebp)[0];
        }

Mi "GetTEB()" es NtCurrentTeb() de NTDLL.DLL-y no es solo Windows 7 y superior como se indica en el MSDN actual. MS junks hasta la documentación. Estuvo allí durante mucho tiempo. Usando el bloque ThreadEnvironment (TEB), no necesitas ReadProcessMemory () ya que sabes que la pila límite inferior y superior. Asumo que esta es la forma más rápida de hacerlo.

Usando el compilador MS, GetEBPForStackTrace () puede ser

inline __declspec(naked) UINT_PTR GetEBPForStackTrace()
{
    __asm
        {
        mov eax, ebp
        ret
        }
}

Como una forma fácil de obtener EBP del hilo actual (pero puede pasar cualquier EBP válido a este bucle siempre y cuando sea para el hilo actual).

Limitación: Esto es válido para x86 bajo Windows.

 2
Author: chksr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-28 09:01:54