Win32 - Backtrace из кода C
В настоящее время я ищу способ получить обратную информацию в Windows, начиная с кода C (без С++).
Я создаю кросс-платформенную библиотеку C с управлением памятью с подсчетом ссылок. Он также имеет встроенный отладчик памяти, который предоставляет информацию о ошибках памяти (XEOS C Foundation Library).
При возникновении ошибки отладчик запускается, предоставляя информацию о сбое и записи в памяти.
![enter image description here]()
В Linux или Mac OS X я могу искать execinfo.h
, чтобы использовать функцию backtrace
, поэтому я могу отображать дополнительную информацию об ошибке памяти.
Я ищу то же самое в Windows.
Я видел Как можно захватить трассировку стека в C? в Stack Overflow. Я не хочу использовать стороннюю библиотеку, поэтому функции CaptureStackBackTrace
или StackWalk
выглядят хорошо.
Единственная проблема заключается в том, что я просто не понимаю, как их использовать, даже с документацией Microsoft.
Я не привык к программированию Windows, поскольку я обычно работаю над совместимыми с POSIX системами.
Каковы некоторые объяснения этих функций и, возможно, некоторые примеры?
ИЗМЕНИТЬ
Теперь я рассматриваю возможность использования функции CaptureStackBackTrace
из DbgHelp.lib
, так как кажется, что немного меньше накладных расходов...
Вот что я пробовал до сих пор:
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 );
}
Я просто становлюсь барахлом. Думаю, я должен использовать что-то еще, чем SymFromAddr
.
Ответы
Ответ 1
Хорошо, теперь я понял.:)
Проблема была в структуре SYMBOL_INFO. Он должен быть выделен в куче, зарезервировать место для имени символа и правильно инициализироваться.
Здесь последний код:
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 );
}
Выход:
6: printStack - 0xD2430
5: wmain - 0xD28F0
4: __tmainCRTStartup - 0xE5010
3: wmainCRTStartup - 0xE4FF0
2: BaseThreadInitThunk - 0x75BE3665
1: RtlInitializeExceptionChain - 0x770F9D0F
0: RtlInitializeExceptionChain - 0x770F9D0F
Ответ 2
Здесь моя супер-низко-финская альтернатива, используемая для чтения стеков из приложения С++ Builder. Этот код выполняется в самом процессе, когда он сбой и получает стек в массив 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 valid? If it 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];
}
ОБНОВЛЕНИЕ
Как только у меня есть стек, я перехожу к его переводу в имена. Я делаю это путем перекрестной ссылки на файл .map
, который выдает С++ Builder. То же самое можно сделать с файлом карты из другого компилятора, хотя форматирование будет несколько иным. Следующий код работает для карт С++ Builder. Это опять-таки довольно низко-fi и, вероятно, не канонический способ MS делать вещи, но он работает в моей ситуации. Код ниже не доставляется конечным пользователям.
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);
После запуска этого кода массив fns
содержит функцию наилучшего соответствия из файла .map.
В моей ситуации у меня на самом деле есть стек вызовов, созданный первой частью кода, отправляемой на PHP script. Я делаю эквивалент кода C выше, используя часть PHP. Этот первый бит анализирует файл карты (опять же, это работает с картами С++ Builder, но может быть легко адаптировано к другим форматам файлов карт):
$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);
Затем этот бит преобразует адрес (в $rowaddr
) в заданную функцию (а также смещение после функции):
$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: вы говорите: "Кто знает, действителен ли стек...": Ну, есть способ узнать, как известны адреса стека. Предполагая, что вам нужна трассировка в текущем потоке, конечно:
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];
}
Мой "GetTEB()" является NtCurrentTeb() из NTDLL.DLL - и это не только Windows 7 и выше, как указано в текущем MSDN. MS объединяет документацию. Это было там долгое время. Используя Блок ThreadEnvironment (TEB), вам не требуется ReadProcessMemory(), поскольку вы знаете нижний и верхний предел стека. Я предполагаю, что это самый быстрый способ сделать это.
Используя компилятор MS, GetEBPForStackTrace() может быть
inline __declspec(naked) UINT_PTR GetEBPForStackTrace()
{
__asm
{
mov eax, ebp
ret
}
}
как простой способ получить EBP текущего потока (но вы можете передать любой действительный EBP в этот цикл, если он предназначен для текущего потока).
Ограничение: Это справедливо для x86 под Windows.