AIGC标识 lua-源码带读-03-代码生成与虚拟机

Lua 5.4 源码带读 第3篇:代码生成与虚拟机

本篇目标

这是 Lua 源码阅读的核心章节。理解字节码如何生成、指令格式是什么、83条操作码如何分类、虚拟机如何执行字节码。本篇将深入到每一条指令的执行逻辑。

前置知识

  • 了解汇编语言的基本概念
  • 了解寄存器和栈的区别
  • 阅读过第1、2篇

1. 代码生成器:lcode.c

1.1 代码生成在解析过程中进行

Lua 的独特设计:lparser.c 在解析语法树的每个节点时,直接调用 lcode.c 生成对应的字节码。没有中间的 AST 表示。

1.2 关键函数

函数 作用
luaK_code(fs, i) 发射一条指令,返回指令索引
luaK_codek(fs, reg, k) 发射加载常量指令(自动处理大常量)
luaK_exp2val(e) 将表达式求值结果放到栈顶
luaK_dischargevars(e) 将变量转为栈上位置
luaK_exp2RK(e) 将表达式放入寄存器或常量槽
luaK_exp2reg(e, reg) 将表达式结果放入指定寄存器
luaK_poswalk(e) 处理条件表达式的跳转链
luaK_patchlist() 跳转指令的回填
luaK_patchtohere() 回填到当前位置

1.3 发射指令

// lcode.c
static int luaK_code(FuncState *fs, Instruction i) {
  Proto *f = fs->f;
  // 检查指令数是否超限
  if (fs->pc == MAX_INT)
    luaX_syntaxerror(fs->ls, "code too long");
  // 确保有足够空间
  if (fs->pc == f->sizecode)
    luaM_growvector(fs->ls->L, f->code, fs->pc, &f->sizecode, MAX_INT);
  // 写入指令
  f->code[fs->pc] = i;
  // 保存行号信息
  luaK_line(fs);
  fs->pc++;
  return fs->pc - 1;  // 返回指令索引
}

1.4 常量折叠

// lcode.c
static int luaK_constfolding(FuncState *fs, int op, expdesc *e1, expdesc *e2) {
  // 如果两个操作数都是常量,编译期直接计算
  TValue v1, v2;
  if (!tovalue(e1, &v1) || !tovalue(e2, &v2))
    return 0;  // 不是常量,不能折叠

  // 根据操作码计算结果
  switch (op) {
    case OP_ADD: res = luaO_intarith(L, LUA_OPADD, i1, i2); break;
    case OP_SUB: res = luaO_intarith(L, LUA_OPSUB, i1, i2); break;
    case OP_MUL: res = luaO_intarith(L, LUA_OPMUL, i1, i2); break;
    // ... 其他操作
  }

  // 设置e1的结果
  e1->u.ival = res;
  return 1;  // 折叠成功
}

1.5 跳转回填

// lcode.c
static void luaK_patchlist(FuncState *fs, int list, int target) {
  while (list != NO_JUMP) {
    int next = getjumpcontrol(fs, list);
    fixjump(fs, list, target);
    list = next;
  }
}

static void fixjump(FuncState *fs, int pc, int dest) {
  Instruction *jmp = &fs->f->code[pc];
  int offset = dest - (pc + 1);  // 相对偏移
  // 写入偏移到JMP指令的sJ字段
  SETARG_sJ(*jmp, offset);
}

2. 指令格式(lopcodes.h)

2.1 32位指令布局

每条指令固定32位,5种格式:

3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
iABC          C(8)     |      B(8)     |k|     A(8)      |   Op(7)     |
iABx                Bx(17)               |     A(8)      |   Op(7)     |
iAsBx              sBx (signed)(17)      |     A(8)      |   Op(7)     |
iAx                           Ax(25)                     |   Op(7)     |
isJ                           sJ (signed)(25)            |   Op(7)     |

2.2 各字段含义

字段 位数 含义
Op 7 操作码(0-127,最多128条指令)
A 8 目标寄存器索引(0-255)
B 8 源寄存器/常量索引(0-255)
C 8 源寄存器/常量索引(0-255)
k 1 B标志位(0=寄存器,1=常量)
Bx 17 无符号大立即数(0-131071)
sBx 17 有符号大立即数(-65536 到 65535)
Ax 25 超大立即数(0-33554431)
sJ 25 有符号跳转偏移

