当没有了frame pointer我们该如何栈回溯?

基于DWARF的栈回溯

使用fp(frame pointer)进行栈回溯的前提是在汇编中有显式的操作frame pointer的部分,那么如果没有这部分我们该如何做栈回溯呢?

栈回溯的三步骤

在进行栈回溯的时候,我们一般会做如下的事情:

  • 找到上一级调用函数的栈帧
  • 找到调用函数返回地址
  • 基于返回地址转换成调用函数名

例如在基于fp的回溯中,对于main->foo->bar的调用,我们从bar开始做回溯:

fp回溯的场景中,这三件事通过如下的方式实现:

  • 找到上一级调用函数的栈帧:通过保存在运行栈中的rsp指针值进行向上回溯;
  • 找到调用函数返回地址:通过栈中找返回地址即可;
  • 基于返回地址转换成调用函数名:通过调试信息(debug info)来进行转换。

那么,在去掉fp信息以后,会影响的就是第一点:如何找到上一级调用函数的栈帧?

函数调用流程

在函数调用的时候,一般会做如下的事情:

  • 压入参数
  • 压入返回地址
  • 压入RBP(开启fp

而这些操作在编译的时候其实都已经确定了,那么是不是在编译的时候我们就可以计算出两个栈帧之间对应的差值呢?

DWARF与CFI

DWARF(Debugging With Attributed Record Formats)是一种计算机程序的格式,它是为了让开发人员能够在不同的操作系统和硬件平台上共享二进制代码而设计的。DWARF格式包含了程序中调试信息的元数据,包括变量名、函数名、行号等等。这些信息可以帮助开发人员在程序出现问题时进行调试和分析。DWARF格式通常与编译器一起使用,编译器会将调试信息嵌入到生成的可执行文件中。在DWARF标准中,定义了一种名为CFI(Call Frame Information)的标准,用来帮助我们进行栈回溯。

例如我们用gcc进行编译时,携带-g选项就会默认带上DWARF调试信息,我们可以用file命令查看:

这里携带了DWARF格式的调试信息,我们可以用readelf查看:

这里由debug开头的就是DWARF相关的信息。一般我们会用strip去掉调试信息,尝试一下:

可以看到这里file以后已经不再提示with debug_info了,并且readelf也无法得到debug_*段的信息了,此时这个二进制就没有调试信息了。

注意,这里的debug_*段在执行时是不会被加载的,那么,我们借助什么来做运行时的栈回溯呢?

LSB格式中,我们定义了eh_frame段,这部分内容不会被strip掉,并且内容符合DWARFCFI的标准,一般libunwind就是通过这部分信息来做栈回溯的。

实际上帮助我们回溯的是CFI信息,后续我们统一将其称为基于DWARF的回溯。

我们可以用readelf -wF来查看ELF中的相关CFI信息:

这里我们看到的部分称为FDE(Frame Description Entry),具体怎么使用且看下文。

基于DWARF的栈回溯流程

为了介绍DWARF的回溯,我们以如下的程序作为例子:

这次我们通过-fomit-frame-pointer选项来进行编译,直接关闭掉fp指针。我们直接查看foo函数执行的相关情况:

我们查看对应的FDE

CIE也是eh_frame的信息。此处不深究。

每个函数对应一个FDEFDE条目记录了一些自己的信息,和对应函数的信息。我们该如何去做回溯呢?

我们假设从foo开始回溯,此时的rsp寄存器的值为A,此时运行的指令是40074b这条指令。

首先我们在foo对应的函数中找到40074b这条指令:

这里的CFA就是调用者的栈帧地址,因此我们直接把当前RSP寄存器中的值拿出来进行计算,算出CFA = RSP + 8 = A + 8,这样我们就知道了上一级调用者的栈帧地址。同时我们可以计算出ra也就是返回地址的内容:RA = c - 8 = A + 8 - 8 = A 。根据RA的值,我们可以继续拿着这个信息向上回溯,因为RA一定是调用者也即这里的main函数的地址,我们进入到mainFDE中重复该步骤,寻找RA对应的条目,并进行回溯。不停循环我们就可以持续不断地向上回溯,直到完成整个调用栈的回溯。

我们用一张图简单的表达一下:

值得注意的是,这种回溯方式还可以帮助我们回溯到其他的通用寄存器信息:

和计算RA寄存器一致的,我们也是先算出CFA的地址,然后再进行相关运算即可。例如在40082b位置时,r14 = c - 24 = rsp + 48 - 34 = rsp + 24

perf中基于DWARF的栈回溯

在使用perf进行profiling的时候,我们可以通过如下的语句进行抓取:

这里的-g选项就是进行栈回溯的选项,默认通过fp来进行回溯,我们不妨用我们的例程进行一次尝试:

在结果中我们发现如下的调用栈:

在结果中,有多种调动栈,foobarmain等函数都有出现过,但是它们的调用栈都是有问题的。这是因为在进行fp的回溯的时候,缺少信息无法向上回溯导致的。只能知道自己的函数名,找不到自己的栈帧导致了错误的回溯。

我们尝试通过DWARF的方式来进行回溯:

可以看到,这样我们就成功的实现了栈回溯。

小结

今天我们介绍了基于DWARF的栈回溯,对比基于fp的栈回溯方式,CFI通过运行时调试信息的方式来帮助回溯。

你是否对CFI还是一知半解呢?我们是否有什么其他的栈回溯方式呢?我们后面接着介绍关于栈回溯的相关内容。

参考资料

相关推荐
小钻风巡山32 分钟前
springboot 视频分段加载在线播放
java·spring boot·后端
豌豆花下猫1 小时前
Python 潮流周刊#100:有了 f-string,为什么还要 t-string?(摘要)
后端·python·ai
小黑随笔1 小时前
【Golang玩转本地大模型实战(一):ollma部署模型及流式调用】
开发语言·后端·golang
江沉晚呤时1 小时前
Redis缓存穿透、缓存击穿与缓存雪崩:如何在.NET Core中解决
java·开发语言·后端·算法·spring·排序算法
光影少年2 小时前
新手学编程前端好还是后端
前端·后端
why1512 小时前
百度网盘golang实习面经
开发语言·后端·golang
hac13225 小时前
SpringBoot多环境配置
java·spring boot·后端
Go高并发架构_王工8 小时前
GoFrame框架深度解析:grpool的优势、最佳实践与踩坑经验
服务器·后端·golang
exe4529 小时前
4 月 28 日项目进展与规划会议纪要
后端
我命由我123459 小时前
C++ - 数据容器之 list(创建与初始化、元素访问、容量判断、元素遍历、添加元素、删除元素)
c语言·开发语言·c++·后端·visualstudio·c#·visual studio