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,略
附上一张 栈帧 以及 本地存储的图
压入栈帧
函数 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))))