嵌入式C语言自我修养:从芯片、编译器到操作系统
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.6 C语言和汇编语言混合编程

在一些嵌入式场合,我们经常看到C程序和汇编程序相互调用、混合编程。如在ARM启动代码中,系统一上电首先运行的是汇编代码,等初始化好内存堆栈环境后,才会跳到C程序中执行。对嵌入式软件进行优化时,在一些性能要求比较高的场合,通常会在C语言程序中内嵌一些汇编代码。作为一名嵌入式工程师,掌握C语言和汇编的混合编程还是很有必要的。

3.6.1 ATPCS规则

无论是在汇编程序中调用C程序,还是在C程序中内嵌汇编程序,往往都要牵扯到子程序的调用、子程序的返回、参数传递这些问题。从指令集层面看C语言和汇编语言,两者其实并无根本差别,都是指令集的不同程度的封装而已,最终都会被翻译成二进制机器指令。一个乌干达人和一个北爱尔兰人,说着不同的语言,用着不同的货币,有着不同的习俗和信仰,只要他们认可并遵守同一套贸易规则,一样可以相互往来做生意。C程序和汇编程序也是这样的,只要共同遵守一些约定的规则,它们之间也可以相互调用。因此,在学习C语言和ARM汇编语言混合编程之前,我们需要先了解一下ATPCS规则。

ATPCS的全称是ARM-Thumb Procedure Call Standard,其核心内容就是定义了ARM子程序调用的基本规则及堆栈的使用约定等。如ATPCS规定了ARM程序要使用满递减堆栈,入栈/出栈操作要使用STMFD/LDMFD指令,只要所有的程序都遵循这个约定,ARM程序的格式也就统一了,我们编写的ARM程序也就可以在各种各样的ARM处理器上运行了。

ATPCS最重要的内容是定义了子程序调用的具体规则,无论是程序员编写程序,还是编译器开发商开发编译器工具,一般都要遵守它。规则的主要内容如下。

● 子程序间要通过寄存器R0~R3(可记作a0~a3)传递参数,当参数个数大于4时,剩余的参数使用堆栈来传递。

● 子程序通过R0~R1返回结果。

● 子程序中使用R4~R11(可记作v1~v8)来保存局部变量。

● R12作为调用过程中的临时寄存器,一般用来保存函数的栈帧基址,记作FP。

● R13作为堆栈指针寄存器,一般记作SP。

● R14作为链接寄存器,用来保存函数调用者的返回地址,记作LR。

● R15作为程序计数器,总是指向当前正在运行的指令,记作PC。

在ARM平台下,无论是C程序,还是汇编程序,只要大家遵守ARM子程序之间的参数传递和调用规则,就可以很方便地在一个C程序中调用汇编子程序,或者在一个汇编程序中调用C程序。

以图3-6为例,我们在一个C源文件main.c中定义了main()函数和sum()函数,在一个汇编源文件SUM.S中定义了一个汇编子程序SUM_ASM。在main()函数中,我们直接调用了汇编子程序SUM_ASM,而在SUM_ASM的汇编代码实现中,又调用了在C源文件中定义的sum()函数。使用交叉编译器arm-linux-gcc编译这两个源文件,你会发现编译没有任何问题,而且还可以在ARM平台上正常运行。

图3-6 C程序和汇编程序的相互调用

3.6.2 在C程序中内嵌汇编代码

为了能在C程序中内嵌汇编代码,ARM编译器在ANSI C标准的基础上扩展了一个关键字__asm。通过这个关键字,我们就可以在C程序中内嵌ARM汇编代码。在C程序中内嵌汇编代码的格式如下。

这里有个细节需要注意一下,如果你想在内嵌的汇编代码中添加注释,记得要使用C语言的/**/注释符,而不是汇编语言的分号注释符。接下来我们就通过一个数据块复制的例子,给大家演示一下在C程序中内嵌汇编代码的方法。

为了能在C程序中内嵌汇编代码,不同的编译器基于ANSI C标准扩展了不同的关键字,使用的汇编格式可能也不太一样。如GNU ARM编译器提供了一个__asm__关键字,它的使用方法如下。

在一个C程序中,如果看到一段代码使用__asm__修饰,表示这段代码为内嵌汇编。__asm__的后面还可以选择使用__volatile__关键字修饰,用来告诉编译器不要优化这段代码。

3.6.3 在汇编程序中调用C程序

在C程序中可以内嵌汇编代码,在汇编程序中同样也可以调用C程序。在调用的时候,我们要注意根据ATPCS规则来完成参数的传递,并配置好C程序传递参数和保存局部变量所依赖的堆栈环境,然后使用BL指令直接跳转即可。

在上面的示例代码中,我们定义了两个文件:汇编文件SUM.S和C源文件main.c。在汇编文件SUM.S中定义了一个汇编子程序SUM_ASM,在C程序源文件main.c中定义了一个C语言函数sum()。在main()函数中,我们首先调用汇编子程序SUM_ASM,然后在SUM_ASM汇编程序中又调用了main.c中的C函数sum(),并通过寄存器R0、R1将参数传递给了sum()函数。使用arm-linux-gcc命令编译这两个源文件并运行,你会发现可以得到正确的运行结果,这也说明了C程序和汇编程序之间相互调用完全可行。

在函数调用过程中,如图3-7所示,当要传递的参数大于4个时,除了前4个参数使用寄存器R0~R3传递,剩余的参数要使用堆栈进行传递,这时候就需要编译器通过栈指针来进行管理和维护,具体细节可以参考第5章。

图3-7 程序调用过程中的参数传递