浅析ARMv8体系结构:Aarch64过程调用标准_aarch64-64-little

(重磅原创)冬之焱: 谈谈Linux内核的栈回溯与妙用-腾讯云开发者社区-腾讯云 (tencent.com)

ARM 架构 dump_stack 实现分析(3.0 printk %pS选项实现)

测试程序:

#include <stdio.h>
int A(int a)
{
}

int B()
{
	int a=5;
	A(a);
}

int C()
{
	B();
}

int main()
{
	C();
	return 0;
}

 objdump -d  a.cout:

ARM64中x29寄存器别名FP,栈开始的地址,高地址,X30别名LR寄存器,bl指令自动保存,ret指令自动弹出到pc。

00000000004005e4 <A>:
  4005e4:	d10043ff 	sub	sp, sp, #0x10
  4005e8:	b9000fe0 	str	w0, [sp, #12]
  4005ec:	d503201f 	nop
  4005f0:	910043ff 	add	sp, sp, #0x10
  4005f4:	d65f03c0 	ret

00000000004005f8 <B>:
  4005f8:	a9be7bfd 	stp	x29, x30, [sp, #-32]!               //将x29,x30保存在sp-16的位置,末尾"!"符号,表示sp自己=sp-32,因此改变sp的值
  4005fc:	910003fd 	mov	x29, sp                              //将sp的值保存在x29寄存器。这个x29将作为被调用函数A的FP(栈帧的起始地址)
  400600:	528000a0 	mov	w0, #0x5                   	// #5
  400604:	b9001fe0 	str	w0, [sp, #28]                        //内部变量使用SP等寄存器,但不改变SP的值。
  400608:	b9401fe0 	ldr	w0, [sp, #28]
  40060c:	97fffff6 	bl	4005e4 <A>							//将PC+4,保存到LR(x30)。跳转到A							
  400610:	d503201f 	nop
  400614:	a8c27bfd 	ldp	x29, x30, [sp], #32					//从sp中取出x29,x30, 然后sp=sp+32    。此时对比第一句sp的值恢复一开始的内容。
  400618:	d65f03c0 	ret

000000000040061c <C>:
  40061c:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!                  //将x29,x30保存在sp-16的位置,末尾"!"符号,表示sp自己=sp-16,因此改变sp的值
  400620:	910003fd 	mov	x29, sp									//将sp的值保存在x29寄存器。这个x29将作为被调用函数B的FP(栈帧的起始地址)
  400624:	97fffff5 	bl	4005f8 <B>								//将PC+4,保存到LR(x30)。跳转到B。
  400628:	d503201f 	nop
  40062c:	a8c17bfd 	ldp	x29, x30, [sp], #16                  //从sp中取出x29,x30, 然后sp=sp+16    。此时对比第一句sp的值恢复一开始的内容。
  400630:	d65f03c0 	ret

0000000000400634 <main>:
  400634:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!
  400638:	910003fd 	mov	x29, sp
  40063c:	97fffff8 	bl	40061c <C>
  400640:	52800000 	mov	w0, #0x0                   	// #0
  400644:	a8c17bfd 	ldp	x29, x30, [sp], #16
  400648:	d65f03c0 	ret
  40064c:	00000000 	.inst	0x00000000 ; undefined

调用关系:
C->B->A
C的栈在地址高位。
假如:当前在B函数。B函数一开始在SP-32处前面保存的X29和X30,x29代表调用者A的栈顶,即A函数一开始SP-16的位置。x30代表A函数的nop位置。

在进入新函数的时候,x29寄存器(FP)和SP相同(即上一次函数栈末尾,新函数栈起始),都指向栈开始的地址。然后SP减一段空间保存X30(LR)和X29(FP).
无论中间如何使用,最后会弹出这两个寄存器,还原最开始的值。

Linux内核的栈回溯dump_stack原理_运维

测试:

[root@localhost test]# cat /proc/kallsyms |grep unwind
ffff80000800e310 T unwind_frame 

用printk  %pS格式打印某地址+偏移:0xffff80000800e310+4

#include <linux/module.h>
#include <linux/gfp.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/sched/task.h>
#include <linux/arm-smccc.h>
#include <linux/cpumask.h>

static int __init test_init(void)
{
	printk("%s %pS\n", "test", (void *)0xffff80000800e314); //
	return 0;
}

static void __exit test_exit(void)
{
	printk(KERN_INFO "test_exit\n");
}

module_init(test_init);
module_exit(test_exit);


MODULE_LICENSE("GPL");

输出: 

 [  232.383519] test unwind_frame+0x4/0x168

dump_backtrace函数解析: 

void dump_backtrace(struct pt_regs *regs, struct task_struct *tsk,
		    const char *loglvl)
{
	if (tsk == current) {  //如果是对当前运行的函数
		start_backtrace(&frame,   //设置初始frame的内容。sp和pc
				(unsigned long)__builtin_frame_address(0),    //gcc内建函数,获取当前函数的栈FP(x29),等于sp:mov	x29, sp   ,保存到frame->fp
				(unsigned long)dump_backtrace);  //以dump_backtrace作为追溯起始函数。保存到frame->pc
	} else {
		/*
		 * task blocked in __switch_to
		 */
		start_backtrace(&frame,
				thread_saved_fp(tsk),
				thread_saved_pc(tsk));
	}
	
		printk("%sCall trace:\n", loglvl);
	do {
		/* skip until specified stack frame */
		if (!skip) {
			dump_backtrace_entry(frame.pc, loglvl);   //走这里打印
		} else if (frame.fp == regs->regs[29]) {
			skip = 0;
			/*
			 * Mostly, this is the case where this function is
			 * called in panic/abort. As exception handler's
			 * stack frame does not contain the corresponding pc
			 * at which an exception has taken place, use regs->pc
			 * instead.
			 */
			dump_backtrace_entry(regs->pc, loglvl);
		}
	} while (!unwind_frame(tsk, &frame));   //解析栈中的最后16字节中的x29和x30寄存器。保存到frame

}

 unwind_frame函数解析:

int notrace unwind_frame(struct task_struct *tsk, struct stackframe *frame)
{
	unsigned long fp = frame->fp; //取出fp的值,这个是地址。
	
	frame->fp = READ_ONCE_NOCHECK(*(unsigned long *)(fp));  //读取fp。其中保存了上级函数的fp的地址。读取出来,下次循环将再读取。。。
	frame->pc = READ_ONCE_NOCHECK(*(unsigned long *)(fp + 8)); //偏移+8地址,其中保存了上级函数的地址x30(lr)
	frame->prev_fp = fp;
	frame->prev_type = info.type;

}