基于MIPS架构的BackTrace实现
基础知识介绍:
1. MIPS32的内部寄存器。
最简单的办法就是通过GDB的命令,可以获得下面的列表
(gdb) info registers zero at v0 v1 a0 a1 a2 a3 R0 00000000 00000001 0000000f 0000000f 00000000 0000000f 0000000e 00000071 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000072 00000001 00000203 80003cb1 80003cb0 0000007f 00000080 00000008 s0 s1 s2 s3 s4 s5 s6 s7 R16 00000001 80003bb0 00000000 00000000 00000000 00000000 00000000 00000000 t8 t9 k0 k1 gp sp s8 ra R24 0000101a 0000000d 00000000 00000000 8000bbd0 807fffb8 00000000 80000830 sr lo hi bad cause pc 00000000 00000000 00000007 00000000 00000000 80000830 fsr fir 00000000 00000000 |
除了32个通用寄存器以及别名外,还有8个专用寄存器,分别是:
sr ( 全称Status ,CP0 Reg12) Processor status and control; interrupt control; and shadow set control
lo ( 全称WatchLo , CP0 Reg18) Low-order watchpoint address
hi ( 全称WatchHi, CP0 Reg19) High-order watchpoint address
bad ( 全称BadVAddr, CP0 Reg8) Reports the address for the most recent address-related exception
cause (CP0 Reg13) Cause of last exception
pc 很明显这个是程序计数寄存器,奇怪的是在32个通用寄存器以及CP0的32个寄存器中都没有找到他,最接近的一个是CP0Reg14 EPC (Program counter at last exception.)
fsr 浮点相关寄存器,具体用途不明
fir浮点相关寄存器,具体用途不明
下表描述32个通用寄存器的别名和用途
;REGISTER |
NAME |
USAGE |
$0 |
$zero |
常量0(constant value 0) |
$1 |
$at |
保留给汇编器(Reserved for assembler) |
$2-$3 |
$v0-$v1 |
函数调用返回值(values for results and expression evaluation) |
$4-$7 |
$a0-$a3 |
函数调用参数(arguments) |
$8-$15 |
$t0-$t7 |
暂时的(或随便用的) |
$16-$23 |
$s0-$s7 |
保存的(或如果用,需要SAVE/RESTORE的)(saved) |
$24-$25 |
$t8-$t9 |
暂时的(或随便用的) |
$28 |
$gp |
全局指针(Global Pointer) |
$29 |
$sp |
堆栈指针(Stack Pointer) |
$30 |
$fp |
帧指针(Frame Pointer) |
$31 |
$ra |
返回地址(return address) |
Table: MIPS registers and the convention governing their use.
Register Name |
Number |
Usage |
zero |
0 |
Constant 0 |
at |
1 |
Reserved for assembler |
v0 |
2 |
Expression evaluation and results of a function |
v1 |
3 |
Expression evaluation and results of a function |
a0 |
4 |
Argument 1 |
a1 |
5 |
Argument 2 |
a2 |
6 |
Argument 3 |
a3 |
7 |
Argument 4 |
t0 |
8 |
Temporary (not preserved across call) |
t1 |
9 |
Temporary (not preserved across call) |
t2 |
10 |
Temporary (not preserved across call) |
t3 |
11 |
Temporary (not preserved across call) |
t4 |
12 |
Temporary (not preserved across call) |
t5 |
13 |
Temporary (not preserved across call) |
t6 |
14 |
Temporary (not preserved across call) |
t7 |
15 |
Temporary (not preserved across call) |
s0 |
16 |
Saved temporary (preserved across call) |
s1 |
17 |
Saved temporary (preserved across call) |
s2 |
18 |
Saved temporary (preserved across call) |
s3 |
19 |
Saved temporary (preserved across call) |
s4 |
20 |
Saved temporary (preserved across call) |
s5 |
21 |
Saved temporary (preserved across call) |
s6 |
22 |
Saved temporary (preserved across call) |
s7 |
23 |
Saved temporary (preserved across call) |
t8 |
24 |
Temporary (not preserved across call) |
t9 |
25 |
Temporary (not preserved across call) |
k0 |
26 |
Reserved for OS kernel |
k1 |
27 |
Reserved for OS kernel |
gp |
28 |
Pointer to global area |
sp |
29 |
Stack pointer |
fp or s8 |
30 |
Frame pointer |
ra |
31 |
Return address (used by function call) |
2. 基于Linux的环境,应用程序可以通过抛出信号的方法挂起当前的任务,操作系统会将该任务控制块信息(TCB)交由应用程序注册的信号处理函数来处理,该信息中包含了
下面这个信号量上下文结构体,里面含有我们需要的CPU寄存器信息。
linux/2.4.20/include/asm-mips/sigcontext.h
/* * Keep this struct definition in sync with the sigcontext fragment * in arch/mips/tools/offset.c */ struct sigcontext { unsigned int sc_regmask; /* Unused */ unsigned int sc_status; unsigned long long sc_pc; unsigned long long sc_regs[32]; unsigned long long sc_fpregs[32]; unsigned int sc_ownedfp; /* Unused */ unsigned int sc_fpc_csr; unsigned int sc_fpc_eir; /* Unused */ unsigned int sc_used_math; unsigned int sc_ssflags; /* Unused */ unsigned long long sc_mdhi; unsigned long long sc_mdlo; unsigned int sc_cause; /* Unused */ unsigned int sc_badvaddr; /* Unused */ unsigned long sc_sigset[4]; /* kernel's sigset_t */ }; |
3. mips32常用汇编指令描述
源代码:
#include <stdio.h> #include <stdlib.h> int func_b(int a) { return 0; } int func_a(int a) { func_b(0); return 0; } int main(int argc, char* argv[]) { int temp = 0; func_a(temp); return 0; } |
下面是将-O2编译出的elf反编译后func_a的汇编指令:
- 0x80000810 <func_a>: lui a0,0x8000 /* a0 = 0x80000000 */ - 0x80000814 <func_a+4>: lui a1,0x8000 /* a1 = 0x80000000 */ - 0x80000818 <func_a+8>: addiu sp,sp,-24 /* sp = sp - 24 = 0x807fffd0 - 24 = 0x807FFFB8 */ - 0x8000081c <func_a+12>: addiu a0,a0,12876 /* a0 = a0 + 12876 = 0x8000324C */ - 0x80000820 <func_a+16>: addiu a1,a1,12888 /* a1 = a1 + 12888 */ - 0x80000824 <func_a+20>: sw ra,16(sp) /* SW Store Word Mem[Rs+offset] = Rt ra = 0x80000858 sp = 0x807FFFB8 功能相当于下面的C代码 *(unsigned int*)(sp + 16) = ra */ - 0x80000828 <func_a+24>: jal 0x80000dd0 <printf> /* JAL Jump and Link GPR[31] = PC + 8 PC = PC[31:28] || offset<<2 ra = pc+8 = 0x80000828 + 8 = 0x80000830 pc = 0x80000dd0 */ 0x8000082c <func_a+28>: li a2,78 - 0x80000830 <func_a+32>: jal 0x800007e0 <func_b> 0x80000834 <func_a+36>: move a0,zero - 0x80000838 <func_a+40>: lw ra,16(sp) - 0x8000083c <func_a+44>: move v0,zero /* v0 = 0 */ - 0x80000840 <func_a+48>: jr ra JR Jump Register PC = Rs 0x80000844 <func_a+52>: addiu sp,sp,24 |
阅读上面代码发现一个问题:
为什么ra = pc+8而不是pc+4呢?这样看来0x8000082c、0x80000834、0x80000844这3个地址对应的指令不会被执行到。
下面这段来自《MIPS32 4K Processor Core Family Software User’s Manual》
Jump and branch instructions change the control flow of a program. All jump and branch instructions occur with a delay of one instruction: that is, the instruction immediately following the jump or branch (this is known as the instruction in the delay slot) always executes while the target instruction is being fetched from storage. |
简单的说,因为MIPS的多级流水机制导致Jump和Branch指令后面的一个指令会被放在延时槽中,无条件执行。
下面是一些网站上找到的描述:
http://gcc.gnu.org/ml/gcc-help/2008-01/msg00059.html
How to traceback call stack on MIPS arch? Gcc saves the frame pointer to fp(s8) register at the beginning of each function if compiling source with -O0. But it won't do so if compiling source with -O2. Without frame pointers, can I trace back call stacks in current function context? Or is there any option which forces gcc to save frame pointers for MIPS arch? PRC 2008/1/8 |
这个问题是关于GCC优化的,看看下面这个表就清楚了。从实际测试情况看,fp(s8)也就是通用寄存器30可以用sp也就是通用寄存器29来代替,因为在函数领空(不包含子函数调用)的时候sp是保持固定值的,因为没有类似于x86的pop和push指令,该问题只着眼于当前函数上下文,没有考虑到向前追溯的问题。
mips_fp_be-gcc -O0 -g test.c -o btO0 mips_fp_be-objdump -S btO0 > asmO0.txt |
mips_fp_be-gcc -O2 -g test.c -o btO2 mips_fp_be-objdump -S btO2 > asmO2.txt |
00400e0c <main>: int main (int argc, char **argv) { 400e0c: 3c1c0fc0 lui gp,0xfc0 400e10: 279c79b4 addiu gp,gp,31156 400e14: 0399e021 addu gp,gp,t9 400e18: 27bdffd8 addiu sp,sp,-40 400e1c: afbc0010 sw gp,16(sp) 400e20: afbf0020 sw ra,32(sp) 400e24: afbe001c sw s8,28(sp) 400e28: afbc0018 sw gp,24(sp) 400e2c: 03a0f021 move s8,sp 400e30: afc40028 sw a0,40(s8) 400e34: afc5002c sw a1,44(s8) print_backtrace (); 400e38: 8f9980a4 lw t9,-32604(gp) 400e3c: 00000000 nop 400e40: 0320f809 jalr t9 400e44: 00000000 nop 400e48: 8fdc0010 lw gp,16(s8) return 0; 400e4c: 00001021 move v0,zero } 400e50: 03c0e821 move sp,s8 400e54: 8fbf0020 lw ra,32(sp) 400e58: 8fbe001c lw s8,28(sp) 400e5c: 03e00008 jr ra 400e60: 27bd0028 addiu sp,sp,40 ... |
00400de4 <main>: int main (int argc, char **argv) { 400de4: 3c1c0fc0 lui gp,0xfc0 400de8: 279c79dc addiu gp,gp,31196 400dec: 0399e021 addu gp,gp,t9 400df0: 27bdffe0 addiu sp,sp,-32 400df4: afbc0010 sw gp,16(sp) 400df8: afbf001c sw ra,28(sp) 400dfc: afbc0018 sw gp,24(sp) print_backtrace (); 400e00: 8f9980a4 lw t9,-32604(gp) 400e04: 00000000 nop 400e08: 0320f809 jalr t9 400e0c: 00000000 nop 400e10: 8fbc0010 lw gp,16(sp) return 0; } 400e14: 8fbf001c lw ra,28(sp) 400e18: 00001021 move v0,zero 400e1c: 03e00008 jr ra 400e20: 27bd0020 addiu sp,sp,32 ... |
You need to use the unwinder. #include <unwind.h> #include <stdio.h> static _Unwind_Reason_Code backtrace_helper (struct _Unwind_Context *ctx, void *a) { void *ip = (void*)_Unwind_GetIP (ctx); fprintf (stdout, " %p/n", ip); return _URC_NO_REASON; } void print_backtrace (void) { _Unwind_Backtrace (backtrace_helper, NULL); } int main (int argc, char **argv) { print_backtrace (); return 0; } |
该回答解释了上面的问题,提出用_Unwind_Backtrace函数来显示caller的地址,其实就MIPS而言对于单枝函数(没有子函数调用的函数)只要读ra寄存器的值就可以了,对于非单枝函数需要从堆栈里恢复出ra并显示。同样的问题这个函数也没有做向前的追溯。
For that to work, you must compile all the code with -fexceptions. You could also try compiling all the code with -fno-omit-framepointer and writing your own unwinder. I posted such an unwinder to java-patches@gcc.gnu.org several years ago. Later versions of GCC are starting to do optimizations in the function prolog that make unwinding without the unwinder meta-data very difficult. David Daney |
该回答给出了2个GCC的参数,也是回答了上面的问题。
根据MIPS寄存器定义和GCC生成的机器码可以得到网上描述的“MIPS不支持C函数的帧结构”。
我的理解是和x86的ESP和EBP寄存器比,MIPS的确是无法直观的从寄存器里找到当前情况下堆栈的底部,每个函数对应的栈的尺寸是由GCC计算出的,函数返回时的栈的恢复也是通过立即数的方式通过指令来实现,如(addiu sp,sp,40),这样我们做BackTrace最重要的一个问题就是确定每级函数的堆栈尺寸。
我觉得要确定每级函数的堆栈尺寸,只能通过解析机器码来实现。凑巧发现netbsd系统在内核代码中实现了对MIPS体系结构backtrace的支持,现在来分析下核心代码。
代码在:
http://cvsweb.netbsd.org/bsdweb.cgi/src/sys/arch/mips/mips/trap.c?rev=1.217.12.21&content-type=text/x-cvsweb-markup
代码的全路径:
/src/sys/arch/mips/mips/trap.c
下面演示将上面的函数移植到Linux下,应用程序出现异常时的BackTrace显示
应用程序代码:
#include <stdio.h> #include <stdlib.h> #include <signal.h> extern int sig_set(int signo); int func_a(unsigned char* a, unsigned char * b, int c, int d); int func_b(unsigned char* a); int func_c(unsigned char* a); int func_a(unsigned char* a, unsigned char * b, int c, int d) { func_b(a); return 0; } int func_b(unsigned char* a) { func_c(a); return 0; } int func_c(unsigned char* a) { *a = "Hello"; return 0; } int main(int argc, char* argv[]) { unsigned char* a; unsigned char buffer[128]; a = NULL; if( sig_set(SIGSEGV) != 0) printf("cannot catch SIGSEGV/n"); if( sig_set(SIGILL) != 0) printf("cannot catch SIGILL/n"); printf("trying to catch SIGFPE/n"); if( sig_set(SIGFPE) != 0) printf("cannot catch SIGFPE/n"); printf("trying to catch SIGBUS/n"); if( sig_set(SIGBUS) != 0) printf("cannot catch SIGBUS/n"); func_a(a, buffer, 2, 3); return 0; } |
先注册了4个信号SIGSEGV、SIGILL、SIGFPE、SIGBUS用自己的处理函数来处理。
在函数func_c调用的时候会出现异常,因为a的地址为NULL,这时将整个函数调用的过程显示出来,输入如下:
fSegvHandler default sigNo [11] pc=00400a84 cause 00000003 badaddr 00000000 d00:00000000 d01:7fff7bd0 d02:00401ee0 d03:00000000 d04:00000000 d05:7fff7d08 d06:00000002 d07:00000003 d08:0000d500 d09:0000000a d10:00000000 d11:00000000 d12:00001000 d13:00000000 d14:0000000a d15:15010000 d16:00401d40 d17:7fff7df4 d18:00401ca0 d19:00000001 d20:00400a9c d21:10012608 d22:ffffffff d23:00000000 d24:00000000 d25:00400a4c d26:00000010 d27:00000000 d28:10008040 d29:7fff7c88 d30:7fff7c88 d31:00400a30 k0(d26):00000010 k1(d27):00000000 gp(d28):10008040 sp(d29):7fff7c88 fp(d30):7fff7c88 ra(d31):00400a30 hi:00000000 lo:00000000 Calling backtrace: Func [400a4c] PC [400a84] Arg0~3 (0,7fff7d08,2,3) RetAddr [400a30] stackSize [16] Func [4009f4] PC [400a30] Arg0~3 (0,7fff7d08,2,3) RetAddr [4009d8] stackSize [40] Func [400990] PC [4009d8] Arg0~3 (0,7fff7d08,2,3) RetAddr [400c38] stackSize [40] Func [400a9c] PC [400c38] Arg0~3 (0,7fff7d08,2,3) RetAddr [2ab18b50] stackSize [176] Func [2ab189b0] PC [2ab18b50] Arg0~3 (0,7fff7d08,2,3) RetAddr [400790] stackSize [32] Func [400790] PC [400790] Arg0~3 (0,7fff7d08,2,3) RetAddr [0] stackSize [0] finished Segmentation fault |
上面输出的结果可以和实际情况对应起来:
这里是函数调用栈上各个函数的基地址
-bash-3.00$ mips_fp_be-objdump -t test1 | grep 400a4c 00400a4c g F .text 00000000 func_c -bash-3.00$ mips_fp_be-objdump -t test1 | grep 4009f4 004009f4 g F .text 00000000 func_b -bash-3.00$ mips_fp_be-objdump -t test1 | grep 400990 00400990 g F .text 00000000 func_a -bash-3.00$ mips_fp_be-objdump -t test1 | grep 400a9c 00400a9c g F .text 00000000 main -bash-3.00$ mips_fp_be-objdump -t test1 | grep 400790 00400790 g F .text 00000000 __start |
PC表示当前函数执行的地址,RetAddr为函数返回地址(和上级函数的PC对应)
例如第一行的输出:
Func [400a4c] PC [400a84] Arg0~3 (0,7fff7d08,2,3) RetAddr [400a30] stackSize [16]
对应于下面反汇编出来的代码,看的更清楚。
int func_c(unsigned char* a) { 400a4c: 3c1c0fc0 lui gp,0xfc0 400a50: 279c75f4 addiu gp,gp,30196 400a54: 0399e021 addu gp,gp,t9 400a58: 27bdfff0 addiu sp,sp,-16 400a5c: afbc0000 sw gp,0(sp) 400a60: afbe000c sw s8,12(sp) 400a64: afbc0008 sw gp,8(sp) 400a68: 03a0f021 move s8,sp 400a6c: afc40010 sw a0,16(s8) *a = "Hello"; 400a70: 8fc30010 lw v1,16(s8) 400a74: 8f828018 lw v0,-32744(gp) 400a78: 00000000 nop 400a7c: 24421ee0 addiu v0,v0,7904 400a80: 00000000 nop 400a84: a0620000 sb v0,0(v1) return 0; 400a88: 00001021 move v0,zero } 400a8c: 03c0e821 move sp,s8 400a90: 8fbe000c lw s8,12(sp) 400a94: 03e00008 jr ra 400a98: 27bd0010 addiu sp,sp,16 |
int func_b(unsigned char* a) { 4009f4: 3c1c0fc0 lui gp,0xfc0 4009f8: 279c764c addiu gp,gp,30284 4009fc: 0399e021 addu gp,gp,t9 400a00: 27bdffd8 addiu sp,sp,-40 400a04: afbc0010 sw gp,16(sp) 400a08: afbf0020 sw ra,32(sp) 400a0c: afbe001c sw s8,28(sp) 400a10: afbc0018 sw gp,24(sp) 400a14: 03a0f021 move s8,sp 400a18: afc40028 sw a0,40(s8) func_c(a); 400a1c: 8fc40028 lw a0,40(s8) 400a20: 8f998030 lw t9,-32720(gp) 400a24: 00000000 nop 400a28: 0320f809 jalr t9 400a2c: 00000000 nop 400a30: 8fdc0010 lw gp,16(s8) return 0; 400a34: 00001021 move v0,zero } 400a38: 03c0e821 move sp,s8 400a3c: 8fbf0020 lw ra,32(sp) 400a40: 8fbe001c lw s8,28(sp) 400a44: 03e00008 jr ra 400a48: 27bd0028 addiu sp,sp,40 |
转载于:https://www.cnblogs.com/dongzhiquan/archive/2012/08/17/2643294.html
基于MIPS架构的BackTrace实现相关推荐
- 计算机组成原理mips描述,计算机组成原理与接口技术:基于MIPS架构/华中科技大学教学改革建设教材...
<计算机组成原理与接口技术:基于MIPS架构/华中科技大学教学改革建设教材>以MIPS微处理器为背景,全面阐述了计算机组成原理与接口技术.首先简要阐述了计算机系统的基本构成.结构模型.工作 ...
- 计算机组成原理+左冬红,计算机组成原理与接口技术--基于MIPS架构(第2版高等学校电子信息类专业系列教材)...
导语 内容提要 华中科技大学电子信息与通信学院基于FPGA平台的"微机原理与接口技术"课程教学改革已进行数载,作者在总结教学经验基础上,对<计算机组成原理与接口技术>进 ...
- linux arm current_thread_info定义,linux中arm/mips架构current_thread_info定义
arm架构 current 宏的定义: linux-3.4\arch\arm\include\asm\current.h 中: static inline struct task_struct *ge ...
- X86、NP、ASIC、MIPS架构对比,存储
X86.NP.ASIC.MIPS架构对比_weixin_34390105的博客-CSDN博客 为什么MIPS架构的路由器CPU能实现比X86高很多的网络吞吐量? - 知乎 ASIC芯片_百度百科 (b ...
- MIPS架构的医院智能导诊系统设计
摘要:通过研究基于MIPS架构的SMP8654芯片的硬件架构,并且利用芯片内部的图形加速引擎GFX的方式实现了具有高清视频显示和图片文字处理功能的播放器.系统以嵌入式Linux和MiniGUI为平台设 ...
- MIPS 架构的 AR9331芯片 编译链相关内容
windows篇============================= 小撸路由是啥?小撸路由顾名思义就是@小撸撸过的 WR703N 之类的路由(系统基于OpenWrt)!因为它使用起来非常方便, ...
- 基于AR9331(MIPS架构)分析系统启动过程(uboot)
前提: 1.AR9331是基于MIPS 24K CPU的一款WIFI1X1芯片,其SDK采用uboot作为引导.AR9331中定义的基地址是:0x9f00,0000 2.MIPS24K芯片,将固定的起 ...
- 基于ar9331 mips架构AP121 uboot分析(3) 启动流程
mips架构u-boot启动流程 u-boot的启动过程大致做如下工作: 1.cpu初始化 2.时钟.串口.内存(ddr ram)初始化 3.内存划分.分配栈.数据.配置参数.以及u-boot代码在内 ...
- MIPS架构对比ARM架构
在很久很久以前,MIPS 与 ARM .X86合称全球三大主流CPU架构.但后来的发展情况,大家都看到了,X86 在英特尔.AMD下占据了PC级和服务器市场,而ARM 占据了移动端,但随着苹果.华为和 ...
最新文章
- 判别模型和生成模型的区别
- shell for循环案例:自动批量添加iptables应用端口规则
- 请教提高代码编写组织能力代码优化、精明
- 伪共享 FalseSharing (CacheLine,MESI) 浅析以及解决方案
- 在Qt(C++)中使用QThread实现多线程
- Java中float和double精度
- 【渝粤题库】陕西师范大学200701 数字逻辑
- C语言(CED)钢条最优切割收益
- 1.4编程基础之逻辑表达式与条件分支_16三角形判断(9分)
- 偶师傅说过的很有意思的话
- AJAX技术开发Back按钮问题的应用程序
- 计算机毕业设计Java校园一卡通管理系统(源码+系统+mysql数据库+Lw文档)
- 饭卡可以用水冲洗吗_关于饭卡使用与管理的规定
- 华硕fl5600l笔记本拆机,在光驱位加装固态硬盘
- 2021-04-20 m_map的几个地形水深数据库安装步骤
- 华为应用市场APP上架流程
- 可视化丨福尔摩斯探案集的数据分析
- java中成员变量的加载时机_工作奇谈——JAVA高级特性之反射
- python安装 Autodesk FBX 包
- 计算机系统安全之利用操作系统自带命令杀毒