ARM(a32)体系结构与汇编指令

本文

主要介绍ARM体系结构与汇编指令,这里的体系结构指ARMv7及以前版本,ARMv8相对之前的版本区别较大,不过熟悉以前的体系结构对ARMv8的理解也有帮助,何况ARMv8本身也支持aarch32。

版本 说明
0.1 初版发布

参考

参考自《朱有鹏老师嵌入式linux核心课程》。

ARM的工作模式

  • 7种工作模式:

    • User : 非特权模式,大部分任务执行在这种模式
    • FIQ : 当一个高优先级(fast) 中断产生时将会进入这种模式
    • IRQ : 当一个低优先级(normal) 中断产生时将会进入这种模式
    • Supervisor :当复位或软中断指令执行时将会进入这种模式
    • Abort : 当存取异常时将会进入这种模式
    • Undef : 当执行未定义指令时会进入这种模式
    • System : 使用和User模式相同寄存器集的特权模式
  • 注意:

    • 除User(用户模式)是Normal(普通模式)外,其他6种都是Privilege(特权模式)。
    • Privilege中除Sys模式外,其余5种为异常模式。
    • 各种模式的切换,可以是程序员通过代码主动切换(通过写CPSR寄存器);也可以是CPU在某些情况下自动切换。
    • 各种模式下权限和可以访问的寄存器不同。
  • CPU为什么设计这些模式?

    • CPU是硬件,OS是软件,软件的设计要依赖硬件的特性,硬件的设计要考虑软件需要,便于实现软件特性。
    • 操作系统有安全级别要求,因此CPU设计多种模式是为了方便操作系统的多种角色安全等级需要。

ARM的寄存器

37个寄存器

  • ARM共有37个寄存器,都是32位长度。
  • 37个寄存器中30个为“通用”型,1个(r15)固定用作PC,一个固定用作CPSR,5个固定用作5种异常模式下的SPSR。
  • ARM寄存器编号为r0~r15,其中r13作为栈指针sp,r14作为链接寄存器lr,r15作为程序计数器pc。
  • ARM每个工作模式下都有自己独有的寄存器和共用的寄存器,而当前模式只见r0~r15。

举例如下:

当前模式为Usr
当前模式为Abort

所以,ARM(aarch32)的寄存器好比影子,每个模式都映射出当前模式下的r0~r15,注意System和User共有一套寄存器。由此可以发现,虽然ARM有37个寄存器,但对软件可见的只有r0~r15和cpsr。

CPSR当前状态寄存器

  • 条件位:

    • N = Negative result from ALU
    • Z = Zero result from ALU
    • C = ALU operation Carried out
    • V = ALU operation oVerflowed
  • Q 位:

    • 仅ARM 5TE/J架构支持
    • 指示饱和状态
  • J 位

    • 仅ARM 5TE/J架构支持
    • J = 1: 处理器处于Jazelle状态(对Jave的优化处理)
  • 中断禁止位:

    • I = 1: 禁止 IRQ.
    • F = 1: 禁止 FIQ.
  • T Bit

    • T = 0: 处理器处于 ARM 状态
    • T = 1: 处理器处于 Thumb 状态
  • Mode位:

    • 处理器模式位
  • 注意:

    • CPSR中各个bit位表明了CPU的某些状态信息,这些信息非常重要,这与汇编指令息息相关。
    • CPSR中的I、F位和开中断、关中断有关。
    • CPSR中的mode位(bit4~bit0共5位)决定了CPU的工作模式,在uboot代码中会使用汇编进行设置。

PC(r15)程序控制寄存器

  • PC(Program control register)为程序指针,PC指向哪里,CPU就会执行哪条指令(所以程序跳转时就是把目标地址代码放到PC中)。
  • 整个CPU中只有一个PC(CPSR也只有一个,但SPSR有5个)。

ARM的异常处理方式

什么是异常

  • 正常工作之外的流程都叫异常。
  • 异常会打断正在执行的工作,并且一般我们希望异常处理完成后继续回来执行原来的工作。
  • 中断是异常的一种。

异常向量表

  • 所有的CPU都有异常向量表,这是CPU设计时就设定好的,是硬件决定的。
  • 当异常发生时,CPU会自动动作(PC跳转到异常向量处处理异常,有时伴有一些辅助动作)
  • 异常向量表是硬件向软件提供的处理异常的支持。

ARM的异常处理机制

  • 当异常产生时, ARM core完成以下操作:
    • 拷贝 CPSR 到 SPSR_
    • 设置适当的 CPSR 位:
    • 改变处理器状态进入 ARM 态(如果当前在Thumb状态下)
    • 改变处理器模式进入相应的异常模式
    • 设置中断禁止位禁止相应中断 (如果需要)
    • 保存返回地址到 LR_
    • 设置 PC 为相应的异常向量
  • 返回时, 异常处理需要:
    • 从 SPSR_恢复CPSR
    • 从LR_恢复PC
  • Note:这些操作只能在 ARM 态执行.
  • 关于保存返回地址到 LR_,这时候分同步异常和异步异常,比如软中断指令引发的异常,属于同步异常,返回地址为该指令的下一条指令位置,而数据异常属于异步异常,返回地址为该指令位置。

