很少有一款操作系统会完全使用汇编语言来编写,也几乎没有一个操作系统是完全使用C语言写成的。汇编语言负责编写启动、堆栈初始化、系统运行段划分等关键部分的代码,而C语言则被用在涉及到算法、逻辑等与硬件关联不大的地方,解决复杂的问题。因此需要将两者完美的结合,才能发挥出各自的优势。
这一节我们就来探讨一下ARM的混合编程问题,为我们以后正式编写操作系统打下基础。
通俗地讲,无论是汇编语言、C或是其他类型的语言,都是人能够理解的语言,都有特定的语法规则支持各种执行逻辑。从这一点出发,所有的语言(包括人类互相交流的语言),彼此之间都没有什么不同。而编译生成的机器码只有机器才能理解,并且不同的硬件平台彼此又语言不通,这样,问题似乎就来了,人能理解的语言机器理解不了,机器能理解的语言人又很难掌握,这个问题该怎样解决呢?
编译器能够帮助我们解决这个问题,编译器的作用就是将人能够理解的语言翻译成机器能够理解的语言,如图2-1所示。从这个角度看,把编译器叫做翻译器,多少也有些道理。
既然人能理解的语言五花八门,而机器能理解的语言对于特定平台又是唯一的,因而不同语言间的混合编程,就要从硬件的角度去理解才会有意义。
图2-1 编译器的作用
要想具体实现汇编语言和C语言之间的混合编程,就必须制定出一套统一的标准并约束双方共同遵守,我们将这套共同遵守的标准称为过程调用标准(Procedure Call Standard),简称为PCS。
过程调用标准其实就是一套规定,任何编程语言在应用的时候,只要去遵守这种约定,就能够与同样遵守该约定的其他编程语言和平共处。因此,想要让C语言和汇编语言能够混合编程并且正确执行,对过程调用标准的严格遵守就显得相当重要了。
其实,过程调用标准并没有具体针对任何语言,也就是说,无论什么编程语言,只要遵循这种标准,就可以直接调用其他同样遵守该标准的编程语言写出来的程序,而不一定非要限制在C语言或汇编语言上。ARM的PCS有很多版本,如ARM 过程调用标准APCS、Thumb过程调用标准TPCS,以及ARM-Thumb之间互相调用时所要遵守的过程调用标准ATPCS。目前“最新”的一套标准名为ARM体系结构过程调用标准(Procedure Call Standard for the ARM Architecture),简称AAPCS,该标准定义了ARM下的若干个子例程如何才能实现独立编写、独立编译、独立汇编,却可以共同运行。
我们依旧会借助一个实际例子来介绍一下一些基本的过程调用规则,看看如何实现C语言和汇编语言的混合编程。
代码2-6
代码2-6是一段汇编程序,它与代码2-5很类似。二者的区别在于代码2-6不是直接将helloworld字符串送给串口FIFO寄存器,而是通过bl指令调用helloworld函数,由该函数负责数据写入。
bl指令我们没有接触过,它的功能与b指令几乎一致。唯一的不同是bl指令在运行时,能够自动地保存程序的返回地址。这样,在成功调用子程序并且运行结束时,就能够返回原先的位置继续运行。
helloworld函数是由C语言写成的。根据AAPCS的规定,当函数发生调用的时候,函数的参数会保存在ARM核心寄存器R0~R3中,如果参数多于4个,则剩下的参数会保存在堆栈中。因此,我们也会把寄存器R0~R3称为参数寄存器(argument register),用别名a1~a4代替。在代码2-6中,调用helloworld函数之前,我们分别对R0、R1两个寄存器赋值,这两个值就是helloworld函数的参数。
下面就让我们来了解一下这个函数具体是怎样实现的。
代码2-7
代码2-7类似于之前用C语言实现的helloworld函数。不同的是该函数多了两个参数,分别是内存地址和要写向该地址的字符串的首地址。
整个函数的功能是将指针p所指向的字符串写向由addr代表的内存地址中。当函数helloworld执行完毕后,程序会返回到代码2-6的.L1地址处继续运行。helloworld函数带有一个int型返回值,该返回值会保存到核心寄存器R0中,如果返回的是一个64位的值,则该值会由R0、R1同时保存。很显然这也是由AAPCS所规定的。
这两段程序在编译的时候会稍有不同,首先我们必须分别编译这两段程序,产生两个目标文件。
将代码2-6命名为“start.s”、代码2-7命名为“helloworld.c”。接着我们要使用类似于命令2-1的命令来生成两个目标文件。然后,使用命令2-5来编译生成ELF格式的可执行文件。
命令2-5
在终端运行skyeye命令,我们可以得到与代码2-1相同的结果。这表示我们已经成功实现了C和汇编的混合编程。