往期文章:
本期正文:
芯片在上电后,最开始并没有运行C语言的环境,需要通过汇编程序来运行配置,直到能够运行C语言后在开始切换到C语言运行环境,这段代码文件也叫启动文件,对学习驱动开发来说,汇编非常有必要学习一下!因为I.MAX是Cortex-A内核的芯片,所以主要了解Cortex-A的汇编指令
因为我们编译器使用的是 GUN cc(GCC)交叉编译器,用来在当前平台上编译其他平台的代码,他有着自己的语法规范,所以我们编写Cortex-A的汇编代码时要符合GUN的语法规范!
GNU汇编语法适用于所有的架构,并不是 ARM 独享的,GNU 汇编由一系列的语句组成, 每行一条语句,每条语句有三个可选部分,如下:
label:instruction @ comment
语句组成:
组成 | 功能 |
---|---|
label | 标号,表示指令地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的“:”,任何以“:”结尾的标识 符都会被识别为一个标号。 |
instruction | 指令,也就是汇编指令或伪指令 |
@comment | 注释内容(@符号,表示后面的是注释) |
示例
add: MOVS R0, #0X12 @设置 R0=0X12
add是标号,表示指令地址,后面可以通过标号进行跳转,MOV R0 ,#0x12将立即数赋值给R0寄存器
@设置 R0=0X12为注释,不参与编译
除了指令之外,汇编还有伪操作和为指令,两者区别如下
伪操作,其操作对象是汇编编译器;而伪指令,其最终的操作对象是指令,也就是说,其对应的是用于CPU执行的指令。这就是他们之间简单但很微妙的区别。
伪指令最终面对的是CPU指令集的指令。伪操作是用来控制汇编器是如何来产生汇编指令的。
ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用!
用户可以使用.section(伪操作)来定义一个段,汇编系统预定义了一些段名如下:(部分)
每个段以段名开始,以下一段名或者文件结尾结束
伪操作 | 功能 |
---|---|
.text | 表示代码段。 |
.data | 初始化的数据段。 |
.bss | 未初始化的数据段。 |
.rodata | 只读数据段。 |
.byte | 定义单字节数据,比如.byte 0x12。 |
.short | 定义双字节数据,比如.short 0x1234。 |
.long | 定义一个 4 字节数据,比如.long 0x12345678。 |
.equ | 赋值语句,格式为:.equ 变量名,表达式,比如.equ num, 0x12,表示 num=0x12。 |
.align | 数据字节对齐,比如:.align 4 表示 4 字节对齐。 |
.end | 表示源文件结束。 |
.global | 定义一个全局符号,格式为:.global symbol,比如:.global _start。 |
extern XXXX | 说明xxxx为外部函数,调用的时候可以遍访所有文件找到该函数并且使用它。 |
我们可以在编写汇编中使用这些预定义的段,完成一些功能,比如汇编程序的默认入口标号是start,不过我们也可以在链接脚本中使用 ENTRY 来指明其它的入口点,下面的代码就是使用start 作为入口标号
.global _start _start: ldr r0, =0x12 @r0=0x12
.global个全局标号,类似 C 语言里面的全局变量一样,声明之后,.global _start 让start符号成为可见的标示符,这样链接器就知道跳转到程序中的什么地方并开始执行,linux寻找这个 _start标签作为程序的默认进入点
除此之外我们也可以自定义伪操作,使用.section来定义一个段,,例如
.section .testsection @定义一个 testsetcion 段
GUN语法支持函数,函数格式如下:
函数名: 函数体 返回语句
比如Cortex-A7的中断服务函数
/* 未定义中断 */ Undefined_Handler: ldr r0, =Undefined_Handler bx r0 /* SVC 中断 */ SVC_Handler: ldr r0, =SVC_Handler bx r0 /* 预取终止中断 */ PrefAbort_Handler: ldr r0, =Pref bx r0
例如 “SVC_Handler” 就是函数名,“ldr r0, =SVC_Handler”是函数体,“bx r0”是函数 返回语句,“bx”指令是返回指令,函数返回语句不是必须的
寻址方式就是处理器根据指令中给出的地址信息来寻找物理地址的方式,目前ARM指令系统支持以下几种寻址方式:立即数寻址、寄存器寻址、寄存器偏移寻址、寄存器间接寻址、基址变址寻址、多寄存器寻址、相对寻址、堆栈寻址和块拷贝寻址
也称为立即数寻址,这种寻址方式指令中就已经给出了操作数。也就是在执行指令的过程中,处理器取得指令的同时也取得了操作数,因此称为立即数寻址。例如:
ADD R0, #1 ;R0+1->R0 ADD R0, R0, #0x3F ;R0+0x3F->R0
其中源操作数就是立即数,要求以“#”开始,对于十六进制的立即数,要求在“#”后面加“0x”或“&”。
即将寄存器中的数值作为操作数,是各类微处理器常用的寻址方式,也是效率较高的寻址方式。例如:
ADD R0, R1, R2 ;R1+R2->R0,将R1和R2的值相加,将结果存入R0
寄存器间接寻址是以寄存器中的值作为操作数的地址,操作数本身存放在寄存器中。例如:
ADD R0, R1, [R2] ;R1+[R2]->R0 LDR R0, [R1] ;[R1]->R0 ;第一条指令,以寄存器R2的值作为操作数的地址,在寄存器中取得一个操作数后与R1相加,将结果存入寄存器R0。 ;第二条指令,将寄存器中以R1为地址的值赋给R0。
[R0]表示以R0寄存器的值为地址的值作为实际值
R0表示寄存器的值作为实际值
基址变址寻址是把基址寄存器的内容与指令中给出的地址偏移量相加,从而得到一个操作数的有效地址。该方式常用于访问基地址附近的某些存储单元,一般有以下几种方式:
将寄存器R1的值加上4作为操作数的有效地址,取得操作数后存入R0中,代码如下
LDR R0, [R1, #4] ;[R1+4]->R0
将寄存器R1的值加上4作为操作数的有效地址,取得操作数后存入R0中,然后寄存器R1的值加上4个字节,代码如下
LDR R0, [R1, #4]! ;[R1+4]->R0、R1+4->R1
将寄存器R1的值作为操作数的有效地址,取得操作数后存入R0中,然后寄存器R1的值加上4个字节,代码如下
LDR R0, [R1], #4 ;[R1]->R0、R1+4->R1
将寄存器R1和R2的值相加作为操作数的有效地址,取得操作数后存入R0中,代码如下
LDR R0, [R1, R2] ;[R1+R2]->R0
使用多寄存器寻址,一条指令可以完成多个寄存器值的传送,一条指令最多可以传送16个通用寄存器的值。例如:
LDMIA R0, {R1,R2,R3,R4,} ;[R0]->R1,[R0+4]->R2,[R0+8]->R3,[R0+12]->R4
该指令后缀IA表示每次执行完读取/存储操作后,R0按字长增加,因此,指令可以将连续存储单元的值传送到R1~R4。
与基址变址寻址类似,相对寻址以程序计数器PC的当前值作为基地址,指令中的地址标号作为偏移量,将两者相加后得到操作数的有效地址。以下程序完成子程序的调用和返回,跳转指令BL采用了相对寻址方式:
BL NEXT ;跳转到子程序NEXT处执行指令 ...... NEXT ...... MV PC, LR ;从子程序返回
堆栈是一种数据结构,按先进后出的方式工作,使用一个称为堆栈指针的专用寄存器指示当前的操作,堆栈指针总是指向堆栈顶端。
当堆栈指针指向最后压入的数据时,称为满堆栈;
当堆栈指针指向下一个将要压入的位置时,称为空堆栈。
根据堆栈的生成方式,可分为递增堆栈和递减堆栈。当堆栈由低地址向高地址生成时,称为递增堆栈,反之称为递减堆栈。排列组合后可得到4中类型的堆栈工作方式,ARM微处理器支持全部4种类型的堆栈工作方式,如下:
处理器内部数据传输主要有以下三个操作:
将数据从一个寄存器传递到另外一个寄存器
将数据从一个寄存器传递到特殊寄存器,如 CPSR 和 SPSR 寄存器
将立即数传递到寄存器
常用指令有:
指令 | 目标 | 源 | 功能 |
---|---|---|---|
MOV | R0 | R1 | 将 R1 里面的数据复制到 R0 中 |
MRS | R0 | CPSR | 将特殊寄存器 CPSR 里面的数据复制到 R0 中。 |
MSR | CPSR | R1 | 将 R1 里面的数据复制到特殊寄存器 CPSR 里中 |
MOV 指令用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面
MOV R0,R1 @将寄存器 R1 中的数据传递给 R0,即 R0=R1 MOV R0, #0X12 @将立即数 0X12 传递给 R0 寄存器,即 R0=0X12
MRS 指令用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令
MRS R0, CPSR @将特殊寄存器 CPSR 里面的数据传递给 R0,即 R0=CPSR
MSR 指令用来将普通寄存器的数据传递给特殊寄存器,也就 是写特殊寄存器,写特殊寄存器只能使用 MSR
MSR CPSR, R0 @将 R0 中的数据复制到 CPSR 中,即 CPSR=R0
ARM 不能直接访问存储器,比如 RAM 中的数据,配置寄存器的时候需要借助存储器访问指令,一般先将要配置的值 写入到 Rx(x=0~12)寄存器中,然后借助存储器访问指令将 Rx 中的数据写入到 I.MX6UL 寄存器中。读取 I.MX6UL 寄存器也是一样的,只是过程相反。常用的存储器访问指令有两种:LDR 和 STR,用法如表
指令 | 描述 |
---|---|
LDR Rd, [Rn , #offset] | 从存储器 Rn+offset 的位置读取数据存放到 Rd 中。 |
STR Rd, [Rn, #offset] | 将 Rd 中的数据写入到存储器中的 Rn+offset 位置。 |
LDR 主要用于从存储器加载数据到寄存器 Rx 中,LDR 也可以将一个立即数加载到寄存器 Rx 中,LDR 加载立即数的时候要使用“=”,而不是“#”。在嵌入式开发中,LDR 最常用的就是读 取 CPU 的寄存器值,比如 I.MX6UL 有个寄存器 GPIO1_GDIR,其地址为 0X0209C004,要读取这个寄存器中的数据,代码如下
1 LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004 2 LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中
STR 就是将数据写入到存储器中,同样以 I.MX6UL 寄存器 GPIO1_GDIR 为例,现在我们要配置寄存器 GPIO1_GDIR 的值为 0X20000002,示例代码如下
1 LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004 2 LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002 3 STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址对应的内存中
LDR 和 STR 都是按照字进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、 半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和 STRB,按半字操作的指令就是 LDRH 和 STRH
函数在运行时,临时存储数据的寄存器是R0-R15,函数运行结束或者切换的时候,需要保存当前运行的参数(保护现场),即把当前寄存器的数据通过SP指针压入到堆栈里面,进行保存,当函数继续运行时,则将原先保存在堆栈的数据弹出到当前R0-R15中(恢复现场)继续运行;。压栈 的指令为 PUSH,出栈的指令为 POP,PUSH 和 POP 是一种多存储和多加载指令(就是实际上执行多条指令),即可以一次 操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址,PUSH 和 POP 的用法如表:
指令 | 描述 |
---|---|
PUSH | 将寄存器列表存入栈中 |
POP | 从栈中恢复寄存器列表 |
假如我们现在要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP 指针指向 0X80000000,处理器的堆栈是向下增长的,使用的汇编代码如下:
PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈
压栈后的堆栈SP指针如下:
如果我们要出栈的话 就是使用如下代码:
POP {R0~R3,R12} @恢复 R0~R3,R12
出栈的就是从栈顶,也就是 SP 当前执行的位置开始,地址依次减小来提取堆栈中的数据 到要恢复的寄存器列表中,PUSH 和 POP 的另外一种写法是“STMFD SP!”和“LDMFD SP!”,功能和上面的相同
跳转一般有两种操作:
指令跳转:
指令 | 描述 |
---|---|
B | 跳转到 label,如果跳转范围超过了+/-2KB,可以指定 B.W 使用 32 位版本的跳转指令, 这样可以得到较大范围的 跳转 BX 间接跳转,跳转到存放于 Rm 中的地址处,并且切换指令集 |
BL | 跳转到标号地址,并将返回地址保存在 LR 中。 |
BLX | 结合 BX 和 BL 的特点,跳转到 Rm 指定的地址,并将返回地 址保存在 LR 中,切换指令集 |
B 指令会将 PC 寄存器的值设置为跳转目标地址, 一旦执行 B 指 令,ARM 处理器就会立即跳转到指定的目标地址,一般用于不会返回的跳转,如下为启动程序完成后最后跳转到main函数开始执行C语言程序!
1 _start: 2 3 ldr sp,=0X80200000 @设置栈指针 4 b main @跳转到 main 函数
BL 指令相比 B 指令,在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以 通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,一般用于需要返回的跳转,比如子程序调用,或者进入中断的情况,如下就是进入中断跳转
1 push {r0, r1} @保存 r0,r1 2 cps #0x13 @进入 SVC 模式,允许其他中断再次进去 3 5 bl system_irqhandler @加载 C 语言中断处理函数到 r2 寄存器中 6 7 cps #0x12 @进入 IRQ 模式 8 pop {r0, r1} 9 str r0, [r1, #0X10] @中断执行完成,写 EOIR
汇编中也可以进行加减乘除算术运算,指令如下:
指令 | 计算公式 | 备注 |
---|---|---|
ADD Rd, Rn, Rm | Rd = Rn + Rm | 加法运算,指令为 ADD |
ADD Rd, Rn, #立即数 | Rd = Rn + #立即数 | 加法运算,指令为 ADD |
ADC Rd, Rn, Rm | Rd = Rn + Rm + 进位 | 带进位的加法运算,指令为 ADC |
ADC Rd, Rn, #立即数 | Rd = Rn + #立即数+进位 | 进位的加法运算,指令为 ADC |
SUB Rd, Rn, Rm | Rd = Rn – Rm | 减法 |
SUB Rd, #立即数 | Rd = Rd - #立即数 | 减法 |
SUB Rd, Rn, #立即数 | Rd = Rn - #立即数 | 减法 |
SBC Rd, Rn, #立即数 | Rd = Rn - #立即数– 借位 | 带借位的减法 |
SBC Rd, Rn ,Rm | Rd = Rn – Rm – 借位 | 带借位的减法 |
MUL Rd, Rn, Rm | Rd = Rn * Rm | 乘法(32 位) |
UDIV Rd, Rn, Rm | Rd = Rn / Rm | 无符号除法 |
SDIV Rd, Rn, Rm | Rd = Rn / Rm | 有符号除法 |
汇编中也有C语言中的逻辑运算,比如与运算或运算等等,具体如下表
指令 | 计算公式 | 备注 |
---|---|---|
AND Rd, Rn | Rd = Rd &Rn | 按位与 |
AND Rd, Rn, #立即数 | Rd = Rn & #立即数 | 按位与 |
AND Rd, Rn, Rm | Rd = Rn & Rm | 按位与 |
ORR Rd, Rn | Rd = Rd |Rn | 按位或 |
ORR Rd, Rn, #立即数 | Rd = Rn |#立即数 | 按位或 |
ORR Rd, Rn, Rm | Rd = Rn |Rm | 按位或 |
BIC Rd, Rn | Rd = Rd & (~Rn) | 位清除 |
BIC Rd, Rn, #立即数 | Rd = Rn & (~#立即数) | 位清除 |
BIC Rd, Rn , Rm | Rd = Rn & (~Rm) | 位清除 |
ORN Rd, Rn, #立即数 | Rd = Rn |(#立即数) | 按位或非 |
ORN Rd, Rn, Rm | Rd = Rn |(Rm) | 按位或非 |
EOR Rd, Rn | Rd = Rd ^ Rn | 按位异或 |
EOR Rd, Rn, #立即数 | Rd = Rn ^ #立即数 | 按位异或 |
&为与,|为或,~为非,^为异或