如何栈回溯


调用约定

参考这篇文章https://www.cnblogs.com/rosesmall/p/14849478.html

看一下几类常见的调用约定:(虽然本文探究的问题似乎不是由调用约定的改变而引起的😊)

笔者目前遇到的问题就是,希望通过栈回溯来恢复完整的函数调用链,但是笔者的目标(内核)中的函数似乎不用rsp+rbp来描述栈帧,也就是说进入函数后根本不push rbp,函数结束的时候也根本不leave😡

framepoiner

在这篇文章中 https://zhuanlan.zhihu.com/p/665401236 , 笔者似乎找到了答案,rbp指向栈帧有一个更确切的名字:framepointer,它的具体定义看一下这里https://www.cnblogs.com/hustdc/p/7631370.html

而这个framepointer则是可以通过添加编译选项-fomit-frame-pointer来关闭的。

关闭framepointer如何栈回溯

仍然是参考这篇文章:https://zhuanlan.zhihu.com/p/665401236。

DWARF与CFI

直接抄原图:😊

但是如果将调试符号给strip了,debug相关的段就不存在了,此时仍然能够被使用的就只剩下eh_frame段了:

然后文章给了我们一种使用readelf获取这类信息的方法:

readelf -wF filename

使用效果如下:(截取其中一个函数)

可以看到对于每个函数而言,第一行表示的是这个函数的相关信息,pc=ffffffff81bf4430..ffffffff81bf4477表示了这个函数的起止地址,然后下面每一行表示栈帧发生改变,笔者认为这应该是该条指令执行前的?第一条指令的栈帧是+8,就是压入了返回地址?(道理理解了,但是不知道+0是不是包含这个返回地址 🤔

那就看一下这个汇编代码😊:

可以看到0xffffffff81bf4435和0xffffffff81bf4432的栈帧是一样的,都是+16,而0xffffffff81bf4435的指令是个push,所以这也就充分说明了这个栈帧偏移是指令执行前的!😊

所以说,一个栈帧偏移为+0的位置就是返回地址咯。😏

详细分析

继续使用这个知识做栈回溯,结果还是不理解了 🤡 为什么输出结果中,有的是用的rbp?🤔 为什么有的地址压根就不是正常控制流能到的地址?🤡

这次参考这篇文章:https://zhuanlan.zhihu.com/p/302726082

首先要理解什么是CFI伪指令:

这是插入到汇编代码中的伪指令,即最终不会生成机器码,而是告诉汇编器需要在这里生成相应的调试信息;

下面我们来看知乎上的这个例子:

可以看到gcc会在许多关键指令的前后生成CFI伪指令,

每个.eh_frame section 包含一个或多个CFI(Call Frame Information)记录,记录的条目数量由.eh_frame 段大小决定。每条CFI记录包含一个CIE(Common Information Entry Record)记录,每个CIE包含一个或者多个FDE(Frame Description Entry)记录。

通常情况下,CIE对应一个文件,FDE对应一个函数

这里说一下笔者的理解:

因此,笔者认为,只要不出现新的“CFA=rsp+xxx”的字样,则rsp关于CFA的距离没有改变,此外,一个函数的ra和CFA之间的距离也应该是固定,因此再回溯过程中,我们是完全可以只关注rsp寄存器的,其做法具体如下:

  1. 从当前pc地址向前回溯,找到最近的一条有“CFA=rsp+xxx”字样的条目,这说明了pc位置的rsp和CFA的距离和这个位置的距离应该是一致的;
  2. 通过rsp找到CFA;
  3. 之后通过ra=c+xxx,通过CFA找到ret_addr;

参考

https://www.cnblogs.com/rosesmall/p/14849478.html

https://zhuanlan.zhihu.com/p/665401236

https://www.cnblogs.com/hustdc/p/7631370.html

https://zhuanlan.zhihu.com/p/302726082

https://blog.csdn.net/treblez/article/details/108855272


文章作者: q1ming
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1ming !
  目录