2.3 k标志位

// 当 k=0 时:R[B] 和 R[C] 是寄存器索引
// 当 k=1 时:K[B] 和 K[C] 是常量表索引
// 这使得一条指令可以混合寄存器和常量操作数

// 例如:a = b + 1
// OP_ADDI  R[a] R[b] sC
// 如果是 a = b + c(常量):
// OP_ADDK  R[a] R[b] K[c]

2.4 有符号数编码

sBx 使用 excess-K 编码:表示的值 = 写入的无符号值 - K/2。这样可以用无符号字段表示有符号数。


3. 操作码完整分类(83条)

3.1 加载/存储(17条)

操作码 格式 作用 使用场景
OP_MOVE iABC R[A] := R[B] 局部变量复制、函数参数传递
OP_LOADI iAsBx R[A] := sBx 加载整数常量
OP_LOADF iAsBx R[A] := (lua_Number)sBx 加载浮点常量
OP_LOADK iABx R[A] := K[Bx] 加载常量表中的值
OP_LOADKX iABx R[A] := K[extra arg] 加载大常量(后续EXTRAARG)
OP_LOADFALSE iABC R[A] := false 加载false
OP_LFALSESKIP iABC R[A] := false; pc++ 条件转布尔
OP_LOADTRUE iABC R[A] := true 加载true
OP_LOADNIL iABC R[A], R[A+1], ..., R[A+B] := nil 加载多个nil
OP_GETUPVAL iABC R[A] := UpValue[B] 获取上值
OP_SETUPVAL iABC UpValue[B] := R[A] 设置上值
OP_GETTABUP iABC R[A] := UpValue[B][K[C]:shortstring] 从全局表获取字段
OP_GETTABLE iABC R[A] := R[B][R[C]] 从表中获取元素
OP_GETI iABC R[A] := R[B][C] 从表中获取整数键
OP_GETFIELD iABC R[A] := R[B][K[C]:shortstring] 从表中获取字符串键
OP_SETTABUP iABC UpValue[A][K[B]:shortstring] := RK(C) 向全局表设置
OP_SETTABLE iABC R[A][R[B]] := RK(C) 向表中设置
OP_SETI iABC R[A][B] := RK(C) 向表中设置整数键
OP_SETFIELD iABC R[A][K[B]:shortstring] := RK(C) 向表中设置字符串键

3.2 表操作(3条)

操作码 格式 作用
OP_NEWTABLE iABC R[A] := {}(创建空表)
OP_SELF iABC R[A+1] := R[B]; R[A] := R[B][RK(C):string](方法调用)
OP_SETLIST iABC R[A][C+i] := R[A+i], 1 <= i <= B(批量设置表字段)

3.3 算术运算(16条)

操作码 格式 作用 操作数来源
OP_ADDI iABC R[A] := R[B] + sC 寄存器+立即数
OP_ADDK iABC R[A] := R[B] + K[C]:number 寄存器+常量
OP_SUBK iABC R[A] := R[B] - K[C]:number 寄存器-常量
OP_MULK iABC R[A] := R[B] * K[C]:number 寄存器*常量
OP_MODK iABC R[A] := R[B] % K[C]:number 寄存器%常量
OP_POWK iABC R[A] := R[B] ^ K[C]:number 寄存器^常量
OP_DIVK iABC R[A] := R[B] / K[C]:number 寄存器/常量
OP_IDIVK iABC R[A] := R[B] // K[C]:number 寄存器//常量
OP_BANDK iABC R[A] := R[B] & K[C]:integer 寄存器&常量
OP_BORK iABC R[A] := R[B] | K[C]:integer 寄存器|常量
OP_BXORK iABC R[A] := R[B] ~ K[C]:integer 寄存器~常量
OP_SHRI iABC R[A] := R[B] >> sC 移位
OP_SHLI iABC R[A] := sC << R[B] 移位
OP_ADD iABC R[A] := R[B] + R[C] 寄存器+寄存器
OP_SUB iABC R[A] := R[B] - R[C] 寄存器-寄存器
OP_MUL iABC R[A] := R[B] * R[C] 寄存器*寄存器
OP_MOD iABC R[A] := R[B] % R[C] 寄存器%寄存器
OP_POW iABC R[A] := R[B] ^ R[C] 寄存器^寄存器
OP_DIV iABC R[A] := R[B] / R[C] 寄存器/寄存器
OP_IDIV iABC R[A] := R[B] // R[C] 寄存器//寄存器
OP_BAND iABC R[A] := R[B] & R[C] 寄存器&寄存器
OP_BOR iABC R[A] := R[B] | R[C] 寄存器|寄存器
OP_BXOR iABC R[A] := R[B] ~ R[C] 寄存器~寄存器
OP_SHL iABC R[A] := R[B] << R[C] 移位
OP_SHR iABC R[A] := R[B] >> R[C] 移位

