用汇编语言编写程序
前面能够运行在虚拟硬件环境中的第一段程序是使用C语言写成的。理论上我们可以完全使用C语言来编写整个操作系统。但在实际应用中,完全使用C语言编写的操作系统却寥寥无几。汇编语言虽然有很多的缺点,但在操作系统底层开发中,有时却能发挥出不可替代的作用,这一点相信读者会在今后的学习中有深入的体会。正因为如此,我们还需要利用一节的篇幅,说一说如何使用汇编语言进行ARM程序开发。
我们仍将采用边做边讲、在遇到问题的时候再去说明的方式,避免因为枯燥地将知识点罗列出来而影响读者的兴趣。
下面仍从一个例子开始。
代码2-5
代码2-5就是helloworld程序的汇编版本。想要让读者理解这段程序,我们并不需要对ARM的汇编程序设计进行太过系统的介绍,我们只需要说清楚两点,一是寄存器,二是指令集。
寄存器是CPU进行运算时所必需的存储设备。ARM上的寄存器很丰富,能够参与普通运算的从R0到R12,就有13个之多。如果没有特殊规定,程序在运算时可以随意选择R0~R12的任意一个寄存器参与运算。除了这些寄存器之外,在ARM体系结构中还包括负责保存堆栈地址的R13寄存器,负责保存程序返回值的R14寄存器以及负责记录程序地址的R15寄存器。这些寄存器由于拥有专门的功能,所以有的时候也使用别名来称呼它们,分别叫做SP、LR和PC。另外,ARM还有一个非常特殊的寄存器专门用来保存程序的运行状态,叫做程序状态寄存器,使用CPSR来表示。CPSR中既包含描述程序在运算过程中是否产生了溢出、负数等状态的相应位,也包含用于描述当前处理模式的模式位。关于处理器模式,因为代码2-5中没有涉及,所以我们会在它们出现的时候进行详细讲述。
说完了寄存器,我们再来聊一聊指令。在汇编程序设计过程中,指令是我们构成汇编程序的基本单元。
指令包括指令助记符和伪指令。每一条指令助记符都代表CPU提供的某一条指令,也可以说指令助记符都唯一地对应了CPU的一条机器码。因为机器码本身就是无规则的二进制数,本身不容易被记忆和使用,所以用助记符来帮助记忆,这其实就是"助记符"三个字的真正意思。
伪指令不像指令助记符那样是在程序运行期间由CPU来执行的,通常伪指令只会作用在编译过程中,对最终生成的文件造成不同程度的影响。有些伪指令在编译的时候并不生成代码,还有些伪指令则会扩展成一条或多条实际的指令。
下面我们就对代码2-5中所使用的指令和伪指令做深入的介绍。
(1).arch伪指令:该伪指令的作用是选择目标体系结构。例如,在代码2-5中,.arch表示该段汇编代码将会被编译生成符合armv4体系结构的代码,从而实现了为特定平台生成特定的代码。如果不想使用该伪指令,那么在程序编译的时候,使用-march参数也能达到同样的效果。
(2).global伪指令:该伪指令的含义是让global过的符号对链接器可见,也就是说,一个函数或变量,通常情况下只在本文件内有效,当需要在外部引用该文件里的某一个函数或变量时,必须首先将该函数或变量使用.global伪指令进行声明。在代码2-5中,helloworld函数作为我们程序的入口函数,必须在链接时用-e参数来指定,或者在链接脚本中用ENTRY命令来做显示声明。因此.global伪指令在这里就显得至关重要了。
(3).equ伪指令:该伪指令其实很简单,相当于C中的宏定义。在代码2-5中,正是使用了.equ伪指令将0x50000020用宏REG_FIFO来代替。
(4).text伪指令表示从当前位置开始的内容被归并到代码段中。
(5).align伪指令:我们并不是第一次遇到align这个词,回想一下链接脚本一节,关键字ALIGN的作用是在链接时,迫使被修饰的内容对齐。这里的.align与ALIGN关键字意义相同,也能够更新位置计数器的值,使代码对齐到某一边界。但此处需要强调的是,该伪指令针对ARM汇编的用法与其他体系结构稍有不同。例如,在m68k、sparc以及运行有ELF文件格式的x86结构中,.align后边的数字直接代表了要对齐的字节数,比如.align 8表示此处代码会按照8字节边界对齐,而在ARM体系结构下,.align后边的数是以幂的形式出现的,正如代码2-5中那样,.align 2表示此处是以4字节对齐的。
(6).ascii伪指令:该伪指令用于在内存中定义字符串,我们要输出到串口中的字符串"helloworld\n"就是由它定义的。
(7)ldr伪指令:很多朋友在看了前面的介绍后可能会得到一个错误结论,即所有伪指令都是以点开头的。其实不然,ldr就是一个反例。ldr被称为常量装载伪指令,其作用是将一个常量装载到寄存器中。因为ARM指令等宽指令格式的限制,不能保证所有的常数都可以通过一条指令装载到寄存器中。程序编译时,如果能够将ldr指令展开成一条常量装载指令,则编译器就会用该指令代替ldr,否则编译器会首先开辟一段空间存储被装载的常量,然后使用一条存储器读取指令将该常量读入到寄存器当中。
(8)adr伪指令:adr伪指令被称做地址装载伪指令,与ldr类似,adr伪指令能够将一个相对地址写入寄存器中。
至此,代码2-5中使用的全部伪指令我们就介绍完了。这里我们想强调的是,伪指令都是与编译器有关的,换句话说,即使是同一种硬件平台,GCC中的伪指令与其他编译器中的伪指令也并不完全兼容。
我们前面说过,伪指令要么作用在编译过程中对最终生成的文件造成影响,要么会扩展成一条或多条实际的指令,这些都是在程序编译过程中由编译器决定的,因此,不同的编译器的伪指令集可能不同。而指令是与硬件平台有关的,即使不同的编译器对同一指令使用的助记符不一致,它们生成的机器码仍然是一致的,并且不同编译器所使用的指令助记符集几乎都彼此相同的,因为指令助记符都是由芯片生产厂商定义的,编译器大多会遵守这种定义。
说完了伪指令,我们来介绍一下代码2-5中的指令。
(1)内存装载指令ldr:ldr如果作为实际指令出现,表示从内存读取数据到寄存器中,而代码中使用了一个b作为后缀,表示读取的只是一个字节的数据。相信读者能够举一反三,明白ldrh指令的含义。在该语句中,[r0]表示地址值,而后边的#0x1是常数1,代表寄存器R0的增量,这种寻址方式被称为立即数后变址寻址。整条语句的意思是,首先从寄存器R0所指向的地址中读取一个字节的数据存储在R2中,然后R0中的值自加1。因为在这之前R0的值已经指向了helloworld字符串的首地址,所以这条语句第一次执行,就会把字符'h'存储在R2中。
(2)内存存储指令str:str能够将寄存器中的值存储到另一个寄存器所指向的内存地址中。代码2-5正是使用了str指令将刚刚读取到的R2中的值存储到R1地址处,而该地址正是串口FIFO寄存器的地址。因此,字符'h'就会被显示到屏幕当中。
(3)比较指令cmp:比较指令是将待比较的两个数相减,然后去影响标志位,所以它实际上是一条不返回运算结果的减法指令。比较指令不保存结果,但是会使当前程序状态寄存器CPSR中零标志位置位或清除,进而影响条件判断语句的执行。
(4)跳转指令b:接下来我们看到的指令bne其实就是跳转指令b,后缀ne是条件码,表示执行条件为"不相等"。整个语句可以解释为如果不相等,则跳转到符号.L2处。这里的不相等指的正是上一条指令比较产生的结果。因为比较指令只影响标志位,所以我们可以说条件码只是根据相应标志位的值来有选择地执行。
程序在每次向串口FIFO寄存器写入数据后,都要与零进行比较。当比较的结果不相等,表示字符串还没有结尾,则继续将下一个字符写入到串口FIFO寄存器中;如果字符与零相等,证明读到了字符串结束符,然后程序进入死循环,这就是程序2-5的执行过程。
最后我们可以使用与代码2-1同样的编译方法来编译这段汇编版的helloworld。
总结一下,现在我们已经学习了ldr、str、cmp和b这四条汇编指令,也学习了arch、global、equ、text、align、ascii、ldr和adr这八条伪指令。这些指令不但常用,而且很重要。在操作系统代码的编写过程中,也会被经常使用。