当没有了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还是一知半解呢?我们是否有什么其他的栈回溯方式呢?我们后面接着介绍关于栈回溯的相关内容。

参考资料

相关推荐
JustHappy20 分钟前
古法编程秘籍(七):互联网到底是什么?把两台电脑怎么说话搞懂就够了
前端·后端·网络协议
Hommy8841 分钟前
【剪映小助手】添加图片接口(Add Images)
后端·github·剪映小助手·视频剪辑自动化
GetcharZp1 小时前
别再盲目用 OpenCV 读图了,这才是 CV 预处理的终极杀手锏!
后端
IT_陈寒5 小时前
Vite热更新失效?可能你在用Windows
前端·人工智能·后端
椰椰椰耶6 小时前
[SpringCloud][14]OpenFeign参数传递方法
后端·spring·spring cloud
onething3656 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 3 —— 消息表设计 + 级联删除 + 事务管理
人工智能·后端
荣江6 小时前
Hermes Agent 代码仓库打包工具使用指南(repomix-rs 高性能版)
后端
王某某人6 小时前
LangChain4j 入门:Java 程序员的第一个 AI 对话程序
人工智能·后端
码农刚子6 小时前
从零开始:在 Windows 服务器上部署 Node.js 项目(小白实战教程)
后端·node.js
Cache技术分享6 小时前
435. Java 日期时间 API - Clock 灵活获取当前时间
前端·后端