blog.74ls74.org/2022/12/31/20221231_rpi_internals_assembly_the_good_parts_1/
阅读本文您不需要掌握的知识有
高深的操作系统理论
高深的计算机体系结构理论
高深的程序设计理论
阅读本文您需要具备
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的两个工具帮助从汇编生成二进制文件
可参考Documentation for binutils 2.39
as 汇编器 Assembler
ld 链接器 Linker
arm-none-eabi-as -g -o filename.o filename.s arm-none-eabi-ld -o filename.elf filename.o
注意后缀名为大写S 需要经过gcc预处理器处理,可以包含宏指令。
当然也可以用gcc命令一步生成elf文件
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常用参数
-O 优化级别 -g debug -mcpu 编译器推断march架构和mtune性能微调 arm1176jzf-s -ffreestanding freestanding模式, 与host模式相对,只提供与host操作系统无关的库
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
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数据处理指令
指令通用格式
INSTRUCTION{S} {
INSTRUCTION 指令名字
S 根据ALU计算结果Rd更新CPSR的flag
c condition 条件执行,在特定条件下flags执行指令
q qualifier NWT (本文内容不涉及)
const 立即数只有12bit构成 8-bit constant and 4-bit rotate value
Rd dest
Rn Rm 两个参数
type
LSL Logical shift left
LSR Logical shift right
ASR Arithmetic shift right
ROR Rotate right
condition代码
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 |
- |
condition代码和flags的对应关系。
立即数
arm处理器的立即数只有12bit!
这就是说立即数不可以是任意32bit二进制数。只能是8bit数字左右移位产生。
如果就需要使用某个数字怎么办?用load指令
ldr sp, =0xC0008000
数据处理指令主要有三类
算术指令
add/sub/mul/sdiv/udiv 加减乘有符号除无符号除
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 |
例如
mov r1, r0 @ 也可写作 mov r1, r1, r0。将r0寄存器32位数放到r1中 addsle r0, r1, r2 @ 将r1和r2的值相加,结果写入r0,s表示结果会影响flags位,le表示在flags位表示小于等于的时候才会执行指令
当然arm也支持乘法、累乘和除法,甚至是单指令流多数据流SIMD的并行乘法操作。这里不再详述
逻辑指令
and/orr/eor 按位与或异或等操作
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 |
标志置位指令
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可以直接在内存上操作数据)
指令格式
ldr |
载入数据到寄存器 |
内存 -> 寄存器 |
str |
存储数据到内存 |
寄存器 -> 内存 |
访存指令也可以有condition code后缀,条件执行
可以加位宽后缀,B for Byte (8bit) , H for Halfword (16bit), D for doubleword (64bit)
ldr指令可以加S,代表signed,符号位拓展
ldrsb r1, [r2] @ r2地址所在的8bit数据符号扩展,然后载入到r1
因此ldr/str的地址位与寄存器相同,都是32bit,可以索引到4GB地址空间
arm默认为小端字节序(低地址保存低字节,数字顺序),当然也可以开启大端模式(低地址保存高字节,字符串阅读顺序)
地址模式
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汇编也有语法糖,一条指令就可以实现!
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指令的别称,它们是等价的
push {r0-r7} @ 等价于 stmfd sp!, {r0-r7} pop {r0-r7} @ 等价于 ldmfd 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指令,它由汇编器解释并执行。下面列出一些常用指令
.align n 填充nop指令,使接下来的指令对齐2的n次方 .ascii "string1", "string2" .. 加入指定字符串 .byte .hfword .word .data 数据段开始 .text 代码段开始 .equ 赋值变量 .extern .global .includ .if .end
汇编器可以做一些简单的数字和逻辑计算并生成一个常量值,灵活使用汇编器指令可以大大增加效率。比如汇编中的labels可以作为常量来用,在处理位置独立position-independent代码和链接器定义地址linker-defined address时非常有用。
8. ABI Application Binary Interfaces 应用二进制接口
ARM Architecture Procedure Call Standard 应用二进制接口是C语言的调用标准。如果不是和C交互,写汇编可以完全不必拘泥于此。
9. 程序段section
可执行程序分为多个程序段
.data |
数据段 |
|
.data |
数据段 |
|
.rodata |
只读常量 read-only constants |
|
.bss (the Block Started by Symbol) |
初始化0的数据 zero initialized data |
C语言中储存未初始化的静态变量 |
10. summary
以上是我对arm(应用程序)汇编的总结,比起高级语言,汇编本身就是一个不太友好的语言,所以本文略显罗嗦和乏味。纸上得来终觉浅,汇编需要多写多调试,才能掌握,使用起来出神入化。
大家如果有兴趣可以到asmbits上面刷刷汇编的oj(可惜leetcode不支持arm汇编),多多练习
Arm index - ASMBits
对应的答案供参考
https://github.com/996refuse/ASMBits-Solutions-ARMv7
如果您也喜欢汇编或者找到谬误,欢迎评论指正!谢谢
reference
官方 Architecture Reference Manual Documentation – Arm Developer
官方 Programmer's Guide Documentation – Arm Developer
http://blog.74ls74.org/2022/12/31/20221231_rpi_internals_assembly_the_good_parts_1/blog.74ls74.org/2022/12/31/20221231_rpi_internals_assembly_the_good_parts_1/