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

前言

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,略

附上一张 栈帧 以及 本地存储的图
Ruby 2.x 源代码学习:YARV 虚拟机 指令

压入栈帧

函数 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))))

相关推荐