把 VM 拆開來看#

上一章畫了整體架構圖,這章把每個元件單獨看。我們要建構一個概念上的「玩具 VM」,它的範圍夠寫一個 50 行的腳本:算術、變數、if、while、函式呼叫、字串。

   bytecode 檔案
        │
        ▼
  ┌──────────┐         ┌──────────┐
  │  Loader  │  ─────► │ ByteCode │ ◄── code section + constants section
  └──────────┘         └────┬─────┘
                            │
                            ▼
                      ┌──────────┐         ┌──────────┐
                      │ VM state │ ◄────►  │   Heap   │
                      │  pc      │         │  (objs)  │
                      │  fp      │         └─────▲────┘
                      │  stack   │               │ GC
                      └────┬─────┘
                           │
                           ▼
                      ┌──────────┐
                      │ Dispatch │ ── 主迴圈:fetch → decode → exec
                      └──────────┘

bytecode 檔案格式#

幾乎所有 bytecode 格式都包含:

Section內容
Header / magic識別字、版本、endian
Constant pool字串常數、數字常數、symbol
Code section實際的 opcode 序列
Symbol / debug函式名、行號、原始檔名
Optional例外表、attribute、metadata

具體例子:

  • Java .class:magic 0xCAFEBABE + version + constant pool + 各 method 的 code
  • Python .pyc:magic + timestamp + marshalled code object
  • mruby .mrb:RITE binary 格式,含多個 IREP(Internal REPresentation)
  • Wasm .wasm:magic \0asm + section 列表

簡化版設計:

struct bytecode_file {
    uint32_t magic;             // 'TVM\0'
    uint16_t version;
    uint32_t const_count;
    Constant constants[];       // 緊隨其後
    uint32_t code_size;
    uint8_t  code[];            // opcode bytes
};

struct Constant {
    uint8_t type;               // INT / DOUBLE / STRING / SYMBOL
    union {
        int64_t  i;
        double   d;
        struct { uint32_t len; char data[]; } str;
    };
};

Loader 與 Linker#

Loader 從檔案 / 記憶體載入 bytecode,做幾件事:

  1. 驗證:magic、版本、checksum、結構完整性
  2. 解析常數池:把字串、數字、symbol 建成 VM 內部物件
  3. 載入 code:通常直接 mmap 或複製到 VM owned memory
  4. 解析 reference:function 之間的呼叫、外部模組

「Linker」的工作 ── 把多個 bytecode unit 之間的 cross-reference 接上。對動態語言通常是 lazy linking:第一次呼叫某個 method 時才 lookup。

常數池(Constant Pool)#

語言常需要重複出現相同的字面量:

"hello" + "hello" + "hello"

每個 "hello" 字串只應在 constant pool 存一份。bytecode 用索引指向:

ldc 0     ;; load constant index 0 = "hello"
ldc 0     ;; 同一個
add
ldc 0
add

這節省 bytecode 大小、節省 runtime 配置(多數語言把字串字面量做成 immortal 物件)。

主迴圈(Dispatch Loop)#

最簡單形式:

void run(VM *vm) {
    for (;;) {
        uint8_t op = *vm->pc++;
        switch (op) {
            case OP_PUSH_INT: {
                int v = read_int(vm->pc); vm->pc += 4;
                push(vm, INT_VAL(v));
                break;
            }
            case OP_ADD: {
                Value b = pop(vm);
                Value a = pop(vm);
                push(vm, INT_VAL(AS_INT(a) + AS_INT(b)));
                break;
            }
            case OP_RET: return;
            // ...
        }
    }
}

整本書 90% 的程式碼會圍繞「switch case 一個 case」展開。下一章專門講 dispatch 策略。

VM 狀態(registers)#

任何時刻,VM 的執行狀態幾乎都能用幾個指標描述:

名稱含義
pcprogram counter,下一條 opcode 的位址
fpframe pointer,當前 frame 起點
spstack pointer,操作堆疊頂端(stack VM)
ippc(不同書的命名習慣)

多數實作會把這些放進一個 struct vm_state

struct VM {
    uint8_t  *pc;
    Value    *fp;
    Value    *sp;
    Value     stack[STACK_MAX];
    Frame     frames[FRAMES_MAX];
    int       frame_count;
    Heap     *heap;
    // ...
};

每次呼叫函式 ➡️ push 一個 frame;返回 ➡️ pop。

操作堆疊 vs 暫存器陣列#

stack VM:

Value stack[STACK_MAX];
Value *sp;

