Win32 – 从C代码回溯
我目前正在寻找一种方法来获取Windows下的回溯信息,从C代码(没有C + +)。
我正在构build一个跨平台的C库,带有引用计数内存pipe理。 它还有一个集成的内存debugging器,提供有关内存错误的信息( XEOS C Foundation Library )。
发生故障时,debugging器启动,提供有关故障的信息以及涉及的存储器logging。
在Linux或Mac OS X上,我可以查找execinfo.h
以使用backtrace
function,因此我可以显示有关内存故障的其他信息。
我在Windows上寻找相同的东西。
我见过如何在C中获取堆栈跟踪? 在堆栈溢出。 我不想使用第三方库,所以CaptureStackBackTrace
或StackWalk
function看起来不错。
唯一的问题是,我只是不知道如何使用它们,即使使用Microsoft文档。
我不习惯Windows编程,因为我通常在POSIX兼容系统上工作。
这些function有什么解释,也许是一些例子?
编辑
我现在正在考虑使用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
以外的SymFromAddr
。
好吧,现在我明白了。 :)
问题出在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
这是我的超级低保真替代scheme,用于从C ++ 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'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
一旦我有了堆栈,我就把它翻译成名字。 我通过与C ++ Builder输出的.map
文件进行交叉引用来实现这一点。 同样的事情可以用另一个编译器的映射文件完成,尽pipe格式会有所不同。 以下代码适用于C ++ Builder地图。 这也是相当低调的,可能不是经典的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脚本产生的调用堆栈 – 我使用一段PHP做了上面的C代码的等价物。 这第一位分析地图文件(同样,这与C + + 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"; }
@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()”是NTDLL.DLL中的NtCurrentTeb() – 它不仅仅是Windows 7及更高版本,如当前MSDN所述。 MS垃圾文件。 在那里已经很久了。 使用ThreadEnvironment块(TEB),你不需要ReadProcessMemory(),因为你知道堆栈的下限和上限。 我认为这是最快的方法。
使用MS编译器,GetEBPForStackTrace()可以
inline __declspec(naked) UINT_PTR GetEBPForStackTrace() { __asm { mov eax, ebp ret } }
作为获取当前线程的EBP的简单方法(但只要针对当前线程,您可以将任何有效的EBP传递给此循环)。
限制:这在Windows下适用于x86。