44 votes

Win32 - Trace arrière du code C

Je cherche actuellement un moyen d'obtenir des informations de backtrace sous Windows, à partir de code C (pas de C++).

Je suis en train de construire une bibliothèque C multiplateforme, avec une gestion de la mémoire par comptage de références. Elle dispose également d'un débogueur de mémoire intégré qui fournit des informations sur les erreurs de mémoire ( Bibliothèque XEOS C Foundation ).

Lorsqu'une erreur se produit, le débogueur est lancé et fournit des informations sur l'erreur et l'enregistrement de mémoire concerné.

enter image description here

Sous Linux ou Mac OS X, je peux chercher execinfo.h afin d'utiliser le backtrace afin que je puisse afficher des informations supplémentaires sur l'erreur de mémoire.

Je cherche la même chose sous Windows.

J'ai vu Comment peut-on obtenir une trace de pile en C ? sur Stack Overflow. Je ne veux pas utiliser de bibliothèque tierce, donc le fichier CaptureStackBackTrace o StackWalk Les fonctions ont l'air bonnes.

Le seul problème est que je ne sais pas comment les utiliser, même avec la documentation de Microsoft.

Je n'ai pas l'habitude de programmer sous Windows, car je travaille habituellement sur des systèmes POSIX.

Quelles sont les explications de ces fonctions, et peut-être quelques exemples ?

EDITAR

J'envisage maintenant d'utiliser le CaptureStackBackTrace de la fonction DbgHelp.lib Il semble qu'il y ait un peu moins de frais généraux...

Voici ce que j'ai essayé jusqu'à présent :

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

Je ne fais qu'acheter de la camelote. Je suppose que je devrais utiliser autre chose que SymFromAddr .

48voto

Macmade Points 27414

D'accord, j'ai compris : )

Le problème se situait dans la structure SYMBOL_INFO. Elle doit être allouée sur le tas, en réservant de l'espace pour le nom du symbole, et initialisée correctement.

Voici le code 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 sortie est :

6: printStack - 0xD2430
5: wmain - 0xD28F0
4: __tmainCRTStartup - 0xE5010
3: wmainCRTStartup - 0xE4FF0
2: BaseThreadInitThunk - 0x75BE3665
1: RtlInitializeExceptionChain - 0x770F9D0F
0: RtlInitializeExceptionChain - 0x770F9D0F

3voto

Jon Bright Points 6834

Voici mon alternative super low-fi, utilisée pour lire des piles à partir d'une application C++ Builder. Ce code est exécuté dans le processus lui-même lorsqu'il se plante et reçoit une pile dans le tableau 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];
    }

MISE À JOUR

Une fois que j'ai la pile, je la traduis en noms. Pour ce faire, j'utilise les références croisées avec le fichier .map que C++Builder produit. La même chose peut être faite avec un fichier map d'un autre compilateur, bien que le formatage soit quelque peu différent. Le code suivant fonctionne pour les cartes de C++Builder. Il s'agit encore une fois d'une méthode peu sophistiquée et qui n'est probablement pas la méthode canonique de MS, mais elle fonctionne dans mon cas. Le code ci-dessous n'est pas fourni aux utilisateurs finaux.

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

Après l'exécution de ce code, le fns contient la fonction de meilleure correspondance du fichier .map.

Dans mon cas, la pile d'appels produite par le premier morceau de code est soumise à un script PHP - je fais l'équivalent du code C ci-dessus en utilisant un morceau de PHP. Ce premier morceau analyse le fichier de carte (encore une fois, cela fonctionne avec les cartes C++Builder mais pourrait être facilement adapté à d'autres formats de fichier de carte) :

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

Ce bit traduit alors une adresse (en $rowaddr ) dans une fonction donnée (ainsi que le décalage après la fonction) :

                    $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";
                    }

2voto

chksr Points 31

@Jon Bright : Vous dites "qui sait si la pile est valide..." : Eh bien, il y a un moyen de le savoir, puisque les adresses de la pile sont connues. En supposant que vous ayez besoin d'une trace dans le thread actuel, bien sûr :

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

Mon "GetTEB()" est NtCurrentTeb() de NTDLL.DLL - et ce n'est pas seulement Windows 7 et plus comme indiqué dans le MSDN actuel. MS jette la documentation à la poubelle. Elle existait depuis longtemps. En utilisant le ThreadEnvironment Block (TEB), vous n'avez pas besoin de ReadProcessMemory() car vous connaissez les limites inférieure et supérieure de la pile. Je suppose que c'est la façon la plus rapide de procéder.

En utilisant le compilateur MS, GetEBPForStackTrace() peut être

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

pour obtenir facilement l'EBP du thread en cours (mais vous pouvez passer n'importe quel EBP valide à cette boucle tant qu'il s'agit du thread en cours).

Limitation : Ceci est valable pour x86 sous Windows.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X