【问题标题】:Win32 - Backtrace from C codeWin32 - C 代码的回溯
【发布时间】:2011-08-07 06:48:07
【问题描述】:

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

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

当发生故障时,调试器会启动,提供有关故障的信息以及所涉及的内存记录。

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

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

我在 Stack Overflow 上见过 How can one grab a stack trace in C?。我不想使用第三方库,所以CaptureStackBackTraceStackWalk 函数看起来不错。

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

我不习惯 Windows 编程,因为我通常在兼容 POSIX 的系统上工作。

对这些函数有什么解释,也许还有一些例子?

编辑

我现在正在考虑使用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以外的其他东西。

【问题讨论】:

  • 实际上,在您的链接帖子 (codeproject.com/KB/threads/StackWalker.aspx) 中作为答案给出的文章也可以用作如何捕获线程堆栈的指南。它为您的所有问题提供答案。此外,还有一个源代码,您可以使用它来了解如何自己做。尝试将文章向下滚动到“兴趣点”部分。
  • 感谢您的评论 :) 仍然没有运气...stackoverflow.com/questions/5705650/…

标签: c windows backtrace


【解决方案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

【讨论】:

  • 知道如何使用相同的方法来获取实际的行号吗?如果是这样,请在这里回答我的问题:stackoverflow.com/questions/22465253/…
  • 感谢代码!请注意,SymFromAddr 可能会失败(例如,如果 .pdb 文件不存在),而stack[i] 的地址可能仍然有用(并且您不打印它)。
  • 没有什么要求SYMBOL_INFO 结构在堆上。它在堆栈上工作得很好。
  • @legalize,它需要动态分配以便为 Name 字段分配额外的大小(否则 Name 的最大长度为 1)。
  • @AvivCohn #include 并将 -lDbgHelp 添加到链接器标志中。是的,我知道我迟到了。 :)
【解决方案2】:

这是我的超低保真替代方案,用于从 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 代码等效的操作。第一个位解析地图文件(同样,这适用于 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";
                    }

【讨论】:

  • 那我如何从这些信息中获取函数名呢?我应该使用SymGetSym() 函数吗?
  • 感谢非常好的编辑 :) 但是如果没有地图文件怎么办?
  • Macmade,从广义上讲,用我的方法,你完蛋了。我也许应该解释说我这样做是为了我实际分发的东西,我不想在其中包含调试信息。由于可执行文件不包含调试信息,因此您需要某种外部参考。这就是地图文件的用途。综上所述,如果您已经在解码自己的回溯,则没有理由不使用地图文件。我知道的每个编译器都可以生成一个。在 C++Builder 中,项目选项中有一个复选框。使用 gcc,安排将 --print-map 传递给 ld。 VC,它在 Linker/Debugging 下。
  • 感谢您的解释。问题是我正在分发一个静态库,其中包含一个集成的内存调试器。除了 Windows 上的回溯之外,一切正常。
  • 找到CaptureStackBackTrace 的方法。感谢您的帮助:)
【解决方案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()”是来自 NTDLL.DLL 的 NtCurrentTeb() - 它不仅是当前 MSDN 中所述的 Windows 7 及更高版本。 MS 将文档丢弃。它在那里待了很长时间。使用 ThreadEnvironment Block (TEB),您不需要 ReadProcessMemory(),因为您知道堆栈的下限和上限。我认为这是最快的方法。

使用MS编译器,GetEBPForStackTrace()可以

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

作为获取当前线程的 EBP 的简单方法(但您可以将任何有效的 EBP 传递给此循环,只要它适用于当前线程)。

限制:这对 Windows 下的 x86 有效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-01-01
    • 1970-01-01
    • 2012-08-18
    • 2011-08-17
    • 1970-01-01
    • 2019-10-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多