- 嵌入式C语言自我修养:从芯片、编译器到操作系统
- 王利涛编著
- 2312字
- 2021-04-30 22:14:38
3.7 GNU ARM汇编语言
在ARM平台下从事嵌入式软件开发,大家会遇到各种不同的集成开发环境和编译器,如IAR、ADS1.2、RVDS、Keil MDK、RealView MDK、ARM交叉编译器arm-linux-gcc等。如果将这些不同的IDE归类,一般可以分为两大类:一类IDE内部集成了ARM编译器,另一类则使用开源的GNU GCC for ARM编译器,为了方便,在后续的文字中我们就简称为GNU ARM编译器。
3.7.1 重新认识编译器
编译器到底是什么?在很多人的概念中,编译器可能就是一个gcc命令,用来将C源程序编译成可执行文件。其实编译器不仅仅是一个简单的gcc或arm-linux-gcc命令,而是一套完整的工具集。一套完整的编译工具集主要包括以下几部分。
● 编译器:用来将C源文件编译成汇编文件。
● 汇编器:用来将汇编文件汇编成目标文件。
● 链接器:用来将目标文件组装成可执行文件。
● 二进制转化工具:objdump、objcopy、strip等。
● 库打包工具:ar。
● 调试工具:gdb、nm。
● 库/头文件:根据C语言标准定义的API实现的C标准库及对应的头文件。
一套完整的编译器工具集,不仅包含编译器,还有各种各样的工具、函数库、头文件等。编译器只不过是我们叫顺口了而已,大家以后可以刷新一下这个概念了。我们口中所说的编译器,其实不仅仅指编译器,还包括各种二进制工具、C标准库的实现、头文件等。
不同的ARM编译器开发商,会根据ARM指令集规定的标准指令去开发各自的编译器软件。目前市面上比较常见的编译器有ARM公司开发的ARMCC编译器、IAR ARM C/C++编译器、开源的GNU GCC for ARM交叉编译器。不同的IDE一般都会内嵌上面三种编译器中的一种,或者IDE和编译器分别独立发布,甚至有些IDE还可以通过配置,支持多种编译器。
各种厂商的编译器因为遵循同一套ARM指令集标准,因此经过不同编译器编译的程序都可以在同一台ARM处理器上运行。市面上各种ARM编译器之间的唯一的区别就是汇编指令的格式有所差异,造成差异的原因是各家编译器厂商各自扩展的伪操作(伪指令)不同,如图3-8所示:各家编译器厂商虽然都遵循同一套ARM指令集,但是都根据自己的产品需求和定位,各自扩展了不同的伪操作。
图3-8 ARM指令集与伪操作
以ARM公司官方发布的ARM编译器和开源的GNU ARM编译器为例,如图3-8所示,它们之间的主要差别在于伪操作。编译器开发商在设计编译器时会参考ARM指令集,将C程序翻译成CPU能够识别并运行的ARM标准指令。除此之外,为了方便汇编程序的编写,不同的编译器还会扩展一些各自的语法特性,这些扩展的伪指令和语法特性被称为伪操作。这些伪操作主要用来辅助程序员在编程时定义数据,定义不同的代码段和数据段,设计汇编程序的分支跳转结构,以及用来将汇编指令组装成一个可以运行的汇编程序。我们学习编写汇编程序,除了要掌握指令集中定义的ARM指令,还要了解不同编译器扩展的伪操作及它们之间的差别。
3.7.2 GNU ARM编译器的伪操作
不同的ARM编译器之间的伪操作差别还是蛮大的。以ARM编译器和GNU ARM编译器为例,我们可以对比一下它们在数据定义、程序结构方面的差别,如表3-5所示。
表3-5 不同编译器的伪操作对比
在后面的内容中,我们会经常使用ARM反汇编代码来分析C语言的底层运行机制。为了能看懂反汇编代码,我们还需要熟悉一下在一个反汇编文件中经常看到的各种GNU ARM伪指令操作,如表3-6所示。
表3-6 常用的GNU ARM伪指令操作
3.7.3 GNU ARM汇编语言中的标号
汇编语言中的符号定义规则,和C语言中标识符的定义规则类似:由字母、数字和下画线构成。GNU ARM编译器除了遵循标识符的一般规则,还有一些特殊的地方需要注意:GNU ARM汇编语言中的标识符可以由字母、数字、下画线和“.”构成,局部标号可以由纯数字构成。GNU格式的局部标号由数字N组成,在引用时使用Nf或Nb的形式,分别表示向前搜索或向后搜索。除此之外,GNU ARM汇编语言使用标号_start作为汇编程序的入口,如果你希望该标号被其他文件引用,只要在定义的地方使用.global伪操作声明一下就可以了。
3.7.4.section伪操作
在GNU ARM汇编语言中,用户可以使用.section伪操作自定义一个段,使用格式如下。
在使用伪操作.section定义一个段时,每个段以段名开始,以下一个段名或文件结尾作为结束标记。在定义段名时,注意不要和系统预留的段名冲突,如.text、.data、.bss、.rodata都是编译器系统预留的段名,分别表示代码段、数据段、BSS段、只读数据段。我们可以通过readelf命令来查看系统预留的段名。
3.7.5 基本数据格式
在GNU ARM汇编语言中,有时候我们需要定义一些常数。在定义数据的过程中有一些细节需要注意。
二进制数据通常以0B或0b开头,八进制数据以0开头,十六进制数据以0x开头,十进制数据则以非0数字开头。负数前面加“-”,取补用“~”,不相等用“<>”,其他运算符号如+、-、*、%、<、<<、>、>>、|、&、^、!、==、>=、&&与C语言语法相似。
字符串常量要用双引号""括起来。使用.ascii定义字符串时要自行在结尾加'\0',.string伪操作可以定义多个字符串,使用.asciz伪操作可以定义一个以NULL字符结尾的字符串,使用.rept伪操作可以重复定义数据。
还有一个需要注意的细节就是,在GNU ARM汇编程序中经常使用小圆点.表示当前指令的地址。这些细节大家最好都了解和学习一下,根据笔者以往的经验,这些不起眼的小细节往往会成为大家分析代码时的阅读障碍,而且在文档中很难找到关于它们的介绍信息。
3.7.6 数据定义
在GNU ARM汇编程序中,如果我们想定义一个浮点数,那么可以使用下面的伪操作来定义。
我们可以使用.float伪操作定义一个浮点数f,并初始化为3.14。如果你想将这个浮点数重新赋值为3.1415,则可以通过.equ伪操作来完成。
.equ伪操作除了给数据赋值,还可以把常量定义在代码段中,然后在代码中直接引用。这一点有点类似C语言中的#define宏定义。
3.7.7 汇编代码分析实战
“光说不练假把式”,有了GNU ARM汇编语言的基础之后,接下来我们做一个实验:在Linux环境下编写一个C程序,使用ARM交叉编译器将其编译为汇编文件,然后利用本节所学的知识分析该汇编文件的组织结构。
C程序源码如下。
接下来我们将这个hello.c源文件编译为汇编程序文件,并对其进行分析。