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