深入理解函数调用过程以及函数栈的变化

前言:
函数在编程中的重要性不言而喻。函数三要素,函数参数以及局部变量存在于栈上,局部变量定义需要初始化等,这些所谓的函数特征都熟背于心,时刻指导着我们设计函数。但这背后的原理是什么呢?底层的技术又是怎么实现?接下来将解开函数调用的神秘面纱!

1 栈帧(Stack Frame)
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数返回地址。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。这里也说明了只含有局部变量(无全局或静态变量)的函数,是可重入的,因为每次调用的栈帧是独立的。
在ARM处理器中,寄存器R11(fp)指向当前的栈帧的底部(高地址),寄存器R13(sp)指向当前的栈帧的顶部(低地址)。

2 ARM指令集(Instruction-Set)
汇编指令,粗略看来有两种,分别用于CPU内部的寄存器之间的数据传递以及寄存器和CPU外部的内存之间的数据传递,前者速度快于后者。
也就是说,CPU处理数据的来源是寄存器,而寄存器的数据来源可以是寄存器,也可以是内存。也就有了不同的汇编指令:

寄存器之间的数据传递(部分指令截取):
在这里插入图片描述

寄存器和内存之间的数据传递:
STR(store register to a virtual address in memory):将寄存器中的数据存储到内存中
LDR(load a single value from a virtual address in memory):将内存中的数据存储至寄存器

3 函数调用过程的汇编代码分析
有了前面1,2节的理解,就可以着手分析函数的汇编代码。
编译器为: arm-none-eabi-gcc

先看源文件(test.c)

#include <stdio.h>
#define MAX 100

int fun_c(int p)
{
	return p -1;
}

int fun_a(int i,int j,int k,int l,int m,int n)
{
	int a = 10;
	int ret = 0;
	ret = a + i + j + k + l + m + n;
	return ret;
}

int fun_b(int x,int y)
{
	int a = 20;
	int max = MAX;
	int q = fun_c(3);
	return a - x - y - q;
}

int main(void)
{
	int c = 2;
	int d = 4;
	c = fun_a(1,2,3,4,5,6);
	d = fun_b(7,8);
	return 0;
}

使用arm-none-eabi-gcc -c test.c 编译test.c,生成test.o;
再使用arm-none-eabi-objdump -d test.o 反汇编,生成汇编文件如下:

Disassembly of section .text:

