指令流水线分析
前言
在ARM核中,为增加处理器指令流的速度,ARM7系列使用3级流水线。允许多个操作同时处理,而非顺
序执行。不同的ARM核,流水线的级数是不一样的,ARM核版本越高,流水线级数越多。对于软件工
程师编程而言,统一按照三级流水线来分析就可以了。
在MDK中断点调试时会发现PC指向的当前代码行,这是因为MDK调试屏蔽了底层流水线细节,以避免产生歧义。
在ARM状态,指令位宽为32位4字节,因此当PC预取(例如)0x8地址的指令时,0x4地址的指令正在被解码、0x0地址的指令正在被执行。Thumb状态是压缩格式的指令集,指令位宽是16位2字节,因此偏差是2。
最佳流水线分析(仅操作寄存器)
内存访问指令流水线
由于内存访问效率较低,因此在执行LDR指令时,无法在一个指令周期内完成(需要E、M、W三个周期)。这样,即使它前面的D跑得快,但解码的指令无法移交给E(它手上的活还没干完),只能干等,F也是一样。
分支(跳转)流水线举例
在E执行跳转指令时,跑在它前面的F、D不得不丢弃已完成的工(解码 0x3004 SUB
,预取 0x3008 AND
)作,转而从目标地址重新开始F、D、E流程。
图中E执行 BL 0X8FEC
时,F正在预取 0x3008
地址的指令,在该指令周期结束时,PC指向 0x3008
,且由于是 BL
跳转,因此该值会被保存到 LR
寄存器中。
当执行 L
指令时,会将LR
回写到 PC
中,但是由于此前对于 0X3004
的解码工作被丢弃了,因此处理器会自动执行一个 A
指令,将 PC
调整到 0x3004
的位置接着执行。
总结
- 当只有寄存器操作(操作效率和CPU处于同一量级)时,指令流线效率是最高的:在每个指令周期,流水线上的三个工人F、D、E都在不停互相配合,高效完成指令执行所需的三个阶段的工作。
- 当访问内存(外设)时,由于物理特性,处理器的工作效率远高于外设,因此工人E只能放慢节奏和外设协同把活干了,这期间即使F、D完成了手头上的活,由于不能向D、E移交,因此也只能干等着。导致了级联停顿(stall)的效应。
- 当发生跳转时,工人F、D超前完成的工作只能被迫丢弃,转而从新的指令地址重新开始干活,且如果跳转回来,之前干完的活还得重新再干一次。
常用伪指令分析
LDR R0,=0x12345678 分析
编译器会将常量 0x12345678
预先放到某个内存中(通常是所有代码指令之后),并将LDR翻译成一条读内存的指令。
LDR R0,=label 分析
标签本质上是一个常量,它的值就是标签后第一条指令的地址,可以理解为给这个地址取了个别名。
代码段起始地址为0x0
对于给定的代码段起始地址和指令位宽,每条指令的存放地址在编译阶段都可以确定,如下将代码段的起始地址配置为 0x0
:
对于ARM状态下(指令位宽为4字节),如下代码的每条指令都可以顺序计算其存储地址
.global _start
_start:
mov r1,#1 @0x00000000
mov r2,#2 @0x00000004
ldr r3,=addr @0x00000008
ldr r4,[r3] @0x0000000C
stop:
b stop @0x00000010
addr: @给0x00000014取了个别名叫addr
.word 0x12345678 @0x00000014 在这个地址存放一个字的数据
需要注意的是:标签的声明并不属于指令,因此不占用地址空间。其中 ldr r3,=addr
等价于 ldr r3,=0x00000014
。
ldr r3,=addr
(将addr表示的地址值加载到R3)分析如下:
ldr r4,[r3]
:从R3对应的地址读取数据(该数据 0x12345678
在编译阶段通过 .word
伪指令预先存储在了内存空间中)
代码段起始地址为0x2000
将代码段起始地址改为0x2000,计算规则是一样的,在编译阶段就能确定各个指令以及 .word
数据存放的地址(注意修改后需要重新编译下):
调试前需要修改下内存映射,默认是从0x0开始执行的,我们需要让0x0到0x2000及其后的一段内存都可执行:
然后在 _start
的第一条指令打个断点,跳过从0x0到0x2000之间的无效指令:
总结
LDR R0,label 分析
.global _start
_start:
mov r1,#1
mov r2,#2
ldr r3,addr
mov r4,r3
stop:
b stop
addr:
.word 0x12345678
ldr r3,=addr
可以理解为将标签 addr
的值当做常量加载到R3中,而 ldr r3,addr
则是从 addr
对应的地址加载数据到R3中,相当于 R3=p
和 R3=*p
的关系
ADR R0,label 动态获取标签地址
This instruction adds an immediate value to the PC value to form a PC-relative address, and writes the result to the destination register.
ADR 是 “Address” 的缩写,它是一条用于生成地址的伪指令。ADR 指令用于计算一个目标地址,并将其存储到指定的寄存器中。目标地址是基于当前 PC 加上一个偏移量计算得出的,因此它只能访问当前代码段附近的地址。
指令格式
ADR <Rd>, <label>
<Rd>
:目标寄存器,用于存储计算出的地址。<label>
:目标地址标签,必须位于当前指令前后一定范围内。
示例
.global _start
_start:
mov r1,#1
mov r2,#2
adr r3,addr
ldr r4,[r3]
stop:
b stop
addr:
.word 0x12345678
对比 LDR R0,=label
如下图,LDR R0,=label
编译时,编译器会根据代码段起始地址和指令位宽计算出label对应的地址值(0x00000014
),并单独开辟一个内存(0x00000018
)存放这个写死的数据,在运行时通过LDR读取这个内存中写死的label地址值到R3中。
如果起始地址为 0x2000
,那么编译器就会在内存中存放一个写死的 0x2014
,显然这是一种静态的做法(和编译时设置的代码段起始地址是强绑定的),如果程序换一个地址空间运行,这个写死的地址值就没有意义了。
而 ADR R0,label
则是运行时动态获取标签对应的地址值,例如编译器知道执行当前指令时,通过在PC上偏移4个字节就能寻址到label对应的地址,那么通过 ADD
指令就能在运行时动态获取该label对应的地址。这种做法是不依赖编译时设定的代码段起始地址的,无论使用什么起始地址,都能够得到一个正确的标签地址。