找回密码
 立即注册
查看: 620|回复: 0

[笔记] Unity mono代码结构分析及阅读(六)——IL字节码解析与翻译

[复制链接]
发表于 2020-12-15 09:35 | 显示全部楼层 |阅读模式
这是基于unity mono代码阅读的第六篇。
上文已经大致分析了mono runtime的框架,本文用两个很具有代表性的opcode Add和Call来深入分析一下CIL指令在mono CLR内的解码和翻译到对应平台机器码的过程。
好了,让我们开始吧。
我们先从简单的add开始
    ldloc num1
    ldloc num2
    //求和
    add推荐阅读
我们用CEE_ADD搜索一下,果然一下子就让我们找到了。
                case CEE_ADD:
                case CEE_SUB:
                case CEE_DIV:
                case CEE_DIV_UN:
                case CEE_REM:
                case CEE_REM_UN:
                case CEE_AND:
                case CEE_OR:
                case CEE_XOR:
                case CEE_SHL:
                case CEE_SHR:
                case CEE_SHR_UN:
                        CHECK_STACK (2);

                        MONO_INST_NEW (cfg, ins, (*ip));
                        sp -= 2;
                        ins->sreg1 = sp [0]->dreg;
                        ins->sreg2 = sp [1]->dreg;
                        type_from_op (ins, sp [0], sp [1]);
                        CHECK_TYPE (ins);
                        ADD_WIDEN_OP (ins, sp [0], sp [1]);
                        ins->dreg = alloc_dreg ((cfg), (ins)->type);
                        /* FIXME: Pass opcode to is_inst_imm */
                        /* Use the immediate opcodes if possible */
                        if (((sp [1]->opcode == OP_ICONST) || (sp [1]->opcode == OP_I8CONST)) && mono_arch_is_inst_imm (sp [1]->opcode == OP_ICONST ? sp [1]->inst_c0 : sp [1]->inst_l)) {
                                int imm_opcode;

                                imm_opcode = mono_op_to_op_imm_noemul (ins->opcode);
                                if (imm_opcode != -1) {
                                 //...
                                }
                        }
                        MONO_ADD_INS ((cfg)->cbb, (ins));

                        *sp++ = mono_decompose_opcode (cfg, ins);
                        ip++;
                        break;
这串代码比较简单,并且把很多基础操作全部包含进来了,那么具体是什么操作呢?其实就是把当前的OPCode通过一个叫Mono_Inst的结构存储起来,这个结构里面有上下的参数和类型信息。并且对于Add和And这些普通数值操作。mono还尝试用immediate Opcodes进行优化。
里面有个函数需要关注一下mono_decompose_opcode
/*
* mono_decompose_opcode:
*
*   Decompose complex opcodes into ones closer to opcodes supported by
* the given architecture.
* Returns a MonoInst which represents the result of the decomposition, and can
* be pushed on the IL stack. This is needed because the original instruction is
* nullified.
* Sets the cfg exception if an opcode is not supported.
*/
MonoInst*
mono_decompose_opcode (MonoCompile *cfg, MonoInst *ins)
{
        MonoInst *repl = NULL;
        int type = ins->type;
        int dreg = ins->dreg;
        ...
}
        