00000000 <fun_c>:
   0:	e52db004 	push	{fp}		; (str fp, [sp, #-4]!)
   4:	e28db000 	add	fp, sp, #0
   8:	e24dd00c 	sub	sp, sp, #12
   c:	e50b0008 	str	r0, [fp, #-8]
  10:	e51b3008 	ldr	r3, [fp, #-8]
  14:	e2433001 	sub	r3, r3, #1
  18:	e1a00003 	mov	r0, r3
  1c:	e28bd000 	add	sp, fp, #0
  20:	e49db004 	pop	{fp}		; (ldr fp, [sp], #4)
  24:	e12fff1e 	bx	lr

00000028 <fun_a>:
  28:	e52db004 	push	{fp}		; (str fp, [sp, #-4]!)
  2c:	e28db000 	add	fp, sp, #0
  30:	e24dd01c 	sub	sp, sp, #28
  34:	e50b0010 	str	r0, [fp, #-16]
  38:	e50b1014 	str	r1, [fp, #-20]	; 0xffffffec
  3c:	e50b2018 	str	r2, [fp, #-24]	; 0xffffffe8
  40:	e50b301c 	str	r3, [fp, #-28]	; 0xffffffe4
  44:	e3a0300a 	mov	r3, #10
  48:	e50b3008 	str	r3, [fp, #-8]
  4c:	e3a03000 	mov	r3, #0
  50:	e50b300c 	str	r3, [fp, #-12]
  54:	e51b2008 	ldr	r2, [fp, #-8]
  58:	e51b3010 	ldr	r3, [fp, #-16]
  5c:	e0822003 	add	r2, r2, r3
  60:	e51b3014 	ldr	r3, [fp, #-20]	; 0xffffffec
  64:	e0822003 	add	r2, r2, r3
  68:	e51b3018 	ldr	r3, [fp, #-24]	; 0xffffffe8
  6c:	e0822003 	add	r2, r2, r3
  70:	e51b301c 	ldr	r3, [fp, #-28]	; 0xffffffe4
  74:	e0822003 	add	r2, r2, r3
  78:	e59b3004 	ldr	r3, [fp, #4]
  7c:	e0822003 	add	r2, r2, r3
  80:	e59b3008 	ldr	r3, [fp, #8]
  84:	e0823003 	add	r3, r2, r3
  88:	e50b300c 	str	r3, [fp, #-12]
  8c:	e51b300c 	ldr	r3, [fp, #-12]
  90:	e1a00003 	mov	r0, r3
  94:	e28bd000 	add	sp, fp, #0
  98:	e49db004 	pop	{fp}		; (ldr fp, [sp], #4)
  9c:	e12fff1e 	bx	lr

000000a0 <fun_b>:
  a0:	e92d4800 	push	{fp, lr}
  a4:	e28db004 	add	fp, sp, #4
  a8:	e24dd018 	sub	sp, sp, #24
  ac:	e50b0018 	str	r0, [fp, #-24]	; 0xffffffe8
  b0:	e50b101c 	str	r1, [fp, #-28]	; 0xffffffe4
  b4:	e3a03014 	mov	r3, #20
  b8:	e50b3008 	str	r3, [fp, #-8]
  bc:	e3a03064 	mov	r3, #100	; 0x64
  c0:	e50b300c 	str	r3, [fp, #-12]
  c4:	e3a00003 	mov	r0, #3
  c8:	ebfffffe 	bl	0 <fun_c>
  cc:	e50b0010 	str	r0, [fp, #-16]
  d0:	e51b2008 	ldr	r2, [fp, #-8]
  d4:	e51b3018 	ldr	r3, [fp, #-24]	; 0xffffffe8
  d8:	e0422003 	sub	r2, r2, r3
  dc:	e51b301c 	ldr	r3, [fp, #-28]	; 0xffffffe4
  e0:	e0422003 	sub	r2, r2, r3
  e4:	e51b3010 	ldr	r3, [fp, #-16]
  e8:	e0423003 	sub	r3, r2, r3
  ec:	e1a00003 	mov	r0, r3
  f0:	e24bd004 	sub	sp, fp, #4
  f4:	e8bd4800 	pop	{fp, lr}
  f8:	e12fff1e 	bx	lr

000000fc <main>:
  fc:	e92d4800 	push	{fp, lr}
 100:	e28db004 	add	fp, sp, #4
 104:	e24dd010 	sub	sp, sp, #16
 108:	e3a03002 	mov	r3, #2
 10c:	e50b3008 	str	r3, [fp, #-8]
 110:	e3a03004 	mov	r3, #4
 114:	e50b300c 	str	r3, [fp, #-12]
 118:	e3a03006 	mov	r3, #6
 11c:	e58d3004 	str	r3, [sp, #4]
 120:	e3a03005 	mov	r3, #5
 124:	e58d3000 	str	r3, [sp]
 128:	e3a03004 	mov	r3, #4
 12c:	e3a02003 	mov	r2, #3
 130:	e3a01002 	mov	r1, #2
 134:	e3a00001 	mov	r0, #1
 138:	ebfffffe 	bl	28 <fun_a>
 13c:	e50b0008 	str	r0, [fp, #-8]
 140:	e3a01008 	mov	r1, #8
 144:	e3a00007 	mov	r0, #7
 148:	ebfffffe 	bl	a0 <fun_b>
 14c:	e50b000c 	str	r0, [fp, #-12]
 150:	e3a03000 	mov	r3, #0
 154:	e1a00003 	mov	r0, r3
 158:	e24bd004 	sub	sp, fp, #4
 15c:	e8bd4800 	pop	{fp, lr}
 160:	e12fff1e 	bx	lr

汇编说明:
140: e3a01008 mov r1, #8

140 表示该条指令的偏移地址,用于CPU通过PC取址;
e3a01008 表示mov r1, #8 这条汇编指令翻译成arm指令集(Instruction-Set)的机器码;
mov r1, #8 表示 将立即数8复制给r1寄存器。

调用过程:
1 main函数的栈帧中,首先将栈底指针fp,返回地址lr入栈,再开辟16字节的栈空间;
2 局部变量c/d入栈;
3 调用fun_a所用到的参数6/5入栈 ();
4 fun_a函数的栈帧中,首先将栈底指针fp入栈,再开辟28字节的栈空间;
5 4个寄存器(r0–r3)中的值入栈,通过栈底指针fp+4,fp+8得到剩余的两个值(),运算后入栈;
6 将结果存入r0寄存器,sp指向fp,释放栈空间,fp出栈。
7 同理,调用fun_b函数。

注意:
1 被调函数参数多于4个的才会入调用函数的栈帧,少于等于4个的直接通过r0–r3传递;
2 被调函数栈帧中的fp是调用函数的sp,通过 fp+偏移量 可以访问到调用函数的栈帧;
3 当fun_a函数返回后,栈帧空间被释放,但其中的值依然存在,当继续运行至fun_b函数时,其栈帧同样使用刚刚fun_a的,只不过地址中的值为fun_a留下的,所以局部变量需要初始化,原因并不是编译器乱分配的,而是上次使用者产生的垃圾数据。
4 多次递归调用的函数一直处于入栈,有可能会导致栈溢出(Stack Overflow)。

5 尽量使用地址传递:原因如下
值传递,相当于一个变量即存在于被调函数栈帧中,又会同样存在于调用函数栈帧中,在被调函数栈帧中修改该值,并不会影响调用函数栈帧中的值,因为地址不一样。可以理解为copy了一份;
地址传递,相当于一个变量即存在于调用函数栈帧中,而在被调用函数栈帧中存放的是该变量的地址,同一个地址,被调函数可以修改该值。

4 总结
在嵌入式C编程中(rom/ram资源有限),函数的参数应尽量不要超过4个,以提高执行效率。最好的方法是将多个参数打包成结构体,并且在传递参数时传递结构体指针,这样的效率最高,因为传递参数只用寄存器参与,并且在被调函数的栈帧中只用压栈传入的地址,而不是压栈全部参数,减少了栈的空间使用。
函数调用嵌套层次不要太深,避免使用递归调用,以免出现堆栈溢出(Stack Overflow)。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值