书接上篇:来学学ARM汇编----基本知识(上)
通过之前的学习,笔者已经对于Armv7-M体系架构有了一些大致的了解,接下来就开始真正的Arm汇编的指令的学习了。
还是那句话,开发者的学习应该追根溯源,故这部分内容的学习是基于Arm编译器armasm用户手册进行的。
源代码行语法
每行汇编代码都具有如下的一般格式:
{symbol} {instruction|directive|pseudo-instruction} {;comment}
每行代码中,以上三个部分都是可选的。
symbol通常是一个标签,在instruction和pseudo-instruction中,它始终是一个标签。在某些directive中,它是变量或者常数的符号。在任何情况下,对directive的描述都会清楚的说明这一点。
symbol必须从首列开始,其中不可以包含任何空格或者制表符,除非它由条形符号(|)包围。
标签是地址的符号表示。可以使用标签来标记要从代码的其他部分引用的特定地址。数字本地标签是以0-99范围内的数字开头的标签的子类。与其他标签不同,可以多次定义数值本地标签。这使得它们在使用宏生成标签时非常有用。
指令向汇编程序提供影响汇编过程或影响最终输出图像的重要信息。
指令和伪指令构成处理器用于执行任务的代码。
注意:
instruction、directive和pseudo-instruction前面必须有空格,例如空格或制表符,不管前面是否有标签。
有些directive不允许使用标签。
注释是源代码行的最后一部分。行上的第一个分号标记注释的开头,除非分号出现在字符串文字中。行的末尾是注释的结尾。注释本身就是有效的一行。汇编程序忽略所有注释。
笔者注:
以上内容中,instruction和directive均可被翻译为指令,但是两种指令存在不同之处:
instruction:指令,是处理器直接执行的指令,该种指令会直接被汇编为机器码并链接进入最终的可执行程序。
directive:伪指令,是对编译器的指令,以告诉编译器应该如何处理待汇编数据。该种指令仅在汇编时使用,他们可能会影响代码的生成方式,但是他们本身不会导致任何代码生成。
参考自:Quroa - What is the difference between an instruction and a directive in assembly language?
此外,编写指令时要么全部采用大写,要么全部采用小写,不可以大小写混用。当然,标签中可以随意使用大小写。
为了使源文件更易于阅读,您可以通过在行的末尾放置反斜杠(\)将一长行源代码拆分为几行。反斜杠后面不能跟任何其他字符,包括空格和制表符。汇编器将反斜杠后跟行尾序列视为空白。您还可以使用空行来提高代码的可读性。
注意:
在带引号的字符串中,不要使用反斜杠后跟行尾序列。
行的长度限制(包括使用反斜杠的任何扩展名)为4095个字符。
指令格式
部分汇编指令具有以下的语法格式:
Operation{s}{cond} Rd Rn,Operand2
以此为例解释下语法的指令格式:
- Operation:操作指令
- cond:条件
- s:状态码
- Rd:目标寄存器
- Rn:源寄存器
- Operand2:附加操作
{s}状态后缀
状态后缀是可选的后缀,指令将根据是否有状态后缀以及操作结果来更新条件标志位(N标、Z标、C标、V标)
;例如:
ADD R1,R2,#4 ;将R2加4后存在R1寄存器内,不更新flag标志
;------
ADDS R1,R2,#4 ;将R2加4后存在R1寄存器内,并更新flag标志
{cond}条件后缀
条件后缀是可选的后缀,表示执行指令所必需的条件,可以使用的后缀及其对应的标志位如下表所示:
后缀 | 标志位 | 含义 |
---|---|---|
EQ | Z标置位 | 相等 |
NE | Z标复位 | 不相等 |
CS/HS | C标置位 | 无符号大于等于(>=) |
CC/LO | C标复位 | 无符号小于(<) |
MI | N标置位 | 负 |
PL | N标复位 | 正或零 |
VS | V标置位 | 有溢出 |
VC | V标复位 | 无溢出 |
HI | C标置位且Z标复位 | 无符号大于(>) |
LS | C标复位或Z标置位 | 无符号小于或等于(<=) |
GE | N标与V标状态相同 | 有符号数大于等于(>=) |
LT | N标与V标状态不同 | 有符号数小于(<) |
GT | Z标复位,N标与V标状态相同 | 有符号数大于(>) |
LE | Z标置位,N标与V标状态不同 | 有符号数小于等于(<=) |
AL | 任意标志 | 正常执行指令;该后缀常省略 |
;例如:
CMP R1,R2 ;比较R1寄存器与R2寄存器的值
ADDEQ R1,R2,#4 ;如果上步比较结果相等:将R2加4后存在R1寄存器内,不更新flag标志;否则不执行该句指令
寻址方式
Arm汇编具有以下几种寻址方式:
-
立即数寻址:
;例如: ADD R0,R0,#0x3f ;将R0寄存器中的值加上0x3F后存入R0寄存器
-
寄存器寻址:
;例如: ADD R0,R1,R2 ;将R1寄存器中的值加上R2寄存器中的值然后存入R0寄存器中
-
寄存器间接寻址
;例如: LDR R0,[R1] ;将R1寄存器中的内存地址值处的值加载到R0寄存器中
-
寄存器移位寻址
;例如: ADD R3,R2,R1,LSL #2 ;将R1中的值左移两位后加上R2然后存入R3寄存器中
-
基址地址寻址
;例如: LDR R0,[R1,#4] ;将R1寄存器中的内存地址值+4处的值加载到R0寄存器中
-
多寄存器寻址
;例如: LDMIA R0,{R1,R2,R3,R4} ;
-
相对寻址
;例如: BL START ;跳转到Start标签处
常用指令
笔者注:
具体的指令语法查询请参考:ARM Compiler armasm User Guide
Arm汇编中常用的指令如下:
寄存器操作指令
数学操作指令
操作码 | 操作数 | 功能 | 表达式 |
---|---|---|---|
ADC | Rd,Rn,Op2 | 带进位的加法运算 | Rd=Rn+Op2+C |
ADD | Rd,Rn,Op2 | 加法运算 | Rd=Rn+Op2 |
MOV | Rd,Op2 | 数据传送 | Rd=Op2 |
MVN | Rd,Op2 | 数据取反传送 | Rd=~Op2 |
RSB | Rd,Rn,Op2 | 翻转减法运算 | Rd=Op2-Rn |
RSC | Rd,Rn,Op2 | 带进位的翻转减法 | Rd=Op2-Rn-!C |
SBC | Rd,Rn,Op2 | 带进位的减法运算 | Rd=Rn-Op2-!C |
SUB | Rd,Rn,Op2 | 减法运算 | Rd=Rn-Op2 |
MUL | Rd,Rm,Rs | 32位乘法 | *Rd=RmRs** |
MLA | Rd,Rm,Rs,Rn | 32位累加乘法 | Rd=Rm*Rs+Rn |
逻辑运算指令
操作码 | 操作数 | 功能 | 表达式 |
---|---|---|---|
AND | Rd,Rn,Op2 | 按位逻辑与 | Rd=Rn&Op2 |
BIC | Rd,Rn,Op2 | 位清零 | Rd=Rn&~Op2 |
EOR | Rd,Rn,Op2 | 按位逻辑异或 | Rd=Rn^Op2 |
ORR | Rd,Rn,Op2 | 按位逻辑或 | Rd=Rn按位或Op2 |
比较操作指令
操作码 | 操作数 | 功能 | 表达式 |
---|---|---|---|
CMP | Rn,Op2 | 比较 | Rn-Op2 |
CMN | Rn,Op2 | 负数比较 | Rn-(-Op2) |
TEQ | Rn,Op2 | 测试相等 | Rn^Op2 |
TST | Rn,Op2 | 测试 | Rn&Op2 |
内存操作指令
单寄存器操作指令
操作码 | 操作数 | 功能 |
---|---|---|
LDR | Rn,Addr | 按照字长从内存读取 |
STR | Rn,Addr | 按照字长存储到内存中 |
SWP | Rt, Rt2, [Rn] | 内存值存入Rt,Rt2值存入内存(若Rt=Rt2则为交换 |
笔者注:
LDR指令在手册中的语法如下(STR同理,略):
LDR{type}{cond}{.W} Rt, label
其中type表示存取的长度类型,可选值有:
B(usigned byte)、SB(signed byte)、H(unsigned half word)、SH(signed half word)、-(表示省略)
示例代码:
;代码段1:读取0x20000000处的一个字到R0寄存器中
MOV R1,#0x20000000
LDR R0,[R1]
;代码段2:读取0x20000000处的半字到R0寄存器中
MOV R1,#0x20000000
LDRH R0,[R1]
;代码段3:读取0x20000000处的一个字节到R0寄存器中
MOV R1,#0x20000000
LDRB R0,[R1]
当内存中的值如下时:
代码段执1行后,R0寄存器中的值为:0x00F42400(不理解四个字节为什么反过来的读者请复习一下有关大小端存储的知识,STM32是小端存储)
代码段执2行后,R0寄存器中的值为:0x00002400
代码段执3行后,R0寄存器中的值为:0x00000000
多寄存器操作
操作码 | 操作数 | 功能 |
---|---|---|
LDM | Rn,regList | 多寄存器读取 |
STM | Rn,regList | 多寄存器写入 |
笔者注:
LDM指令在手册中的语法如下(STM同理,略):
LDM{addr_mode}{cond} Rn{!}, reglist{^}
其中addr_mode表示存取时的地址模式,可选值有:
IA(increase after)、IB(increase before)、DA(decrease after)、DB(decrease before)
其中IB和DA均仅支持A32指令集,故在笔者的平台上无法使用。
程序状态寄存器操作
程序状态寄存器不可直接修改,故提供了如下指令来帮助我们修改程序状态寄存器(PSR)中的值:
操作码 | 操作数 | 功能 |
---|---|---|
MRS | Rd, psr | 程序状态寄存器移动到通用寄存器中 |
MSR | psr_fields, Rm | 通用寄存器移动到程序状态寄存器中 |
笔者注:
为了更容易的记住这两条指令的方向,需要对指令有一点自己的理解,例如:
MRS可以理解为:
MOV Reg,Status
,故其方向自然是将状态寄存器移入通用寄存器。
跳转指令
操作码 | 操作数 | 功能 |
---|---|---|
B | lable | 跳转代码到lable处 |
BL | lable | 跳转到lable处并为LR寄存器赋值以便后续返回 |
BX | lable | 跳转到lable处并切换指令集到Thumb |
BLX | lable | 跳转到lable并赋值LR并切换指令集到Thumb |
异常指令
操作码 | 操作数 | 功能 |
---|---|---|
SVC(SWI) | #imm | 触发异常,CPSR保存到SPSR,调用SVC_Handler |
BKPT | #imm | 触发断点异常 |
常用伪指令
基于不同的平台,常用的伪指令可以大致分为两个Arm伪指令和GNU伪指令,不同平台的伪指令绝大部分不可兼容,以下列举为Arm伪指令:
基本伪指令
伪指令 | 用法 | 功能 |
---|---|---|
AREA | 声明段名,段功能,属性 | |
CODE16/CODE32 | 无 | 声明以下指令的指令位数,不做切换 |
ENTRY | ENTRY addr | 定义汇编程序入口点 |
EQU | name EQU const | 定义常量 |
END | 无 | 源程序结尾标志 |
EXPORT | EXPORT 符号 | 定义全局符号 |
IMPORT | IMPORT 符号 | 静态导入全局符号 |
EXTERN | EXTERN 符号 | 动态引用全局符号 |
GET | GET “文件名” | 包含文件到当前文件中 |
RN | name RN Rn | 寄存器定义别名 |
变量相关伪指令
汇编支持的变量有三种:数字变量、逻辑变量和字符串变量。
变量的生命周期有两种:全局和局部。
伪指令 | 用法 | 功能 |
---|---|---|
GBLA/GBLL/GBLS | GBLA name | 定义全局数字/逻辑/字符串变量 |
LBLA/LBLL/LBLS | LBLA name | 定义局部数字/逻辑/字符串变量 |
SETA/SETL/SETS | name SETA value | 为数字/逻辑/字符串变量赋值 |
RLIST | nameRLIST {RegList} | 定义寄存器列表变量 |
数据定义相关伪指令
伪指令 | 用法 | 功能 |
---|---|---|
DCB | name DCB value | 分配连续的字节存储单元 |
DCW | name DCW value | 分配连续的半字存储单元 |
DCD | name DCD value | 分配连续的字存储单元 |
DCFD | 浮点双精度 | |
DCFS | 浮点单精度 | |
SPACE | name SPACE value | 分配value个字节的连续存储单元 |
MAP | MAP addr | 用于构建数据结构 |
FIELD | 用于构建数据结构 |
控制相关伪指令
伪指令 | 用法 | 功能 |
---|---|---|
IF ELSE ENDIF | 分支 | |
WHILE WEND | 循环 | |
MACRO MEND | 宏定义 | |
MEXIT | 跳出宏 |
混合编程
C语言作为一种高级语言,有时候并不能实现核心寄存器的操作,而为了实现这部分操作,往往需要C程序和汇编程序进行混合编程。以下是常用的几种混合编程方式:
C中内联汇编
C语言中内联汇编是由编译器实现的功能,所以不同的编译器实现的语法与方式都并不相同,此处笔者介绍的是AC6对应的标准,具体的编译器的标准可以查看ARM编译器手册来进一步了解。
内联汇编可以使用__asm
关键字来实现,使用其内联汇编语句的一般语法如下:
// 基础语法
__asm [volatile] (<code>);
// 扩展语法
__asm [volatile] (<code_template>
:<output_operand_list>
[:<input_operand_list>
[:<clobbered_register_list>]]);
对于<code>
其实就是普通的汇编语言,例如:"ADD R0, R1, R2"
。
而<code_template>
则是汇编代码模板,例如:
"ADD %[result], %[input_i], %[input_j]"
<output_operand_list>
是输出操作数列表,每个操作数有一个由方括号包含的符号名,一个约束修饰符以及一个由圆括号包含的C表达式组成,例如:
[result] "=r" (res)
该列表可以为空。
<input_operand_list>
是输入操作数列表,格式同上,由逗号分隔。例如:
[input_i] "r" (i), [input_j] "r" (j)
<clobbered_register_list>
是可破坏的寄存器列表(必须使用小写列表),除寄存器外还可以填入两个特殊参数:
”cc“:使指令影响条件标志位
”memory“:使指令访问未知内存地址
一个简单的使用示例如下:
int add(int a,int b){
int res;
__asm ("ADD %[result], %[input_i], %[input_j]"
: [result] "=r" (res)
: [input_i] "r" (a), [input_j] "r" (b)
: "r5","r6","cc","memory"
);
return res
}
C中调汇编
C中调用汇编函数其实很简单:
-
汇编文件中EXPORT函数名
-
C文件中extern对应函数
其中需要提到的一点就是ARM过程调用标准(APCS)中定义的函数传参标准:R0-R3这四个寄存器是用来传参的寄存器,可用于向汇编函数中传递参数。
汇编中调C
与上节大同小异:
- 汇编文件中IMPORT函数名
- C文件中定义函数即可
传参标准仍然参照APCS。