【问题标题】:GDB - How does it know about function calling stack?GDB - 它如何知道函数调用堆栈?
【发布时间】:2016-11-04 14:37:18
【问题描述】:

使用gdb调试汇编程序时,bt会打印调用堆栈。

问题是:

  • (a) gdb 是否根据 rbp 存储在当前函数寄存器中的值以及以前 rbp 值在堆栈中的值知道这一点?
  • 如果是,(b-1) gdb 如何根据rbp 值知道它是哪个函数? (b-2) 当编译时指定-g 选项时,堆栈基数和函数之间的映射是否存储在可执行文件中? (b-3) 以及如何使用readelf 读取映射数据?哪一部分?
  • 如果不是,(c) 那么gdb如何跟踪函数调用栈呢?

【问题讨论】:

  • rbp 值是堆栈地址,并且没有与功能代码地址/名称的 1:1 映射。您必须为此查看退货地址。罗斯的回答解释了如何在回溯时找到堆栈上的返回地址。
  • @PeterCordes 我猜是rip 值有助于在rbp 的帮助下跟踪函数调用堆栈。是的,罗斯的回答很棒,我认为当可执行文件不遵循 rbp/rsp 约定时,实际上使用了unwind info。
  • 是的,回溯需要找到的是保存的返回地址(RIP 值)。你是对的,在老式的push rbp stack-frame-making 约定中,保存的rbp 值形成一个链表,返回地址位于每个帧内的已知位置。 (顺便说一句,这不仅仅是一个隐喻;它实际上是一个链表。RIP 值只是每个节点中的数据,而不是它们自己的链表。只有 rbp 值指向下一个节点。) ,我应该说EIP/EBP的值,因为我认为.eh_frame的数据默认用于64位。

标签: assembly gdb stack-trace backtrace


【解决方案1】:

像 GDB 这样的调试器有两种主要的方法来遍历堆栈以打印回溯。它们要么假设帧指针寄存器 (RBP) 中的值是指向堆栈帧链表开始的指针,要么它们使用存储在描述如何遍历(展开)堆栈的可执行文件中的特殊展开信息。

使用帧指针

当使用帧指针时,假设它指向当前函数保存其调用者帧指针值的位置。它还假设在保存的帧指针之前是存储当前函数的返回地址的位置。这就是它如何知道调用函数的 RBP 值是什么,以及哪个函数调用了当前函数,它可以从返回地址轻松确定。然后它可以通过遍历链接的 RBP 值来找到堆栈上所有以前的堆栈帧和函数。

但是,这假定函数以这种方式使用帧指针,并且通常不能保证它们会这样做。基本上它假设函数 prologue 和 epilogue 看起来像这样:

func:
    push %rbp         # save previous frame pointer
    mov  %rsp, %rbp   # new frame pointer points to previous value
    sub  $24, %rsp    # allocate stack space for this funciton

    ...

    pop %rbp          # restore previous frame pointer
    ret

但在优化时,许多编译器不会这样做,因为它们很少需要使用帧指针,而是会将 RBP 视为任何其他通用寄存器并将其用于其他用途。

使用展开信息

因此,要跨不使用 RBP 作为帧指针的函数生成回溯,调试器可能会使用展开信息。这是存储在可执行文件(和动态库)中的特殊数据,它准确地描述了如何在执行该函数的任何时候虚拟撤消该函数执行的所有堆栈操作。展开信息的格式和位置因可执行格式和 CPU 类型而异。对于 ELF x86-64 可执行文件,展开信息以基于 DWARF 调试格式的展开信息的格式存储在 .eh_frame 部分中。此格式过于复杂,此处无法描述,但您可以阅读System V AMD64 ABI 了解更多详细信息。

【讨论】:

  • 哪个是真正的历史原因,哪个是结果?我认为创建展开元数据是为了允许在需要在运行时展开自己的堆栈的程序中进行这种优化(例如,支持异常)。在不需要异常的优化代码中启用有用的回溯是一个好处。
  • 将堆栈上的 rbp 值描述为链表的好比喻,我知道它的机制,但以前从来没有这样过。 rip 堆栈上的值存储在每个 rbp 的正上方,对吗?它也像一个链表,因此能够知道 rip 属于哪个函数。而且,当我删除push rbp 指令时,gdb 仍然可以提供函数调用堆栈,我猜它正在使用unwind 信息。很好的答案!
  • @EricWang 是的,当编译器创建基于“正常”帧指针的堆栈帧时,调用者的 RIP 值存储在 RBP 之前的下一个更高的堆栈槽中。
  • @PeterCordes 我真的不确定什么是先来的。 DWARF 展开信息于 1993 年在 DWARF 2 规范中引入,给出的理由是:“调试器通常需要能够查看和修改调用堆栈上任何子例程激活的状态。”没有提到将它用于异常展开。 1994 年出版的《C++ 的设计和演变》说,“至少在理论上”C++ 异常可以在抛出异常之前没有运行时成本,这意味着当时没有 C++ 实现使用展开信息。
  • @PeterCordes 另一方面,当 PECOFF 于 1993 年在 Windows NT 中引入时,它支持 i386 目标的 FPO(帧指针省略)记录和(与 DWARF 相比粗略)RISC(MIPS)的展开信息, Alpha) 目标。前者仅用于调试器,而后者主要用于 SEH 异常。
猜你喜欢
  • 2015-01-28
  • 1970-01-01
  • 1970-01-01
  • 2011-04-21
  • 2012-10-06
  • 2012-04-06
  • 2023-01-12
  • 1970-01-01
  • 2018-05-08
相关资源
最近更新 更多