c语言lr分析器的设计与实现_【C/C++】C语言之函数调用过程分析

本文深入探讨了ARM处理器上嵌入式C语言函数的调用机制,包括栈帧概念、ARM指令集解析及函数调用过程的汇编代码分析。详细介绍了函数参数传递方式、局部变量存储位置等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

5cc9e11e12d82d837e41f087d7873735.png

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

以ARM处理器上编写C语言函数为例!

1 栈帧(Stack Frame)

从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数返回地址。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。这里也说明了只含有局部变量(无全局或静态变量)的函数,是**可重入**的,因为每次调用的栈帧是独立的。

在ARM处理器中,寄存器R11(fp)指向当前的栈帧的底部(高地址),寄存器R13(sp)指向当前的栈帧的顶部(低地址)。

2 ARM指令集(Instruction-Set)

汇编指令,粗略看来有两种,分别用于CPU内部的寄存器之间的数据传递以及寄存器和CPU外部的内存之间的数据传递,前者速度快于后者。

也就是说,CPU处理数据的来源是寄存器,而寄存器的数据来源可以是寄存器,也可以是内存。也就有了不同的汇编指令:

寄存器之间的数据传递(部分指令截取):

5d2cf8329fa68aee3a1ce8b50e9c3f99.png

寄存器和内存之间的数据传递:

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 

使用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)。

参考资料:

  1. ARM ® v7-M Architecture.pdf

2. ARM System Developer's Guide.pdf (5.5节)

238f41ab4163635beb9e06201b3cc069.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值