5.4新增的优化指令:OP_ADDI、OP_ADDK 等。将常见的常量操作从"加载常量到寄存器 → 运算"优化为"运算时直接使用常量",减少了一条指令。

3.4 元方法调用(4条)

操作码 格式 作用
OP_MMBIN iABC 调用R[A]和R[B]的二元元方法(运算失败后调用)
OP_MMBINI iABC 调用R[A]和sB的二元元方法
OP_MMBINK iABC 调用R[A]和K[B]的二元元方法
OP_UNM iABC R[A] := -R[B]
OP_BNOT iABC R[A] := ~R[B]
OP_NOT iABC R[A] := not R[B]
OP_LEN iABC R[A] := #R[B](长度运算符)

3.5 连接操作(1条)

操作码 格式 作用
OP_CONCAT iABC R[A] := R[A].. ... ..R[A + B - 1]

3.6 函数调用/返回(5条)

操作码 格式 作用
OP_CALL iABC R[A], ..., R[A+C-2] := R[A](R[A+1], ..., R[A+B-1])
OP_TAILCALL iABC return R[A](R[A+1], ..., R[A+B-1])
OP_RETURN iABC return R[A], ..., R[A+B-2]
OP_RETURN0 iABC return
OP_RETURN1 iABC return R[A]

OP_CALL 语义

  • B=0:参数从R[A+1]到栈顶(动态参数数量)
  • C=0:返回值不确定,设置top为最后结果+1

3.7 测试/跳转(11条)

操作码 格式 作用
OP_EQ iABC if ((R[A] == R[B]) ~= k) then pc++
OP_LT iABC if ((R[A] < R[B]) ~= k) then pc++
OP_LE iABC if ((R[A] <= R[B]) ~= k) then pc++
OP_EQK iABC if ((R[A] == K[B]) ~= k) then pc++
OP_EQI iABC if ((R[A] == sB) ~= k) then pc++
OP_LTI iABC if ((R[A] < sB) ~= k) then pc++
OP_LEI iABC if ((R[A] <= sB) ~= k) then pc++
OP_GTI iABC if ((R[A] > sB) ~= k) then pc++
OP_GEI iABC if ((R[A] >= sB) ~= k) then pc++
OP_TEST iABC if (not R[A] == k) then pc++
OP_TESTSET iABC if (not R[B] == k) then pc++ else R[A] := R[B]
OP_JMP isJ pc += sJ

比较指令的 k 标志:k 指定了比较条件的真值。例如 a == b 生成 OP_EQ R[a] R[b] 1,而 a ~= b 生成 OP_EQ R[a] R[b] 0

3.8 循环指令(5条)

操作码 格式 作用
OP_FORLOOP iABx 更新计数器,如果循环继续则 pc-=Bx
OP_FORPREP iABx 检查值并准备计数器,如果不需要运行则 pc+=Bx+1
OP_TFORPREP iABx 为泛型for创建上值
OP_TFORCALL iABC R[A+4], ..., R[A+3+C] := R[A](R[A+1], R[A+2])
OP_TFORLOOP iABx 如果 R[A+2] ~= nil 则

3.9 闭包/变量参数(3条)

