基于fp的栈回溯源码
栈回溯方法
1.基于fp的栈回溯
1. 编译器不会优化fp.
2. 用固定的abi来做
3. 栈是没被破坏的
2.特定架构的方法
1. arm32 基于 exidx/unwind (-funwind-tables supported in gcc>4.5) , arm32的基于fp的叫做apcs(-mapcs supported in gcc<5.0)
2. arm64 基于 dwarf/dwarf23
调用约定与实例分析
我们分析这些东西,是为了知道 当进行函数调用时, caller 和 callee 做了什么事情
其实我们没有必要分析这些代码,调用是有规范的,这个和具体架构有关系.具体请搜索 函数调用约定 + 架构名
注意: arm64/32 x86 riscv32/64 都是满减栈
满减栈,当前指针有值,先移动指针再存
基于栈帧的回溯: gcc 编译器内部已经做了实现
__builtin_return_address(0)
__builtin_return_address(1)
__builtin_frame_address(0)
__builtin_frame_address(1)
栈回溯抽象
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(7) + 1;
}
我们分析了这么多,没有抽象出概念,总结出理论,那么很快就会忘掉
在分析一个具体的函数栈调用的时候,例如xxx->main->f->g
例如我们分析的函数名为f时候,我们关注4个点
1. 栈底
2. 栈顶/即栈帧
3. 栈大小
4. 函数返回地址
5. 如何获取到(上一层调用函数)main的栈顶与栈底
6. 如何为(下一层被调函数)g准备栈底
栈回溯抽象
a. 我们函数f中能够获取 1&2的信息,从而我们可以获取f的1-6.即我们可以获取到 main 的栈顶与栈底
b. 我们已经获取到main的1&2的信息,从而我们可以获取main的1-6.
...
以下说的栈底和栈顶都是地址.里面牵扯到了地址和地址中的值,请注意分辨!!!
x86
以f为例
1.栈底是%ebp (高地址),等于调用函数的栈顶-4字节
2.栈顶是%esp(低地址), 运行时决定
3.栈大小由运行时决定
4.函数返回地址 所在位置 位于 栈底(%ebp) +4字节 偏移位置存储的值
5. main 的栈顶与栈底地址
main栈底 : f栈底(%ebp) +0字节 偏移位置存储的值存储的值
main栈顶 : f栈底(%ebp) +4字节
6. g 的栈顶
g栈底 : f栈顶(%esp)-4字节
g栈顶 : 运行时决定
riscv64
以f为例
1.栈底是s0/fp (高地址),等于调用函数的栈顶
2.栈顶是sp(低地址), 编译器决定
3.栈大小由编译时决定,我们可以在代码中根据当前sp和fp计算出来
4.函数返回地址 所在位置 位于 栈底(s0/fp) -8字节 偏移位置存储的值
5. main 的栈顶与栈底
main栈底 : f栈底(s0/fp) -16字节 偏移位置存储的值
main栈顶 : f栈底(s0/fp)
6. g 的栈顶
g栈底 : f栈顶(sp)
g栈顶 : 编译器决定
riscv32
以f为例
1.栈底是s0/fp (高地址),等于调用函数的栈顶
2.栈顶是sp(低地址), 编译器决定
3.栈大小由编译时决定,我们可以在代码中根据当前sp和fp计算出来
4.函数返回地址 所在位置 位于 栈底(s0/fp) -4字节 偏移位置存储的值
5. main 的栈顶与栈底
main栈底 : f栈底(s0/fp) -8字节 偏移位置存储的值
main栈顶 : f栈底(s0/fp)
6. g 的栈顶
g栈底 : f栈顶(sp)
g栈顶 : 编译器决定
arm32
以f为例
1.栈底是fp (高地址),等于调用函数的栈顶
2.栈顶是sp(低地址), 运行时决定
3.栈大小由运行时决定
4.函数返回地址 所在位置 位于 栈底(fp) - 4字节 偏移位置存储的值
5. main 的栈顶与栈底
main栈底 : f栈底(fp) - 12字节 偏移位置存储的值
main栈顶 : f栈底(fp) - 8字节 偏移位置存储的值
6. g 的栈顶
g栈底 : f栈顶(sp)-4字节
g栈顶 : 编译器决定
arm64
以f为例
1.栈底是fp (高地址),等于调用函数的栈顶-32(不确定)字节
2.栈顶是sp(低地址), 编译器决定
3.栈大小由编译时决定,我们可以在代码中根据当前sp和fp计算出来
4.函数返回地址 所在位置 位于 栈底(fp) +8字节 偏移位置存储的值
5. main 的栈顶与栈底
main栈底 : f栈底(fp) 0字节 偏移位置存储的值
main栈顶 : f栈底(fp) +32(不确定)
6. g 的栈顶
g栈底 : f栈顶(sp)-32(不确定)
g栈顶 : 编译器决定
栈回溯原理
注意: 在用户代码中,栈底是一直不变的,栈顶是在变化的!!!
注意: 每个架构中 "从当前fp寻址 caller fp 的方法" 不会随着代码的变化而变化, "从当前fp寻址返回值的方法"也不会随着代码的变化而变化
注意: 这两个不变奠定了 栈回溯的 基础!!!
注意: 在栈回溯中.我们只需要在用户代码中
1.在f中获取fp寄存器的值,然后根据 "f 寻址 main的栈底(fp)的方法" 可以寻址到 main的栈底
2.main的栈底(fp寄存器的值)已经通过1获得,然后根据 "f 寻址 main的栈底(fp)的方法" 可以寻址到上一级xxx的栈底
3.xxx的栈底(fp寄存器的值) 已经通过2获取.如果xxx的栈底fp为0.则为回溯根部
注意: 其实现在我们已经实现了栈回溯,但是实际应用上我们要取得栈回溯得到的符号名(就是那种在gdb中打bt命令出来的那种东西,或者在linux中调用dump_stack出来的东西)
注意: 我们现在先不做符号名,而是先把符号名有关的地址做出来,这个地址就是与 fp 密切相关的 "返回地址"
注意: 我们只是得到了每层的 fp ,但这个跟代码符号没有一点关系,因为 fp 是栈的东西,代码符号是 pc轨迹相关的东西
但是我们可以通过 fp 得到 该层栈 中的返回地址 .
注意 :
x64/x86/riscv32/riscv64/arm32/arm64 都是先保存 返回地址,再保存caller的栈帧.
一开始是汇编,然后是c,以此为例子
_start:
1. 初始化fp为0
2. 设置sp
3. bl c_code
c_code :
1. 存储 上级函数 的fp
2. 存储 c_code 函数的 返回值
3. 设置 c_code 函数的 fp
当我们进行栈回溯时,回溯到c_code . 根据c_code 的 fp(非0) 获取到 _start 的 fp(值为0) , 那么表示到了栈回溯的根
所以我们需实现两个函数
void * current_fp(void);
void * caller_fp(void * currentfp);
void dump_stack(void);
栈回溯实现
架构无关码
#include <stdio.h>
void dump_stack(void) {
void * fp_current = get_current_stack_frame();
void * fp_caller = fp_current;
void * ret_callee = NULL;
printf("FP:%p,CALLEE:%08x\n",fp_caller,dump_stack);
for (;fp_caller;) {
ret_callee = get_callee_return_address(fp_caller);
fp_caller = get_caller_stack_frame(fp_caller);
printf("FP:%p,CALLEE:%08x\n",fp_caller,ret_callee-4);
}
return ;
}
int main(void){
dump_stack();
return 0;
}
#include <stdio.h>
void dump_stack_core(void *fp) {
void * ret_callee = NULL;
void * fp_caller = get_caller_stack_frame(fp);
if (fp_caller != NULL){
ret_callee = get_callee_return_address(fp_caller);
dump_stack_core(fp_caller);
}
printf("FP:%p,CALLEE:%08x\n",fp_caller,ret_callee == 0 ? ret_callee : ret_callee -4);
return ;
}
void dump_stack(void) {
void * fp_current = get_current_stack_frame();
dump_stack_core(fp_current);
printf("FP:%p,CALLEE:%08x\n",fp_current,get_callee_return_address(fp_current)-4);
printf("FP:0x00000000,CALLEE:%08x\n",dump_stack);
return ;
}
int main(void){
dump_stack();
return 0;
}
架构相关码
inline __attribute__((always_inline)) void * get_current_stack_frame(void){
void* fp;
asm volatile ("mov %0, ebp" : "=r" (fp));
return fp;
}
inline __attribute__((always_inline)) void * get_caller_stack_frame(void * current_fp)
{
void* caller_fp;
caller_fp = (void *)(*(unsigned long int *)(current_fp));
return caller_fp;
}
inline __attribute__((always_inline)) void * get_callee_return_address(void * current_fp)
{
void* callee_ret;
callee_ret = (void *)(*(unsigned long int *)((char *)(current_fp) + sizeof(void*)));
return callee_ret;
}
inline __attribute__((always_inline)) void * get_current_stack_frame(void){
void* fp;
asm volatile ("mov %0, rbp" : "=r" (fp));
return fp;
}
inline __attribute__((always_inline)) void * get_caller_stack_frame(void * current_fp)
{
void* caller_fp;
caller_fp = (void *)(*(unsigned long int *)(current_fp));
return caller_fp;
}
inline __attribute__((always_inline)) void * get_callee_return_address(void * current_fp)
{
void* callee_ret;
callee_ret = (void *)(*(unsigned long int *)((char *)(current_fp) + sizeof(void*)));
return callee_ret;
}
inline __attribute__((always_inline)) void * get_current_stack_frame(void){
void* fp;
asm volatile("mv %0, fp" : "=r" (fp));
return fp;
}
inline __attribute__((always_inline)) void * get_caller_stack_frame(void * current_fp)
{
void* caller_fp;
caller_fp = (void *)(*(unsigned long int *)(current_fp - sizeof(void*)*2));
return caller_fp;
}
inline __attribute__((always_inline)) void * get_callee_return_address(void * current_fp)
{
void* callee_ret;
callee_ret = (void *)(*(unsigned long int *)((char *)(current_fp) - sizeof(void*)));
return callee_ret;
}
inline __attribute__((always_inline)) void * get_current_stack_frame(void){
void* fp;
asm volatile("mv %0, fp" : "=r" (fp));
return fp;
}
inline __attribute__((always_inline)) void * get_caller_stack_frame(void * current_fp)
{
void* caller_fp;
caller_fp = (void *)(*(unsigned long int *)(current_fp - sizeof(void*)*2));
return caller_fp;
}
inline __attribute__((always_inline)) void * get_callee_return_address(void * current_fp)
{
void* callee_ret;
callee_ret = (void *)(*(unsigned long int *)((char *)(current_fp) - sizeof(void*)));
return callee_ret;
}
inline __attribute__((always_inline)) void * get_current_stack_frame(void){
void* fp;
asm volatile ("mov %0, fp" : "=r" (fp));
return fp;
}
inline __attribute__((always_inline)) void * get_caller_stack_frame(void * current_fp)
{
void* caller_fp;
caller_fp = (void *)(*(unsigned long int *)((unsigned long int)current_fp - sizeof(void*)*3));
return caller_fp;
}
inline __attribute__((always_inline)) void * get_callee_return_address(void * current_fp)
{
void* callee_ret;
callee_ret = (void *)(*(unsigned long int *)((unsigned long int)(current_fp) - sizeof(void*)*1));
return callee_ret;
}
inline __attribute__((always_inline)) void * get_current_stack_frame(void){
void* fp;
asm volatile ("mov %0, x29" : "=r" (fp));
return fp;
}
inline __attribute__((always_inline)) void * get_caller_stack_frame(void * current_fp)
{
void* caller_fp;
caller_fp = (void *)(*(unsigned long int *)(current_fp));
return caller_fp;
}
inline __attribute__((always_inline)) void * get_callee_return_address(void * current_fp)
{
void* callee_ret;
callee_ret = (void *)(*(unsigned long int *)((char *)(current_fp + sizeof(void *))));
return callee_ret;
}
栈回溯实例运行
~$ stacktrace
FP:0x408ffe90,CALLEE:40006564
FP:0x408ffee0,CALLEE:40007cc4
FP:0x408fff00,CALLEE:40007ce8
FP:0x408fff20,CALLEE:40007d08
FP:0x408fff40,CALLEE:40005e1c
FP:0x408ffff0,CALLEE:40003668
FP:(nil),CALLEE:4000003c
$ stacktrace
FP:(nil),CALLEE:00000000
FP:0x408ffff0,CALLEE:4000003c
FP:0x408fff40,CALLEE:40003668
FP:0x408fff20,CALLEE:40005e1c
FP:0x408fff00,CALLEE:40007d68
FP:0x408ffee0,CALLEE:40007d48
FP:0x408ffeb0,CALLEE:40007d28
FP:0x00000000,CALLEE:40006600
然后去 baremetal.elf.asm 中 对比 地址!!!
利用栈回溯知识debug
之前遇到一个bt命令,打印出来的栈不全的问题, 提示说栈被破坏了
那么到底是什么被破坏了呢?
1. 每一层的fp
2. 每一层的返回值
函数调用与函数声明 参数类型 不匹配!!!
调用的小,用的大,导致fp被破坏
int main(void){
int data[4];
memset(data,0,sizeof(data)*4);
}