这个大意是把将传入的opcode分解贴合当前架构(比如X86),其实主要的想法还是把通用的代码在运行时特化一下进行优化。
好了,现在我们大概了解了普通opcode会在mono_method_to_ir函数内被解码成一个一个的Mono_Inst。这里一个一个MonoInst应该是MonoInstructions的缩写也就是一个一个mono指令。
好吧,到这里为止,一切跟我们猜想的都差不多,mono把opcode翻译为一个一个的MonoInstructions,然后在mono_codegen里面会通过函数mono_arch_output_basic_block生成当前架构的机器码。
当然如果是ADD这样的简单操作,mono还会考虑要不要把原来CLR虚拟机的栈式操作变成寄存器操作,当然这些寄存器操作也是平台相关的。
/*
* mono_peephole_pass_1:
*
*   Perform peephole opts which should/can be performed before local regalloc
*/
void
mono_arch_peephole_pass_1 (MonoCompile *cfg, MonoBasicBlock *bb)
{
        MonoInst *ins, *n;

        MONO_BB_FOR_EACH_INS_SAFE (bb, n, ins) {
                MonoInst *last_ins = ins->prev;

                switch (ins->opcode) {
                case OP_IADD_IMM:
                case OP_ADD_IMM:
                        if ((ins->sreg1 < MONO_MAX_IREGS) && (ins->dreg >= MONO_MAX_IREGS)) {
                                /*
                                 * X86_LEA is like ADD, but doesn't have the
                                 * sreg1==dreg restriction.
                                 */
                                ins->opcode = OP_X86_LEA_MEMBASE;
                                ins->inst_basereg = ins->sreg1;
                        } else if ((ins->inst_imm == 1) && (ins->dreg == ins->sreg1))
                                ins->opcode = OP_X86_INC_REG;
                        break;
                       ...
               }
}

void
mono_arch_output_basic_block (MonoCompile *cf, MonoBasicBlock *bb)
{
    .....
    case OP_X86_INC_REG:
                        x86_inc_reg (code, ins->dreg);
                        break;
    ....
}
比如我们刚刚看的OP_ADD_IMM操作就会在x86平台下优化成寄存器操作,然后在mono_arch_output_basic_block被x86_inc_reg替换来提升效率。
好了,看完了ADD操作,我们对mono的解码有了一个大致的认识,mono解码出来后的一般操作会存储在MonoInst中,在MonoInst结构中的指令,最后会被替换成一套平台相关的机器操作码。
如果我们要解码的CIL只包含一些简单的逻辑运算,或者只是简单的操作数据,那么本文的分析已经可以结束了。但是CIL不仅仅只包含这些逻辑运算,还包含函数的调用和跳转。
比如说,上文中,我们用来测试的TestCPlus.cs最后会被编译为如下的CIL
.method private hidebysig static void  Main() cil managed
{
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  ldstr      "Hello World."
  IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000a:  ret
} // end of method Program::Main

里面在ldstr将Hello World 压栈后,会调用Call这个指令调用[mscorlib]System.Console::WriteLine
我们可以在Opcode.def这个文件中查询到相关的解码逻辑CIL的call指令会被翻译成mono Runtime里面的CEE_CALL指令。在mono_method_to_ir简单搜搜看。
还真有,CEE_CALL和CEE_CALLI和CEE_CALLVIRT三个在同一个swich case里面处理。
如果你有兴趣翻一翻这块的代码,你会发现,太恐怖了,
从当前的case到下一个return case有足足686行。
如果还好我们有上面ADD指令的一些基础。我们耐心点,一步一步看下去,不过CALL的确很复杂,可以先从jump,条件跳转,还有ret和throw,这些指令入手?
读者可以先尝试阅读阅读哦。
好了,我们正式开始分析Call这类跳转。
mono使用了一套mono_basic_block来描述这一系列控制跳转。并且我们上面的接触到的mono_arch_output_basicblock,也是在codegen里面随着一串一串的basic blocks控制块被循环调用的
void
mono_codegen (MonoCompile *cfg)
{
       ....
        /* emit code all basic blocks */
        for (bb = cfg->bb_entry; bb; bb = bb->next_bb) {
                bb->native_offset = cfg->code_len;
                //if ((bb == cfg->bb_entry) || !(bb->region == -1 && !bb->dfn))
                        mono_arch_output_basic_block (cfg, bb);

                if (bb == cfg->bb_exit) {
                        cfg->epilog_begin = cfg->code_len;

                        if (cfg->prof_options & MONO_PROFILE_ENTER_LEAVE) {
                                code = cfg->native_code + cfg->code_len;
                                code = mono_arch_instrument_epilog (cfg, mono_profiler_method_leave, code, FALSE);
                                cfg->code_len = code - cfg->native_code;
                                g_assert (cfg->code_len < cfg->code_size);
                        }

                        mono_arch_emit_epilog (cfg);
                }
        }
        ....
}
那么什么是basic blocks?
/*
* The IR-level extended basic block.  
*
* A basic block can have multiple exits just fine, as long as the point of
* 'departure' is the last instruction in the basic block. Extended basic
* blocks, on the other hand, may have instructions that leave the block
* midstream. The important thing is that they cannot be _entered_
* midstream, ie, execution of a basic block (or extened bb) always start
* at the beginning of the block, never in the middle.
*/
struct MonoBasicBlock {
        MonoInst *last_ins;

        /* the next basic block in the order it appears in IL */
        MonoBasicBlock *next_bb;

        /*
         * Before instruction selection it is the first tree in the
         * forest and the first item in the list of trees. After
         * instruction selection it is the first instruction and the
         * first item in the list of instructions.
         */
        MonoInst *code;
        ....
}
basic blocks简单翻译就是 IR级别扩展基本块。然后这个基本快可以有多个出口,代码可以从代码控制快中间break,但是不能从代码控制块中间运行。说实话,从这个描述,我就想起了一个十分标准的图。。
IDA反编译的结果,也是有一个一个的控制块组成,这里mono也是类似,把一个一个操作变成一个一个basic blocks。通常正常的OP_Code是用不到basic blocks的,因为他们并没有分支和跳转。而需要使用basic blocks应该只有那么几个。我们从basic blocks入手,查看一下什么情况下才会产生basic blocks。
。}/* *
* link_bblock: Links two basic blocks
*
* links two basic blocks in the control flow graph, the 'from'
* argument is the starting block and the 'to' argument is the block
* the control flow ends to after 'from'.
*/
static void
link_bblock (MonoCompile *cfg, MonoBasicBlock *from, MonoBasicBlock* to)
{
        MonoBasicBlock **newa;
        int i, found;
}
这里有个关键函数,由于控制块是一个链式结构,所以mono写了一个统一的函数link_bblock,我们查看一下link_bblock的索引很快就发现。link_bblock跟我们猜想的一样。只在条件跳转,无条件跳转Jump,各种Call,还有各种异常处理中出现。
这,很合理嘛。也只有这些操作会出现控制流转变,从而需要一个新的basic blocks来描述。
mono_arch_output_basicblock这个函数是个很重要的函数,在这个函数内,所有的op_code被翻译成平台相关的代码。
这里就回到我们上面所提出的一个问题来了。如果遇到地址跳转或者Call这样的指令,这个函数是怎么处理的?
                case OP_BR:
                        if (ins->inst_target_bb->native_offset) {
                                x86_jump_code (code, cfg->native_code + ins->inst_target_bb->native_offset);
                        } else {
                                mono_add_patch_info (cfg, offset, MONO_PATCH_INFO_BB, ins->inst_target_bb);
                                if ((cfg->opt & MONO_OPT_BRANCH) &&
                                    x86_is_imm8 (ins->inst_target_bb->max_offset - cpos))
                                        x86_jump8 (code, 0);
                                else
                                        x86_jump32 (code, 0);
                        }
                        break;
                case OP_BR_REG:
                        x86_jump_reg (code, ins->sreg1);
                        break;
