JPCSP源码解读15:动态二进制翻译3(翻译引擎最终章)

今天,我们从CodeInstruction. compile(CompilerContextcontext, MethodVisitor mv)这个函数说起。

其中,CompilerContext是编译时刻的现场,比如当前正在编译哪个函数,当前正在编译哪条指令,等等这样的信息。在编译某些指令时,需要知道这些信息。举例说,我们正在处理一条分支指令,那么需要从编译时刻上下文查询当前正在编译的codeBlock,以便取得分支目标位置的指令标号。

MethodVisitor用于书写java字节码。Jpcsp的翻译引擎为每个函数生成一个类,然后生成这个类的一个exec方法,这样运行时刻可以用翻译得到的exec方法来代替原有的mips编码的函数。MethodVisitor正是在书写这个exec方法。

这个函数很短,全部贴出来:

public voidcompile(CompilerContext context, MethodVisitor mv)

{

   startCompile(context,mv);//将contex的当前指令置为这个指令

  

   if (isBranching()) {                        //分支指令

          compileBranch(context,mv);         //其中调用getBranchingOpcode,这里有路径处理了延迟槽中的指令

   }

   else if (insn == Instructions.JR) {       //跳转指令

          compileJr(context, mv);

   }

   else if (insn == Instructions.JALR) {     //跳转并链接

            compileJalr(context, mv);

   }

   else {

          insn.compile(context,getOpcode());//非跳转指令

   }

 

   context.endInstruction();

}

 

首先,startCompile(context, mv),在编译上下文中记录当前指令,该函数中的核心语句:

   context.setCodeInstruction(this);

   if (hasLabel()) {

       mv.visitLabel(getLabel());//如果该指令有标号,先在字节码中记录标号

   }

然后,可以看到,对branch,jr,jalr这三类指令做了特殊处理,其他指令则直接调用insn.compile(context, getOpcode())去生成代码了。这里的其他指令,通常就是功能性的指令,其compile方法实现相应(运算)功能即可。

注意,在jpcsp中,jump和jal指令都作为branch指令处理,jr和jalr则另行处理。

在mips指令集中,分支指令有很多种,包括带likely的,和带link的。分支指令是相对当前位置做跳转。带likely的,如果分支发生,则延迟槽中指令有效,否则延迟槽中指令要无效掉。Link就是链接,这样的分支指令是保存了返回地址的,返回地址保存在31号寄存器(ra)中。

Branch指令的处理流程是,加载分支条件相关的寄存器,然后编译延迟槽指令,之后再加一个条件跳转。

来看compileBranch函数:

private voidcompileBranch(CompilerContext context, MethodVisitor mv)

{

   int branchingOpcode = getBranchingOpcode(context, mv);

   if (branchingOpcode != Opcodes.NOP)

   {           

       CodeInstructionbranchingToCodeInstruction = context.getCodeBlock().getCodeInstruction(getBranchingTo());

       if (branchingToCodeInstruction != null)  

       {               

          context.visitJump(branchingOpcode,branchingToCodeInstruction);

       }

       else

       {

          context.visitJump(branchingOpcode,getBranchingTo());

       }

    }

}

首先,调用了getBranchOpcode函数。该函数做的事情是,确定分支类型(将mips的分支指令转换为java字节码的分支指令,包括一些简单优化),为特定类型的分支指令,加载寄存器。然后,编译延迟槽指令,并返回一个java字节码形式的分支指令。注意,分支指令只是被识别并返回,而没有生成在exec函数中。生成操作在前述这个compileBranch函数中。

回到compileBranch函数,如果经getBranchOpcode函数优化过后,分支指令仍然需要生成(取得的branchOpcode不为NOP),就生成这条指令。这里先从当前codeBlock中查询分支的目标位置:

CodeInstructionbranchingToCodeInstruction =

context.getCodeBlock().getCodeInstruction(getBranchingTo())

如果能找到,说明是一个分支指令,生成这个分支指令即可,java字节码中的分支指令中,目标位置有标号作为参数:

context.visitJump(branchingOpcode,branchingToCodeInstruction);

注意传进去的是目标位置的codeInstruction,所以这个visitJump可以取得目标位置指令的标号。实际就是这样实现的,代码就不贴了。

如果找不到,说明是j或者jal指令,长跳转,跳转到了当前函数之外,应该是函数调用:

context.visitJump(branchingOpcode,getBranchingTo());

在这个visitJump函数中,传入的参数是目标位置的地址。此时生成的核心代码是一个函数调用指令,去调用RuntimeContext.jump:

mv.visitMethodInsn(Opcodes.INVOKESTATIC,runtimeContextInternalName, "jump", "(III)I");

RuntimeContext.jump会根据地址去查询(或编译)对应的函数(codeBlock),然后调用其exec方法。

这里有一个问题:j是不带链接的,jalr是带链接的。所以j的目标位置那个函数,其返回时,也意味着当前这个函数(包含j指令的这个函数)的返回。而Jalr返回,则表示返回到当前函数,也就是jalr指令的延迟槽之后的位置。

为了应对这个问题,在jpcsp中,为exec方法提供三个参数和一个返回值。注意,exec方法对应了一个mips函数,这个mips函数的传参和接收参数的过程都已经包含在mips汇编码中了,这些操作对应的指令也被翻译引擎逐条翻译。而现在说的exec方法的参数和返回值,是面向模拟器的运行时上下文。三个参数分别是:

