前言 汇编语言分为指令语句和伪指令语句。
- 指令语句:汇编后生成 CPU 可以直接执行的机器目标代码,由 CPU 运行。
- 伪指令语句:不会产生任何机器码,CPU 不会执行伪指令。伪指令是给汇编器、链接器使用的控制命令,用来完成段划分、内存分配、符号别名定义、多文件之间互相调用等编译相关工作。
ARM 汇编书写硬性规范
- CPU 指令助记符,例如
MOV、ADD、SUB、LDR、STR、BL、BX属于保留关键字,绝对不能作为自定义标号,就算加上冒号也会编译报错。 - 自定义标号后面必须加冒号
:虽然 ARM 汇编语法允许省略冒号,但省略后极易把标号误认为指令,作业、考试一律书写冒号。 - CPU 指令、伪指令(
AREA、ENTRY、EQU、DCD、SPACE等)后面不能加冒号。 - 尽量不要使用伪指令名称作为自定义标号,部分汇编器虽然可以编译通过,但可读性差,存在歧义风险。
- ARM 汇编符号区分大小写。
1. EQU 等值赋值伪指令
格式
标号 EQU 操作数功能:给数字或者内存地址定义别名,本质是文本替换,不占用任何内存单元,整个源文件内该标号的值固定不变。
;示例1:给内存地址0x1000设置别名 AREA_ADDR EQU 1000H ;示例2:定义程序使用的数值常量 MAX_NUM EQU 50 MASK EQU 0x0F ;代码中直接使用别名,汇编器自动替换成对应数值 MOV R0,#AREA_ADDR ;等价于 MOV R0,#0x1000 MOV R1,#MAX_NUM ;等价于 MOV R1,#502. AREA 段定义伪指令(ARM 汇编专属)
格式
AREA 段名,属性1,属性2……功能:把程序划分成多个独立模块,称为 “段”。有的段存放执行代码,有的段存放变量数据;同时可以设置段的读写权限、内存对齐、是否初始化。
表格
| 属性 | 说明 |
|---|---|
CODE | 存放 CPU 执行指令,默认属性为READONLY只读 |
DATA | 存放变量与数据,默认属性为READWRITE可读可写 |
READONLY | 内存只允许读取,不可修改 |
READWRITE | 内存可读,允许修改写入 |
NOINIT | 程序启动时,该段内存不做初始化 |
ALIGN=n | 内存对齐字节 = \(2^n\);例如ALIGN=3代表 8 字节对齐 |
;定义只读代码段,存放程序指令 AREA CODE_SEG, CODE, READONLY ENTRY START: MOV R0,#0 ;定义可读可写数据段,存放运行时会修改的变量 AREA DATA_SEG, DATA, READWRITE DATA_BUF SPACE 1003. ENTRY 程序入口伪指令
格式
ENTRY功能:告诉链接器,CPU 上电复位后,从此标记位置开始执行程序。
注意:ENTRY 仅作为标记,不生成机器码;一个工程至少要有一个
ENTRY,必须写在CODE代码段内部。
AREA MY_CODE,CODE,READONLY ENTRY ;伪指令,标记程序入口,无冒号 START: MOV R1,#1 ADD R2,R1,#2 END4. ORG 地址定位伪指令(51、x86 汇编常用,ARM 很少使用)
格式
ORG 绝对地址功能:修改汇编位置计数器,指定后续代码、数据存放的起始内存地址;直到遇见下一条ORG指令,地址才会重新改变。
ORG 8000H ;下面所有内容,从内存0x8000地址开始存放 ADD R1,#1 MOV R2,#2 ORG 0x2000 ;切换存储起始地址,后续数据存放在0x2000之后 DATA_BUF1: DB 0x11,0x225. DB / DW 数据定义伪指令
DB:定义1 字节(8bit)数据,可以存放数字、ASCII 字符。 格式:[标号:] DB 字节常数 | 字符 | 表达式DW:定义16 位字(2 字节)数据,用法与 DB 完全相同。
ORG 8000H DATA1: DB 43H,09H,08H ;连续保存3个1字节数据 DATA2: DB 07H MY_STR: DB 'A','B','C',0 ;定义ASCII字符串,末尾0作为字符串结束标记 WORD_BUF: DW 0x1234,0x5678 ;DW,每一项占用2字节内存⚠️注意:不要使用
STR:作为标号,STR是 ARM 存储指令,属于保留关键字。
6. DCD / DCDU 32 位字分配伪指令(ARM 汇编)
格式
标号 DCD / DCDU 表达式列表DCD:分配连续 32 位(4 字节)内存单元,严格 4 字节字对齐。DCDU:分配 32 位内存单元,不强制字对齐。
表达式既可以写数字,也可以写标号,用来保存标号对应的内存地址,中断向量表大量使用该伪指令。
DATA_TEST: DCD 4,5,6 ;分配3个4字节空间,分别存入4、5、6 ;向量表 VECTOR_TABLE: DCD 0x20000000 DCD Reset_Handler ;存储标号Reset_Handler的内存地址 UNALIGN_DATA: DCDU 0x1111,0x22227. SPACE 内存空间分配伪指令
格式
标号 SPACE 字节数简写符号%等价于 SPACE(单位:字节)
功能:划出一块连续内存区域,分配的内存全部初始化为 0;多用于缓冲区、栈空间。
DATA_SPACE: SPACE 1000 ;分配1000字节内存,全部清零 STACK_BUF: SPACE 256 ;分配256字节栈内存 BUF2: % 64 ;简写写法,分配64字节并清零8. PRESERVE8 堆栈 8 字节对齐伪指令(ARM 汇编)
格式
PRESERVE8 {FALSE}功能:通知链接器当前文件堆栈是否保持 8 字节对齐。
汇编语言和 C 语言互相调用时,必须开启该伪指令;如果不满足 8 字节对齐,程序运行会发生异常崩溃。
PRESERVE8 ;开启堆栈8字节对齐,C与汇编混编必备 ;PRESERVE8 {FALSE} ;关闭堆栈8字节对齐9. EXPORT、EXTRN、IMPORT 多文件模块伪指令
应用场景
实际项目代码量很大,不会把全部代码写在一个汇编文件里。 举例:
- 文件
A.s:写了一个子程序_STARTUP - 文件
B.s:需要调用A.s里面已经写好的_STARTUP子程序
两个.s文件是独立的,单独汇编的时候,互相看不到对方内部定义的标号。 需要依靠EXPORT、EXTRN/IMPORT,打通多个汇编文件之间的符号访问。
EXPORT:将本文件的标号对外公开
在 A.s 中,使用 EXPORT 把内部标号公开,其他汇编文件才可以访问调用该标号;如果不写 EXPORT,标号只在当前文件内部有效,外部不可见。
文件:A.s
;A.s完整内容 AREA CODE,CODE,READONLY EXPORT _STARTUP ;对外公开标号_STARTUP,允许别的文件访问 _STARTUP: ;A.s内部自定义的子程序入口标号 MOV R0,#0 BX LR ;子程序执行完毕,返回调用处EXTRN:声明标号来自外部其他文件
B.s 想要调用 A.s 的
_STARTUP,但是 B.s 没有定义_STARTUP。汇编编译 B.s 时,汇编器会报 “找不到符号” 错误。
使用
EXTRN告诉汇编器:这个标号不是本文件定义的,定义在别的文件,等链接阶段再去找真实地址。
文件:B.s
;B.s完整内容 AREA CODE,CODE,READONLY EXTRN _STARTUP:CODE ;声明:_STARTUP是外部的代码标号,不在本文件定义 ENTRY START: BL _STARTUP ;BL指令:调用子程序,调用A.s中的_STARTUP MOV R1,#2 END完整工作步骤
- A.s:编写子程序
_STARTUP:,使用EXPORT _STARTUP对外公开符号。 - B.s:使用
EXTRN _STARTUP:CODE声明该标号属于外部。 - 汇编器分别编译 A.s、B.s,生成
A.o、B.o两个目标文件。 - 链接器把 A.o 与 B.o 合并。链接器发现 B 要调用
_STARTUP,就到 A.o 里面找到_STARTUP真实内存地址,填入 B 的机器码中。
IMPORT 外部符号导入(ARM 常用)
IMPORT功能和 EXTRN 接近,ARM 汇编更习惯使用 IMPORT。 语法:IMPORT 标号 {[WEAK]}
[WEAK]弱符号:就算整个工程找不到这个标号的定义,链接器也不会报错。
AREA INIT,CODE,READONLY IMPORT Main ;Main函数写在C语言文件,汇编中声明这是外部标号 ENTRY START: B Main ;跳转到C语言的Main函数 IMPORT Err_Handler [WEAK] B Err_Handler10. CODE16 / CODE32 指令集声明伪指令
格式
CODE16 CODE32CODE32:告诉汇编器,后续代码编译为 32 位 ARM 指令。CODE16:告诉汇编器,后续代码编译为 16 位 Thumb 指令。
⚠️重要提醒:该伪指令仅仅控制汇编器如何编译源代码,不能改变 CPU 硬件运行状态;真正切换 ARM/Thumb 处理器模式,需要使用
BX指令。
AREA MIX_CODE,CODE,READONLY CODE32 ;后续编译32位ARM指令 ARM_FUNC: MOV R0,#1 CODE16 ;后续编译16位Thumb指令 THUMB_FUNC: MOVS R1,#211. PROC / ENDP 过程(子程序)伪指令
格式
子程序名 PROC 指令 RET ENDP功能:用来定义汇编子程序;PROC标记子程序开始,ENDP标记子程序结束,二者必须成对出现;使用BL指令调用子程序。
ADD_SUB PROC ;子程序开始,子程序名字ADD_SUB ADD R0,R0,R1 RET ;RET:子程序返回 ENDP ;子程序结束 BL ADD_SUB ;调用上面定义好的子程序12. END 源程序结束伪指令
格式
END功能:标记当前这一个汇编源文件编译到此结束。
注意:一个
.s源文件,只能写一条 END,写在文件的末尾;END 之后书写的全部代码,汇编器直接忽略,不会参与编译。
AREA TEST,CODE,READONLY ENTRY START: MOV R0,#1 END ;本汇编源文件到此结束综合完整示例(ARM 简易启动框架)
PRESERVE8 AREA DEMO_CODE,CODE,READONLY EXPORT Reset_Handler IMPORT Main ENTRY Reset_Handler PROC LDR SP,=0x20001000 ;设置栈指针 B Main ;跳转到C语言main函数 ENDP AREA DEMO_DATA,DATA,READWRITE BUFF: SPACE 64 ;分配64字节清零缓冲区 CONST_BYTES: DB 0x01,0x02,0x03 END附录:标号使用小结
✅合法标号示例:START:、MY_STR:、DATA_BUF:
❌禁止作为标号:MOV:、LDR:、STR:(CPU 指令,属于保留关键字)
标号可以单独占一行,但是冒号不要省略冒号:
START: MOV R0,#1补充区分:
- 51 单片机汇编没有
LDR、STR指令,所以STR:作为标号是合法;- ARM32 汇编中 LDR、STR 是硬件指令,严禁用作标号,两套汇编规则不要混淆。