在嵌入式开发过程中,经常会遇到出问题了想知道函数调用关系,不过目前我用到的toochain的Libc提供的backtrace只能出来2层,所以就只好另外写代码来backtrace。目前嵌入式mips32在gcc编译器上的ABI都遵循o32, 从一段反汇编简单看一下o32 ABI的帧结构
0040064c <Test2>: 40064c: 27bdffe0 addiu sp,sp,-32 ##mips不会自动处理sp,这里进行函数堆栈开辟 400650: afbf001c sw ra,28(sp) ##将ra入栈 400654: afbe0018 sw s8,24(sp) ##其它寄存器入栈 400658: 03a0f021 move s8,sp 40065c: 0c100184 jal 400610 <Test1> 400660: 00000000 nop 400664: 24420001 addiu v0,v0,1 400668: 03c0e821 move sp,s8 40066c: 8fbf001c lw ra,28(sp) ##从堆栈取出ra 400670: 8fbe0018 lw s8,24(sp) ##其它寄存器出栈 400674: 27bd0020 addiu sp,sp,32 ##恢复堆栈 400678: 03e00008 jr ra ##跳回到函数调用处 40067c: 00000000 nop
从上面看堆栈中大约保存的就是各个要保护的寄存器内容,但不同的函数保护的内容不一样,因此帧内数据不一定一样,例如下面这个函数就不用保存ra
00400610 <Test1>: 400610: 27bdffe8 addiu sp,sp,-24 400614: afbe0014 sw s8,20(sp) 400618: 03a0f021 move s8,sp 40061c: 24020003 li v0,3 400620: afc20008 sw v0,8(s8) 400624: 24020004 li v0,4 400628: afc2000c sw v0,12(s8) 40062c: 8fc30008 lw v1,8(s8) 400630: 8fc2000c lw v0,12(s8) 400634: 00621021 addu v0,v1,v0 400638: 03c0e821 move sp,s8 40063c: 8fbe0014 lw s8,20(sp) 400640: 27bd0018 addiu sp,sp,24 400644: 03e00008 jr ra 400648: 00000000 nop
在C/C++函数中,启动程序函数,再向上就是汇编了, 这是能backtrace C/C++的最后一层
00400440 <__start>: 400440: 3c1c0042 lui gp,0x42 ##Load GOT 400444: 279c8920 addiu gp,gp,-30432 400448: 0000f821 move ra,zero 40044c: 3c040040 lui a0,0x40 400450: 248406c0 addiu a0,a0,1728 400454: 8fa50000 lw a1,0(sp) 400458: 27a60004 addiu a2,sp,4 40045c: 2401fff8 li at,-8 400460: 03a1e824 and sp,sp,at 400464: 27bdffe0 addiu sp,sp,-32 400468: 3c070040 lui a3,0x40 40046c: 24e70710 addiu a3,a3,1808 400470: 3c080040 lui t0,0x40 400474: 250807b8 addiu t0,t0,1976 400478: afa80010 sw t0,16(sp) 40047c: afa20014 sw v0,20(sp)
从上面的分析来看,
1. 进入函数最先做的事情就是开辟堆栈:
27bdffe8 addiu sp,sp,-24
2.如果ra要保护,则需要入栈
afbf001c sw ra,28(sp)
3. 最顶层的C/C++函数是要准备GOT的
400440: 3c1c0042 lui gp,0x42
4.一个函数结束,需要跳回到ra指向的地址
400644: 03e00008 jr ra
有了以上三点,就可以backtrace了:
#include "syscall.h" #define abs(X) ((X)>=0?(X):(-(X))) int autobt_mips32(void **btbuffer,int size) { unsigned long *addr; unsigned long *ra; unsigned long *sp; unsigned int raoffset; unsigned int stacksize; unsigned int bt; if(!btbuffer || size<0) { return -1; } //获取当前函数的ra和sp __asm__ __volatile__( "move %0, $ra\n" "move %1, $sp\n" :"=r"(ra),"=r"(sp) ); //因为当前函数是叶子函数,所以ra不会再被占用,因此不会将ra入栈,所以不用去找ra的在sp中的偏移地址,因此ra中的值就是调用autobt_mips32的下一条指令地址 stacksize = 0; for(addr=(unsigned long*)autobt_mips32;;++addr) //从当前函数的起始地址找堆栈大小 { if((*addr&0xffff0000) == 0x2fbd0000) //0x2fbd is "addiu sp,sp",前面分析过,这个指令是为函数开辟堆栈的 { //当发现是开辟堆栈的指令时,取出堆栈大小 stacksize = abs((short)(*addr&0xffff)); //mips堆栈是负增长,所以要取绝对值 if(stacksize != 0) { break; } } else if(*addr == 0x3e00008) //0x3e00008 is "jr ra" { //发现返回指令,说明已经找到头了,退出查找 break; } } //找到了autobt_mips32使用堆栈的大小,就可以算出autobt_mips32调用者的堆栈指针 sp =(unsigned long *)((unsigned long)sp + stacksize); //做backtrace for(bt=0;bt<size&&ra;bt++) { btbuffer[bt]=ra; raoffset = 0; stacksize = 0; for(addr=ra; raoffset==0||stacksize==0; addr--) //从ra开始向上找 { switch(*addr&0xffff0000) //get instruction { case 0x27bd0000: //找到开辟堆栈的指令,保存堆栈的值 stacksize = abs((short)(*addr&0xffff)); break; case 0xafbf0000: //从前面的分析可以知道0xafbf 是"sw ra (XX)sp",这里就是ra存放的偏移地址 raoffset = (short)(*addr&0xffff); break; case 0x3c1c0000: //找到C/C++ 函数的最后一层, 停止 backtrace。0x3c1c 是"lui gp" return bt+1; break; default: break; } } //设置上一层调用者的调用本层函数的返回地址(ra的地址在上一层函数中)和堆栈地址 ra =(unsigned long *)((unsigned long)ra + raoffset); sp =(unsigned long *)((unsigned long)sp + stacksize); } return bt; }
当想要知道一个函数的backtrace时,可以在这个函数内call autobt_mips32, 函数的返回值就是backtrace的层数,而btbuffer中保存的是backtrace的地址,根据这些地址和运行程序的反汇编,很容易就可以对应出实际函数的调用关系