前言
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))))
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。