把 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:magic0xCAFEBABE+ 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,做幾件事:
- 驗證:magic、版本、checksum、結構完整性
- 解析常數池:把字串、數字、symbol 建成 VM 內部物件
- 載入 code:通常直接 mmap 或複製到 VM owned memory
- 解析 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 的執行狀態幾乎都能用幾個指標描述:
| 名稱 | 含義 |
|---|---|
pc | program counter,下一條 opcode 的位址 |
fp | frame pointer,當前 frame 起點 |
sp | stack pointer,操作堆疊頂端(stack VM) |
ip | 同 pc(不同書的命名習慣) |
多數實作會把這些放進一個 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, 2, 3推到 stack 或寫入 R0..R2 - 配新 frame,
base = sp - 3、return_pc = pc pc = f->code,跳進去執行- 函式體執行
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 不會自己實作所有東西 ── puts、Math.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 ➡️ 嵌入式環境的取捨