#define push(v) (*sp++ = (v))
#define pop()   (*--sp)

register VM:

Value regs[REG_MAX];   // 通常是當前 frame 的 register window

兩者都儲存「執行中的中間值」。stack VM 隱式編號(永遠是 sp 附近),register VM 顯式編號(opcode 直接寫 R0、R5)。

Frame:函式呼叫的籃子#

每個函式呼叫對應一個 frame:

struct Frame {
    Function *func;       // 哪個函式
    uint8_t  *return_pc;  // 呼叫者該返回的位址
    Value    *base;       // 這個 frame 的 register / stack 起點
    int       num_args;
    int       num_locals;
};

呼叫 f(1, 2, 3)

  1. 1, 2, 3 推到 stack 或寫入 R0..R2
  2. 配新 frame,base = sp - 3return_pc = pc
  3. pc = f->code,跳進去執行
  4. 函式體執行 OP_RET 時:把回傳值移到適當位置、pc = frame->return_pc、pop frame

下一章詳細展開 dispatch,第六章詳細展開 frame 設計。

Value:所有值的型別#

語言裡 42"hello"[1, 2, 3]SomeClass.new 都得用同一個 C 型別表示,這樣 stack 與 register 才能放任何東西:

typedef struct Value Value;

如何在一個 Value 裡塞下所有可能型別 + 型別 tag ── 第四章專門講(NaN-boxing、tagged pointer 等)。

Heap#

存放動態配置的物件(字串、陣列、實例、closure)。VM 自己管 heap,userspace malloc/free 介入會干擾 GC。

最簡單的 heap:

struct Heap {
    Object *all_objects;     // GC scan 用的鏈
    size_t  bytes_allocated;
    size_t  next_gc;         // 下次 GC 的閾值
};

void *gc_alloc(Heap *h, size_t n) {
    h->bytes_allocated += n;
    if (h->bytes_allocated > h->next_gc)
        collect_garbage(h);
    return malloc(n);   // 真實 VM 多半用 bump pointer 或 free list
}

GC 的細節在第十章。

物件模型#

「字串、陣列、實例」── 怎麼在 C 結構裡表示?常見 layout:

typedef enum { OBJ_STRING, OBJ_ARRAY, OBJ_INSTANCE, OBJ_CLOSURE } ObjType;

struct Object {
    ObjType type;
    bool    is_marked;       // GC 標記位
    Object *next;            // GC scan 鏈
};

struct String {
    Object obj;              // 「繼承」基底
    int    len;
    uint32_t hash;
    char   data[];           // flexible array
};

struct Array {
    Object obj;
    int    len, cap;
    Value *items;
};

慣用的 C 「繼承」:把 Object 放在第一個欄位,所有子型別都能 cast 成 Object*。第八章展開。

Native function / FFI#

VM 不會自己實作所有東西 ── putsMath.sin、I/O 全得呼叫 host 平台。慣例:

typedef Value (*NativeFn)(VM *vm, int nargs, Value *args);

void define_native(VM *vm, const char *name, NativeFn fn);

call 時 dispatch loop 看到 native function,直接呼叫對應 C 函式而非繼續 dispatch bytecode。

全鏈路:一條 print(1 + 2) 的旅程#

source: print(1 + 2)
              │
              ▼
         AST: Call(print, [BinOp(+, 1, 2)])
              │ compiler 編成
              ▼
         bytecode:
            CONST 0           ;; constants[0] = 1
            CONST 1           ;; constants[1] = 2
            ADD
            CALL_NATIVE 0     ;; natives[0] = print
            RET
              │ VM 載入
              ▼
         VM dispatch loop:
            fetch CONST → push 1 → sp++
            fetch CONST → push 2 → sp++
            fetch ADD   → pop 2,1 → push 3
            fetch CALL_NATIVE → 呼叫 print(3) → 印出 "3"
            fetch RET   → 退出

接下來讀什麼#

每個方塊都是一章:

  • 03 ➡️ dispatch loop 細節(switch / threaded / direct / computed goto)
  • 04 ➡️ Value 怎麼塞所有型別(NaN-boxing 等)
  • 05 ➡️ opcode 設計(算術、控制流)
  • 06 ➡️ frame 與函式呼叫
  • 07 ➡️ closure 怎麼捕獲
  • 08, 09 ➡️ object model、shape、inline cache
  • 10 ➡️ GC
  • 11 ➡️ JIT
  • 12 ➡️ 嵌入式環境的取捨