![嵌入式C语言自我修养:从芯片、编译器到操作系统](https://wfqqreader-1252317822.image.myqcloud.com/cover/248/37669248/b_37669248.jpg)
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平台上正常运行。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_128_1.jpg?sign=1738883314-okLYIXgZHjPZSKWcJACm33mTaytZkHEJ-0-cbb5eb22f08990030365cedc1dd70f94)
图3-6 C程序和汇编程序的相互调用
3.6.2 在C程序中内嵌汇编代码
为了能在C程序中内嵌汇编代码,ARM编译器在ANSI C标准的基础上扩展了一个关键字__asm。通过这个关键字,我们就可以在C程序中内嵌ARM汇编代码。在C程序中内嵌汇编代码的格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_129_1.jpg?sign=1738883314-n7zVd2RALNqJSEEPjlMYO9VpTpM4CRM2-0-a5bba402f3983803a63656b8b48d8bac)
这里有个细节需要注意一下,如果你想在内嵌的汇编代码中添加注释,记得要使用C语言的/**/注释符,而不是汇编语言的分号注释符。接下来我们就通过一个数据块复制的例子,给大家演示一下在C程序中内嵌汇编代码的方法。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_129_2.jpg?sign=1738883314-BI4EZ36spVajtpb1civImvcC06FmkM1X-0-c9635c353b4f9c6e386e4c2f5898fc87)
为了能在C程序中内嵌汇编代码,不同的编译器基于ANSI C标准扩展了不同的关键字,使用的汇编格式可能也不太一样。如GNU ARM编译器提供了一个__asm__关键字,它的使用方法如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_130_1.jpg?sign=1738883314-UtlgCIuCtb5SNkQWGptdatRKcevQrPN1-0-d7374f1d6ab01835ed5f858d970f393a)
在一个C程序中,如果看到一段代码使用__asm__修饰,表示这段代码为内嵌汇编。__asm__的后面还可以选择使用__volatile__关键字修饰,用来告诉编译器不要优化这段代码。
3.6.3 在汇编程序中调用C程序
在C程序中可以内嵌汇编代码,在汇编程序中同样也可以调用C程序。在调用的时候,我们要注意根据ATPCS规则来完成参数的传递,并配置好C程序传递参数和保存局部变量所依赖的堆栈环境,然后使用BL指令直接跳转即可。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_130_2.jpg?sign=1738883314-qEF3U049J5FVFx0qrE5kVbdRCga0NbOX-0-d8424732a5bdadc97d36aaf3c5908299)
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_131_1.jpg?sign=1738883314-ZL468KU6CG12BuDGLJrRJw3jC2bsCmA1-0-04aa12aa74c1ffd6a8b84fb5d6250712)
在上面的示例代码中,我们定义了两个文件:汇编文件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程序和汇编程序之间相互调用完全可行。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_131_2.jpg?sign=1738883314-xvysloRTBw8Onf4wxK520Bukw6oCKdhR-0-82b9929d403bc1fe6122572956bc7d74)
在函数调用过程中,如图3-7所示,当要传递的参数大于4个时,除了前4个参数使用寄存器R0~R3传递,剩余的参数要使用堆栈进行传递,这时候就需要编译器通过栈指针来进行管理和维护,具体细节可以参考第5章。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_131_3.jpg?sign=1738883314-SXDbXeX0Se7YBva2wHLEvnT7Utht7a3g-0-27cbd17d0eaf5a94ceb89108cdf84a65)
图3-7 程序调用过程中的参数传递