大概意思就是arm每个函数开始都会将PC、LR、SP以及FP四个寄存器入栈。
下面我们看一下这四个寄存器里面保存的是什么内存
arm-linux-gnueabi-gcc unwind.c -mapcs -w -g -o unwind(需要加上-mapcs才会严格按照上面说的入栈)
bash
#include <stdio.h>
#include <stdlib.h>
struct stackframe {
unsigned long fp;//低地址
unsigned long sp;
unsigned long lr;
unsigned long pc;//高地址
};
void backtrace() {
struct stackframe *frame = NULL;
unsigned long *sp = NULL;
asm volatile ("mov %0, ip" : "=g"(sp));//ip里面保存的是还未压栈的sp
printf("sp poniter 0x%lx\n", sp);
frame = (char*)sp - sizeof(struct stackframe);
printf("fp 0x%lx, pc 0x%lx, sp 0x%lx\n", frame->fp,frame->pc, frame->sp);//通过打印栈帧里面的sp确实和ip里面的一样的
/* 不知道怎么结束循环.... */
for (; frame->fp < 0xdeadbeef; frame = frame->fp - sizeof(struct stackframe) + sizeof(unsigned long)) {
printf("Function enter at [<%08x>] from [<%08x>]\n", frame->pc, frame->lr);
}
}
void f3(int c) {
printf("%d\n", c);
backtrace();
}
void f2(int b) {
f3(b);
}
void f1(int a) {
char arr[5] = {0};
f2(a);
}
int main(int argc, char *argv[]) {
printf("programe %s\n", argv[0]);
f1(1);
return 0;
}
arm-linux-gnueabi-objdump -S unwind > objdump
bash
void backtrace() {
8500: e1a0c00d mov ip, sp
8504: e92dd810 push {r4, fp, ip, lr, pc}
8508: e24cb004 sub fp, ip, #4
850c: e24dd00c sub sp, sp, #12
......................................
000085c0 <f3>:
void f3(int c) {
85c0: e1a0c00d mov ip, sp
85c4: e92dd800 push {fp, ip, lr, pc}
85c8: e24cb004 sub fp, ip, #4
85cc: e24dd008 sub sp, sp, #8
........................................
85dc: ebffff6b bl 8390 <_init+0x20>
backtrace();
85e0: ebffffc6 bl 8500 <backtrace>
}
85e4: e24bd00c sub sp, fp, #12
85e8: e89da800 ldm sp, {fp, sp, pc}
85ec: 0000878c .word 0x0000878c
000085f0 <f2>:
void f2(int b) {
85f0: e1a0c00d mov ip, sp
85f4: e92dd800 push {fp, ip, lr, pc}
85f8: e24cb004 sub fp, ip, #4
85fc: e24dd008 sub sp, sp, #8
8600: e50b0010 str r0, [fp, #-16]
f3(b);
8604: e51b0010 ldr r0, [fp, #-16]
8608: ebffffec bl 85c0 <f3>
}
860c: e24bd00c sub sp, fp, #12
8610: e89da800 ldm sp, {fp, sp, pc}
00008614 <f1>:
void f1(int a) {
8614: e1a0c00d mov ip, sp
8618: e92dd800 push {fp, ip, lr, pc}
861c: e24cb004 sub fp, ip, #4
8620: e24dd018 sub sp, sp, #24
..........................................
f2(a);
8644: e51b0020 ldr r0, [fp, #-32]
8648: ebffffe8 bl 85f0 <f2>
}
864c: e59f3018 ldr r3, [pc, #24] ; 866c <f1+0x58>
00008670 <main>:
int main(int argc, char *argv[]) {
8670: e1a0c00d mov ip, sp
8674: e92dd800 push {fp, ip, lr, pc}
8678: e24cb004 sub fp, ip, #4
867c: e24dd008 sub sp, sp, #8
8680: e50b0010 str r0, [fp, #-16]
8684: e50b1014 str r1, [fp, #-20]
printf("programe %s\n", argv[0]);
8688: e51b3014 ldr r3, [fp, #-20]
868c: e5933000 ldr r3, [r3]
8690: e59f001c ldr r0, [pc, #28] ; 86b4 <main+0x44>
8694: e1a01003 mov r1, r3
8698: ebffff3c bl 8390 <_init+0x20>
f1(1);
869c: e3a00001 mov r0, #1
86a0: ebffffdb bl 8614 <f1>
return 0;
86a4: e3a03000 mov r3, #0
}
上面是样例代码对应的汇编代码截取。在函数的最开头都存在如下代码
bash
8500: e1a0c00d mov ip, sp
8504: e92dd810 push {r4, fp, ip, lr, pc}
8508: e24cb004 sub fp, ip, #4
就是文章最开始说的函数一开始都会将fp、sp、lr以及pc压栈。那这几个寄存器里面的内容是什么呢?
sp即栈顶指针 ,sp里面记录的是当前函数的栈顶位置;并且从汇编代码里面能看到先是将sp给ip,然后将ip入栈。因此栈中记录的sp位置是压栈之前的
lr用于保存函数的返回地址 (若f2调用f3,那在样例代码中对应的位置就是这一行8558: e89da800 ldm sp, {fp, sp, pc})
pc指针, 程序计数器,用于记录当前执行到哪条指令。但是由于ARM采用流水线机制。当正确读取PC时,该值为当前指令(正在执行的指令)地址+8个字节。即PC执行当前指令的下两条地址。所以这就解释了样例代码的打印是0000850c
void backtrace() {
8500: e1a0c00d mov ip, sp
8504: e92dd810 push {r4, fp, ip, lr, pc}//执行到这里时,pc里面记录的是下面两条指令
8508: e24cb004 sub fp, ip, #4
850c: e24dd00c sub sp, sp, #12
......................................
具体可以查看这篇文章
fp:frame pointer:同样也是这段代码。sub fp, ip, #4// fp = ip - 4。那fp其实保存的就是上一个函数的函数栈起始位置-4。这也是for循环里面下一个函数栈需要写为
for (;; frame = frame->fp - sizeof(struct stackframe) + sizeof(unsigned long))
即下一个函数栈是fp + 4 - 12
为什么是上一个函数栈呢?
我们看下面的代码f1调用f2。函数f2最开始压入的fp,这个fp寄存器里面记录的是什么值呢。它里面其实就是上一个函数里面的sub fp, ip, #4得到的啊。ip里面又是上一个函数f1的函数栈开始位置。
bash
000085f0 <f2>:
void f2(int b) {
85f0: e1a0c00d mov ip, sp
85f4: e92dd800 push {fp, ip, lr, pc}
85f8: e24cb004 sub fp, ip, #4
85fc: e24dd008 sub sp, sp, #8
8600: e50b0010 str r0, [fp, #-16]
f3(b);
8604: e51b0010 ldr r0, [fp, #-16]
8608: ebffffec bl 85c0 <f3>
}
860c: e24bd00c sub sp, fp, #12
8610: e89da800 ldm sp, {fp, sp, pc}
00008614 <f1>:
void f1(int a) {
8614: e1a0c00d mov ip, sp
8618: e92dd800 push {fp, ip, lr, pc}
861c: e24cb004 sub fp, ip, #4
8620: e24dd018 sub sp, sp, #24
..........................................
f2(a);
8644: e51b0020 ldr r0, [fp, #-32]
8648: ebffffe8 bl 85f0 <f2>
}
864c: e59f3018 ldr r3, [pc, #24] ; 866c <f1+0x58>
因此最终的函数栈构成了下图所示。那我怎么感觉文章开始的那张图片是错的呢。。。。
最后样例代码运行结果如下图。由于不知道怎么算回溯结束,所以程序报错了
另外程序打印出来的地址也会汇编代码吻合.具体可以看汇编信息
另外用arm-linux-gnueabi-addr2line解析出来的行号也是准确的