总结

  • 异常处理中有一些是硬件自动做的,有一些是程序员需要自己做的。需要搞清楚哪些是需要自己做的,才知道如何写代码。
  • 以上说的是CPU设计时提供的异常向量表,一般成为一级向量表。有些CPU为了支持多个中断,还会提供二级中断向量表,处理思路类似于这里说的一级中断向量表。

指令集

两个概念:指令与伪指令

  • (汇编)指令是CPU机器指令的助记符,经过编译后会得到一串10组成的机器码,可以由CPU读取执行。
  • (汇编)伪指令本质上不是指令(只是和指令一起写在代码中),它是编译器环境提供的,目的是用来指导编译过程,经过编译后伪指令最终不会生成机器码。

两种不同风格的ARM指令

  • ARM官方的ARM汇编风格:指令一般用大写、Windows中IDE开发环境(如ADS、MDK等)常用。如: LDR R0, [R1]
  • GNU风格的ARM汇编:指令一般用小写字母、linux中常用。如:ldr r0, [r1]
  • 一般交叉编译都使用GNU风格的ARM汇编,编译器下载地址如下: https://releases.linaro.org/components/toolchain/binaries/

ARM汇编的特点

  • ARM采用RISC架构,CPU本身不能直接处理内存数据,而需要先将内存数据加载入CPU中通用寄存器中才能被CPU处理。
    • ldr(load register)指令将内存内容加载入通用寄存器。
    • str(store register)指令将寄存器内容存入内存空间中。
    • ldr/str组合用来实现 ARM CPU和内存数据交换。
  • 8种寻址方式:
    • 寄存器寻址 mov r1, r2
    • 立即寻址 mov r0, #0xFF00
    • 寄存器移位寻址 mov r0, r1, lsl #3
    • 寄存器间接寻址 ldr r1, [r2]
    • 基址变址寻址 ldr r1, [r2, #4]
    • 多寄存器寻址 ldmia r1!, {r2-r7, r12}
    • 堆栈寻址 stmfd sp!, {r2-r7, lr}
    • 相对寻址 beq flag
  • 指令后缀:
    • B(byte)功能不变,操作长度变为8位
    • H(half word)功能不变,长度变为16位
    • S(signed)功能不变,操作数变为有符号
    • 如 ldr ldrb ldrh ldrsb ldrsh
    • S(S标志)功能不变,影响CPSR标志位
    • 如 mov和movs movs r0, #0
  • 条件执行后缀:

数据传输和跳转指令

  • 数据处理指令:
    • 数据传输指令 mov mvn
    • 算术指令 add sub rsb adc sbc rsc
    • 逻辑指令 and orr eor bic
    • 比较指令 cmp cmn tst teq
    • 乘法指令 mvl mla umull umlal smull smlal
    • 前导零计数 clz
  • cpsr访问指令:
    • mrs用来读psr,msr用来写psr
    • CPSR寄存器比较特殊,需要专门的指令访问,这就是mrs和msr
  • 跳转(分支)指令:
    • b & bl & bx
    • b 直接跳转(就没打开算返回)
    • bl branch and link,跳转前把返回地址放入lr中,以便返回,以便用于函数调用
    • bx跳转同时切换到ARM模式,一般用于异常处理的跳转。
  • 访存指令:
    • 单个字/半字/字节访问 ldr/str
    • 多字批量访问 ldm/stm (为什么设有多字批量访问指令,比如发生异常时需要将现场寄存器压入栈,而返回时出栈以回到异常前的现场,这里多字批量访问指令对多个寄存器压入和弹出栈很有用)
    • 数据交换:swp r1, r2, [r0]
  • 立即数:
    • 合法立即数与非法立即数
    • ARM指令都是32位,除了指令标记和操作标记外,本身只能附带很少位数的立即数。因此立即数有合法和非法之分。
    • 合法立即数:经过任意位数的移位后非零部分可以用8位表示的即为合法立即数
    • 一般使用伪指令 ldr r0, 0x123456 可以不必关心立即数的合法性,编译器会自动完成
  • 软中断指令:
    • swi(software interrupt)
    • 软中断指令用来实现操作系统中系统调用

协处理器和协处理器指令

  • 协处理器cp15操作指令:
    • mrc用于读取CP15中的寄存器
    • mcr用于写入CP15中的寄存器
  • 什么是协处理器:
    • SoC内部另一处理核心,协助主CPU实现某些功能,被主CPU调用执行一定任务。
    • ARM设计上支持多达16个协处理器,但是一般SoC只实现其中的CP15.(cp:coprocessor)
    • 协处理器和MMU、cache、TLB等处理有关,功能上和操作系统的虚拟地址映射、cache管理等有关。
  • MRC & MCR的使用方法:
    • mcr{} p15, <opcode_1>, , , , {<opcode_2>}
    • mrc p15, 0, r0, c1, c0, 0
    • opcode_1:对于cp15永远为0
    • Rd:ARM的普通寄存器
    • Crn:cp15的寄存器,合法值是c0~c15
    • Crm:cp15的寄存器,一般均设为c0
    • opcode_2:一般省略或为0
  • 关于协处理器的操作不必深究,一般通过协处理器访问系统寄存器,不过大多处理器已经取消了协处理器。

ldm/stm与栈的处理

  • 为什么需要多寄存器访问指令:
    • ldr/str每周期只能访问4字节内存,如果需要批量读取、写入内存时太慢,解决方案是stm/ldm
    • ldm(load register mutiple)
    • stm(store register mutiple)
    • 如:stmia sp, {r0 - r12} ,将r0存入sp指向的内存处(假设为0x30001000);然后地址+4(即指向0x30001004),将r1存入该地址;然后地址再+4(指向0x30001008),将r2存入该地址······直到r12内容放入(0x3001030),指令完成。一个访存周期同时完成13个寄存器的读写
  • 8种后缀:
    • ia(increase after)先传输,再地址+4
    • ib(increase before)先地址+4,再传输
    • da(decrease after)先传输,再地址-4
    • db(decrease before)先地址-4,再传输
    • fd(full decrease)满递减堆栈
    • ed(empty decrease)空递减堆栈
    • fa(full ascending) 满递增堆栈
    • ea(empty ascending)空递增堆栈
  • 四种栈:
    • 空栈:栈指针指向空位,每次存入时可以直接存入然后栈指针移动一格;而取出时需要先移动一格才能取出
    • 满栈:栈指针指向栈中最后一格数据,每次存入时需要先移动栈指针一格再存入;取出时可以直接取出,然后再移动栈指针
    • 增栈:栈指针移动时向地址增加的方向移动的栈
    • 减栈:栈指针移动时向地址减小的方向移动的栈
  • !的作用:
    • ldmia r0, {r2 - r3}
    • ldmia r0!, {r2 - r3}
    • 感叹号的作用就是r0的值在ldm过程中发生的增加或者减少最后写回到r0去,也就是说ldm时会改变r0的值。
  • ^的作用:
    • ldmfd sp!, {r0 - r6, pc}
    • ldmfd sp!, {r0 - r6, pc}^
    • ^的作用:在目标寄存器中有pc时,会同时将spsr写入到cpsr,一般用于从异常模式返回。
  • 总结:
    • 批量读取或写入内存时要用ldm/stm指令
    • 各种后缀以理解为主,不需记忆,最常见的是stmia和stmfd
    • 谨记:操作栈时使用相同的后缀就不会出错,不管是满栈还是空栈、增栈还是减栈
    • 提供这么多种栈的模式,实际使用最多的是满减栈,而且编译器默认使用满减栈。

伪指令

  • 伪指令的意义:
    • 伪指令不是指令,伪指令和指令的根本区别是经过编译后会不会生成机器码。
    • 伪指令的意义在于指导编译过程。
    • 伪指令是和具体的编译器相关的,我们使用gnu工具链,因此学习gnu环境下的汇编伪指令。
  • gnu汇编中的一些符号:
    • @, // , /* */用来做注释。(不同编译器可能不同,根据实际情况为准)
    • :以冒号结尾的是标号
    • . 点号在gnu汇编中表示当前指令的地址
    • # 立即数前面要加#,表示这是个立即数
  • 常用gnu伪指令:
    • .global _start // 给_start外部链接属性
    • .section .text // 指定当前段为代码段
    • .ascii .byte .short .long .word
    • .quad .float .string // 定义数据
    • .align 4 // 以16字节对齐
    • .balignl 16 0xabcdefgh // 16字节对齐填充
  • 偶尔使用的gnu伪指令:
    • .end // 标识文件结束
    • .include // 头文件包含
    • .arm / .code32 // 声明以下为arm指令
    • .thumb / .code16 // 声明以下为thubm指令
  • 最重要的几个伪指令:
    • ldr 大范围的地址加载指令
    • adr 小范围的地址加载指令
    • nop 空操作
    • ARM中有一个ldr指令,还有一个ldr伪指令,一般都使用ldr伪指令实现立即数赋值和长跳转
  • adr与ldr:
    • adr编译时会被1条sub或add指令替代,而ldr编译时会被一条mov指令替代或者文字池方式处理
    • adr总是以PC为基准来表示地址,因此指令本身和运行地址有关,可以用来检测程序当前的运行地址在哪里,ldr加载的地址和链接时给定的地址有关,由链接脚本决定

文章原创,可能存在部分错误,欢迎指正,联系邮箱 cao_arvin@163.com。