arm 函数栈回溯

大概意思就是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

......................................

具体可以查看这篇文章

ARM体系结构相关杂记_这个我好像学过的博客-CSDN博客

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解析出来的行号也是准确的

相关推荐
VekiSon2 小时前
Linux内核驱动——杂项设备驱动与内核模块编译
linux·c语言·arm开发·嵌入式硬件
AI+程序员在路上3 小时前
Nand Flash与EMMC区别及ARM开发板中的应用对比
arm开发
17(无规则自律)9 小时前
深入浅出 Linux 内核模块,写一个内核版的 Hello World
linux·arm开发·嵌入式硬件
梁洪飞21 小时前
内核的schedule和SMP多核处理器启动协议
linux·arm开发·嵌入式硬件·arm
代码游侠1 天前
学习笔记——Linux字符设备驱动
linux·运维·arm开发·嵌入式硬件·学习·架构
syseptember2 天前
Linux网络基础
linux·网络·arm开发
代码游侠2 天前
学习笔记——Linux字符设备驱动开发
linux·arm开发·驱动开发·单片机·嵌入式硬件·学习·算法
程序猿阿伟2 天前
《Apple Silicon与Windows on ARM:引擎原生构建与模拟层底层运作深度解析》
arm开发·windows
wkm9562 天前
在arm64 ubuntu系统安装Qt后编译时找不到Qt3DExtras头文件
开发语言·arm开发·qt
unicrom_深圳市由你创科技2 天前
基于ARM+DSP+FPGA异构计算架构的高速ADC采集卡定制方案
arm开发·fpga开发