GNU 汇编语法
编写汇编的过程中,其指令、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用。
1. 汇编语句的格式
label: instruction @ comment
label
即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的" : ",任何以" : "结尾的标识符都会被识别为一个标号。在汇编语言中,_start
是一个特殊的标号(label),_start
是程序执行的入口点,程序执行的起始地址,当程序加载到内存中时,处理器会跳转到_start
标签所在的地址,从这里开始执行代码。_start
主要作用是初始化程序运行环境,比如栈、全局变量、动态链接环境,然后跳转到用户定义的main
函数执行。instruction
即指令,也就是汇编指令或伪指令@
表示后面的是注释,跟 C 语言的“/*”和“*/”一样,在 GNU 汇编文件中也可以使用“/*”和“*/”来注释comment
就是注释内容
2. 预定义段名
.section
伪操作,用于指定汇编代码或数据应当放入哪个段, 或者定义自己的自定义段,每个段以段名开始,以下一段名或者文件结尾结束
.text
表示代码段
.data
初始化的数据段
.bss
未初始化的数据段
.rodata
只读数据段
.byte
定义单字节数据,比如.byte 0x12
.equ
赋值语句,比如.equ num,0x12
,表示num=0x12
.short
定义双字节数据,比如.short 0x1234
.long
定义一个 4字节数据,比如.long 0x12345678
.align
数据字节对齐,比如.align4
表示4字节对齐
.end
表示源文件结束
.global
定义一个全局符号,比如.global _start
.section .data // 数据段
message:.asciz "Hello, World!" // 字符串常量.section .text // 代码段
.global _start // 声明程序入口
_start:mov eax, 4 // 系统调用号,写操作mov ebx, 1 // 文件描述符(1是标准输出)lea ecx, [message] // 将字符串地址加载到 ecxmov edx, 13 // 要写入的字节数int 0x80 // 调用内核mov eax, 1 // 系统调用号,退出程序xor ebx, ebx // 返回值为0int 0x80 // 调用内核
Cortex-A7 常用汇编指令
处理器内部数据传输指令
数据传输常用的指令有三个: MOV、 MRS 和 MSR。
指令 | 目标寄存器 | 源寄存器 | 功能 |
MOV R1, R0 | R0 | R1 | 将 R1 里面的数据复制到 R0 中 |
MRS R0, CPSR | R0 | CPSR | 将特殊寄存器 CPSR 里的数据复制到 R0 |
MSR CPSR, R1 | CPSR | R1 | 将 R1 里面的数据复制到特殊寄存器 CPSR |
1. MOV(Move)
MOV
指令将一个立即数或一个寄存器的值赋值到另一个寄存器中,只能操作通用寄存器,不能直接访问系统状态寄存器(CPSR)。
MOV R0, #0x1234 //将立即数 0x1234 赋值到寄存器 R0, 即R0 = 0X1234
MOV R1, R0 //将寄存器 R0 的值复制到寄存器 R1, 即R1 = R0
2. MRS(Move Register from System)
MRS
指令将系统状态寄存器中的值读取到通用寄存器中。
MRS R0, CPSR //将系统状态寄存器 (CPSR) 的值存入 R0, 即R0=CPSR
MRS R1, SPSR //将系统状态寄存器 (SPSR) 的值存入 R1, 即R0=SPSR
3. MSR(Move System Register)
MSR
指令将系统状态寄存器中的值读取到通用寄存器中。
MSR SPSR, R0 //将 R0 的值写入 SPSR 系统寄存器中, 即SPSR=R0
存储器访问指令
常用的存储器访问指令有两种: LDR 和 STR 在编写汇编驱动的时候最常用这两个指令。在 32 位处理器中 LDR 和 STR 都是操作 4 个字节的地址,也就是 32 位数据。
指令 | 功能 |
LDR Rd, [Rn, #offset] | 从 Rn+offset 的地址读取数据存到 Rd 中 |
STR Rd, [Rn, #offset] | 将 Rd 寄存器中的数据写入到 Rn+offset 地址 |
1. LDR(Load Register)
LDR
指令用于从内存中加载一个值到寄存器,[ R1 ] 表示以 R1 存储的数字为地址。
LDR R0, = 0x20000000 //将立即数 0x20000000 加载到寄存器 R0 中//如果地址 0x20000000 存储的值是 0x1234, 则执行后 R0 = 0x1234
LDR R0, [R1] //将内存地址 R1 指向的数据加载到寄存器 R0 中//带偏移量的加载, 如果地址 0x20000004 存储的值是 0x6789, 则执行后 R0 = 0x6789
LDR R0, [R1, #4] //将 R1 + 4 的地址处的数据加载到 R0 中
2. STR(Store Register)
STR
指令用于将寄存器中的值存储到内存地址中。
STR R0, [R1] //将 R0 的值存储到内存地址 R1 指向的地址
STR R0, [R1, #4] //将 R0 的值存储到 R1 + 4 的地址处
栈指针
SP
指针(Stack Pointer,栈指针)是计算机体系结构中一个重要的寄存器 ,在不同的处理器架构中,SP
寄存器是硬件层面预定义的,处理器会专门保留一个寄存器用来表示栈指针。在 ARM 架构中 R13 是专门的寄存器,SP
是它的别名。SP
指向栈的当前顶部或下一条可用地址。
1. 栈的应用
在程序执行过程中,栈通常用于管理函数调用、局部变量和保存寄存器内容等任务。当函数被调用时,局部变量、返回地址、保存的寄存器等会被压入栈中,SP
指针会自动调整,指向栈的最新位置,当函数返回时,这些数据会被弹出,SP
指针恢复到调用前的位置。
2. 栈指针位置
SP
设置栈指针(C 语言运行的时候需要出栈和入栈,所以需要栈内存),SP
指针可以指向内部 RAM 也可以指向 DDR,以指向 DDR 为例,假设 DDR 的大小是 512MB,起始地址为 0x80000000,那么 DDR 的地址范围是 0x8000_0000~0x9FFF_FFFF,设置栈大小为 2MB=0x0020_0000(对于裸机运行已经够大了),栈的增长方式是向下增长,即高地址向低地址增长,所以设置栈地址为 0x8020_0000,保证栈不会溢出。
.global _start // 声明程序入口
_start:ldr sp, =0X80200000 // 设置栈指针的起始地址
b main //跳转到main函数
压栈和出栈指令
常用的栈操作指令是 PUSH 和 POP 。压栈和出栈指令通常用于保存和恢复寄存器的值,或者用于函数调用的返回地址。
1. PUSH(压栈)
PUSH
指令将寄存器的值压入栈中,并自动更新栈指针。它是对栈操作的简化形式,常用于保存寄存器的值。例如: PUSH {R0, R1, R2, R3}
2. POP(出栈)
POP
指令从栈中弹出寄存器的值,恢复寄存器的内容,并自动更新栈指针。例如:POP {R0, R1, R2, R3}
在压栈出栈的过程中,要保证先进后出的特性,最后压入栈中的寄存器最先出栈。例如,压栈的顺序为R0 -> R1 -> R2 -> R3,那么出栈的顺序为R3 -> R2 -> R1 -> R0。下面以中断场景举例压栈和出栈,在中断服务例程中,保存和恢复多个寄存器的值也非常常见。由于中断会打断程序的正常执行流程,所以需要保存现场,确保中断处理完成后能够正确地恢复执行。
ISR_Handler:PUSH {R0, R1, R2, R3} // 保存现场, 保存 R4, R5, R6, R7 到栈中//中断处理逻辑POP {R0, R1, R2, R3} // 恢复现场, 恢复 R4, R5, R6, R7BX LR // 返回到调用者
跳转指令
常用的跳转指令有 B 和 BK,其中 B 指令跳转后不会回到原来位置,BK 跳转后待函数执行完还会回到原来位置。
1. B(Branch)
B
指令这是最简单的跳转指令, B 指令会将 PC 寄存器的值设置为跳转目标地址, 一旦执行 B 指令, ARM 处理器就会立即跳转到指定的目标地址。如果要调用的函数不会再返回到原来的执行处,那就可以用 B 指令,如下示例
_start:
ldr sp, =0X80200000 //设置栈指针
b main //跳转到 main 函数//用 B 指令实现 loop 死循环
loop:b loop
2. BL(Branch with Link )
BL
指令相比 B 指令的区别是 BL 会跳转到指定函数,待函数执行完再返回原来位置继续执行,因为在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用一个基本但常用的手段。比如 Cortex-A 处理器的 irq 中断服务函数都是汇编写的,主要用汇编来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数,所以就会存在汇编中调用 C 函数的问题。
PUSH {r0, r1} //保存 r0,r1
CPS #0x13 //进入 SVC 模式,允许其他中断再次进去
BL system_irqhandler //加载 C 语言中断处理函数到 r2 寄存器中
CPS #0x12 //进入 IRQ 模式
POP {r0, r1}
STR r0, [r1, #0X10] //中断执行完成,写 EOIR
算术运算指令
常用加法、减法,乘除很少用
指令 | 计算公式 | 说明 |
ADD Rd, Rn, Rm | Rd = Rn + Rm | 加法运算,指令为 ADD |
ADD Rd, Rn, #immed | Rd = Rn + #immed | |
ADC Rd, Rn, Rm | Rd = Rn + Rm + 进位 | 带进位的加法运算,指令为 ADC |
ADC Rd, Rn, #immed | Rd = Rn + #immed +进位 | |
SUB Rd, Rn, Rm | Rd = Rn – Rm | 减法 |
SUB Rd, #immed | Rd = Rd - #immed | |
SUB Rd, Rn, #immed | Rd = Rn - #immed | |
SBC Rd, Rn, #immed | Rd = Rn - #immed – 借位 | 带借位的减法 |
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 | 有符号除法 |
在 CPU 运行过程中,如果想执行运算操作,必须要先将数据加载到寄存器中,然后才能执行运算操作!
// 实现 DDR 两个地址数据相加
LDR R0, [#0x20000000] // 从地址 0x20000000 加载数据到 R0
LDR R1, [#0x20000004] // 从地址 0x20000004 加载数据到 R1
ADD R2, R0, R1 // 将 R0 和 R1 的值相加,结果存入 R2// 实现两个立即数相加
MOV R0, #0x1 // 将立即数 0x1 加载到 R0
MOV R1, #0x2 // 将立即数 0x2 加载到 R1
ADD R2, R0, R1 // 将 R0 和 R1 的值相加,结果存入 R2
逻辑运算指令
指令 | 计算公式 | 说明 |
AND Rd, Rn | Rd = Rd &Rn | 按位与 |
AND Rd, Rn, #immed | Rd = Rn &#immed | |
AND Rd, Rn, Rm | Rd = Rn & Rm | |
ORR Rd, Rn | Rd = Rd | Rn | 按位或 |
ORR Rd, Rn, #immed | Rd = Rn | #immed | |
ORR Rd, Rn, Rm | Rd = Rn | Rm | |
BIC Rd, Rn | Rd = Rd & (~Rn) | 位清除 |
BIC Rd, Rn, #immed | Rd = Rn & (~#immed) | |
BIC Rd, Rn , Rm | Rd = Rn & (~Rm) | |
ORN Rd, Rn, #immed | Rd = Rn | (#immed) | 按位或非 |
ORN Rd, Rn, Rm | Rd = Rn | (Rm) | |
EOR Rd, Rn | Rd = Rd ^ Rn | 按位异或 |
EOR Rd, Rn, #immed | Rd = Rn ^ #immed | |
EOR Rd, Rn, Rm | Rd = Rn ^ Rm |