ARM核学习(三)指令流水线分析及伪指令


指令流水线分析

前言

在ARM核中,为增加处理器指令流的速度,ARM7系列使用3级流水线。允许多个操作同时处理,而非顺
序执行。不同的ARM核,流水线的级数是不一样的,ARM核版本越高,流水线级数越多。对于软件工
程师编程而言,统一按照三级流水线来分析就可以了。

image-20241124174742518

在MDK中断点调试时会发现PC指向的当前代码行,这是因为MDK调试屏蔽了底层流水线细节,以避免产生歧义。

在ARM状态,指令位宽为32位4字节,因此当PC预取(例如)0x8地址的指令时,0x4地址的指令正在被解码、0x0地址的指令正在被执行。Thumb状态是压缩格式的指令集,指令位宽是16位2字节,因此偏差是2。

最佳流水线分析(仅操作寄存器)

image-20241124175951254

内存访问指令流水线

image-20241124180204732

由于内存访问效率较低,因此在执行LDR指令时,无法在一个指令周期内完成(需要E、M、W三个周期)。这样,即使它前面的D跑得快,但解码的指令无法移交给E(它手上的活还没干完),只能干等,F也是一样。

分支(跳转)流水线举例

image-20241124181209542

在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超前完成的工作只能被迫丢弃,转而从新的指令地址重新开始干活,且如果跳转回来,之前干完的活还得重新再干一次。

常用伪指令分析

image-20241124183925641

LDR R0,=0x12345678 分析

image-20241124184043373

image-20241124184314696

编译器会将常量 0x12345678预先放到某个内存中(通常是所有代码指令之后),并将LDR翻译成一条读内存的指令。

LDR R0,=label 分析

标签本质上是一个常量,它的值就是标签后第一条指令的地址,可以理解为给这个地址取了个别名。

代码段起始地址为0x0

对于给定的代码段起始地址和指令位宽,每条指令的存放地址在编译阶段都可以确定,如下将代码段的起始地址配置为 0x0

image-20241124185737443

对于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)分析如下:

image-20241124190511217

image-20241124190550278

ldr r4,[r3]:从R3对应的地址读取数据(该数据 0x12345678 在编译阶段通过 .word伪指令预先存储在了内存空间中)

image-20241124190809542

代码段起始地址为0x2000

将代码段起始地址改为0x2000,计算规则是一样的,在编译阶段就能确定各个指令以及 .word数据存放的地址(注意修改后需要重新编译下):

image-20241124200510540

调试前需要修改下内存映射,默认是从0x0开始执行的,我们需要让0x0到0x2000及其后的一段内存都可执行:

image-20241124200751229

然后在 _start的第一条指令打个断点,跳过从0x0到0x2000之间的无效指令:

image-20241124201005483 image-20241124200325028

总结

image-20241124195231017

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=pR3=*p的关系

image-20241124201244164

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	
		

image-20241124202620567

对比 LDR R0,=label

如下图,LDR R0,=label编译时,编译器会根据代码段起始地址和指令位宽计算出label对应的地址值(0x00000014),并单独开辟一个内存(0x00000018)存放这个写死的数据,在运行时通过LDR读取这个内存中写死的label地址值到R3中。

如果起始地址为 0x2000,那么编译器就会在内存中存放一个写死的 0x2014,显然这是一种静态的做法(和编译时设置的代码段起始地址是强绑定的),如果程序换一个地址空间运行,这个写死的地址值就没有意义了。

image-20241124203311920

ADR R0,label则是运行时动态获取标签对应的地址值,例如编译器知道执行当前指令时,通过在PC上偏移4个字节就能寻址到label对应的地址,那么通过 ADD指令就能在运行时动态获取该label对应的地址。这种做法是不依赖编译时设定的代码段起始地址的,无论使用什么起始地址,都能够得到一个正确的标签地址

image-20241124204230866

🌟如何判别代码在实际内存中运行的地址?

image-20241124204803253


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录