IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Ruby 2.x 源代码学习:YARV 虚拟机 指令

    xingpingz发表于 2017-01-25 11:05:42
    love 0

    前言

    YARV 大约有 90 多条指令,这些指令定义在 insns.def 文件中,编译 Ruby 源代码的时候会根据该文件生成 vm.inc 和 insns.inc 两个(include)文件,这两个文件会被包含在 Ruby 虚拟器核心代码里头

    栈帧

    虚拟机模拟物理机执行方法调用的方式, 每执行一个方法(or block)都会将一个 栈帧压入堆栈中
    rb_control_frame_t 结构封装了栈帧结构

    // vm_core.h
    
    typedef struct rb_control_frame_struct {
        const VALUE *pc;        /* cfp[0] */
        VALUE *sp;            /* cfp[1] */
        const rb_iseq_t *iseq;    /* cfp[2] */
        VALUE self;            /* cfp[3] / block[0] */
        const VALUE *ep;        /* cfp[4] / block[1] */
        const void *block_code;     /* cfp[5] / block[2] */ /* iseq or ifunc */
    
    #if VM_DEBUG_BP_CHECK
        VALUE *bp_check;        /* cfp[6] */
    #endif
    } rb_control_frame_t;
    • pc, 指令指针

    • sp,操作数栈指针

    • iseq,当前执行的代码序列(使用 pc 索引)

    • self,略

    • ep,本地存储指针,方法(or block)参数和局部变量存储在 ep 指向的区域

    • block_code,略

    压入栈帧

    函数 vm_push_frame 实现了将一个栈帧压入当前线程的堆栈之中

    // vm_insnhelper.c
    
    static inline rb_control_frame_t *
    vm_push_frame(rb_thread_t *th,
              const rb_iseq_t *iseq,
              VALUE type,
              VALUE self,
              VALUE specval,
              VALUE cref_or_me,
              const VALUE *pc,
              VALUE *sp,
              int local_size,
              int stack_max)
    {
        // 每个线程都有一个栈,cfp 指向栈顶部(向下生长),指针减 1 给新的 rb_control_frame_t 预留空间
        rb_control_frame_t *const cfp = th->cfp - 1;
        int i;
    
        ...
    
        // 更新线程栈帧
        th->cfp = cfp;
    
        // 根据函数输入参数,初始化新的栈帧
        /* setup new frame */
        cfp->pc = (VALUE *)pc;
        cfp->iseq = (rb_iseq_t *)iseq;
        cfp->self = self;
        cfp->block_code = NULL;
    
        /* setup vm value stack */
        // sp 指向线程栈空间底部(向上生长),这里根据 本地变量 的大小,预留空间
        /* initialize local variables */
        for (i=0; i < local_size; i++) {
        *sp++ = Qnil;
        }
    
        /* setup ep with managing data */
        // 为虚拟机内部使用的变量 cref, specval, type 预留空间
        ...
        *sp++ = cref_or_me; /* ep[-2] / Qnil or T_IMEMO(cref) or T_IMEMO(ment) */
        *sp++ = specval     /* ep[-1] / block handler or prev env ptr */;
        *sp   = type;       /* ep[-0] / ENV_FLAGS */
    
        cfp->ep = sp;
        cfp->sp = sp + 1;
    
    #if VM_DEBUG_BP_CHECK
        cfp->bp_check = sp + 1;
    #endif
    
        if (VMDEBUG == 2) {
        SDR();
        }
    
        return cfp;
    }

    弹出栈帧

    弹出栈帧的操作相对比较简单:

    /* return TRUE if the frame is finished */
    static inline int
    vm_pop_frame(rb_thread_t *th, rb_control_frame_t *cfp, const VALUE *ep)
    {
        VALUE flags = ep[VM_ENV_DATA_INDEX_FLAGS];
    
        if (VM_CHECK_MODE >= 4) rb_gc_verify_internal_consistency();
        if (VMDEBUG == 2)       SDR();
    
        th->cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
    
        return flags & VM_FRAME_FLAG_FINISH;
    }
    
    void
    rb_vm_pop_frame(rb_thread_t *th)
    {
        vm_pop_frame(th, th->cfp, th->cfp->ep);
    }

    指令格式定义

    insns.def 文件的开头注释部分对指令格式进行了说明:

    // insns.def
    
    /** ##skip
      instruction comment
      @c: category
      @e: english description
      @j: japanese description
    
      instruction form:
        DEFINE_INSN
        instruction_name
        (instruction_operands, ..)
        (pop_values, ..)
        (return value)
        {
           .. // insn body
        }
    
     */
    • instruction_name 指令名称

    • (instruction_operatns, ..) 指令操作数

    • (pop_values, ..) 指令执行时从操作数栈弹出的 VALUE

    • (return value) 指令完成之后压如操作数栈 VALUE

    Ruby 虚拟机和 Java 虚拟机一样是基于栈的虚拟机

    • 指令需要的操作数必须先压入操作数栈

    • 指令结果保存在操作数栈

    • 操作数栈和本地存储(存放方法参数和局部变量的地方)之间可以交换(load or store)数据

    getlocal 指令

    getlocal 指令将指定的本地变量(local var)从本地存储加载到操作数栈
    先来看看 getlocal 指令在 insns.def 文件中的定义:

    // insns.def
    
    /**
      @c variable
      @e Get local variable (pointed by `idx' and `level').
         'level' indicates the nesting depth from the current block.
      @j level, idx で指定されたローカル変数の値をスタックに置く。
         level はブロックのネストレベルで、何段上かを示す。
     */
    DEFINE_INSN
    getlocal
    (lindex_t idx, rb_num_t level)
    ()
    (VALUE val)
    {
        int i, lev = (int)level;
        const VALUE *ep = GET_EP();
    
        /* optimized insns generated for level == (0|1) in defs/opt_operand.def */
        for (i = 0; i < lev; i++) {
        ep = GET_PREV_EP(ep);
        }
        val = *(ep - idx);
    }

    指令需要 两个参数 idx 和 level

    • idx,本地变量在 本地存储中的索引

    • level,本地存储允许嵌套,level 用于指定本地存储的级别

    GET_EP 宏用于访问当前 栈帧 的 ep 寄存器(可以理解成 本地存储的基地址)

    // vm_insnhelper.h
    
    #if VM_COLLECT_USAGE_DETAILS
    #define COLLECT_USAGE_REGISTER_HELPER(a, b, v) \
      (COLLECT_USAGE_REGISTER((VM_REGAN_##a), (VM_REGAN_ACT_##b)), (v))
    #else
    #define COLLECT_USAGE_REGISTER_HELPER(a, b, v) (v)
    #endif
    
    #define GET_EP()   (COLLECT_USAGE_REGISTER_HELPER(EP, GET, VM_REG_EP))

    这里又是一大堆嵌套的宏定义,根据是否定义了 VM_COLLECT_USAGE_DETAILS,GET_EP 会以不同的形式展开,我们先看简单的情况,即没有定义 VM_COLLECT_USAGE_DETAILS,此时 GET_EP 被展开成 VM_REG_EP
    VM_REG_CFP 最终展开成 reg_cfg->ep,reg_cfg 即上文我们提到的 虚拟机 当前 栈帧

    #define VM_REG_CFP (reg_cfg)
    #define VM_REG_EP (VM_REG_CFP->ep)

    我们来看一下 vm.inc 中最终生成的 getlocal 指令的处理函数

    // vm.inc
    
    INSN_ENTRY(getlocal){
    {
      VALUE val;
      // 获取第二个操作数
      rb_num_t level = (rb_num_t)GET_OPERAND(2);
      // 获取第一个操作数
      lindex_t idx = (lindex_t)GET_OPERAND(1);
    
      DEBUG_ENTER_INSN("getlocal");
      // PC 指针指向下一条指令,本指令占用一个字节,操作数占用两个字节,所以增量为 1 + 2
      ADD_PC(1+2);
      // gcc 编译器 hack,模拟 CPU 取下一条指令
      PREFETCH(GET_PC());
      #define CURRENT_INSN_getlocal 1
      #define INSN_IS_SC()     0
      #define INSN_LABEL(lab)  LABEL_getlocal_##lab
      #define LABEL_IS_SC(lab) LABEL_##lab##_##t
      COLLECT_USAGE_INSN(BIN(getlocal));
      COLLECT_USAGE_OPERAND(BIN(getlocal), 0, idx);
      COLLECT_USAGE_OPERAND(BIN(getlocal), 1, level);
    {
    // 这部分代码是从 insns.def 通过生成器拷贝过来的
    #line 60 "insns.def"
        int i, lev = (int)level;
        const VALUE *ep = GET_EP();
    
        /* optimized insns generated for level == (0|1) in defs/opt_operand.def */
        for (i = 0; i < lev; i++) {
        ep = GET_PREV_EP(ep);
        }
        val = *(ep - idx);
    
    #line 65 "vm.inc"
      CHECK_VM_STACK_OVERFLOW_FOR_INSN(REG_CFP, 1);
      // 将本地变量 val 压入堆栈
      PUSH(val);
    #undef CURRENT_INSN_getlocal
    #undef INSN_IS_SC
    #undef INSN_LABEL
    #undef LABEL_IS_SC
      END_INSN(getlocal);}}}

    上面对关键代码段进行了注释,这里再介绍一下里面用到的几个宏定义

    GET_OPERAND

    getlocal 指令的操作数被编码在指令序列中,GET_OPERAND 通过 当前 PC 指针以及偏移量 n 获取操作数

    // vm_insnhelper.h
    
    #define GET_OPERAND(n) (GET_PC()[(n)])

    PUSH

    PUSH 包含两个操作来模拟 getlocal 获取到的本地变量压入操作数栈的操作

    • SET_SV,将变量设置到 栈帧 中 sp 指针指向的位置

    • INC_SP,递增 sp 指针

    // vm_insnhelper.h
    
    #define PUSH(x) (SET_SV(x), INC_SP(1))
    #define SET_SV(x) (*GET_SP() = (x))
    #define INC_SP(x)  (VM_REG_SP += (COLLECT_USAGE_REGISTER_HELPER(SP, SET, (x))))


沪ICP备19023445号-2号
友情链接