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