操作码 格式 作用
OP_CLOSURE iABx R[A] := closure(KPROTO[Bx])(创建闭包)
OP_VARARG iABC R[A], R[A+1], ..., R[A+C-2] = vararg
OP_VARARGPREP iABC 调整可变参数

3.10 其他(3条)

操作码 格式 作用
OP_CLOSE iABC 关闭所有 >= R[A] 的上值
OP_TBC iABC 标记变量A为"to be closed"(5.4新增:可选的to-be-closed变量)
OP_EXTRAARG iAx 前一条指令的额外参数

4. 虚拟机:lvm.c

4.1 寄存器式虚拟机

Lua 5.4 使用寄存器式VM:寄存器 = 值栈上的槽位。每个函数帧的寄存器从 ci->u.l.base + 1 开始。指令通过索引引用寄存器。

// lvm.c
void luaV_execute(lua_State *L) {
  CallInfo *ci = L->ci;
  StkId base = ci->u.l.base;  // 当前函数的基址
  Instruction *pc = ci->u.l.savedpc;  // 程序计数器

  for (;;) {
    const Instruction i = *pc++;
    OpCode op = GET_OPCODE(i);
    const TValue *rb, *rc;

    // 获取操作数(寄存器位置)
    StkId ra = RKB(i);  // 目标寄存器

    // 根据操作码执行
    switch (op) {

      case OP_MOVE: {
        setobjs2s(L, ra, RKB(i));
        vmbreak;
      }

      case OP_LOADI: {
        setivalue(ra, GETARG_sBx(i));
        vmbreak;
      }

      case OP_LOADK: {
        TValue *kb = k + GETARG_Bx(i);
        setobjs2s(L, ra, kb);
        vmbreak;
      }

      case OP_ADDI: {
        TValue *rb = RB(i);
        lua_Integer b;
        if (tointeger(rb, &b)) {
          setivalue(ra, b + GETARG_sC(i));
          // 如果是整数,快速路径
        } else {
          // 如果是浮点,转为浮点计算
          lua_Number nb;
          if (tonumber(rb, &nb)) {
            setfltvalue(ra, cast_num(b) + cast_num(GETARG_sC(i)));
          } else {
            // 元方法调用
            pc--;
            luaT_callTM(L, rb, ...);
          }
        }
        vmbreak;
      }

      // ... 80+ 条指令
    }
  }
}

4.2 Dispatch 机制

// ljumptab.h - 跳转表
#define vmbreak		goto *disptabs[opcode]

// 或者使用 computed goto(GCC扩展):
#define vmbreak		vmcase(op) \
          goto *dispatchtable[op];

跳转表实现高效分发:避免每次循环重新计算 switch 目标地址。在现代CPU上,间接跳转可以被分支预测器优化。

4.3 Lua函数调用

// ldo.c
static StkId adjust_varargs(lua_State *L, Proto *p, int actual) {
  int nfixargs = p->numparams;
  // 将多余参数复制到栈顶
  StkId base = L->top.p - actual;
  StkId func = L->top.p - (actual + 1);
  for (int i = actual; i < nfixargs; i++) {
    if (!ttisnil(base + i))
      setnilvalue(base + i);
  }
  // 设置新的基址
  return base;
}

int luaD_precall(lua_State *L, StkId func, int nresults) {
  StkId base;
  Proto *p = clLp(func);  // 获取Lua闭包的Proto

  // 检查栈空间
  if (!luaD_checkstack(L, p->maxstacksize))
    return 0;

  // 调整参数
  if (p->is_vararg)
    base = adjust_varargs(L, p, narg);
  else
    base = func + 1;  // 固定参数

  // 创建新的CallInfo
  CallInfo *ci = next_ci(L);
  ci->func = func;
  ci->u.l.base = base;
  ci->u.l.savedpc = p->code;  // 从函数开头开始
  ci->nresults = nresults;
  ci->callstatus = CIST_LUA;

  // 切换到新函数
  L->ci = ci;
  L->top = base + p->maxstacksize;
  return 0;
}

4.4 C函数调用

