ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从汇编角度理解C语言篇 (三) —— C语言函数的实现

从汇编角度理解C语言篇 (三) —— C语言函数的实现 1. C语言函数组成// 返回类型 函数名 参数列表 int add (int a, int b) { // 函数体 int ret a b; // 返回值 return ret; }在C语言中函数是执行特定任务的独立代码块。一个函数可以接收参数如果有的话执行一系列操作然后可选地返回一个值。C语言中的函数组成主要包括以下几个部分‌返回类型‌函数的返回类型指定了函数执行完成后返回值的类型。例如int、void、float、char等。void表示该函数不返回任何值。‌函数名‌这是函数的标识符用于在程序中调用该函数。函数名必须是合法的标识符且在程序中是唯一的。‌参数列表‌参数列表定义了函数执行时可以接收的数据。参数可以有零个或多个每个参数都有一个类型和一个名称。参数之间用逗号分隔。如果函数不需要参数则参数列表为空写成void例如int add(int a, int b)。‌函数体‌函数体是包含了一组执行特定任务的语句的大括号{}内的部分。这是函数的实际代码实现。‌返回值‌在函数体内部可以使用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通用变量寄存器 / 帧指针FPThumb 指令集中常作为栈帧指针R8V5通用变量寄存器需被调用者保存-R9V6通用变量寄存器 / 平台寄存器部分操作系统中用作全局变量指针SBR10V7通用变量寄存器需被调用者保存-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 位结果通过R0R1返回寄存器保存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, #0x00function_1 进入子函数 function_2 前需要把 LR寄存器 PUSH压栈返回的时候POP 值直接给到PCCPU立即跳回调用者的下一行。更多层的函数嵌套也是一样的。这里有一个小技巧在调试时我们可以使用调试器的调用栈功能如果代码出现fault就能很直观的看到函数调用关系方便解bug。4. C语言代码优化经过上面的内容我们能明显发现一个问题每次调用函数都需要进行参数拷贝不仅需要消耗拷贝时间参数多的时候也消耗内存空间。另外函数调用的越深栈使用的也会越多。在使用内存资源比较小的芯片我们需要尽可能的减少资源消耗。1. 对于参数拷贝问题我们可以使用以下方法优化用宏替代简单的函数。inline 内联函数。需要注意的是内联函数代码及变量需要尽可能的少否则在内联函数展开后可能会适得其反。但其实一般情况下编译器已经能做的足够好只要我们提高编译优化等级就能很好的优化掉此类问题。2. 对于函数调用深度问题可以考虑以下方法避免过度封装函数定时器等回调函数可以设置触发标志在另一处进行具体函数处理使用事件驱动型架构降低调用层级
返回列表