像ARM7TDMI这样经典的ARM处理器会按照程序的顺序来执行指令或访问数据。而最新的ARM处理器会对执行指令和访问数据的顺序进行优化。举个例子,ARM v6/v7的处理器会对以下指令顺序进行优化。
LDR r0,[r1] ; 从普通/可Cache的内存中读取,并导致cache未命中
STR r2,[r3] ; 写入普通/不可Cache的内存
假设第一条LDR指令导致Cache未命中,这样Cache就会填充行,这个动作一般会占用好几个时钟周期的时间。经典的ARM处理器(带Cache的),比如ARM926EJ-S会等待这个动作完成,再执行下一条STR指令。而ARM v6/v7处理器会识别出下一条指令(STR)并不需要等待第一条指令(LDR)完成(并不依赖于r0的值),于是就会先执行STR指令,而不是等待LDR指令完成。
在有些情况下,类似上面提到的这种推测读取或者乱序执行的处理器优化并不是我们所期望的,因为可能使程序不按我们的预期执行。在这种情况下,就有必要在需要严格的、“类经典ARM”行为的程序中插入内存隔离指令。ARM提供了3种内存隔离指令。简单起见,以下的描述都是在单处理器环境下。
l数据同步隔离(DSB):等待所有在DSB指令之前的指令完成。
l数据内存隔离(DMB):在DMB之后的显示的内存访问执行前,保证所有在DMB指令之前的内存访问完成。
l指令同步隔离(ISB):清理(flush)流水线,使得所有ISB之后执行的指令都是从cache或内存中获得的(而不是流水线中的,也就是说等待流水线中所有指令执行完之后才执行ISB之后的指令,译注)。
需要注意,ARMv6中的CP15等价隔离指令在ARM v7中是弃用的。因此,可能的话,建议任何使用这些指令的代码应该改用以上3条新的隔离指令。
互斥量
以下情况软件必须使用DMB:
l在请求资源期间,比如通过锁定一个互斥量或减少信号量以及任何形式的对资源的访问。
l在使资源可用之前,比如通过解锁一个互斥量或增加信号量。
下面是一个阻塞互斥量的实现例子
llock_mutex请求一个互斥量并阻塞直到请求到资源。如果阻塞了,在重试之前它会等待事件唤醒(通过WFE)。
lunlock_mutex释放一个互斥量并发送一个事件来通知等待的“进程”。
LOCKED EQU1
UNLOCKED EQU 0
lock_mutex
; 互斥量是否锁定?
LDREX r1,[r0]; 检查是否锁定
CMP r1,#LOCKED; 和"locked"比较
WFEEQ; 互斥量已经锁定,进入休眠
BEQlock_mutex; 被唤醒,重新检查互斥量是否锁定
; 尝试锁定互斥量
MOV r1, #LOCKED
STREX r2, r1,[r0];尝试锁定
CMP r2,#0x0; 检查STR指令是否完成
BNElock_mutex; 如果失败,重试
DMB; 进入被保护的资源前需要隔离,保证互斥量已经被更新
BX lr
unlock_mutex
DMB;保证资源的访问已经结束
MOV r1,#UNLOCKED; 向锁定域写"unlocked"
STR r1, [r0]
DSB; 保证在CPU唤醒前完成互斥量状态更新
SE V;像其他CPU发送事件,唤醒任何等待事件的CPU
BX lr
DSB指令保证了在发送事件之前,同步变量已经被更新了。
内存重映射
当复位服务程序或启动代码在flash(ROM)中,它们被映射在地址0x0处来保证程序能正确地从向量表中启动,通常它们驻留在内存的底部。如下图左侧所示。
在系统初始化之后,你可能希望关闭flash的映射,这样就可以将底部的位置给RAM使用(如上图右侧)。下面的代码(永远在Flash中运行)在运行内存复制例程将一些数据复制到内存底部(RAM)前关闭了flash的映射。
MOV r0, #0
MOV r1,#REMAP_REG
STR r0,[r1]; 关闭flash映射
DMB; 保证STR完成
BLblock_copy_routine(); 复制代码到RAM
ISB; 保证流水线清空
BLcopied_routine(); 运行复制后的代码 (RAM中)
如果没有STR和BL中间的DMB指令,就不能保证这条STR指令在复制代码到底部内存之前已经完成,因为复制例程可能会在通过STR写入的数据还在写缓冲(Write Buffer)中时就运行。DMB指令强迫所有DMB之前的数据访问完成。而ISB指令防止了在复制代码结束之前就从RAM中取指令。
中断
下面的框图表示了包含有基于通用中断控制器(GIC)的系统结构。当中断控制器检测到中断发生时,它会发送nIRQ信号给处理器。这会触发一系列事件包括处理器运行中断处理例程,并且屏蔽IRQ中断源(忽略接着到来的nIRQ的变化)。
下面的中断服务例程通过读取中断确认寄存器(IAR)来确认中断。例程不仅返回挂起中最高优先级的中断ID,还告诉中断控制器解除nIRQ的信号。在这之后,中断服务例程需要重新使能中断,以使更高等级的中断能抢占当前的中断。
interrupt_handler
; ...
LDR r0, =GIC_CPUIF_BASE; 获得中断控制器基地址
LDR r1, [r0,#0x0c]; 读取IAR,同时解除nIRQ信号
DSB;确保内存访问结束,并且没有其他的指令运行
; 继续运行之前
CPSIEi; 重新允许中断
; ...
RFE sp!;从服务程序返回
在这个过程中,正确的操作需要在CPSIE使能之前完成IAR的读取。因为这两条指令之间没有数据依赖,所以CPU会在LDR完成之前就执行CPSIE。这会导致处理器再次响应相同的中断(中断允许,且nIRQ没有解除,译注)。因此应该在这两条命令中插入一条DSB指令。
自修改代码
自修改代码必须在ISB之后运行,因为内核流水线中可能包含过期的指令。
下面的例子演示了一段将ROM中的代码搬运到RAM中,并跳转过去执行的代码。
Overlay_manager
; ...
BLblock_copy; 将新例程从ROM复制到RAM
Brelocated_code; 跳转到新例程
如果你使能了分支预测,并且像上面的例子一样重定位代码,处理器会预测到第二条分支指令即将执行,然后从其指示例程中取指。这就会导致处理器运行旧的例程。如果复制例程和跳转到新例程的分支指令离得很近,这样的问题就会发生。
为了确保这样的优化不要发生,你必须在新的重定位过的代码运行前插入一条ISB指令,以此来保证预取指缓冲在处理器重新取指前已经被清理:
Overlay_manager
; ...
BLblock_copy; 将新例程从ROM复制到RAM
ISB; 保证流水线被清理
Brelocated_code; 跳转到新例程
如果你正在复制的内存被设置为了“写回型可cache的”,那么你应该清理(clean)cache以保证数据已经写入到了主存中。并且,指令cache应该被设置为无效,这样处理器就不会执行其他“被cache”的指令了。
Overlay_manager
; ...
BLblock_copy; 将新例程从ROM复制到RAM
DMB; 保证内存访问结束
data_cache_clean; 清理cache保证新例程已经被写入RAM
instruction_cache_invalidate; 将指令cache设置为无效,这样旧指令就不会再被cache
DSB; 在内存访问前,清理并使cache无效
ISB; 保证流水线被清理
Brelocated_code; 跳转到新例程
类似的需要隔离的地方:
lJIT(Just-In-Time)编译器,比方说将Jazelle字节码转换为ARM代码。
lPost链接器或加载器,在运行时将代码重定位到内存中。