int luaD_precall(lua_State *L, StkId func, int nresults) {
  lua_CFunction func = clCvalue(func)->f;
  int n = (*func)(L);  // 调用C函数
  // 将返回值放到正确位置
  luaD_poscall(L, func, n, nresults);
  return 1;
}

4.5 返回处理

int luaD_poscall(lua_State *L, StkId func, int nresults, int n) {
  StkId base = func + 1;
  StkId ret = (nresults == LUA_MULTRET) ? L->top.p : base + nresults - 1;

  // 将返回值移动到调用者期望的位置
  for (int i = 0; i < n && i < nresults; i++) {
    setobjs2s(L, base + i, ret - i);
  }

  // 恢复调用栈
  CallInfo *ci = L->ci;
  L->ci = ci->previous;  // 弹出当前CallInfo
  L->top = ci->func + nresults;  // 设置栈顶

  return (nresults != LUA_MULTRET && nresults < 0);
}

5. 寄存器式 vs 栈式 VM

特性 栈式(JVM/CPython) 寄存器式(Lua)
操作数 隐式从栈弹出 显式指定寄存器索引
指令数 多(需要push/pop) (直接引用)
指令宽度 1-2字节 4字节(固定)
执行效率 较低 较高
编译器复杂度 较低 较高(需分配寄存器)
代码密度 中等

Lua 的优势:寄存器式VM减少了大量临时变量的push/pop操作,更适合嵌入式场景。


6. 特殊指令详解

6.1 OP_MMBIN 的连锁效应

-- Lua 5.4 中 a + b 的字节码
-- 先尝试快速路径:
-- OP_ADD  R[a] R[b] R[c]
-- OP_MMBIN  0 0 0    ← 如果快速路径失败,跳到这里调用元方法
-- 实际执行:如果OP_ADD失败(没有快速路径),直接执行OP_MMBIN
-- OP_MMBIN成功后跳过下一条指令(pc++)

6.2 OP_LFALSESKIP

-- 用于将条件表达式转为布尔值:cond and true or false
-- OP_LFALSESKIP R[a]  → R[a] = false; pc++ (跳过下一条true)
-- OP_LOADTRUE R[a]    → 如果执行到这里,R[a] = true

6.3 OP_TAILCALL

-- 尾调用优化:如果一个函数的最后操作是调用另一个函数,且不需要处理返回值,
-- 可以重用当前栈帧,避免栈增长。
-- OP_TAILCALL R[a] R[b] R[c]
-- → 调用 R[a](R[a+1], ..., R[a+B-1]),结果直接覆盖当前函数的返回位置

7. 本篇小结

概念 要点
代码生成 lcode.c 在解析中直接生成字节码
指令格式 32位固定宽度,5种格式
k标志 区分寄存器和常量操作数
操作码 83条:加载/运算/调用/跳转/闭包/循环/元方法
VM模型 寄存器式,寄存器=值栈槽位
Dispatch 跳转表高效分发
调用链 precall → 函数帧切换 → poscall
5.4新增 OP_ADDI等立即数优化、OP_TBC(to-be-closed变量)、分代GC支持
元方法 OP_MMBIN 与运算指令配对

思考题

  1. 为什么Lua选择寄存器式VM而非栈式?这在嵌入式场景下有什么优势?
  2. 常量折叠在什么情况下不适用?(提示:元方法)
  3. 尾调用(OP_TAILCALL)是如何优化递归的?
  4. 如果要给Lua添加新的运算符(如幂赋值 a **= b),需要修改哪些源文件?
  5. OP_EQI 和 OP_EQK 有什么区别?什么情况下使用哪条?
  6. OP_SETLIST 如何处理大规模表构造器 {1,2,3,...,50000}
  7. OP_TBC 在5.4中引入了什么新特性?它与OP_CLOSE有什么区别?

思考题解答

1. 为什么Lua选择寄存器式VM而非栈式?

寄存器式VM的优势(尤其在嵌入式场景):

特性 栈式VM 寄存器式VM
指令数 多(需要push/pop) 少30-50%
执行效率 较低 较高
指令宽度 1-2字节(可变) 4字节(固定)

具体例子a = b + c

栈式VM(多条指令):

