Changchun Master Li

[RPi bring up] 深入树莓派内部,arm汇编语言精粹(上)

2022-12-31

阅读本文您不需要掌握的知识有

  • 高深的操作系统理论
  • 高深的计算机体系结构理论
  • 高深的程序设计理论

阅读本文您需要具备

  • 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
2
arm-none-eabi-as -g -o filename.o filename.s
arm-none-eabi-ld -o filename.elf filename.o

注意后缀名为大写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
2
3
4
-O 优化级别
-g debug
-mcpu 编译器推断march架构和mtune性能微调 arm1176jzf-s
-ffreestanding freestanding模式, 与host模式相对,只提供与host操作系统无关的库<float.h>,<iso646.h>,<limits.h>,<stdarg.h>,<stdbool.h>,<stddef.h>,<stdint.h>

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数据处理指令

指令通用格式

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
2
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 按位与或异或等操作

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
2
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指令,它由汇编器解释并执行。下面列出一些常用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
.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

可执行程序分为多个程序段

程序段
.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/

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章