目录
人们利用助记符代替机器指令的操作码,用标号代替指令及操作数的地址,这就形成了汇编语言( Assembly Language)。 它是介于机器语言与高级语言之间的计算机语言,被人们称为第二代计算机语言。
用汇编语言写成的程序不能直接放入计算机内部的存储器中去执行,必须先转为机器语言。把用汇编语言写成的源程序“翻译”成机器语言的工具称为汇编程序或汇编器( Assembler),以下统称作汇编器。
汇编语言源程序可以用通用的文本编辑软件编辑,以文本形式存盘。不同的汇编器中的汇编语言源程序的格式有所区别。同时,汇编器除了识别计算机指令系统外,为了能够正确地产生目标代码以及方便汇编语言的编写,还提供了一些在汇编时使用的命令和操作符号。在编写汇编程序时,也必须正确使用它们。由于汇编器提供的指令仅是对把汇编语言源程序“翻译”成机器码工作的辅助,并不产生运行阶段执行的机器码,因此这些指令被称为伪指令( Pseudo Instruction)。
一、GUN 汇编书写格式
汇编语言源程序以行为单位进行设计,每一行最多可以包含以下 4 部分。
标号: 操作码 操作数 注释
1. 标号(Label)
标号表示地址位置,是可选的。有些指令的前面可能会有标号,通过这个标号可以得到指令的地址,如在跳转语句转移的位置前就应该加标号。标号也可以用于表示数据地址,如在字符串前可以放一个标号来表示该字符串的首地址,这个标号有点类似 C 语言中的数组名。
对于标号有下列要求及说明。
- 如果一个语句有标号,则标号必须书写在汇编语句的开头部分;标号后必须带冒号 “:”;标号是区别字母大小写的,但指令不区分字母大小写;一个标号在一个文件(程序)中只能定义一次,否则其会被视为重复定义,不能通过汇编;一行语句只能有一个标号,代表当前指令的存储地址。
- 可以组成标号的字符有英文大小写字母、数字 0~9、下划线 “_”、美元符号 “$”,但第一个符号不能为数字或 $。
- 标号长度基本上不受限制,但实际使用时最好不要超过 20 个字符。若希望标号能被更多的汇编器识别,建议标号(或变量名)的长度小于 8 个字符。
2. 操作码(Opcodes)
操作码可以是指令和伪指令, 其中伪指令是指 Arm-GUN 汇编器可以识别的伪指令。对于有标号的行,必须用至少一个空格或制表符(即 TAB 键)将标号与操作码隔开。对于没有标号的行,不能从第一列开始写指令码,应以空格或制表符开头。汇编器不区分操作码中字母的大小写。
3. 操作数(Operands)
操作数可以是地址、标号或指令码定义的常数,也可以是伪运算符构成的表达式。若一条指令或伪指令有操作数,则操作数与操作码之间必须用空格隔开书写。操作数多于一个时,操作数之间用英文逗号 “,” 分隔。操作数中一般都有一个存放结果的寄存器,这个寄存器位于操作数的最前面。
Arm-GUN 汇编器识别的伪运算符
运算符 | 功能 | 类型 | 实例 |
---|---|---|---|
+ | 加法 | 二元 | mov r3,#30+40 等价于 mov r3,#70 |
- | 减法 | 二元 | mov r3,#40-30 等价于 mov r3,#10 |
* | 乘法 | 二元 | mov r3,#5*4 等价于 mov r3,#20 |
/ | 除法 | 二元 | mov r3,#20/4 等价于 mov r3,#5 |
% | 取模 | 二元 | mov r3,#20%7 等价于 mov r3,#6 |
|| | 逻辑或 | 二元 | `mov r3,#1 |
&& | 逻辑与 | 二元 | mov r3,#1&&0 等价于 mov r3,#0 |
<< | 左移 | 二元 | mov r3,#4<<2 等价于 mov r3,#16 |
>> | 右移 | 二元 | mov r3,#4>>2 等价于 mov r3,#1 |
^ | 按位异或 | 二元 | mov r3,#4^6 等价于 mov r3,#2 |
& | 按位与 | 二元 | mov r3,#4^2 等价于 mov r3,#0 |
| | 按位或 | 二元 | `mov r3,#4 |
== | 等于 | 二元 | mov r3,#1==0 等价于 mov r3,#0 |
!= | 不等于 | 二元 | mov r3,#1!=0 等价于 mov r3,#1 |
<= | 小于等于 | 二元 | mov r3,#1<=0 等价于 mov r3,#0 |
>= | 大于等于 | 二元 | mov r3,#1>=0 等价于 mov r3,#1 |
+ | 正号 | 一元 | mov r3,#+1 等价于 mov r3,#1 |
- | 负号 | 一元 | ldr r3,= -325 等价于 ldr r3,=0xfffffebb |
~ | 取反运算 | 一元 | ldr r3,=~325 等价于 ldr r3,= 0xfffffeba |
> | 大于 | 一元 | mov r3,#1>0 等价于 mov r3,#1 |
< | 小于 | 一元 | mov r3,#1<0 等价于 mov r3,#0 |
关于操作数,有以下几点补充说明。
- 圆点 “.” 的用法。若圆点 “.” 单独出现在语句的操作码之后的操作数位置上,则代表当前程序计数器的值被放置在圆点的位置。例如:
b .
指令代表转向本身,相当于永久循环,在调试时若希望程序停留在某个地方,则可以添加这种语句,调试之后应删除。 - 操作数中常数的进制表示:十进制(默认不需要前缀标识)、十六进制(前缀标识 0x)、二进制(前缀标识 0b)。
- 立即数的表示方法:常数前添加 “#” 时表示一个立即数,不加 “#” 时表示一个地址;当立即数大于或等于 256 时, 若使用 LDR 指令, 则立即数前应加 “=”。需要注意的是,初学时常常会将立即数前的 “#” 遗漏,汇编器识别不到这种错误,且只有在只能使用立即数的指令时,汇编器才会提示错误。
mov r3, 1 //给寄存器 r3 赋值为 1(这个语句是错误的)
汇编时会提示 “immediate expression requires a # prefix – ' mov r3,1'”,故其应该改为:
mov r3,#1 //寄存器 r3 赋值为 1(这个语句是正确的)
4. 注释(Comments)
注释即说明文字,是对汇编指令的作用和功能进行解释,有助于对指令的理解,可以采用单行边注释和整行注释,建议使用 “//” 引导。以 “/*” 开始和 “*/” 结束的注释用于保留调试时屏蔽语句行。
二、GUN 汇编常用伪指令
不同集成开发环境下的伪指令不同。 伪指令书写格式与所使用的汇编器有关,读者可参照具体的工程样例“照葫芦画瓢”。
伪指令主要有常量、宏的定义、条件判断、文件包含等,在 Arm-GUN 下,所有的伪指令都是以 “.” 开头的。
1. 系统预定义的段
.data //已初始化的数据段
.bss //未初始化的数据段
.text //代码段
汇编语言程序在经过汇编和链接之后,最终生成可执行文件。.elf 可执行文件是以段为单位来组织文件的,通常划分为 .text
、 .data
和 .bss
等段。 其中,.text
是只读的代码段,是程序存放的地方,实际代码存储在 flash 区;.data
是可读可写的数据段,而 .bss
则是可读可写且没有初始化的数据段,启动时会清零,两者都是用来存放变量的,实际数据存储在 RAM 区。这些段分别从哪个地址开始,这在链接文件中会指明。
2. 常量的定义
.equ(或.set) 常量名,表达式
在汇编程序中使用常量定义,能够提高程序代码的可读性,并且可使代码维护更加简单。常量的定义可以使用 .equ
或 .set
伪指令。
下面是 GNU 汇编器的一个常量定义的例子。
.equ _NVIC_ICER,0xE000E180 //定义常量名_NVIC_ICER=0xE000E180
LDR R0,=_NVIC_ICER //将常量名_NVIC_ICER的值0xE000E180放R0中
.set ROM_size,128*102 //定义常量 ROM_size
3. 数据定义
数据定义伪指令
数据类型 | 长度 | 举例 | 备注 |
---|---|---|---|
.word |
字(4 字节) | .word 0x12345678 |
定义多个数据时,数据间用 “,” 隔开 |
.hword |
半字(2 字节) | .hword 0x1234 |
定义多个数据时,数据间用 “,” 隔开 |
.byte |
字节(1 字节) | .byte 0x12 |
定义多个数据时,数据间用 “,” 隔开 |
.ascii |
字符串 | .ascii “hello\n\0” |
定义的字符串不以 “\0” 结尾,要自行添加 “\0” |
.asciz 或 .string |
字符串 | .asciz “hello\n” |
定义的字符串以 “\0” 结尾 |
在定义一个数据类型之前,一般会给一个标号(相当于 C 语言中的变量名或数组名),该标号表示这个数据的起始地址,然后利用这个标号通过直接寻址方式就可以访问到这个数据,具体用法如下。
LDR R3,=NUMNER //得到 NUMNER 的存储地址
LDR R4,[R3] //将 0x123456789 读到 R4 中
……
LDR R0,=HELLO_TEXT //得到 HELLO_TEXT 的起始地址
BL PrintText //调用 PrintText 函数以显示字符串
……
ALIGN 4
NUMNER:
.word 0x123456789
HELLO_TEXT:
.asciz "hello\n" //以'\0'结束的字符
4. 条件伪指令
.ifdef 表达式 //当表达式为真时,执行代码 1
代码 1
.else //否则,表示表达式为假,执行代码 2
代码 2
.endif
.if
条件伪指令后面紧跟着一个恒定的表达式(即该表达式的值为真),并且最后要以 .endif
结尾。中间如果有其他条件,可以用 .else
填写汇编语句。
5. 文件包含伪指令
.include "filename"
其中,filename 是一个文件名(以 .s 或 .inc 为扩展名),可以包含文件的绝对路径或相对路径,建议同一工程的相关文件统一放到同一个文件夹中,更多的时候使用相对路径。
类似高级语言中的文件包含一样,在汇编语言中也可以使用 “.include” 进行文件包含。 .include
是一个附加文件的链接指示命令,利用它可以把另一个源文件插入当前的源文件一起汇编,成为一个完整的源程序。
6. 其他常用伪指令
除了上述的伪指令外,GNU 汇编还有其他常用的伪指令。
-
.section
伪指令:用户可以通过.section
伪指令来自定义一个段。- 格式:
.section <段名>{,"<标志>"}
其中,标志可选
a
(允许段)、w
(可写段)和x
(执行段)。.section .isr_vector,"a" //定义一个.isr_vector 段,"a"表示允许段
- 格式:
-
.global
伪指令:可以用来定义一个全局符号。- 格式:
.global symbol
.global main //定义一个全局符号main
- 格式:
-
.extern
伪指令:声明一个外部函数,调用的时候可以遍访所有文件以找到该函数,并且使用它。- 格式:
.extern symbol
.extern main //声明 main 为外部函数 bl main //调用 main 函数
- 格式:
-
.align
伪指令:可以通过添加填充字节,使当前位置满足一定的对齐方式。- 格式:
.align [exp[,fill]]
其中,
exp
的取值必须是 2 的幂指数,20~231 都是合法的, 表示下一条指令或数据对齐至exp
个字节地址。若未指定,则将当前位置对齐到下一个字的位置,fill
指出为对齐而填充的字节值,其可省略,默认为 0x00。.align 2 //确保下一条指令或数据对齐到2字节地址
- 格式:
-
.end
伪指令:声明汇编文件的结束。