阅读本文您不需要掌握的知识有
- 高深的操作系统理论
- 高深的计算机体系结构理论
- 高深的程序设计理论
阅读本文您需要具备
- GNU工具链(make/GCC/LD) ★★☆☆☆
- C语言 ★★☆☆☆
- 安装有raspbian的树莓派
阅读本文您可以得到什么
- 可以用汇编语言刷online judge
- 仍然无法做出让人振奋的东西,满足成就感
0. 历史
1985年,第一代arm处理器在Acorn诞生,此时arm指Acorn RISC Machine。
1990年,Acorn、VLSI和Apple联合成立了ARM公司,Advanced RISC Machine高级精简指令集机器。
新成立的公司决定公司的主要业务是设计并出售芯片的IP license,不做芯片制造和销售。夏普、德州仪器、三星成为他们第一批客户。发展到今天,intel、AMD、Qualcomm、broadcom、ZTE、Huawei等很多芯片巨头都是ARM的客户。ARM以及众多半导体IP设计公司一道设计出久经考验的IP block, 今天的芯片公司把处理器核心、内存控制器、外设、总线等IP像搭积木一样拼装在一起,从而在一块芯片上实现完整的嵌入式系统,大大提高了开发效率。而且因为ARM完善的软件生态,使芯片公司在软件适配方面节省了大量资金和人力。
1. 为什么学习arm汇编
当操作系统需要一些细粒度的操作时,c语言不够用。设置堆栈、保存和切换上下文、设置中断函数开关中断、系统调用软中断、虚拟内存、缓存管理,调用一些特殊系统指令、访问系统寄存器。当我们有这些需求,arm汇编是绕不过去的。
知道的太少,不懂的太多。一些新想法的实现中遇到的困难总是层峦叠嶂。我会尽量少的引入新的知识,语言精粹,为了拥有流畅的学习体验。待到勇攀高峰,一切就会拨云见日。
2. 树莓派arm汇编
arm汇编为了保持向后兼容性,越来越复杂,包含整型计算、thumb、64位的支持、数值计算相关。
arm32汇编是ARMv8-A之前架构所通用的汇编。本文以ARMv7-A为主,介绍arm32汇编的基础子集,包含整型等基础指令。我们首先要理清几个概念。
2.1 thumb
thumb指令集是arm32指令集的一个子集,指令长度16bit,它的指令和32位arm指令是对应的,这样设计的目的是为了实现更高的代码密度code density。有趣的是,处理器在执行thumb指令时会实时转换成32位arm指令,不会造成性能损失。
本文内容不会涉及thumb
2.2 arm64
从ARMv8-A架构开始,arm准备在A系列内核逐步放弃32位从而走上64位,使用全新设计的arm64指令集。第三代树莓派所搭载的soc是bcm2837,它的处理器内核Cortex-A53就是一款ARMv8-A架构处理器,支持arm64指令集。
本文内容不会涉及arm64
2.3 aarch32, aarch64
在ARMv8-A架构中有两个模式aarch32和aarch64,aarch32模式可以向后兼容,和arm32几乎完全兼容。
本文内容不会涉及aarch64
2.4 VFP, NEON, 饱和算术指令
VFP, NEON和饱和算术指令是arm32指令集的子集,包含浮点和向量计算等数学相关的指令,这些指令被用来应对日益增长的多媒体需求。操作系统并不会涉及,交给专业的人和工具吧!
https://developer.arm.com/documentation/den0018/a/Compiling-NEON-Instructions/NEON-libraries
本文内容不会涉及vfp和neon
硬件操作相关的内容会在下一篇章介绍。
arm处理器架构有很多变种,例如ARMv4T,ARMv5T,ARMv6等,arm32指令几乎与它们完全兼容。树莓派soc bcm2835的核心ARM1176JZFS是ARMv6架构。
不得不说兼容性带来了沉重的历史包袱,也带来繁荣的软件生态。就像intel划时代的x86处理器,arm保持了强大的向后兼容性,也背上了沉重的历史。ARMv9-A为了达到更高的性能,新架构甩掉历史包袱,取消了对aarch32的支持。但arm32指令集应用至今,2022年11月,联发科发布了天矶9200芯片,包含一个Cortex-X3超大核心,两个Cortex-A715大核、两个Cortex-A710中核和三个Cortex-A510小核。与树莓派一样,这颗A510小核仍然保留对arm32的支持。
3. GNU工具
首先需要工具
GNU 工具链toolchain,是底层开发必不可少的工具。如果开发机不是arm native架构,我们需要安装交叉编译cross-compiling的版本。
https://developer.arm.com/downloads/-/gnu-rm
arm-none-eabi 分别代表了ARCH-VENDOR-OS-LIBC(vendor省略了)
汇编的本质就是32bit二进制指令的助记符,一行一行的汇编指令对应的恰好是一个一个计算机能理解的32bit数字,我们需要用GNU Binutils的两个工具帮助从汇编生成二进制文件
参考https://sourceware.org/binutils/docs/
as 汇编器 Assembler
ld 链接器 Linker
1 | arm-none-eabi-as -g -o filename.o filename.s |
注意后缀名为大写S的源文件,需要先经过gcc预处理器处理。要使用宏指令需要大写S后缀。
也可以用gcc命令一步生成elf文件
1 | arm-none-eabi-gcc -O2 -g -ffreestanding -mcpu=arm1176jzf-s -o test.o -S test.s |
https://developer.arm.com/documentation/den0013/d/Optimizing-Code-to-Run-on-ARM-Processors/
Compiler-optimizations/GCC-optimization-options
http://sourceware.org/binutils/docs/as/index.html
这里列出一些gcc常用参数
1 | -O 优化级别 |
4. arm寄存器介绍
arm拥有r0-r15 共16个32bit寄存器,1个CPSR寄存器(也称为APSR application)
r15 也写作pc 程序计数寄存器,当前cpu所在指令的地址。顺序执行时,每完成一个指令自加4(4×8=32bit)。写r15会立即跳转到写入的地址。
r14 也写作lr 链接寄存器,bl指令跳转时会将(pc+4)储存在lr上,用来函数返回。
r13 也写作sp 栈指针,push和pop指令作用的栈顶地址
CPSR用来记录当前程序运行的状态,重点关注以下4个和处理器算术逻辑单元ALU相关的标志位
flags
- N - Negative 负值
- Z - Zero 零
- C - Carry out 进位
- V - Overflowed 溢出
数据处理指令可以根据处理结果改变flags的值,
分支指令会根据flags当前状态来决定确定pc之后的值。
branch可能导致处理器流水线停顿,分支预测就是就是解决这个问题的技术。
5. arm汇编语法
通用格式
label: instruction @ comment
汇编程序就是由这样多行指令构成的
6. arm指令分类详解
6.1 ALU数据处理指令
指令通用格式
1 | INSTRUCTION{S}<c><q> {<Rd>,} <Rn>, Operand2 |
- INSTRUCTION 指令名字
- S 根据ALU计算结果Rd更新CPSR的flag
- c condition 条件执行,在特定条件下flags执行指令
- q qualifier NWT (本文内容不涉及)
- Rd dest
- Operand2 可以是一个立即数
- const 立即数只有12bit构成 8-bit constant and 4-bit rotate value
- rotate value
- LSL Logical shift left
- LSR Logical shift right
- ASR Arithmetic shift right
- ROR Rotate right
下面是condition代码和flags的对应关系。
cond code | defination | flags |
---|---|---|
EQ | Equal | Z = 1 |
NE | Not equal | Z = 0 |
CS | Carry set (identical to HS) | C = 1 |
HS | Unsigned higher or same | C = 1 |
CC | Carry clear (identical to LO) | C = 0 |
LO | Unsigned lower (identical to CC) | C = 0 |
MI | Minus or negative result | N = 1 |
PL | Positive or zero result | N = 0 |
VS | Overflow | V = 1 |
VC | Now overflow | V = 0 |
HI | Unsigned higher | C = 1 AND Z = 0 |
LS | Unsigned lower or same | C = 0 OR Z = 1 |
GE | Signed greater than or equal | N = V |
LT | Signed less than | N != V |
GT | Signed greater than | Z = 0 AND N = V |
LE | Signed less than or equal | Z = 1 OR N != V |
AL | Always. This is the default | - |
立即数
arm处理器的立即数只有12bit!
这就是说立即数不可以是任意32bit二进制数。只能是8bit数字左右移位产生。
如果就需要使用某个数字怎么办?用load指令
1 | ldr sp, =0xC0008000 |
伪指令 ADR Rn, =label LDR Rn, =label 可以帮助生成任意数字
数据处理指令主要有三类
算术指令
add/sub/mul/sdiv/udiv 加减乘有符号除无符号除
instruction | parameters | defination | formula |
---|---|---|---|
ADC | Rd, Rn, Op2 | Add with carry | Rd = Rn + Op2 + C |
ADD | Rd, Rn, Op2 | Add | Rd = Rn + Op2 |
MOV | Rd, Op2 | Move | Rd = Op2 |
MVN | Rd, Op2 | Move NOT | Rd = ~Op2 |
RSB | Rd, Rn, Op2 | Reverse Subtract | Rd = Op2 - Rn |
RSC | Rd, Rn, Op2 | Reverse Subtract with Carry | Rd = Op2 - Rn - !C |
SBC | Rd, Rn, Op2 | Subtract with carry | Rd = Rn - Op2 -!C |
SUB | Rd, Rn, Op2 | Subtract | Rd = Rn - Op2 |
例如
1 | mov r1, r0 @ 也可写作 mov r1, r1, r0。将r0寄存器32位数放到r1中 |
当然arm也支持乘法、累乘和除法,甚至是单指令流多数据流SIMD的并行乘法操作。这里不再详述
逻辑指令
and/orr/eor 按位与或异或等操作
instruction | parameters | defination | formula |
---|---|---|---|
AND | Rd, Rn, Op2 | AND | Rd = Rn & Op2 |
BIC | Rd, Rn, Op2 | Bit Clear | Rd = Rn & ~ Op2 |
EOR | Rd, Rn, Op2 | Exclusive OR | Rd = Rn ^ Op2 |
ORR | Rd, Rn, Op2 | OR | Rd = Rn 或 Op2(OR NOT)Rd = Rn 或 ~Op2 |
标志置位指令
instruction | parameters | defination | formula |
---|---|---|---|
CMP | Rn, Op2 | Compare | Rn - Op2 |
CMN | Rn, Op2 | Compare Negative | Rn + Op2 |
TEQ | Rn, Op2 | Test EQuivalence | Rn ^ Op2 |
TST | Rn, Op2 | Test | Rn & Op2 |
置位指令是数据处理指令的别名alias。CMP指令就等价于SUBS
6.2 MEMORY访存指令
寄存器数量是有限的,需要用内存为程序储存更多的数据。
arm是精简指令集处理器,一个标志性特点就是有单独的load/store指令,数据处理指令不可以直接操作内存(x86可以直接在内存上操作数据)
指令格式
instruction | defination | direction |
---|---|---|
ldr | 载入数据到寄存器 | 内存 -> 寄存器 |
str | 存储数据到内存 | 寄存器 -> 内存 |
访存指令也可以加一个condition code后缀,条件执行
可以加位宽后缀,B for Byte (8bit) , H for Halfword (16bit), D for doubleword (64bit)
ldr指令可以加S,代表signed,符号位拓展
1 | ldrsb r1, [r2] @ r2地址所在的8bit数据符号扩展,然后载入到r1 |
ldr/str的地址位与寄存器相同,都是32bit,可以索引到4GB地址空间
arm默认为小端字节序(低地址保存低字节,数字顺序),当然也可以开启大端模式(低地址保存高字节,字符串阅读顺序)
地址模式
address mode | defination |
---|---|
ldr r0, [r1] | addr = r1 |
ldr r0, [r1, r2] | addr = r1 + r2 |
ldr r0, [r1, r2, lsl #2] | addr = r1 + r2 * 4 / addr = r1 + r2 << 2 |
ldr r0, [r1, #4]! 前索引写回 | addr = r1 + 4, then r1 = r1 + 4 |
ldr r0, [r1], #4 后索引写回 | addr = r1, then r1 = r1 + 4 |
这是在循环中访问数组的底层实现
多指令传送Multiple transfers
如果要连续读写内存是不是要重复写很多行ldr/str指令呢?
arm汇编也有语法糖,一条指令就可以实现!
1 | ldmia r13!, { r0-r9 } |
这里一条指令可以实现r0到r9 10个32位数共40个byte写到r13地址上,然后r13自增40
- ! 表示写回
- 连字符hyphens表示一连串寄存器
注意的是,低序号寄存器永远在低地址上
这里有四个种后缀可以影响基地址寄存器,索引后缀和堆栈后缀是对应的
Stack-oriented suffix 堆栈后缀 | For store or push instructions 索引后缀 | For load or pop instructions 索引后缀 |
---|---|---|
FD (Full Descending stack) | DB (Decrement Before) | IA (Increment After) |
FA (Full Ascending stack) | IB (Increment Before) | DA (Decrement After) |
ED (Empty Descending stack) | DA (Decrement After) | IB (Increment Before) |
EA (Empty Ascending stack) | IA (Increment After) | DB (Decrement Before) |
FD是arm栈stack的默认模式,push/pop指令是stm/ldm指令的别称,它们是等价的
1 | push {r0-r7} @ 等价于 stmfd sp!, {r0-r7} |
多指令在栈操作和内存拷贝方面很好用。注意,它只能作用于word对齐的数据(32bit对齐)
6.3 分支指令
就四个指令
link | exchange | |
---|---|---|
b |
||
bl |
返回值地址保存到lr | |
bx |
可切换Thumb指令集 | |
blx |
返回值地址保存到lr | 可切换Thumb指令集 |
注意,
label是相对跳转 relative branches,地址范围是 +/-32MB
通过Rm则能跳转到任意32bit地址
6.4 其他指令
arm还有一类特殊的指令,包括协处理器指令、特权指令、PSR修改指令、cache指令等等,例如
- nop 空指令,它可以用来填充bin,占位置,对处理器无效果。
- barrier,因为在处理器执行指令过程中,实际上并不是顺序执行的,它保证指令执行顺序
- wait 指令,wfi wfe,可以使处理器内核进入省电模式。
- msr mrs,系统寄存器操作指令
本文暂且简单介绍
7. 汇编器指令 assembler directives
汇编器指令都有一个“.”作为前缀,汇编器指令并不是arm指令,它由汇编器解释并执行。下面列出一些常用指令
1 | .align n 填充nop指令,使接下来的指令对齐2的n次方 |
汇编器可以做一些简单的数字和逻辑计算并生成一个常量值,灵活使用汇编器指令可以大大增加效率。比如汇编中的labels可以作为常量来用,在处理位置独立position-independent代码和链接器定义地址linker-defined address时非常有用。
8. ABI Application Binary Interfaces 应用二进制接口
ARM Architecture Procedure Call Standard 应用二进制接口是C语言的调用标准。如果不是和C交互,写汇编可以完全不必拘泥于此。
9. 程序段section
可执行程序分为多个程序段
程序段 | ||
---|---|---|
.test | 代码段 | |
.data | 数据段 | |
.rodata | 只读常量 read-only constants | |
.bss (the Block Started by Symbol) | 初始化0的数据 zero initialized data | C语言中储存未初始化的静态变量 uninitialized static data |
10. summary
以上是我对arm(应用程序)汇编的总结,比起高级语言,汇编本身就是一个用户不太友好的语言,所以本文略显罗嗦和乏味。纸上得来终觉浅,汇编需要多写多调试,才能掌握。
大家如果有兴趣可以到asmbits上面刷刷汇编的oj(可惜leetcode不支持arm汇编),多多练习
https://asmbits.01xz.net/wiki/Arm_index
对应的答案供参考
https://github.com/996refuse/ASMBits-Solutions-ARMv7
如果您也喜欢汇编,欢迎交流评论和指正!谢谢
reference
官方 Architecture Reference Manual https://developer.arm.com/documentation/ddi0406/latest/
官方 Programmer’s Guide https://developer.arm.com/documentation/den0013/latest/
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章