标题:浅谈模拟环境下JIT技术的实现(ARM部分)
作者:LOVEHINA-AVC
类型:跨平台应用
对应级别:中级
声明:没有什么特别的声明,转载时注明作者及出处就可以了

  JIT,即Just In Time,在程序设计领域,它指的是一种将字节码即时编译为本地代码的技术。本文所说的JIT并非我们熟知的将平台无关代码编译成平台相关代码的技术,实际上,它是一种变相JIT的实现,其本质内容是将平台相关的代码转译到另外一个平台上运行,该技术适合RISC到RISC及RISC到CISC的转换。为什么说基于RISC架构的指令集适合JIT?原因只有一个——RISC架构要求CPU指令拥有统一的长度,我们可以在预处理期间就正确的反汇编所有指令的内容。这一点是很重要的,作为对比,我们来看看CISC——它的指令长度可变,并且没有边界对齐要求,这使得反汇编器无法精确的反汇编每一条指令,假如我们在代码段中插入一些数据,或者干脆使用几条花指令,一个再聪明的反汇编器也可能产生错误的反汇编代码。
JIT首先要求译码器能够正确的反汇编并翻译目标代码,例如,一个典型的ARM满降序栈寄存器保护指令
stmfd sp!, {r0-r12, lr}
将被转译为以下本地代码:
Arm32Context STRUCT
 r0  dd ?
 r1  dd ?
 r2  dd ?
 r3  dd ?
 r4  dd ?
 r5  dd ?
 r6  dd ?
 r7  dd ?
 r8  dd ?
 r9  dd ?
 r10  dd ?
 r11  dd ?
 r12  dd ?
 r13  dd ?
 r14  dd ?
 pc  dd ?
 cpsr  dd ?
 spsr_svc dd ?
 spsr_irq dd ?
 spsr_abt dd ?
 spsr_und dd ?
 spsr_fiq dd ?
 r8_fiq  dd ?
 r9_fiq  dd ?
 r10_fiq  dd ?
 r11_fiq  dd ?
 r12_fiq  dd ?
 r13_fiq  dd ?
 r14_fiq  dd ?
 r13_svc  dd ?
 r14_svc  dd ?
 r13_irq  dd ?
 r14_irq  dd ?
 r13_abt  dd ?
 r14_abt  dd ?
 r13_und  dd ?
 r14_und  dd ?
ENDS
 push edi
 lea edx,[esi + Arm32Context.r12] ;esi = context
 mov edi,[esi + Arm32Context.r13]
 mov eax,[esi + Arm32Context.r14]
 sub edi,4
 mov ecx,13
 mov [edi],eax
 mov [esi + Arm32Context.r0],ebx ;如果使用了临时寄存器来优化模拟指令的存取速度(此处为ebx=r0, edi=cpsr),它们所包含的内容就必须被先行写回目标CPU的上下文存储单元
0:
 sub edi,4
 mov eax,[edx]
 sub edx,4
 mov [edi],eax
 dec ecx
 jnz <0
 mov [esi + Arm32Context.r13],edi
 add [esi + Arm32Context.pc],4
 pop edi
注意:如果目标指令的寄存器列表不连续,并且拥有2个以上的不连续区段,则应当将压栈循环分割成各个独立的mov指令组以提高执行效率。对此译码器必须具备一定的识别能力,并根据寄存器列表的内容来决定使用何种本地代码生成策略。最终生成的本地代码将可能被加载到任意基址的内存空间当中,因此所有对外部地址的引用(call [mem]、mov r32,[mem]等)都必须加以修正(类似于DLL的IAT处理),否则这些代码将无法正常执行。对于ARM特权级状态转换,相关的代码应当被插入到任何一个试图改变cpsr寄存器内容的本地指令组当中。一个常见的做法是置换影子寄存器组与当前寄存器的值,并设置相应的模拟处理器状态标志。

  32位ARM处理器支持ARM与Thumb两种长度不同的指令集,并通过cpsr中的某个位域来决定当前的执行模式。在转译阶段我们无从得知何处为ARM指令,何处为Thumb指令,因此译码器必须生成两份不同的指令翻译,一份完全以ARM模式翻译,另一份完全以THUMB模式翻译。译码器可以选择对无效指令不敏感,并为每一条无效指令生成一组异常响应代码(最节省空间的如int 3中断),这样即使执行到了无效指令也可以进入相应的异常处理模块。通常情况下,当使用bx/blx指令间接寻址,且目标地址的低位被置位时,处理器将切换到Thumb模式执行,否则切换到ARM模式,这意味着任何一种模式都可以在不切换当前模式的情况下跳转到任意地址。我们需要四个地址映射表,分别对应ARM_BIOS、ARM_ROM、THUMB_BIOS和THUMB_ROM,表中的每一项都是目标指令对应的本地指令地址,以此来快速实现目标地址(虚拟地址)到本地地址(实际地址)的转换。
