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

相关推荐
海滩游侠7 小时前
ARM assembly: Lesson 10
arm开发
Eternal-Student8 小时前
arm-伪指令
arm开发
Eternal-Student8 小时前
everyday_question dq20240731
开发语言·arm开发·php
Tlog嵌入式1 天前
蓝桥杯【物联网】零基础到国奖之路:十六. 扩展模块之矩阵按键
arm开发·stm32·单片机·mcu·物联网·蓝桥杯·iot
Q8343158192 天前
华为 海思22AP10(SS524)H.265 编解码处理器用户指南
arm开发·人工智能·嵌入式硬件·音视频·硬件工程·h.265·视频编解码
星羽空间2 天前
win11下 keil报错Cannot load driver ‘D:\Keil_v5\ARM\Segger\JL2CM3.dll‘
arm开发·keil
日晨难再2 天前
AMBA:APB的历史(从APB1到APB5)
arm开发·arm·硬件工程·fpga·数字ic
Tlog嵌入式2 天前
蓝桥杯【物联网】零基础到国奖之路:十八. 扩展模块之光敏和AS312
arm开发·stm32·单片机·mcu·物联网·蓝桥杯·iot
汽车电子助手3 天前
【STM32开发环境搭建】-4-在STM32CubeMX中新增Keil(MDK-ARM) 5的工程目录(包含指定路径的C和H文件)
c语言·arm开发·stm32·stm32cubemx·keil
海滩游侠3 天前
ARM Assembly 6: Shift 和 Rotate
arm开发