1

前言

回顾一下 Ruby 解释器处理流程

  • 词法分析

  • 语法分析

  • AST 生成

  • 虚拟机指令生成

虚拟机指令有两种存储结构:

  • 链式结构,通过 LINK_ELEMENT 链表节点

  • 数组(线性)结构

解释器会先遍历 AST 生成链式结构存储的指令列表,但是虚拟机并不会直接解释执行该列表,而是进一步将指令列表“序列化”成字节数组,PC(指令指针)寄存器可以索引指令数组(取指,分支跳转)

本文简要介绍 Ruby 2.x 源代码中将链式结构转化成数组结构的源代码,从中可以进一步了解 Ruby 虚拟机相关数据结构的设计与实现

由 AST 生成虚拟机指令(简要回顾)

由此前的系列文章可以,rb_iseq_compile_node 函数负责遍历 AST 生成虚拟机指令:

// compile.c

VALUE rb_iseq_compile_node(rb_iseq_t *iseq, NODE *node) {
    DECL_ANCHOR(ret);
    INIT_ANCHOR(ret);
    ...
    return iseq_setup(iseq, ret);
}

DECL_ANCHOR 和 INIT_ANCHOR 声明了一个双向链表用于收集在遍历 AST 根节点 node 过程中生成的虚拟机指令,到达函数末尾时,ret 指向指令链表的头部,然后 iseq_setup 函数登场,对指令链表进行优化,序列化等操作

iseq_setup

为了便于分析,去掉了 iseq_setup 函数中 compile_debug 开关控制的调试输出以及一些编译选项(比如 stack caching, instructions unification .etc)

// compile.c

static int iseq_setup(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) {
    iseq_optimize(idea, anchor);
    ...
    if (!iseq_set_sequence(iseq, anchor)) {
        return COMPILE_NG;
    }
    ...
    return COMPILE_OK;
}

iseq_set_sequence 将 anchor 转化为 instruction sequence 存储在 iseq 中

iseq_set_sequence

iseq_set_sequence 函数有一段注释表明了该函数的用途

// compile.c

/**
 ruby insn object list -> raw instruction sequence
 **/
static int iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) {
    struct iseq_line_info_entry *line_info_table;
    unsigned int last_line = 0;
    LINK_ELEMENT *list;
    VALUE *generated_iseq;

    int insn_num, code_index, line_info_index, sp, stack_max = 0, line = 0;

}

函数开头的局部变量定义,信息量还是很大的

  • line_info_table,行号表

  • last_line

  • list,遍历 anchor 的中间变量

  • generated_iseq,VALUE 数组,用于存放序列化后的指令

  • insn_num,anchor 中包含的指令个数

  • code_index

  • line_info_index

  • sp

  • stack_max,执行指令过程中堆栈的最大值

  • line

iseq_set_sequence 会遍历两次 anchor(指令链表),第一次用于搜集指令个数 insn_num,序列后的指令数组的大小 generated_iseq 以及确定 label(标签)的跳转位置。第二次遍历用于生成指令序列

第一次遍历指令链表

第一次遍历指令链表的主体代码框架如下:

// compile.c

static int iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) {
    ...
    /* fix label position */
    list = FIRST_ELEMENT(anchor);
    insn_num = code_index = 0;
    while (list) {
        switch(list->type) {
            case ISEQ_ELEMENT_INSN:
                ...
                break;
            case ISEQ_ELEMENT_LABEL:
                ...
                break;
            case ISEQ_ELEMENT_ADJUST:
                ...
                break;
            default:
                ...
                return COMPILE_NG
        }
        list = list->next;
    }
}
ISEQ_ELEMENT_INSN
// iseq_set_sequence @ compile.c

case ISEQ_ELEMENT_INSN: {
    INSN *iobj = (INSN*) list;
    line = iobj->line_no;
    // 调用 insn_data_length 获取指令占用的字节数并累加到 code_index
    code_index += insn_data_length(jobs);
    // 指令个数加 1
    insn_num++;
    break;
}
ISEQ_ELEMENT_LABEL

ISEQ_ELEMENT_LABEL 类型的指令是一条伪指令,并不生成具体的指令序列,只是为了确定分支跳转的指令偏移量。在创建 label 的时候 position,set 属性并未赋值,要等到此处才能确定它们的真实值

// iseq_set_sequence @ compile.c

case ISEQ_ELEMENT_LABEL: {
    LABEL *lobj = (LABEL*) list;
    lobj->position = code_index;
    lobj->set = TRUE;
    break;
}
ISEQ_ELEMENT_NONE
ISEQ_ELEMENT_ADJUST

第二次遍历指令链表

准备工作

使用第一次遍历得到的 code_index 和 insn_num 为 generated_iseq 和 line_info_table 分配空间

// iseq_set_sequence @ compile.c

generated_iseq = ALLOC_N(VALUE, code_index);
line_info_table = ALLOC_N(struct iseq_line_info_entry, insn_num);
...

总结


xingpingz
122 声望64 粉丝

博学,审问,慎思,明辨,力行


引用和评论

0 条评论