1 GNU 汇编格式
label:instruction @ comment
label 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的“:”,任何以“:”结尾的标识符都会被识别为一个标号。 instruction 即指令,也就是汇编指令或伪指令。 @符号,表示后面的是注释,就跟 C 语言里面的“/”和“/”一样,其实在 GNU 汇编文件中我们也可以使用“/*”和“*/”来注释。 comment 就是注释内容。
add:
MOVS R0, #0X12 @设置 R0=0X12
注意: ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用
1.1伪操作
1.1.1 .section
来定义一个段,汇编系统预定义了一些段名:
.text 表示代码段。
.data 初始化的数据段。
.bss 未初始化的数据段。
.rodata 只读数据段。
定义一个 testsetcion 段
.section .testsection
汇编程序的默认入口标号是_start,不过我们也可以在链接脚本中使用 ENTRY 来指明其它的入口点。
1.1.1 .global
.global _start
_start:
ldr r0, =0x12 @r0=0x12
.global 是伪操作,表示_start 是一个全局标号,类似 C 语言里面的全局变量一样,常见的伪操作有:
.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。
1.2 函数定义
函数名:
函数体
返回语句
GNU 汇编函数返回语句不是必须的,如下代码就是用汇编写的Cortex-A7 中断服务函数:
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC 中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
以函数 Undefined_Handler 为例我们来看一下汇编函数组成,“Undefined_Handler”就是函数名,“ldr r0, =Undefined_Handler”是函数体,“bx r0”是函数返回语句,“bx”指令是返回指令,函数返回语句不是必须的.
2 ARMv7汇编指令
2.1 数据移动指令
数据移动指令都是cpu内部寄存器之间的数据拷贝。
2.1.1 MOV
MOV R0,R1 @将寄存器 R1 中的数据传递给 R0,即 R0=R1
MOV R0, #0X12 @将立即数 0X12 传递给 R0 寄存器,即 R0=0X12
2.1.2 MRS
读取特殊寄存器的数据只能使用 MRS 指令:
MRS R0, CPSR @将 CPSR 里面的数据传递给 R0,即 R0=CPSR
2.1.3 MSR
和 MRS 刚好相反,通用寄存器写入到特殊寄存器
MSR CPSR, R0 @将 R0 中的数据复制到 CPSR 中,即 CPSR=R0
举个例子利用MRS MSR进行清bss (_bss_start, __bss_end定义在链接脚本):
.global _start
.global _bss_start
.global _bss_end
_bss_start:
.word __bss_start
_bss_end:
.word __bss_end
_start:
//disable watchdog, disable icache dcache
//init_clk
//enter svc mode
/*clear bss*/
ldr r0, _bss_start
ldr r1, _bss_end
move r2, 0
clr_bss:
stmia r0!, {r2} //复制一r2中的数据给r0, 并将指针r0增加4
cmp r0, r1
ble clr_bss /*if r0
特权模式下(除了用户模式,剩余的模式都是特权模式),可以通过CPS指令直接修改CPSR寄存器的M[4:0],让处理器进入不同的模式。
CPS #0x12 /*irq mode*/
CPS #0x13 /*svc mode*/
2.2 数据存取指令(访问存储器RAM)
2.2.1 LDR
数据加载指令,从指定地址读取到cpu寄存器。
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中
2.2.2 STR
数据存放指令,从cpu寄存器写入指定地址。
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002
STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中
LDR 和 STR 都是按照4 byte进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和STRB,按半字(16位)操作的指令就是 LDRH 和 STRH。
2.2.3 多寄存器加载存储指令LDMIA,STMIA等
1.LDMIA指令、LDMIB指令、LDMDB指令、LDMDA指令 LDM是LDR指令的增强型 , 将连续的数据加载到多组寄存器。 DB (Decrement Before)栈指针先减小再操作、DA(Decrement After)栈指针先操作再减小。 IB(Increment Before)栈指针先增加再操作、IA(Increment After)栈指针先操作再增加。
LDMIA指令,IA表示每次传送后地址加4
LDMIB指令,IB表示每次传送前地址加4
LDMDA指令,DA表示每次传送后地址减4
LDMDB指令,DB表示每次传送前地址减4
LDMIA R14,{R0-R3,R12} /*从R14寄存器指向的地址取出5个32位数据分别存进到R0-R4以及R12*/
//等效于
//R0=*R14
//R1=*(R14+4)
//R2=*(R14+8)
//R3=*(R14+12)
//R12=*(R14+16)
LDMIA R1!,{R4-R11} /*从R1指向的地址取8个32位数据存入R4-R11, 每取一次,让R1指针加4,因此最后R1指针加了32*/
2.STMIA指令、STMIB指令、STMDB指令、STMDA指令 同理,STM是STR指令的增强型 , 将多组寄存器数据保存进连续地址空间。
STMIA R13!,{R0-R1} /*将R0,R1寄存器中的数据存入R13指向的栈空间, r13指向的地址存入R0数据,再地址+4后存入R1的数据*/
2.3 入栈出栈指令
函调调用过程中离不开现场的保护和恢复。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。
2.3.1 PUSH
比如要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP (stack pointer)指针指向 0X80000000,我们知道栈空间的地址是向下增长的,堆空间地址向上增长。
PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈
那么压栈完成以后的堆栈如下:入栈保护现场完这5个寄存器后,SP指向0X7FFFFFEC(每压栈一个寄存器,SP地址减4)
再次保存LR寄存器,进行压栈:
PUSH {LR} @将 LR 进行压栈
2.3.2 POP
POP {LR} @先恢复 LR
POP {R0~R3,R12} @在恢复 R0~R3,R12
可以看出入栈出栈本质都是对SP指针进行加减,入栈减,出栈加,入栈把寄存器依次保存进SP指向的地址去,出栈从SP地址依次取出数据。
2.3.3 STMFD和LDMFD
入栈出栈的另外一种写法是“STMFD SP!”和“LDMFD SP!”。
STMFD SP!,{R0~R3, R12} @R0~R3,R12 入栈
STMFD SP!,{LR} @LR 入栈
bl xxx
LDMFD SP!, {LR} @先恢复 LR
LDMFD SP!, {R0~R3, R12} @再恢复 R0~R3, R12
STMFD 可以分为两部分:STM 和 FD,同理,LDMFD 也可以分为 LDM 和 FD。前面我们讲了 LDR 和 STR,这两个是数据加载和存储指令,但是每次只能读写存储器中的一个数据。STM 和 LDM 就是多存储和多加载,可以连续的读写存储器中的多个连续数据。 FD 是 Full Descending 的缩写,即满递减的意思。根据 ATPCS 规则,ARM 使用的 FD 类型的堆栈,SP 指向最后一个入栈的数值,堆栈是由高地址向下增长的,也就是前面说的向下增长的堆栈,因此最常用的指令就是 STMFD 和 LDMFD。STM 和 LDM 的指令寄存器列表中编号小的对应低地址,编号高的对应高地址.
2.4 跳转指令
2.4.1 B 指令
B 指令会将 PC 寄存器的值设置为跳转目标地址,如果要调用的函数不会再返回到原来的执行处,那就可以用 B 指令.
_start:
ldr sp,=0X80200000 @设置栈指针
b main @跳转到 main 函数
在汇编中初始化 C 运行环境,然后跳转到 C 文件的 main 函数中运行,上述代码只是初始化了 SP 指针,有些处理器还需要做其他的初始化,比如初始化 DDR 等等.
2.4.2 BL 指令
有返回的跳转,跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用一个基本但常用的手段。 比如 Cortex-A 处理器的 irq 中断服务函数都是汇编写的,主要用汇编来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数,所以就会存在汇编中调用 C 函数的问题。而且当 C 语言版本的中断处理函数执行完成以后是需要返回到irq 汇编中断服务函数,因为还要处理其他的工作,一般是恢复现场。
push {r0, r1} @保存 r0,r1
cps #0x13 @进入 SVC 模式,允许其他中断再次进去
bl system_irqhandler @加载 C 语言中断处理函数到 r2 寄存器中
cps #0x12 @进入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0X10] @中断执行完成,写 EOIR
跳转指令总结: 有多种跳转操作,比如: ①、直接使用跳转指令 B、BL、BX 等。 ②、直接向 PC 寄存器里面写入数据。
2.5 算数运算指令
加减乘除,常用的运算指令用法:
2.6 逻辑运算指令
与或非指令用法:
来看一个例子利用arm汇编进行初始化C语言环境。让arm进入svc模式,才能访问特殊寄存器如cpsr, spsr, sp指针。
.global _start
_start:
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置栈指针 */
b main /* 跳转到main函数 */
2.7 内存屏障指令
2.7.1 Data Memory Barrier(DMB):数据内存屏障
DMB指令确保在DMB之前的所有显式数据内存传输指令都已经在内存中读取或写入完成,同时确保任何后续的数据内存传输指令都将在DMB执行之后开始执行,否则有些数据传输指令可能会提前执行。保证了两个内存访问能按正确的顺序执行。 应用场景: (1)DMA 在使用DMA控制器时,需要在CPU内存访问和DMA操作之间插入DMB屏障,以确保CPU当前的内存读写操作在DMA开始之前完成。
(2)多核系统中的信号量 在多核系统中,使用信号量进行核间同步。需要使用DMB来强制指定内存执行顺序,以避免潜在的竞态条件或数据不一致性。当一个核要访问共享资源之前,它会先检查信号量的状态。如果信号量已经被另一个核获取,当前核就必须等待,直到信号量状态变为可用。这个等待过程需要保证在一个核释放信号量之后,其他核能够立即看到信号量状态的变化,而不是因为处理器优化或缓存导致的无效读取而产生错误。
2.7.2 Data Synchronization Barrier(DSB):数据同步屏障
在多线程编程中,两个线程同时对共享的内存进行读写操作,由于读/写操作的重排序,就会导致数据的不一致, DSB指令时,它确保在DSB之前的所有显式数据内存传输指令都已经在内存中读取或写入完成,同时确保任何后续的指令都将在DSB执行之后开始执行。 应用场景: 例如启用或禁用特定的中断、配置时钟、设置系统控制位等。为了确保对SCS的修改在下一条指令执行之前生效,需要使用DSB指令进行数据同步。一些特殊的指令如SVC(Supervisor Call,特权级调用)、WFI(Wait For Interrupt,等待中断)、WFE(Wait For Event,等待事件)等操作,涉及到特权级的转换或者等待系统事件发生,需要使用DSB指令。
2.7.3 Instruction Synchronization Barrier(ISB):指令同步屏障
插入ISB指令,处理器会将流水线中的指令全部刷新,从而确保之前的指令不会影响后续指令的执行,并且后续指令将从正确的上下文开始重新获取。 应用场景: 在进行异常进入之前,处理器会执行ISB操作。这样做的目的是刷新指令流水线,确保异常处理程序的指令是从正确的地址开始执行,避免异常之前的指令对异常处理程序造成干扰。 在进行异常返回之前,处理器同样会执行ISB操作。这样做的目的是刷新指令流水线,确保返回时从正确的地址重新获取指令,避免异常处理程序的指令对正常任务造成干扰。
3 arm-v7 cpu运行模式
以前的 ARM 处理器有 7 中运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef和 System,其中 User 是非特权模式,其余 6 中都是特权模式。
到了Cortex-A7 处理器有 9 种处理模式: | 模式 | 描述 | | :------------: | :------------: | | User(USR) | 用户模式,非特权模式,大部分程序运行的时候就处于此模式。 | | FIQ | 快速中断模式,进入 FIQ 中断异常 | | IRQ | 一般中断模式。 | | Supervisor(SVC) | 超级管理员模式,特权模式,供操作系统使用。 | | Monitor(MON) | 监视模式?这个模式用于安全扩展模式。 | | Abort(ABT) | 数据访问终止模式,用于虚拟存储以及存储保护。 | | Hyp(HYP) | Hyp(HYP) 超级监视模式?用于虚拟化扩展。 | | Undef(UND) | Undef(UND) 未定义指令终止模式。 | | System(SYS) | System(SYS) 系统模式,用于运行特权级的操作系统任务 |
九种模式所对应的寄存器:
arm920t cpu模式
4 arm-v7 cpu通用和特殊寄存器
ARM 架构提供了 16 个 32 位的通用寄存器(R0~R15)供软件使用,前 15 个(R0~R14)可以用作通用的数据存储,R15 是程序计数器 PC,用来保存将要执行的指令。ARM 还提供了一个当前程序状态寄存器 CPSR 和一个备份程序状态寄存器 SPSR,SPSR 寄存器就是 CPSR 寄存器的备份。
4.1 通用寄存器
R0~R15 就是通用寄存器,通用寄存器可以分为以下三类: ①、未备份寄存器,即 R0~R7。 ②、备份寄存器,即 R8~R14。 ③、程序计数器 PC,即 R15。
4.1.2 未备份寄存器R0-R7
未备份寄存器指的是 R0~R7 这 8 个寄存器,因为在所有的处理器模式下这 8 个寄存器都是同一个物理寄存器,在不同的模式下,这 8 个寄存器中的数据就会被破坏.
4.1.3 备份寄存器
4.1.3.1 R8~R12
R8~R12 这 5 个寄存器有2种物理寄存器.在快速中断模式下(FIQ)它们对应着 Rx_irq(x=8~12)物理寄存器,其他模式下对应着 Rx(8~12)物理寄存器. FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。
4.1.3.2 R13 (SP)
R13 一共有 8 个物理寄存器,其中一个是用户模式(User)和系统模式(Sys)共用的,剩下的 7 个分别对应 7 种不同的模式。R13 也叫做 SP,用来做为栈指针。基本上每种模式 都有一个自己的 R13 物理寄存器,应用程序会初始化 R13,使其指向该模式专用的栈地址,这就是常说的初始化 SP 指针.
4.1.3.2 R14 (LR)
R14 一共有 7 个物理寄存器,其中一个是用户模式(User)、系统模式(Sys)和超级监视模式(Hyp)所共有的,剩下的 6 个分别对应 6 种不同的模式. LR被叫做链接寄存器: ①用来存放子函数的返回地址。 在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回,比如在子程序中可以使用如下代码:
MOV PC, LR
②当异常发生以后,该异常模式对应的 R14寄存器被设置成该异常模式将要返回的地址.
subs pc, lr, #4 /* 将lr-4赋给pc */
比如下面代码示例:
0X2000 MOV R1, R0 ;执行
0X2004 MOV R2, R3 ;译指
0X2008 MOV R4, R5 ;取值 PC
当前正在执行 0X2000地址处的指令“MOV R1, R0”,但是 PC 里面已经保存了 0X2008 地址处的指令“MOV R4, R5”。假设此时发生了中断,中断发生的时候保存在 lr 中的是 pc 的值,也就是地址 0X2008。
4.1.3.2 R15 (PC)
R15 保存着当前执行的指令地址值加 8 个字节,这是因为 ARM的流水线机制导致的。ARM 处理器 3 级流水线:取指->译码->执行,这三级流水线循环执行,比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放在 R15(PC)中. 对于arm32位处理器:
R15 (PC)值 = 当前执行的程序位置 + 8 个字节
4.2 特殊寄存器
4.2.1 CPSR
当前程序状态寄存器(current program status register),所有模式共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。
N(bit31):当两个补码表示的 有符号整数运算的时候,N=1 表示运算对的结果为负数,N=0表示结果为正数。 Z(bit30):Z=1 表示运算结果为零,Z=0 表示运算结果不为零,对于 CMP 指令,Z=1 表示进行比较的两个数大小相等。 C(bit29):在加法指令中,当结果产生了进位,则 C=1,表示无符号数运算发生上溢,其它情况下 C=0。在减法指令中,当运算中发生借位,则 C=0,表示无符号数运算发生下溢,其它情况下 C=1。对于包含移位操作的非加/减法运算指令,C 中包含最后一次溢出的位的数值,对于其它非加/减运算指令,C 位的值通常不受影响。 V(bit28):对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时,V=1 表示符号位溢出,通常其他位不影响 V 位。 Q(bit27):仅 ARM v5TE_J 架构支持,表示饱和状态,Q=1 表示累积饱和,Q=0 表示累积不饱和。 IT1:0:和 IT7:2一起组成 IT[7:0],作为 IF-THEN 指令执行状态。 J(bit24):仅 ARM_v5TE-J 架构支持,J=1 表示处于 Jazelle 状态,此位通常和 T(bit5)位一起表示当前所使用的指令集:
GE3:0:SIMD 指令有效,大于或等于。 IT7:2:参考 IT[1:0]。 E(bit9):大小端控制位,E=1 表示大端模式,E=0 表示小端模式。 A(bit8):禁止异步中断位,A=1 表示禁止异步中断。 I(bit7):I=1 禁止 IRQ,I=0 使能 IRQ。 F(bit6):F=1 禁止 FIQ,F=0 使能 FIQ。 T(bit5):控制指令执行状态,表明本指令是 ARM 指令还是 Thumb 指令,通常和 J(bit24)一起表明指令类型,参考 J(bit24)位。 M[4:0]:处理器模式控制位 cpsr最常用就是来控制处理器模式 | M[4:0] | 处理器模式 | | ------------ | ------------ | | 10000 | User 模式 | | 10001 | FIQ 模式 | | 10010 | IRQ 模式 | | 10011 | Supervisor(SVC)模式 | | 10110 | Monitor(MON)模式 | | 10111 | Abort(ABT)模式 | | 11010 | Hyp(HYP)模式 | | 11011 | Undef(UND)模式 | | 11111 | System(SYS)模式 |
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */