Win32 - C代码的回溯

50

我目前正在寻找一种方法,在Windows下从C代码(非C ++)获取回溯信息。

我正在构建一个跨平台的C库,具有引用计数内存管理。它还拥有一个集成的内存调试器,提供有关内存错误的信息(XEOS C Foundation Library)。

当出现故障时,调试器会启动,并提供有关故障和所涉及的内存记录的信息。

enter image description here

在Linux或Mac OS X上,我可以查找execinfo.h以使用backtrace函数,以便显示有关内存故障的其他信息。

我在Windows上寻找同样的东西。

我在Stack Overflow上看到了How can one grab a stack trace in C?。 我不想使用第三方库,因此CaptureStackBackTraceStackWalk函数看起来很不错。

唯一的问题是,即使有Microsoft文档,我也不知道如何使用它们。

由于我通常在符合POSIX的系统上工作,因此我不太熟悉Windows编程。

这些函数的一些解释和示例是什么?

编辑

我现在正在考虑使用DbgHelp.lib中的CaptureStackBackTrace函数,因为它似乎有点少开销...

以下是我到目前为止尝试过的:

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
实际上,你链接帖子中给出的答案中提供的文章(http://www.codeproject.com/KB/threads/StackWalker.aspx)也可以用作捕获线程堆栈的指南。它回答了你所有的问题。此外,还有源代码可供你使用,以便理解如何自己完成它。尝试将文章滚动到“Points of Interest”部分。 - bezmax
谢谢您的评论:)还是没有运气... https://dev59.com/w2025IYBdhLWcg3w9qqQ - Macmade
3个回答

56

好的,现在我懂了。: )

问题出在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

有什么想法可以使用相同的方法来获取实际的行号吗?如果有,请在这里回答我的问题:https://dev59.com/OH3aa4cB1Zd3GeqPg7VM - Alexandru
1
感谢您提供的代码!请注意,SymFromAddr 可能会失败(例如,如果 .pdb 文件不存在),而 stack[i] 的地址仍然可能有用(但您没有打印它)。 - Paul
3
SYMBOL_INFO 结构体并不需要放在堆上,它也可以成功地存储在栈中。 - legalize
1
@legalize,需要动态分配才能为Name字段分配额外的空间(否则Name的最大长度将为1)。 - Mark Ingram
1
@AvivCohn #include <dbghelp.h> 并在链接器标志中添加 -lDbgHelp。 是的,我知道我迟到了。 :) - Refugnic Eternium
显示剩余5条评论

4

以下是我用于读取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];
    }

更新

一旦我获得了堆栈,我就开始将其翻译成名称。我通过与C++Builder输出的.map文件进行交叉引用来完成这个过程。尽管格式可能有所不同,但可以使用另一个编译器的映射文件执行相同的操作。以下代码适用于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代码的等效操作。这个第一部分解析map文件(同样,这适用于C++Builder maps,但可以轻松地适应其他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);

这段代码将一个地址(在$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";
                    }

那么我该如何从这些信息中获取函数名呢?我应该使用 SymGetSym() 函数吗? - Macmade
谢谢您的精心编辑:)但是如果没有地图文件怎么办? - Macmade
2
Macmade,总的来说,使用我的方法,你就完了。也许我应该解释一下,我是为我实际分发的东西做这个的,我不想包含调试信息。由于可执行文件不包含调试信息,所以需要某种外部引用。这就是地图文件的作用。尽管如此,如果您已经解码自己的回溯,那么没有理由不使用地图文件。我知道的每个编译器都可以生成一个。在C++Builder中,项目选项中有一个复选框。对于gcc,请安排传递--print-map到ld。VC,则在链接器/调试下。 - Jon Bright
谢谢解释。问题是我正在分发一个包含集成内存调试器的静态库。除了在Windows上的回溯之外,一切都正常运行。 - Macmade
通过CaptureStackBackTrace找到了一种方法。谢谢你的帮助 : ) - Macmade

2

@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()——它不仅适用于当前MSDN中所述的Windows 7及以上版本。微软的文档有点混乱。它已经存在很长时间了。使用线程环境块(TEB),您不需要使用ReadProcessMemory(),因为您知道堆栈的下限和上限。我认为这是最快的方法。
在使用微软编译器时,可以使用GetEBPForStackTrace()。
inline __declspec(naked) UINT_PTR GetEBPForStackTrace()
{
    __asm
        {
        mov eax, ebp
        ret
        }
}

获取当前线程的EBP的简易方法(但只要是当前线程的任何有效的EBP,您都可以将其传递给此循环)。

限制:此方法仅适用于Windows下的x86。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接