Win32 - Backtrace à partir du code C
je suis actuellement à la recherche d'un moyen pour obtenir des informations backtrace sous Windows, à partir du code C (no C++).
je construis une bibliothèque multiplateformes C, avec gestion de la mémoire de comptage de références. Il dispose également d'un débogueur mémoire intégré qui fournit des informations sur les erreurs de mémoire ( XEOS C Foundation Library ).
Lorsqu'une défaillance se produit, le débogueur est lancé, fournissant des informations sur la défaillance, et le dossier de mémoire impliqués.
sur Linux ou Mac OS X, je peux chercher execinfo.h
pour utiliser la fonction backtrace
, donc je peux afficher des informations supplémentaires sur la faille de mémoire.
Je cherche la même chose sur Windows.
j'ai vu Comment peut-on saisir une trace de la pile en C? sur un Débordement de Pile. 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 comprends pas comment les utiliser, même avec la documentation de Microsoft.
Je ne suis pas habitué à la programmation Windows, car je travaille habituellement sur des systèmes conformes à POSIX.
quelles sont quelques explications pour ces fonctions, et peut-être quelques exemples?
MODIFIER
j'envisage maintenant d'utiliser la fonction CaptureStackBackTrace
de DbgHelp.lib
, car 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( "%sn", symbol.Name );
}
je prends juste de la camelote. Je suppose que je devrais utiliser autre chose que SymFromAddr
.
3 réponses
très bien, maintenant je l'ai. :)
le problème était dans la structure SYMBOL_INFO. Il doit être attribué sur le tas, en réservant 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 );
}
sortie:
6: printStack - 0xD2430
5: wmain - 0xD28F0
4: __tmainCRTStartup - 0xE5010
3: wmainCRTStartup - 0xE4FF0
2: BaseThreadInitThunk - 0x75BE3665
1: RtlInitializeExceptionChain - 0x770F9D0F
0: RtlInitializeExceptionChain - 0x770F9D0F
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 quand il s'écrase 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];
}
UPDATE
une fois que j'ai la pile, je vais la traduire en noms. Je le fais en croisant avec le fichier .map
que C++Builder sort. La même chose pourrait être faite avec un fichier de carte d'un autre compilateur, bien que le formatage serait quelque peu différent. Le code suivant fonctionne pour C++Builder maps. Encore une fois, il s'agit d'une façon assez discrète et probablement pas la façon canonique de faire les choses, mais cela fonctionne dans ma situation. Le code ci-dessous n'est pas livré 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]='"151910920"';
}
}
} while (!feof(map));
fclose(map);
après l'exécution de ce code, le tableau fns
contient la fonction la mieux adaptée de la .fichier cartographique.
dans ma situation, j'ai en fait la pile d'appels comme produit 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 map (encore une fois, cela fonctionne avec C++Builder maps mais pourrait être facilement adapté à d'autres formats de fichier map):
$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);
puis ce bit traduit une adresse (dans $rowaddr
) en une fonction donnée (ainsi que l'offset 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";
}
@Jon Bright: vous dites "qui savait si la pile est valide...": Eh bien, il y a un moyen de le savoir, car les adresses de la pile sont connues. En supposant que vous avez besoin d'une trace dans le fil courant, 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() à partir de NTDLL.DLL-et il n'est pas seulement Windows 7 et plus comme indiqué dans le MSDN actuel. Mme junks up la documentation. Il était là pour une longue période de temps. En utilisant le bloc Threadenvironnement (TEB), vous n'avez pas besoin ReadProcessMemory () comme vous connaissez les limites inférieure et supérieure de la pile. Je suppose que c'est le moyen le plus rapide de le faire.
en utilisant le compilateur MS, GetEBPForStackTrace () peut être
inline __declspec(naked) UINT_PTR GetEBPForStackTrace()
{
__asm
{
mov eax, ebp
ret
}
}
comme moyen facile d'obtenir EBP du thread courant (mais vous pouvez passer n'importe quel EBP valide à cette boucle aussi longtemps qu'il est pour le thread courant).
Limitation: ceci est valable pour x86 sous Windows.