前言
如今C语言很强大,在嵌入式编程当中应用广泛,但是为什么还要在c语言中还要嵌入汇编语言,对于没有学过汇编语言的情况下,难学又不易理解?
因为汇编语言实时性比C语言好,占用单片机资源少,生成的执行文件更小,汇编语言程序直接被转换成机器指令。执行效率更高,这对某些嵌入式领域所要求的高实时性有一定帮助,此外,有时在涉及到硬件底层操作的代码时必须使用汇编代码。
所以说,在一些性能要求比较高的情况下,通常会在c语言程序在内嵌一些汇编代码。下面让我们来体会一下,在Keil环境下C与汇编语言的混合使用。
一、在c函数中调用汇编函数
1.调用不带参数的汇编函数
#include <stdio.h>
extern void Init_1(void);
int main()
{
Init_1();
return 0;
}
AREA My_Function ,CODE,READONLY
EXPORT Init_1 ;将Init_1导出,供工程中其他文件调用实现
Init_1
MOV R1, #1 ;将立即数1放到寄存器R1中,即将R1寄存器初始化为1
MOV R2, #2 ;将立即数2放到寄存器R2中,即将R2寄存器初始化为2
LOOP ;循环开始的地方
CMP R1, #10 ;比较R1与10的大小
BHS LOOP_END ;如果R1大于等于10,则跳转到LOOP_END,循环结束;否则,执行下一语句
ADD R2, #1 ;R2=R2+1;
ADD R1, #1 ;R1=R1+1;
B LOOP ;无条件跳转到LOOP执行下一次循环
LOOP_END ;循环结束
NOP ;空指令,延时等待
END
注意: 必须空格后再写END,不然会被认为是段名,表示程序结束
关于新建一个MD工程,并添加一个新项目的步骤 可参考我的另一篇博客
MDK下汇编语言调试分析
- 开始调试
- 单步执行
程序执行到如上图所示时,可以发现R0 ,R1 寄存器的值已经变为1 ,2
2.调用带形参的汇编函数
下面由于要涉及到arm寄存器的一些分析,所以我在这先简单介绍一下。
R1~R3通常用来传递函数参数 R4~R11用来保存程序运算的中间结果或函数的局部变量 R12通常用来作为函数调用过程中的临时寄存器 R13寄存器是堆栈寄存器(SP),用来保存堆栈的当前指针 R14寄存器是链接寄存器(LR),用来保存函数的返回地址 R15寄存器是程序寄存器(PC),指向程序当前的地址
将原汇编语言 Init_1 函数的类型改为 int Init_1(init) ,此函数功能修改为 传入一个整型数x,函数运行后返回整型数 x+100
修改main.c
#include <stdio.h>
extern int Init_1(int a);
int main()
{
Init_1(66);
return 0;
}
修改func.s
Init_1
ADD R0,R0,#100 ;R0记录传入的形参,并且执行R0=R0+100;
BX LR ;跳转的LR的地址执行,在这里,LR记录main函数调用子函数的返回地址
可以看到我传入的函数参数是66,转换为16进制即为0x42 ,说明函数形参已被写入寄存器R0 中
我们看到在进入子函数时,一般有两个准备工作
1)初始化形参 2)R14(LR)寄存器值发生变化,记录从子函数返回当前函数的地址
我们看到,下一条指令执行是R0=R0+0x64 ,此时R0的值为0x42,我们可以预测执行完这步,R0的值应变为0xA6
果然R0的值变为0xA6 ,如下图
当程序运行到如上图所示时,点击单步调试,注意观察到R14(LR)中的地址-1被加载到R15(PC)指针,PC指针指向该地址,即可返回主程序。
至此,一个函数的调用过程到此为止。
本例是以一个参数为例的,但当函数参数多于4个时,所调用的方式有所不同。前4个参数分别用R0~R3寄存器来记录,多余参数被加载到内存中使用栈来传递。
关于ARM体系下有多个函数形参的情况下,我写了一篇博客具体分析了一下
ARM体系下函数形参调用寄存器详解
二、在汇编函数中调用c函数
int sum(int a,int b)
{
int c;
a=100;
b=200;
c=a+b;
return c;
}
AREA My_Function ,CODE,READONLY
IMPORT sum
ENTRY
EXPORT __main
__main
BL sum
BX LR
END
调用子函数的过程与c文件调用汇编函数的过程大同小异,有兴趣的话可以自己尝试一下仿真跟踪调试。
注意: ARM汇编指令不支持顶格写,否则不能识别;声明变量时不要有空格,不然会出现奇奇怪怪的错误。
三、汇编函数与c函数混合调用
extern int SUM_ASM(void);
int sum(int a,int b)
{
int c;
a=100;
b=200;
c=a+b;
return c;
}
int main(void)
{
SUM_ASM();
return 0;
}
AREA My_Function ,CODE,READONLY
IMPORT sum
EXPORT SUM_ASM
ENTRY
SUM_ASM
LDR R0,=0X3
LDR R1,=0X4
BL sum
MOV PC,LR
END
- 小结
此代码实现了在c函数中调用汇编函数,再从汇编函数中调用c函数,调用方式与之前大同小异,但是了解c程序和汇编程序相互调用,混合编程还是很有帮助的。
如在MDK下stm32程序的执行过程为
先执行.s的汇编程序,再.s文件中执行以下操作
- 初始化堆栈指针 SP=_initial_sp
- 初始化 PC 指针 =Reset_Handler
- 初始化中断向量表
- 配置系统时钟
- 调用 C 库函数 _main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界
然后再跳转到main函数,执行一些c语言程序。 在一些对性能要求比较高的情况下,有时也会在c语言程序在内嵌一些汇编代码。
总结
无论是在汇编程序中调用c程序,还是c程序中调用(内嵌)汇编程序,往往都涉及到子程序的调用、参数的传递、子程序的返回等问题。不同的语言有着不同程度的指令集封装,在了解这些函数调用过程时,首先要初步了解ATPCS(ARM-Thumb Processor Call standard),其核心内容是子程序调用的基本规则和堆栈的使用约定等。
此外,了解了这些子程序的具体调用规则,设计程序仿真调试去验证、理解每一步如何得来,亲自实践操作对于理解子程序的调用方式是很有帮助的。
以上是我的相关学习体会,如有问题,望大家不吝指教。
参考书籍 1.嵌入式c语言自我修养 从芯片、编译器到操作系统 电子工业出版社 王利涛著
2.<STM32库开发指南–基于野火指南者开发板>
|