下面是ARM/Thumb指令bx r0转译后的代码:
 mov eax,[esi + Arm32Context.r0] ;esi = context
 jmp BranchExchangeProc
BranchExchangeProc: ;公共部分
 test eax,1
 jnz >0
ExecutingInArmMode:
 
 cmp eax,0x8000000 ;假设ROM加载于0x8000000,若目标地址低于该值,则接下来引用的地址映射表将是BIOS(FLASH1)而不是ROM(FLASH2)
 jae >1
 mov edx,[ArmAddressMappingTableForBios]
 jmp >2
1:
 mov edx,[ArmAddressMappingTableForRom]
2:
 and eax,0xFFFFFF
 jmp [edx + eax]
ExecutingInThumbMode:
0:
 dec eax
 cmp eax,0x8000000
 jae >3
 mov edx,[ThumbAddressMappingTableForBios]
 jmp >4
3:
 mov edx,[ThumbAddressMappingTableForRom]
4:
 and eax,0xFFFFFF
 jmp [edx + eax * 2] ;Thumb的指令长度为16bits,因此需要的映射地址量为ARM模式的2倍

关于模拟地址空间的问题:
  与传统的x86不同,ARM仅通过映射内存地址的方式来访问外围设备的控制寄存器及通信端口等资源。FLASH1、FLASH2和RAM的基址各不相同,且RAM可以含有多个BANK组,每个BANK都被映射到不同的地址上面,这就要求模拟器必须拥有辨识地址范围的功能,以便将特定区域的内存访问重新映射到实际分配的地址当中,并根据需要转换成对模拟部件的行为控制。为了达成这一目的,模拟器通常要对目标指令访问的内存空间做多次确认,这意味着任何一个对托管内存的访问都要被分析、重定位,这会大大降低一些指令的模拟执行效率。在此我推荐一个更为行之有效的方法来管理内存访问——利用虚拟内存机制模拟目标平台的平坦内存模型。例如,以下对VirtualAlloc的调用将保留256Mbytes的地址空间:
 invoke VirtualAlloc, NULL, 10000000h, MEM_RESERVE, PAGE_NOACCESS
由此得到的内存总基址,将被视为在装载本地转译代码时对引用地址的修正基准。接下来我们可以在这个地址上分配提交的内存页,并根据目标平台的硬件约定在合适的地址上载入BIOS(FLASH1)和ROM(FLASH2)数据。由于先前对保留页面赋予了PAGE_NOACCESS属性,任何对无效区域的访问都会产生一个线程级的GPE(一般保护异常),我们可以在预先安装的SEH(结构化异常处理)中得到该异常的详细信息,并根据引用内存的地址做进一步分类处理。

一些优化方案:

常量引用分析:
  如果某条目标指令以寄存器间接寻址的方式访问内存区域(或执行bx跳转),在译码期间就可以向前搜索对该寄存器进行立即数赋值的指令,如mov r0, #imm、ldr r0, =imm(引用的字常量必须位于只读代码段),并实行寄存器内容预测,以免去辨识地址范围的开销,提高本地代码的执行效率。如果能够找到匹配项,且匹配指令与当前指令之间不存在任何跳转及可能改变该寄存器内容的指令,则寄存器的内容是能够被预测的,对相关访问区域的判断可以在本地转译代码生成阶段被预先处理。
寄存器缓冲:
  经常被访问的模拟寄存器,如r0~r3、cpsr等,应当在本地寄存器中建立长期有效的副本。但x86仅有7个通用寄存器,显然无法满足需求,我们仅能退而求其次,将使用频率最高的r0与cpsr映射到本地寄存器当中。在实行这一优化策略时,有一点是需要注意的——不要使用eax和edx,ebp也最好不要使用。这是因为一些x86指令的操作数限定为eax与edx(典型的如mul/div),而ebp在作为操作数时产生的指令有时要比其它操作数更长(虽然并不会消耗额外的时钟周期)。
寄存器合并与传播优化:
  这是一种在一串连续目标指令(可包含立即数寻址跳转)当中求出某个或数个模拟寄存器的最大作用域、期间不将临时寄存器的内容写回CPU上下文的优化方式。