LOAD b     -- push b
LOAD c     -- push c
ADD        -- pop b,c; push b+c
STORE a    -- pop result; store to a

寄存器式VM(1条指令):

ADD R[a] R[b] R[c]

嵌入式场景优势:更少的指令意味着更少的解码开销、更少的内存访问、更紧凑的字节码占用ROM/RAM。代价是编译器需要更复杂的寄存器分配算法。

2. 常量折叠在什么情况下不适用?

常量折叠的条件:两个操作数都是编译时已知的常量值。

不适用的情况

  1. 元方法存在时:如果操作数有元表,1 + "hello" 应该调用 __add 元方法而非在编译期报错。常量折叠无法检测元表。
  2. 字符串连接"a" .. "b" 需要分配新字符串,编译期做可能影响GC。
  3. 除零1 / 0 在编译期折叠会导致运行时行为不一致(浮点除零产生inf而非错误)。
  4. 溢出检查math.maxinteger + 1 的结果取决于运行时数值类型。
  5. 自定义操作符:通过元方法重载的运算符,编译器无法在编译期计算。

实际策略:只处理两个整数或两个浮点的纯算术运算。

3. 尾调用如何优化递归?

尾调用:函数的最后一步是调用另一个函数,且不需要处理其返回值。

普通调用:
  栈帧1: f(x) -> 调用g -> 栈帧2: g(x) -> 返回 -> 栈帧2销毁

尾调用优化:
  栈帧1: f(x) -> 调用g -> 栈帧2: g(x)
  栈帧2直接复用栈帧1的空间,不需要额外栈空间

递归函数如果使用尾递归,栈空间不会增长,可以处理无限递归而不溢出。

4. 新增运算符需要修改哪些文件?

假设新增幂赋值 a **= b

文件 修改内容
llex.h 定义新token TK_POW_ASSIGN
llex.c 识别 **= 字符序列
lparser.h 可能需要新的赋值类型枚举
lparser.c 在赋值解析中处理 **=
lcode.c 生成 OP_POW + 赋值的字节码序列
lopcodes.h 如果需要OP_POWA指令则添加(通常不需要)

5. OP_EQI 和 OP_EQK 的区别

操作码 B操作数来源 C操作数来源 使用场景
OP_EQI 立即数sB(有符号) 标志位(是否浮点) a == 1a == 0等小整数
OP_EQK 常量表K[B] 标志位 a == "hello"a == 3.14等大值

选择依据:立即数用OP_EQI(8位有符号,范围-128~127),超出范围或非整数用OP_EQK。

6. OP_SETLIST 如何处理大规模表构造器?

{1, 2, 3, ..., 50000}

机制

  • 编译器每累积 LFIELDS_PER_FLUSH(50)个元素后生成一条 OP_SETLIST 指令
  • 最后一次调用可能处理剩余元素
  • 如果元素超过C字段的8位容量(255),使用 EXTRAARG 指令扩展
OP_SETLIST  R[a] 50 1      -- 设置 R[a][1..50] = R[a+1..R[a+50]]
OP_SETLIST  R[a] 50 2      -- 设置 R[a][51..100] = R[a+51..R[a+100]]
...
OP_SETLIST  R[a] 50 1000   -- 设置 R[a][4951..5000] = ...

优化:避免了为每个元素生成单独的SETTABLE指令,大幅减少指令数。

7. OP_TBC 与 OP_CLOSE 的区别

指令 含义 用途
OP_TBC 标记变量为"to-be-closed" 在变量作用域结束时自动关闭上值
OP_CLOSE 关闭所有>=R[A]的上值 显式关闭(如块结束、错误处理)

OP_TBC 是5.4新增,实现了 __close 元方法(类似C++的RAII/defer):

local fd <close> = io.open("file.txt")
-- 当fd离开作用域时,自动调用fd:close()
-- 即使发生错误也会关闭

OP_CLOSE 是传统的上值关闭机制,在块结束或错误传播时调用。OP_TBC 允许用户自定义关闭行为(通过 __close 元方法),而 OP_CLOSE 只是简单地复制值并关闭上值。

posted @ 2026-09-04 17:26  IcarusLee  阅读(5)  评论(0)    收藏  举报