Linux内核调用栈一————原理介绍

前言


Linux OOPS信息有出问题时候的函数调用栈;研究Linux内核代码的时候,很多时候会通过dump_stack()来查看linux的函数调用栈。那么这些函数调用栈是如何获取的,本文旨在以ARM64为例,给出一个前因后果。

APCS


这个话题要从APCS说起,APCS的全程是ARM Procedure Call standard,即ARM程序调用标准。该标准定义了ARM平台下函数调用过程中,每个寄存器的用途。实际上这个标准是编译器必须严格遵守的,但是如果自己写汇编的话,其实并不一定要遵守。如果工作中需要经常通过反汇编来调试问题的话,一定要看下APCS标准。
我们这里并不介绍完整的APCS标准,我们只关注三个寄存器的SP,R30和R29。下面是APCS对这三个寄存器定的规则:
Linux内核调用栈一————原理介绍_第1张图片SP:栈指针寄存器,指向栈顶,这没什么好说的,相信大家对它也绝不陌生。
LR:Link寄存器,指向返回地址,这个大家应该也都很清楚,一般用于函数调用的跳转指令会自动将返回地址填充到LR寄存器。
FP:Frame寄存器,这个寄存器需要注意了,它指向当前函数的栈底。试想一下一个函数从该函数的栈底开始依次存放LR,以及上一级函数的栈底,那么久很容易找到上一级函数。

一个简单的例子

c代码

#include 

int b(int i)
{
	printf("%d\n",i);

	return 0;
}

int a(int i)
{
	b(i);
	return 0;
}

void main()
{
	a(1);
}

反汇编代码:

00000000004005c0 :
  4005c0:	a9be7bfd 	stp	x29, x30, [sp,#-32]!
  4005c4:	910003fd 	mov	x29, sp
  4005c8:	b9001fa0 	str	w0, [x29,#28]
  4005cc:	90000000 	adrp	x0, 400000 <_init-0x3f0>
  4005d0:	911b0000 	add	x0, x0, #0x6c0
  4005d4:	b9401fa1 	ldr	w1, [x29,#28]
  4005d8:	97ffffa2 	bl	400460 
  4005dc:	52800000 	mov	w0, #0x0                   	// #0
  4005e0:	a8c27bfd 	ldp	x29, x30, [sp],#32
  4005e4:	d65f03c0 	ret

00000000004005e8 :
  4005e8:	a9be7bfd 	stp	x29, x30, [sp,#-32]!
  4005ec:	910003fd 	mov	x29, sp
  4005f0:	b9001fa0 	str	w0, [x29,#28]
  4005f4:	b9401fa0 	ldr	w0, [x29,#28]
  4005f8:	97fffff2 	bl	4005c0 
  4005fc:	52800000 	mov	w0, #0x0                   	// #0
  400600:	a8c27bfd 	ldp	x29, x30, [sp],#32
  400604:	d65f03c0 	ret

0000000000400608 
: 400608: a9bf7bfd stp x29, x30, [sp,#-16]! 40060c: 910003fd mov x29, sp 400610: 52800020 mov w0, #0x1 // #1 400614: 97fffff5 bl 4005e8 400618: d503201f nop 40061c: a8c17bfd ldp x29, x30, [sp],#16 400620: d65f03c0 ret 400624: 00000000 .inst 0x00000000 ; undefined

我们看到main函数中首先将LR和FP入栈,然后将SP的值赋值给X29(FP),这里FP指向了main函数的栈底,之所以说FP是指向栈底是因为一开始就将SP赋值给了FP,后面SP还会向下增长。
main函数调用函数a,我们看到函数a里面的处理逻辑跟main函数一致,首相保存LR和FP,然后FP指向a函数的栈底;同样b函数也是。
栈结构如下:
Linux内核调用栈一————原理介绍_第2张图片如果当前在b函数中执行,那么FP指向b函数的栈底,从b函数的栈底可以得到返回地址以及a函数的栈底,这两点结合起来就知道从哪个函数的哪个执行调用到b函数的。

本文从原理出发,并通过一个简单的例子做了介绍。可以看出来追溯函数的调用栈真的非常简单,下一篇文章Linux内核调用栈二,将会从OOPS的调用栈打印以及dump_stack两个例子介绍内核追溯函数调用栈的实例。

你可能感兴趣的:(Linux调试)