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, 指令指針spa
sp,操做數棧指針線程
iseq,當前執行的代碼序列(使用 pc 索引)指針
self,略code
ep,本地存儲指針,方法(or block)參數和局部變量存儲在 ep 指向的區域orm
block_code,略blog
附上一張 棧幀 以及 本地存儲的圖索引
函數 vm_push_frame 實現了將一個棧幀壓入當前線程的堆棧之中ip
// 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 指令將指定的本地變量(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);}}}
上面對關鍵代碼段進行了註釋,這裏再介紹一下里面用到的幾個宏定義
getlocal 指令的操做數被編碼在指令序列中,GET_OPERAND 經過 當前 PC 指針以及偏移量 n 獲取操做數
// vm_insnhelper.h #define GET_OPERAND(n) (GET_PC()[(n)])
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))))