- 概述
backtrace获取的是当前函数的纵向调用信息。一般思路是,给定当前函数的栈地址和当前运行位置(PC),通过计算得到当前函数的返回地址和上层函数(caller)的栈地址,然后以当前函数的返回地址作为caller的PC,来继续解析caller的返回地址和caller of caller的栈地址,以此类推进行纵向解析。
不管什么处理器架构,其GCC工具链提供的libgcc库中,都自带一个unwind_backtrace的函数,libc中的backtrace()函数其实就是转调用到libgcc中的unwind_backtrace接口。
然而,实际测试发现,mipsel-linux-gcc的unwind_backtrace在某些场景下无法正常工作。一种场景:
在利用ld_preload来hack libc的malloc函数时,如果我们自己实现的malloc内调用libc的backtrace函数,会发生死锁,因为mipsel-linux-gcc的unwind_backtrace内部会依赖libc的malloc。
因此我们不得不自行实现backtrace功能。
- MIPS栈结构与函数对栈的处理
- 栈帧结构

图1 mips栈帧结构
- 函数对栈的处理
mipsel-linux-gcc默认遵循stdcall的函数调用标准,也就是说,本函数内要负责保存和恢复caller的栈帧。
一个非内联函数的指令执行过程分成3步,如下所示。
step1 (prologue):
调整sp寄存器,调整前sp指向caller的栈顶,调整后指向当前栈顶;
示例指令如下,
addiu sp,sp,-SIZE
其中SIZE是当前函数栈大小,编译时确定。
保存caller的相关寄存器至本函数栈中,如fp,ra等;
调整fp指向当前栈顶;
将入参从a0~an寄存器中保存至当前栈中;
初始化栈中的本地变量;
step2 (逻辑计算):
进行实际的逻辑计算,也包括子函数调用;
将返回值设给v0寄存器;
step3 (epilogue):
将本栈中保存的返回地址重新赋给ra寄存器,也即ra指向caller的下下一条指令(跳过指令延迟槽)
将本栈中的保存的fp重新赋给fp寄存器,也即fp指向caller;
调整sp使其重新指向caller的栈顶,”addiu sp,sp,SIZE” ;
跳转执行返回地址 jr ra ;
- 函数反汇编举例
void hello(void)
{
ret = mybacktrace(array, depth);
printf("hello. \n"); //mybacktrace 的返回地址
}
00400d0c <mybacktrace>:
400d0c: 27bdffe0 addiu sp,sp,-32 //将hello函数栈顶指针sp向下移动32字节,得到当前函数栈顶指针,意味着当前函数的栈大小为32字节
400d10: afbf001c sw ra,28(sp) //*(sp+28)=ra, 将ra寄存器的值保存至sp的32字节偏移处,意味着sp+28地址处能得到函数返回地址(hello调完mybacktrace后继续执行的指令地址),ra寄存器被jal/bal等带有链接功能的跳转/分支指令自动更新
//mybacktrace是非叶子函数,也就意味着接下来还有子函数调用,因此此处必须临时保存mybacktrace的返回地址,而叶子函数是没有类似sw ra,n(sp)的指令的
400d14: afbe0018 sw s8,24(sp) //*(sp+24)=s8, 将s8(hello的帧指针fp)保存至sp+24处
400d18: 03a0f021 move s8,sp //fp=sp
400d1c: afc40020 sw a0,32(s8) //保存第1个形参array至fp+32处
400d20: afc50024 sw a1,36(s8) //保存第2个形参depth至fp+36处
400d24: 8fc40020 lw a0,32(s8) //将fp+32处的参数array加载到a0寄存器(以传递给子函数mybacktrace_mips)
400d28: 8fc50024 lw a1,36(s8) //将fp+36处的参数depth加载到a1寄存器(以传递给子函数mybacktrace_mips)
400d2c: 0c1005a3 jal 40168c <mybacktrace_mips> //跳转执行子函数
400d30: 00000000 nop //指令延迟槽
400d34: 03c0e821 move sp,s8 //sp = s8 恢复sp的值(重新指向当前函数栈顶)
400d38: 8fbf001c lw ra,28(sp) //ra = *(sp+28) 恢复ra(重新指向mybacktrace的返回地址)
400d3c: 8fbe0018 lw s8,24(sp) //s8 = *(sp+24) 恢复fp(重新指向hello的frame)
400d40: 27bd0020 addiu sp,sp,32 //sp +=32 恢复sp,重新指向hello的栈顶
400d44: 03e00008 jr ra //返回执行hello函数
400d48: 00000000 nop //指令延迟槽