web-dev-qa-db-fra.com

Win32 - Retour sur le code C

Je suis actuellement à la recherche d'un moyen d'obtenir des informations de trace sous Windows, à partir du code C (pas de C++).

Je construis une bibliothèque C multiplateforme, avec gestion de la mémoire de comptage de références. Il dispose également d'un débogueur de mémoire intégré qui fournit des informations sur les erreurs de mémoire ( XEOS C Foundation Library ).

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

enter image description here

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

Je cherche la même chose sur Windows.

J'ai vu Comment peut-on récupérer une trace de pile en C? sur Stack Overflow. Je ne veux pas utiliser une bibliothèque tierce, donc les fonctions CaptureStackBackTrace ou StackWalk semblent bonnes.

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

Je ne suis pas habitué à la programmation Windows, car je travaille habituellement sur des systèmes compatibles POSIX.

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

[~ # ~] modifier [~ # ~]

J'envisage maintenant d'utiliser la fonction CaptureStackBackTrace de DbgHelp.lib, comme il semble, il y a 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 fais juste de la camelote. Je suppose que je devrais utiliser autre chose que SymFromAddr.

44
Macmade

D'accord, maintenant je l'ai. :)

Le problème était dans la structure SYMBOL_INFO. Il doit être alloué sur le tas, réserver de l'espace pour le nom du symbole et initialisé 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
48
Macmade

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

METTRE À JOUR

Une fois que j'ai la pile, je vais la traduire en noms. Je le fais en croisant avec le .map fichier généré par C++ Builder. La même chose pourrait être faite avec un fichier map d'un autre compilateur, bien que la mise en forme soit quelque peu différente. Le code suivant fonctionne pour les cartes C++ Builder. C'est encore une fois assez bas-fi et probablement pas la façon canonique de faire MS, mais cela fonctionne dans ma situation. 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 avoir exécuté ce code, le tableau fns contient la fonction la mieux adaptée du fichier .map.

Dans ma situation, j'ai en fait la pile d'appels produite par le premier morceau de code soumis à un script PHP - je fais l'équivalent du code C ci-dessus en utilisant un morceau de PHP. Ce premier bit 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);

Ensuite, ce bit traduit 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";
                    }
3
Jon Bright

@Jon Bright: Vous dites "qui savait si la pile est valide ...": Eh bien, il existe un moyen de le savoir, car les adresses de 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 supérieur comme indiqué dans le MSDN actuel. MS jonque la documentation. C'était là depuis longtemps. À l'aide du bloc ThreadEnvironment (TEB), vous n'avez pas besoin de ReadProcessMemory () car vous connaissez la limite inférieure et supérieure de la pile. Je suppose que c'est le moyen le plus rapide de le faire.

À l'aide du compilateur MS, GetEBPForStackTrace () peut être

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

comme moyen facile d'obtenir l'EBP du thread actuel (mais vous pouvez passer n'importe quel EBP valide à cette boucle tant qu'il l'est pour le thread actuel).

Limitation: ceci est valable pour x86 sous Windows.

2
chksr