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

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寄存器介绍

[RPi bring up] 深入树莓派内部,arm汇编语言精粹(上)_第1张图片

编辑

寄存器

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} {,} , Operand2

  • 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语言中储存未初始化的静态变量
uninitialized static data

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/

你可能感兴趣的:(arm开发,嵌入式硬件)