我们发现,在有条件跳转里面,如果对方native_offset也就是非托管地址,那么是直接调用对应平台的jump进行跳转。如果是托管的地址,比如说CLR中的一个函数,那么这个时候翻译器会用mono_add_patch_info把当前要跳转的信息全部记录到一个patch_info里面。那么这个patch_info最终会在哪里被处理呢?
我们通过调试可以知道
最终在mono_codegen里面会调用mono_arch_patch_code然后在mono_resolve_patch_target里面,最终被替换成一个个Trampoline。并用Trampoline来替换当前的这些跳转操作。
Trampoline是什么呢?Trampoline是mono runtime中为了处理call和jump,虚函数这些确定或者不确定的地址跳转引入的一个概念。
推荐大家继续阅读之前前提前阅读一下笔者翻译的mono 官方对Trampolines的介绍。
在jump和call这类的跳转被Specific_Trampoline替换后,在CLR运行的时候,如果这些Specific_Trampoline被执行到了,会进入到一个mono_magic_trampoline函数
就像上图所展示的那样,0x31e0066这样的堆栈是刚刚mono_codegen动态生成的地址,mono_codegen生成的代码中运行时候如果触发到了这些Specific_Trampoline就会进入mono_magic_trampoline。
mono_magic_trampoline其实做的事情也很简单。把对应要跳转的地址检查一下。如果没有编译就触发编译,编译完了之后就跳转到对应的跳转地址运行。并且编译完了后还会将编译后的方法地址替换当前这个魔法的Trampolines。
至于为啥mono要这么做,我觉得,由于CIL语言的特性有很多东西其实我们是并不知道到底会不会被调用到的。比如通过一个if分支调用不同的函数的操作。其实很多情况下,我们或许只需要编译一个分支就好了。另外一个分支99%的情况都不会走到。
mono通过Trampolines这样的操作,就像埋了一个桩,做到了懒编译的过程,节省了大量的编译时间。
另外还有一个就是,由于CIL支持多态,也就是CIL里面的ICALL这样的操作,在没有收到当前的对象的时候,最终可以调用的方法实际上也是不确定的。有了Trampolines这个转接,就可以尽可能的达到用时再编译的效果。
好啦,时间也不早啦,今天就到这里吧。大家晚安。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-19 18:30 , Processed in 0.092038 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表