ARM 内核寄存器 和 基本汇编语言讲解
前言
开头直接来看几个简单的汇编指令:
MOV R0,R1
MOV PC,R14
上面的指令中使用了汇编 MOV
指令,但是其中的 R0,R1,R14,PC 分别是什么?哪来的?怎么用?
要讲 ARM 汇编语言,必须得先了解 ARM 的内核寄存器,内核处理所有的指令计算,都需要用到内核寄存器,所以 ARM 汇编里面指令大都是基于寄存器的操作。
文章前推荐韦东山老师的单片机核心视频,视频可以在韦东山老师官网里面找到。
ARM 版本简单介绍:
一、ARM 内核寄存器
内核寄存器与外设寄存器:
内核寄存器与外设寄存器是完全不同的概念;
内核寄存器是指 CPU 内部的寄存器,CPU 处理所有指令数据需要用到这些寄存器保存处理数据;
外设寄存器是指的 串口,SPI,GPIO 口这些设备有关的寄存器。
在我的另一篇博文:FreeRTOS记录(三、FreeRTOS任务调度原理解析_Systick、PendSV、SVC)内核中断管理 章节讲到过Cortex-M的寄存器
的相关内容,这里我们再简单说明一下:
1.1 M3/M4 内核寄存器
对于 M3/M4 而言:
R13,栈指针(Stack Pointer)
R13 寄存器中存放的是栈顶指针,M3/M4 的栈是向下生长的,入栈的时候地址是往下减少的。
裸机程序不会用到 PSP,只用到 MSP,需要运行 RTOS 的时候才会用到 PSP。
堆栈主要是通过 POP,PUSH 指令来进行操作。在执行 PUSH 和 POP 操作时, SP 的地址寄存器,会自动调整。
R14 ,连接寄存器(Link Register)
LR 用于在调用子程序时存储返回地址。例如,在使用 BL(分支并连接, Branch and Link)指令时,就自动填充 LR 的值(执行函数调用的下一指令),进而在函数退出时,正确返回并执行下一指令。如果函数中又调用了其他函数,那么 LR 将会被覆盖,所以需要先将 LR 寄存器入栈。
保存子程序返回地址。使用 BL 或 BLX 时,跳转指令自动把返回地址放入 r14 中;子程序通过把 r14 复制到 PC 来实现返回
当异常发生时,异常模式的 r14 用来保存异常返回地址,将 r14 如栈可以处理嵌套中断
R15,程序计数器(Program Count)
在 Cortex-M3 中指令是 3 级流水线,出于对 Thumb 代码的兼容的考虑,读取 pc 时,会返回当前指令地址+4 的值。
读 PC 时返回的值是当前指令的地址+4,关于 M3、M4 和 A7 的 PC 值的问题需要单独来解释一下。
其中程序状态寄存器 XPSR:
程序状态寄存器,该寄存器由三个程序状态寄存器组成:
应用 PSR(APSR) : 包含前一条指令执行后的条件标志,比较结果:大于等于,小于,进位等等;
中断 PSR(IPSR ) : 包含当前 ISR 的异常编号
执行 PSR(EPSR) : 包含 Thumb 状态位
1.2 A7 内核寄存器
对于 A7 而言:
(上图取自原子教材,此图在官方文档《ARM Cortex-A(armV7)编程手册 V4.0》中第 3 章.ARM Processor Modes and Registers 部分有英文原版,这里用中文版本更容易理解)
A7 的 R13、R14、R15 的作用和 M3/4 类似。
需要注意的一点就是,对于 A7 而言 R15,程序计数器(Program Count):
读 PC 时返回的值是当前指令的地址+8, PC 指向当前指令的下两条指令地址。
由于 ARM 指令总是以字对齐的,故 PC 寄存器 bit[1:0] 总是 00。
A7 内核的程序状态寄存器 CPSR:
1.3 ARM 中的 PC 指针的值
因为 ARM 指令采用三级流水线机制,所以 PC 指针的值并不是当前执行的指令的地址值:
当前执行地址 A 的指令,
同时已经在对下一条指令进行译码,
同时已经在读取下下一条指令:PC = A +4 (Thumb/Thumb2 指令集)、PC = A + 8 (ARM 指令集)
在文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition》中对于 PC 的值有明确的说明:
M3/M4/M0:
PC 的值 = 当前地址 + 4;
下面是一个 STM32F103 反汇编程序,找了一段有[pc,#0]的代码,方便判断:
A7:
PC 的值 = 当前地址 + 8;
二、ARM 汇编语言
ARM 芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),具体说明在下面这篇博文 5.4 小结有过说明:
STM32 的内存管理相关(内存架构,内存管理,map 文件分析)https://xie.infoq.cn/article/625b2009e810086435349be30
2.1 ARM 汇编基础
2.1.1 ARM 指令集说明
最初,ARM 公司发布了两类指令集:
ARM 指令集,32 位的 ARM 指令,每条指令占据 32 位,高效,但是太占空间;
Thumb 指令集,16 位的 Thumb 指令,每条指令占据 16 位,节省空间;
比如: MOV R0,R1
这条指令,可能是 16 位的,也可能是 32 位的
那么在汇编中是如何在 ARM 指令 和 Thumb 指令之间切换呢:
对于 A7、ARM7、ARM9 内核而言它们支持 16 位的 Thumb 指令集 和 32 位的 ARM 指令集
对于 M3、M4 内核而言它们支持的是 Thumb2 指令集,它支持 16 位、32 位指令混合编程
对于内核来说使用的是 ARM 指令集 还是 Thumb 指令集,就是在 XPSR 和 CPSR
在 M3/M4 中, XPSR 寄存器的 T(bit24):1 表示 Thumb 指令集
根据上面所述,M3 是使用的 Thumb2 指令集,所以会有 T 总是 1.
在 A7 中 CPSR 中的:T(bit5) :控制指令执行状态,表明本指令是 ARM 指令还是 Thumb 指令,通常和 J(bit24)一起表明指令类型:
回到开始的指令 MOV R0,R1
2.1.2 ARM 汇编格式
编码格式:
不同指令集的编码格式(以 LDR 为例),摘自《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition》:
以“数据处理”(其他的还有内存访问,分支跳转等)指令为例,UAL 汇编格式为:
Operation
表示各类汇编指令,比如 ADD、MOV;
cond
表示 conditon,即该指令执行的条件,如 EQ,NE 等;
S
表示该指令执行后,是否会影响 CPSR 寄存器的值, 是否影响 CPSR 寄存器的值,书写时影响 CPSR,否则不影响;
Rd
为目的寄存器,用来存储运算的结果;
Rn
第一个操作数的寄存器;
Operand2
第二个操作数 ,其可以有 3 种操作源:
1-- 立即数
2-- 寄存器
3-- 寄存器移位
其指令编码格式如下(32 位):
举个例子:
对于“数据处理”处理指令中的 Operation ,指令集如下:
对于其中的条件 cond ,如下:
2.1.3 立即数
在一条 ARM 数据处理指令中,除了要包含处理的数据值外,还要标识 ARM 命令名称,控制位,寄存器等其他信息。这样在一条 ARM 数据处理指令中,能用于表示要处理的数据值的位数只能小于 32 位;
在上面的 ARM 汇编格式中我们介绍过,ARM 在指令格式中设定,只能用指令机器码 32 位中的低 12 位来表示要操作的常数。
那么对于指令MOV R0, #value
(把 value 的值存入 R0 寄存器)而言,value 的值也不能是任意的值,其值只能是符合某些规定的数,在官方文档中 value 的值需要满足如下条件:
什么是立即数?
满足上图中条件的数我们称之为立即数,立即数就是符合一定规矩的数。
立即数表示方式:每个立即数由一个 8 位的常数循环右移偶数位得到。其中循环右移的位数由一个 4 位二进制的两倍表示。
立即数 = 一个 8 位的常数 循环位移 偶数位
一个 8bit 常数循环右移(Y*2 = {0,2,4,6,8, ...,26, 28, 30})就得到一个立即数了;(为什么是 0 到 30 的偶数下面解释)
如果需要深入理解立即数,推荐一篇博文:深刻认识 -->> 立即数
ARM 处理器是按 32 位来处理数据的,ARM 处理器处理的数据是 32 位,为了扩展到 32 位,因此使用了构造的方法,在 12 位中用 8 位表示基本数据值,用 4 位表示位移值,通过用 8 位基本数据值往右循环移动 4 位位移值*2 次,来表示要操作的常数。
这里要强调最终的循环次数是 4 位位移值乘以 2 得到的,所以得到的最终循环次数肯定是一个偶数,为什么要乘以 2 呢,实质还是因为范围不够,4 位表示位移次数,最大才 15 次(移位 0,等于没有循环),加上 8 位数据还是不够 32 位,这样只能通过 ALU 的内部结构设计将 4 位位移次数乘以 2,这样就能用 12 位表示 32 位常数了。
所以 12bit 数据存放格式如下:
但是我们去判断一个数是否立即数,实在是太麻烦了,但是我们想把任意数值赋给 R0 寄存器,怎么办? 这就需要用到伪指令了,下面说一说什么是伪指令。
2.2 汇编伪指令
汇编语言分成两块:标准指令集和非标准指令集。 伪指令属于非标准指令集。
什么是伪指令?
类似于宏的东西,把复杂的有好几天指令进行跳转的完成的小功能级进行新的标签设定,这就是伪指令。
类似于学 c 语言的时候的预处理,在预处理的时候把它定义于一堆的宏转化为真正的 c 语言的代码。同样,伪指令是在定义好之后的汇编,汇编的时候会把它翻译成标准指令,也许一条简单的伪指令可以翻译成很多条标准的汇编指令集,所以这就是伪指令最重要的作用。
我们前面说的 CODE16
CODE32
也是伪指令,用来指定其后的代码格式。
伪指令的作用?
基本的指令可以做各类操作了,但操作起来太麻烦了。伪指令定义了一些类似于带参数的宏,能够更好的实现汇编程序逻辑。(比如我现在要设置一个值给寄存器 R0,但下次我修改了寄存器 R0 之后又需要读出来刚才的值,那我们就要先临时保存值到 SPSR,CPSR,然后不断切换。)
伪指令只是在汇编器之前作用,汇编以后翻译为标准的汇编令集。
伪指令的类别伪指令可分为 ARM 汇编伪指令和 GNU 汇编伪指令
ARM 汇编伪指令是 ARM 公司的,GNU 汇编伪指令是 GNU 平台的。他们有自己的汇编器,不同的汇编器的解释语法可以设成不同。
2.2.1 GNU 汇编伪指令
这里列出部分伪指令说明,具体的伪指令可以结合 ARM 汇编伪指令分析:
2.2.2 ARM 汇编伪指令
在我的另一篇博文:STM32 的启动过程(startup_xxxx.s 文件解析)
https://xie.infoq.cn/article/bb7ca59b25e72f6e854e9e81f
里面有过一些对伪指令意思的的说明,下面也列出部分说明:
AREA:
用于定义一个代码段或数据段。属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。
其中,段名若以数字开头,则该段名需用 “ | ” 括起来:
ALIGN:
ALIGN 伪指令可通过添加填充字节的方式,使当前位置满足一定的对其方式。其中,表达式的值用于指定对齐方式,可能的取值为 2 的幂,如 1 、2 、4 、8 、16 等。
若未指定表达式,则将当前位置对齐到下一个字的位置。
CODE16 和 CODE32:
指定其后面的指令为 ARM 指令还是 Thumb 指令,前面介绍过。
ENTRY:
用于指定汇编程序的入口点。在 一个完整的汇编程序 中至少要有一个 ENTRY (也可以有多个,当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在 一个源文件 里最多只能有一个 ENTRY。
在startup_stm32f103xg.s
里面就没有。
END:
用于通知编译器已经到了源程序的结尾。
IMPORT 和 EXPORT:
IMPORT 定义表示这是一个外部变量的标号,不是在本程序定义的
EXPORT 表示本程序里面用到的变量提供给其他模块调用的
2.2.3 LDR
和 ADR
LDR
伪指令:
简单介绍了伪指令基础,回到上一小结留下的问题,想要把任意值复制给 R0,怎么处理,我们使用伪指令: LDR R0, =value
编译器会把“伪指令”替换成真实的指令:
LDR R0, =0x12
0x12 是立即数,那么替换为:
MOV R0, #0x12
LDR R0, =0x12345678
0x12345678 不是立即数,那么替换为:
LDR R0, [PC, #offset]
// 2. 使用 Load Register 读内存指令读出值,offset 是链接程序时确定的……Label DCD 0x12345678
// 1. 编译器在程序某个地方保存有这个值
ADR
伪指令:
ADR 的意思是:address,用来读某个标号的地址:
ADR{cond} Rd, labe1
2.3 ARM 汇编指令集
在《ARM Cortex-M3 与 Cortex-M4 权威指南》一文中第 5 章节有详细的指令集说明:
汇编指令可以分为几大类:数据处理、内存访问、跳转、饱和运算、其他指令。
数据传输命令 MOV
MOV 指令,用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器。
MOV 指令的格式为:MOV{条件}{S} 目的寄存器,源操作数
状态寄存器访问 MRS 和 MSR
MRS 指令,用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器。
MSR 指令,和 MRS 相反,用来将普通寄存器的数据传递给特殊寄存器。
存储器访问 LDR 和 STR
LDR:
LDR 指令用于从存储器中将一个 32 位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取 32 位的字数据到通用寄存器,然后对数据进行处理。
指令的格式为:LDR{条件} 目的寄存器,<存储器地址>
当程序计数器 PC 作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。
LDRB: 字节操作
LDRH: 半字操作
STR:
STR 指令用于从源寄存器中将一个 32 位的字数据传送到存储器中。该指令在程序设计中比较常用,且寻址方式灵活多样,使用方式可参考指令 LDR。
指令的格式为:STR{条件} 源寄存器,<存储器地址>
STRB: 字节操作,从源寄存器中将一个 8 位的字节数据传送到存储器中。该字节数据为源寄存器中的低 8 位。
STRH: 半字操作,从源寄存器中将一个 16 位的半字数据传送到存储器中。该半字数据为源寄存器中的低 16 位。
压栈和出栈 PUSH 和 POP
PUSH :
压栈,将寄存器中的内容,保存到堆栈指针指向的内存上面,将寄存器列表存入栈中。
PUSH < reg list >
POP :
出栈,从栈中恢复寄存器列表
POP < reg list >
以 M3 内核来举个例子:
假设当前 MSP 值为 0x2000 2480;
寄存器 R0 的值为 0x3434 3434
寄存器 R1 的值为 0x0000 1212
寄存器 R2 的值为 0x0000 0000
执行push {R0, R1,R2}
之后,
内存地址的数据为:
0x2000 2474 的值为: 0x3434 3434 (R0 的值)
0x2000 2478 的值为: 0x0000 1212 (R1 的值)
0x2000 247C 的值为: 0x0000 0000 (R2 的值)
MSP 的值变成 0x2000 2474
高位寄存器保存到高地址,先入栈,如果是 POP,数据先出到低位寄存器。
跳转指令 B 和 BL
B :
ARM 处理器将立即跳转到指定的目标地址,不再返回原地址。
B 指令的格式为:B{条件} 目标地址
注意存储在跳转指令中的实际值是相对当前 PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算。
BL :
BL 跳转指令,在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,是子程序调用的常用的方法。
BLX:
该跳转指令是当子程序使用 Thumb 指令集,而调用者使用 ARM 指令集时使用。
BLX 指令从 ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态有 ARM 状态切换到 Thumb 状态,该指令同时将 PC 的当前内容保存到寄存器 R14 中。
BX:
BX 指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是 ARM 指令,也可以是 Thumb 指令。
算数运算指令
算数运算指令和下面的逻辑运算指令表格摘自《【正点原子】I.MX6U 嵌入式 Linux 驱动开发指南》:
逻辑运算指令
三、代码反汇编简析
汇编
汇编文件转换为目标文件(里面是机器码,机器码是给 CPU 使用的,烧录保存在 Flash 空间的就是机器码)。
反汇编
可执行文件(目标文件,里面是机器码),转换为汇编文件。
3.1 不同编译器的反汇编
3.1.1 Keil 下面生成反汇编文件
fromelf –text -a -c –output=(改成你想生成的反汇编名字一般是工程名字).dis (需要的 axf 文件,根据你工程生成 axf 的路径填写).axf
设置好以后编译之后就会生成反汇编.dis 文件:
打开如下所示:
对于上图中的红色圈出来的语句,我们可以根据本文 第 二 章节的第 2 小节 ARM 汇编格式中的介绍来分析一下:
简单分析如下(立即数就不分析了= =!):
3.1.2 gcc 下生成反汇编文件
在 X86 架构下的电脑上生成 ARM 架构的汇编代码有两种方式:
使用交叉编译工具链 指定-S 选项可以生成汇编中间文件。ex:gcc -S test.c
使用 objdump 反汇编 arm 二进制文件。
上述两种方法的区别为:
(1)反汇编可以生成 ARM 指令操作码,-S 生成的汇编没有指令码(2)反汇编的代码是经过编译器优化过的。(3)反汇编代码量很大。
对于 ARM Cortex-M,使用的是 arm-none-eabi-objdump,常用指令如下:
arm-none-eabi-objdump -d -S(可省) a1.o 查看 a1.o 反汇编可执行段代码
arm-none-eabi-objdump -D -S(可省) a1.o 查看 a1.o 反汇编所有段代码
arm-none-eabi-objdump -D -b binary -m arm ab.bin 查看 ab.bin 反汇编所有代码段
对于使用 arm-none-eabi-gcc 工具链(以 STM32CUbeMX)的内核来说,使用如下方式生成反汇编文件:
$(OBJDUMP) -D -b binary -m arm (需要的 elf 文件,一般是工程名字).elf > (改成你想生成的反汇编名字,一般是工程名字).dis # OBJDUMP = arm-none-eabi-objdump
-D 表示对全部文件进行反汇编,-b 表示二进制,-m 表示指令集架构
Makefile 修改如下:
执行 make dis 即可生成 .dis 文件:
打开文件查看,发现怎么这个汇编语言有点不一样:
经过研究了一段时间,加上了-M force-thumb
后稍微有点样子了:
在网上有各种参考,但是我都测试过了,并没有找到合适的生成完全和标准汇编一致的那种,-M 后面的参数也不能乱加,需要根据自己的交叉编译器,因为这里用的是 arm-none-eabi-gcc,所以可以通过arm-none-eabi-objdump --help
查看能用的命令和参数:
gcc 工具链下的汇编还是不太熟悉,所以我们下面反汇编文件与 C 语言的对比,使用 Keil 下的反汇编进行说明。
3.2 C 和 汇编 比较分析
前面介绍了那么多,最终用一个简单的程序对比一下 C 语言反汇编后的汇编语言,加深一下印象,当作个实战总结。
基于 STM32L051(Cortex-M0)内核,目的是为了比较 C 和汇编,用了个最简单的程序来分析,没有用到任务外设,程序如下:
反汇编的代码对应部分如下(因为基于硬件平台,其他异常中断,堆,栈,包括其他一些也有汇编代码,这里省略):
3.2.1 MOV 后面 立即数的疑问
在对比分析这段代码前,在 main 函数中的第一句:
就有一个大大的疑问, MOV r4,#0x3039
中 0x3039 并不是立即数(按照我们第二章 立即数的说明) ,包括接下来的 0xb26e 也不是立即数,怎么可以直接用 mov,按理来说需要用 LDR 伪指令的??
至于这个问题,网上简单查找了一下,找到一篇有关说明的文章:ARM 汇编的mov操作立即数的疑问 其中有说到,在 keil 公司方网站里关于 arm 汇编的说明里有这么一段:
Syntax
MOV{cond} Rd, #imm16
where: imm16 is any value in the range 0-65535.
所以是不是在 Keil 中的 arm 汇编 立即数可以使 16 位的?
为了验证一下,我稍微修改了一下程序,就是把 a 的值赋值超过 16 位(当然定义函数之类的也要跟着改,测试代码中 a 为 u16 的无符号整形),测试了一下。
a 赋值为 65535,结果如下(65535 不是立即数,也可以直接 mov):
a 赋值为 65536,结果如下(65536 是立即数,可以直接 mov):
a 赋值为一个大于 16 位的,不是立即数的数,比如:0x1FFFF:
果然,最后当 a 大于 16 位,不是立即数时候,会使用伪指令 LDR,所以我们可以得出结论:
在 Keil 中的 arm 汇编中,16 位内(包括 16 位)的数都直接使用 MOV 赋值,大于 16 位,如果是立即数,直接使用 MOV,不是立即数用 LDR (立即数的判断方式还是前面讲的那样)
3.2.2 反汇编文件解析
对于上面的示例程序的汇编码,简单解析如下:
添加一个有意思的测试对于delay
函数中的语句,上图是while(count--);
改成while(--count);
后汇编代码如下:
对于上面的测试程序,汇编中并没有使用到 PUSH 和 POP 指令,因为程序太简单了,不需要使用到栈,为了能够熟悉下单片机中必须且经常需要用到的 栈,我们稍微修改一下add
函数,在 add 函数中调用了delay
函数:
对于的 add 函数汇编代码如下:
(汇编中可以看到指令后面后面加了个 S ,MOVS 、ADDS,这就是我们前面说到的,带了 S 会影响 xPSR 寄存器中的值)
可以看到,因为存在函数的多次调用,main
函数中调用add
函数,add
函数中调用delay
函数,所以在 add 函数运行之前,通过 push
把 r4,r5,lr 寄存器的值先存入栈中,等待程序执行完(函数调用结束)再吧 r4,r5,lr 寄存器的值恢复。
上面的程序虽然简单,但是通过我们 C 程序 与 汇编程序的对比分析,能够让我们更加深入的理解汇编语言。
结语
文章内容真的是有点多,即便我再次回顾一下,觉得都应该分一个专栏慢慢讲解的。
如果后面有机会,我会把本文的每一小节细分的单独来说明。 ...... by 矜辰所致
版权声明: 本文为 InfoQ 作者【矜辰所致】的原创文章。
原文链接:【http://xie.infoq.cn/article/cfefa0a0ebfdf48d948978d8a】。文章转载请联系作者。
评论 (1 条评论)