【问题标题】:Writing an efficient backtrace function编写高效的回溯函数
【发布时间】:2020-06-24 09:02:33
【问题描述】:

我遇到了下面的步行回溯代码

struct stack_frame {
  struct stack_frame *prev;
    void *return_addr;
} __attribute__((packed));
typedef struct stack_frame stack_frame;

__attribute__((noinline, noclone))
void backtrace_from_fp(void **buf, int size)
{
    int i;
    stack_frame *fp;

    __asm__("movl %%ebp, %[fp]" :  /* output */ [fp] "=r" (fp));

    for(i = 0; i < size && fp != NULL; fp = fp->prev, i++)
        buf[i] = fp->return_addr;
}

寻找此代码背后的原因是我们正在使用第 3 方 malloc 挂钩,因此不想使用再次分配内存的回溯。以上不适用于 x86_64,我将 asm 语句修改为

    __asm__("movl %%rbp, %[fp]" :  /* output */ [fp] "=r" (fp));

我崩溃了

(gdb) bt
#0  backtrace_from_fp (size=10, buf=<optimized out>) at src/tcmalloc.cc:1910
#1  tc_malloc (size=<optimized out>) at src/tcmalloc.cc:1920
#2  0x00007f5023ade58d in __fopen_internal () from /lib64/libc.so.6
#3  0x00007f501e687956 in selinuxfs_exists () from /lib64/libselinux.so.1
#4  0x00007f501e67fc28 in init_lib () from /lib64/libselinux.so.1
#5  0x00007f5029a32503 in _dl_init_internal () from /lib64/ld-linux-x86-64.so.2
#6  0x00007f5029a241aa in _dl_start_user () from /lib64/ld-linux-x86-64.so.2
#7  0x0000000000000001 in ?? ()
#8  0x00007fff22cb8e24 in ?? ()
#9  0x0000000000000000 in ?? ()
(gdb)
(gdb) p $rbp
$2 = (void *) 0x7f501e695f37
(gdb) p (stack_frame *)$rbp
$3 = (stack_frame *) 0x7f501e695f37
(gdb) p *$3
$4 = {prev = 0x69662f636f72702f, return_addr = 0x6d6574737973656c}
(gdb) x /1xw 0x69662f636f72702f
0x69662f636f72702f:     Cannot access memory at address 0x69662f636f72702f
(gdb) fr
#0  backtrace_from_fp (size=10, buf=<optimized out>) at src/tcmalloc.cc:1910
1910    in src/tcmalloc.cc
(gdb)

我错过了什么吗?关于如何通过代码重建相同的任何帮助?

【问题讨论】:

  • 在 64 位模式下需要 movq 而不是 movl:__asm__("movq %%rbp, %[fp]" : /* output */ [fp] "=r" (fp));
  • 如果你使用gcc,你可以使用各种return address builtins
  • 您不能依赖rbp 作为帧指针,因为理智的软件不再使用帧指针。帧指针是(错误的)汇编编程和错误编译器的遗留物,带有错误的调试信息。当您拥有相当好的工具时,没有需要帧指针,因此现代工具链使用rbp 寄存器作为通用寄存器,这样可以加快代码速度而没有任何相关的缺点。跨度>
  • @Jester 即使我也想做同样的事情,但问题是我不确定那里有多少帧,因此在不存在索引的情况下调用这个内置函数会导致崩溃。
  • 它通常会在崩溃之前返回 0,就像您自己的 fp != NULL 条件一样。

标签: c++ assembly gdb x86-64 tcmalloc


【解决方案1】:

我错过了什么吗?

您引用的代码假定编译后的代码使用的是帧指针寄存器链。

直到大约 5-7 年前,这是(32 位)i*86 上的默认设置,并且自 ~forever 以来一直不是x86_64 上的默认设置。

代码很可能在未优化的构建中运行良好,但在 32 位和 64 位 x86 平台上使用非古代版本的编译器进行优化时会惨遭失败。

如果您可以用-fno-omit-frame-pointer 重建所有代码(包括libc),那么这段代码大部分时间都可以工作(但不是所有时间,因为libc 可能有手工编码的程序集,而该程序集不会有帧指针链)。

一种解决方案是使用libunwind。不幸的是,如果您(或您使用的任何库)也使用dlopen,从malloc 内部使用它仍然会遇到problem

【讨论】:

  • 我猜glibc所有的手写asm函数都是叶子函数;它们不是通过回调的任何其他调用链的一部分,它们不调用malloc。示例包括 memcpystrchr 和一些 ISA 的一些 FP 数学函数。
  • re: 避免 malloc: 也许你可以编译libunwind 以使用直接使用原始mmap 的自定义分配器,如果它不需要很快的话。或者一个单独的简单 malloc/free。只要它的分配不需要与真正的free兼容。
  • @PeterCordes 我依稀记得手写的汇编函数调用初始化程序(然后可以调用任何东西)。而libunwind 已经不打电话给malloc。但是它必须调用dl_iterate_phdr,这需要加载器锁,什么时候可以在malloc锁之前获得。血腥细节:lists.nongnu.org/archive/html/libunwind-devel/2010-05/…
  • 也许只是我的选择偏差,因为我只关注了与性能相关但简单的功能。我想不出任何需要初始化查找表但用 asm 编写的东西;例如isalnum 之类的字符分类函数是用 C 语言编写的。但可以肯定的是,可能是这样的。
猜你喜欢
  • 2010-09-21
  • 1970-01-01
  • 1970-01-01
  • 2019-10-04
  • 1970-01-01
  • 1970-01-01
  • 2016-08-04
  • 1970-01-01
  • 2019-10-09
相关资源
最近更新 更多