前言
相信大家或多或少都遇到过这样的窘境:
1 | // ... |
当我们反复采用print大法debug时,我们其实是想知道哪一行代码导致了panic或非预期的结果。显然,逐行添加调试信息的方法既不优雅,又不高效。我们希望有一种更优雅的方式来定位错误代码,像 gdb 的 backtrace 命令那样:
经过一些搜索和研究,我发现在mips确实可以实现获取调用栈的功能。在本文中,我将介绍获取内核调用栈的原理。
(符号就无解了,与objdump对照着看吧......)
前置条件
请复习 MIPS Calling Convention 中关于栈帧、函数 Prologue 和 Epilogue、非页函数有关的内容。
实现原理
MIPS 调用约定
寄存器使用
我们知道, 在MIPS的函数调用中,这些寄存器是比较重要的:
$sp
:栈指针寄存器,指向当前栈顶$fp
:帧指针寄存器(不是在所有调用约定中都被使用)$ra
:返回地址寄存器,存储被调用函数的返回地址
函数的 Prologue 和 Epilogue
在一个函数的函数体前后分别存在的两段代码。
前面的代码被称为 Prologue,用于设置栈指针和帧指针,以及保存需要保存的寄存器;
后面的代码被称为 Epilogue, 用于恢复调用前的状态。
以一个简单的非叶函数为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18; fn recursive_fn() {
; *** Prologue ***
addiu $sp, $sp, -0x18 # 获取栈空间
sw $ra, 0x14($sp) # 保存寄存器
sw $fp, 0x10($sp)
move $fp, $sp # 设置帧指针
; *** function body ***
; recursive_fn();
jal recursive_fn # 函数调用
nop
; }
; *** Epilogue ***
move $sp, $fp # 设置栈指针,一般没什么用
lw $fp, 0x10($sp) # 恢复寄存器
lw $ra, 0x14($sp)
addiu $sp, $sp, 0x18 # 释放栈空间
jr $ra # 返回主调函数
nop其中 Prologue 部分获取了
0x18
字节的栈空间,将当前$ra
和$fp
存入其中;Epilogue (尽管在这个示例中它并不会执行) 部分按照 Prologue 的顺序恢复了
$fp
和$ra
,并释放了栈空间。
实现思路
基本原理
假设我们有一个获取调用栈的函数fn backtrace()
,显然,调用它的函数,调用调用它的函数的函数,...
...(套娃中),直到最上层的函数都是非叶函数,也就是说,它们的栈帧中都保存有返回地址$ra
。如果我们能逐层解析栈帧中的返回地址,就可以追踪函数的调用。
根据这张栈帧结构图,我们已经知道栈指针$sp
,如前所述,我们还需要得知return address
,而为了在追踪完当前函数后,继续追踪它的调用者,我们还需要当前帧的大小fsize
。
获取 $ra
和
fsize
不幸的是,$ra
在栈帧中保存的位置并不固定,fsize
也不存储于栈帧中,我们必须通过其他方法获取它们的值。
函数的Prologue派上用场的时候到了。因为要获取栈空间,函数的Prologue总会出现这条指令:
1 | addiu $sp, $sp, IMM # 0x27bdxxxx |
其中 IMM
是一个16位的有符号数,它的绝对值就是栈帧的大小
fsize
,而指令的高16位则是固定的
0x27bd。这样我们可以从函数的开头向后遍历其指令,从而获取fsize
。
类似地,我们可以利用
1 | sw $ra, offset($sp) # 0xafbfxxxx |
找到$ra
在栈中的偏移量,从而获取$ra
。
递归获取调用栈
找到fsize
后,用$sp
加上这个值,就得到上一层函数的栈帧位置。我们从$ra
指向的指令向前遍历,同样可以从Prologue中获取fsize
和$ra
,重复这一步骤直到无法获得合法的$ra
,就得到了完整的内核调用栈。
(因为调用过程中的函数一般都遵循MIPS调用约定,所以不必担心没有遍历到Prologue就退出的情况)
调用栈与Debug
我获得了调用栈信息,然后呢?
确保你在用Debug模式构建
如果你不清楚自己是不是在用Debug模式构建,那么你就是在用Debug模式构建
使用objdump反编译
你在使用MOS
make objdump
你在使用Rust
rust-objdump -DS <kernel_file> > <output_file>
打开反编译的文件,搜索 RA 或 Subroutine 字段的值
4. 针对性检查对应函数实现
参考代码
实现了基于Rust的内核栈追踪,可以很轻易地迁移到C语言或用户空间。
码风略烂,欢迎提出改进意见。
1 | fn backtrace() -> String { |
参考资料
最后修改于:
最后回复于: