调用约定
参考这篇文章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寄存器的,其做法具体如下:
- 从当前pc地址向前回溯,找到最近的一条有“CFA=rsp+xxx”字样的条目,这说明了pc位置的rsp和CFA的距离和这个位置的距离应该是一致的;
- 通过rsp找到CFA;
- 之后通过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