Int returnAddress

Int alternativeReturnAddress

boolean isJump

其中前两个是返回地址。isJump表示,这个exec方法对应的函数,是通过一个不带链接的指令调用的。

bal指令允许调用的目标函数返回到当前函数的返回地址,jal不允许这样的更改,所以传入的返回地址有两个。

///

xsb:这个调用和传参过程在CompilerContext.java中:

public voidvisitCall(int address, int returnAddress, int returnRegister, booleanuseAltervativeReturnAddress)

该函数传递两个返回地址的顺序似乎反了,本函数原来的返回地址应该是多出来的备选项,延迟槽之后的地址才是本该使用的选项。好在使用上应该没影响,两个返回地址地位对等。

/

返回值是一个地址,表示下一步要跳转到哪里。

通常mips中从一个函数返回,是使用jr ra这样的语句。

对于一个长跳转,如果跳转的目标位置是返回地址,表示当前mips函数的返回,则exec方法返回这个跳转的目标地址,是返回到运行时上下文,运行时上下文中此时检查返回值,如果和最初传入的返回地址相等,表示一次mips函数的返回。

对于长跳转,如果其所处的exec方法接收的isJump为true,则表示这个函数是通过一个不带链接的跳转指令,调用了RuntimeContext.jump,然后在此处调用的。此时不论跳转的目标位置是哪里,只要不带链接,就可以直接返回自己期望的跳转地址了。返回到了运行时上下文,在运行时上下文中来判定下一步跳转的地址,是否是不带链接的跳转指令传入的期望的返回地址。如果是,则mips函数返回。如果不是,则在此处继续根据期望的目标地址,来调用exec方法。

///

这里的逻辑稍微有点乱了,需要重新梳理一下。

注意,下文中各个标识符中,带不同数字后缀表示同一个方法的不同实例,而不是表示不同方法。

初始时,不论通过何种方式,总之一个函数开始运行了。

函数中会遇到不带链接的长跳转指令,以及带链接的长跳转指令。

对于不带链接的长跳转指令(记为j_1),会调用RuntimeContex.jump,传的参数包括期望的目标位置,以及期望的返回地址。RuntimeContex.jump中有一个while循环(记为loop_1)。循环体中的内容是调用目标位置的exec_1方法。传入isJump为true,告知该方法是通过一个不带链接的长跳转指令(j_1)而被调用的。

对于带链接的长跳转指令(记为jalr_1),会调用RuntimeContex.call,该函数也是调用目标位置的exec_2方法,但是传入isJump为false,告知该方法是通过一个带链接的长跳转指令(jalr_1)而被调用。

于是,对于接收到isJump参数的exec方法,其中遇到不带链接的长跳转(记为j_2)时,根据isJump分两种情况:

1.如果isJump为true,表示这个exec_1方法是通过一个不带链接的长跳转(记为j_1)进入,所以此时该exec方法中遇到的不带链接长跳转(j_2),不论其跳转目标如何,都可以直接返回这个期望跳转到的目标位置。是返回到了前述的loop_1,在loop_1中判定期望跳转的目标位置是否是之前传入的期望的返回值。如果是,则表示一次mips函数的返回,要处理一下内存中的函数栈。如果不是,则表示继续跳转,也就是继续调用目标位置的exec_3方法,并传入isJump为true。

2.如果isJump为false,表示这个exec方法是通过一个带链接的长跳转(jalr_1)进入的,此时要在这个exec方法中检查(j_1的)目标位置是否是返回地址,如果是则返回(到达RuntimeContex.call中调用该exec_2的位置),然后进一步返回到最初的jal_1延迟槽之后的位置。如果j_1的目标职位不是返回地址,则调用RuntimeContex.jump

/

希望这个问题已经说清楚了。。。综上,对于不带链接的长跳转指令,其翻译逻辑应该是:

if(isJump)

   return jumpTargetAddress;

else if(jumpTargetAddress==returnAddreee)

   return jumpTargetAddress

else

   jumpTargetAddress= RuntimeContext.jump(jumpTargetAddress,returnAddress)

这个逻辑对应源码中的位置是CompileContext.visitJump()

/

前文描述了jpcsp中对分支指令,jr,jalr这三类指令的处理。

特别指出的是,j,jal,bal这三种指令都被包含在了CodeInstruction.compileBranch中处理。其中j的处理逻辑同jr,只是跳转的目标地址来源不同。jal的逻辑却是选择了和bal相同的逻辑,是直接生成了调用目标位置exec方法的java字节码。做的优化是,会先检查是否目标位置对应本地码,有对应则直接调用本地码。

xsb:这里似乎包含潜在的问题,因为对于跳转的目标位置,可能并不是已经编译好了,这样会导致运行时找不到期望的那个exec方法。jalr就不会有这样的问题,因为他把函数的调用交给了RuntimeContex,如果目标位置还没有编译,RuntimeContex调用的getExecutable方法会去呼叫编译器编译。也可能exec方法本身可以catch并处理目标位置没编译好的exception,对这种可能性并不看好。

总结:本文描述了jpcsp翻译引擎中,对于各种分支和跳转指令的处理逻辑。其他指令的处理逻辑都比较局部,没有和其他地方的复杂关联,就不做阐述了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值