从汇编角度理解C语言篇 (三) —— C语言函数的实现
2026/9/18 22:17:42 网站建设 项目流程

1. C语言函数组成

// 返回类型 函数名 参数列表 int add (int a, int b) { // 函数体 int ret = a + b; // 返回值 return ret; }

在C语言中,函数是执行特定任务的独立代码块。一个函数可以接收参数(如果有的话),执行一系列操作,然后可选地返回一个值。C语言中的函数组成主要包括以下几个部分:

  1. 返回类型‌:
    函数的返回类型指定了函数执行完成后返回值的类型。例如,intvoidfloatchar等。void表示该函数不返回任何值。

  2. 函数名‌:
    这是函数的标识符,用于在程序中调用该函数。函数名必须是合法的标识符,且在程序中是唯一的。

  3. 参数列表‌:
    参数列表定义了函数执行时可以接收的数据。参数可以有零个或多个,每个参数都有一个类型和一个名称。参数之间用逗号分隔。如果函数不需要参数,则参数列表为空,写成void(例如int add(int a, int b))。

  4. 函数体‌:
    函数体是包含了一组执行特定任务的语句的大括号{}内的部分。这是函数的实际代码实现。

  5. 返回值‌:
    在函数体内部,可以使用return语句返回一个值。返回值的类型必须与函数的返回类型匹配。如果函数的返回类型是void,则不能使用return语句返回一个值(但可以使用return;仅表示提前退出)。

2. C语言函数传入传出参数

在读下面内容之前,可以先思考一个问题:为什么使用变量传入参数给函数,在函数里面无法直接改变传入变量的值?

还是以 add 函数为例。c语言代码:

int add(int a, int b) { return a + b; } int main(void) { add(1, 2); return 0; }

对应ARM汇编代码:

add: 0x080001AE 4602 MOV r2, r0 ; 把r0的值给 r2寄存器 0x080001B0 1850 ADDS r0, r2, r1 ; r2和r1的值相加,给r0, 用于传出参数 0x080001B2 4770 BX lr ; 跳转回函数调用地址 main: 0x080001B6 2102 MOVS r1, #0x02 ; 立即数赋值. 把 数字2 写入 r1寄存器 0x080001B8 2001 MOVS r0, #0x01 ; 把 数字1 写入 r0寄存器 0x080001BA F7FFFFF8 BL.W add (0x080001AE) ; 调用/进入 add 函数 0x080001BE 2000 MOVS r0, #0x00 ; return 0

从汇编代码可以看出:

  • 传入参数:对函数传入参数时,需要把参数传给 r 寄存器,进入函数后,函数再从 r 寄存器里获取数据。
  • 传出参数:函数传出或者返回值时,同样通过 r 寄存器向外传入参数。

也就是说 r 寄存器可以作为函数传入传出的介质。

以下为ARM寄存器组说明:

寄存器别名主要用途特殊说明
R0A1函数第 1 个参数 / 函数返回值ARM 标准调用约定下,函数返回值必须通过R0返回
R1A2函数第 2 个参数-
R2A3函数第 3 个参数-
R3A4函数第 4 个参数-
R4V1通用变量寄存器(需被调用者保存)若函数修改了R4-R11,需手动保存到栈
R5V2通用变量寄存器(需被调用者保存)-
R6V3通用变量寄存器(需被调用者保存)-
R7V4通用变量寄存器 / 帧指针(FP)Thumb 指令集中常作为栈帧指针
R8V5通用变量寄存器(需被调用者保存)-
R9V6通用变量寄存器 / 平台寄存器部分操作系统中用作全局变量指针(SB
R10V7通用变量寄存器(需被调用者保存)-
R11V8通用变量寄存器(需被调用者保存)-
R12IP内部过程调用寄存器用于动态链接、库函数调用时的临时中转
R13SP栈指针(Stack Pointer)指向栈顶,硬件自动维护(Cortex-M 为满递减栈)
R14LR链接寄存器(Link Register)保存函数 / 中断的返回地址,BL指令会自动写入
R15PC程序计数器(Program Counter)指向当前执行指令的下一条指令地址,修改PC会直接跳转

ARM约定,R0-R3 寄存器传递函数前 4 个参数。

那么如果函数大于四个参数呢?

聪明的你应该已经想到了,内存也可以用于保存参数。ARM正是这样做的:超过4个参数,则进行压栈处理。

操作规则
传参R0-R3传递前 4 个参数,超过部分压入栈
返回值32 位 / 64 位结果通过R0(+R1)返回
寄存器保存R0-R3, R12由调用者保存;R4-R11, LR由被调用者保存

当然,不同芯片架构,不同编译器也会有区别,这里不展开讨论。

回到上面的问题:为什么使用变量传入参数给函数,在函数里面无法直接改变传入变量的值?

简单来说,因为数据经过了拷贝,在函数里是改变的并不是原来的变量里的内容。

3. 函数调用和返回

我们还是看上面的例子,但只看调用和返回部分。

add: 0x080001B2 4770 BX lr ; 跳转回函数调用地址 0x080001BE main: 0x080001BA F7FFFFF8 BL.W add (0x080001AE) ; 调用/进入 add 函数 0x080001BE 2000 MOVS r0, #0x00 ; return 0

我们在第一篇讲过,汇编代码组织方式可以按功能流程组织代码块,执行不同功能时只需要跳转到对应的代码块。

C语言进入到子函数也是通过跳转方式实现的,跳转前BL会自动将下一条指令的地址放入LR寄存器,子函数通过LR寄存器的地址返回到原来的地址。

我们再看看多层嵌套函数:

function_2: 0x080001B8 4770 BX lr function_1: 0x080001BA B500 PUSH {lr} 0x080001C0 F7FFFFF8 BL.W function_2 (0x080001B4) 0x080001C4 BD00 POP {pc} main: 0x080001CA F7FFFFF6 BL.W function_1 (0x080001BA) 0x080001CE 2000 MOVS r0, #0x00

function_1 进入子函数 function_2 前,需要把 LR寄存器 PUSH压栈,返回的时候POP 值直接给到PC,CPU立即跳回调用者的下一行。更多层的函数嵌套也是一样的。

这里有一个小技巧:在调试时,我们可以使用调试器的调用栈功能,如果代码出现fault,就能很直观的看到函数调用关系,方便解bug。

4. C语言代码优化

经过上面的内容,我们能明显发现一个问题,每次调用函数都需要进行参数拷贝,不仅需要消耗拷贝时间,参数多的时候也消耗内存空间。另外,函数调用的越深,栈使用的也会越多。在使用内存资源比较小的芯片,我们需要尽可能的减少资源消耗。

1. 对于参数拷贝问题,我们可以使用以下方法优化:

  • 用宏替代简单的函数。
  • inline 内联函数。需要注意的是,内联函数代码及变量需要尽可能的少,否则在内联函数展开后可能会适得其反。

但其实一般情况下,编译器已经能做的足够好,只要我们提高编译优化等级就能很好的优化掉此类问题。

2. 对于函数调用深度问题,可以考虑以下方法:

  • 避免过度封装函数
  • 定时器等回调函数可以设置触发标志,在另一处进行具体函数处理
  • 使用事件驱动型架构,降低调用层级

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询