Lua源码分析 - 虚拟机篇 - 语义解析之编译过程(16)
目录虚拟机篇 - 编译过程的核心数据结构虚拟机篇 - 指令集存储结构Instruction上一章节,讲解了语法的解析功能luaX_next,这一章节主要讲解虚拟机代码编译成操作码的过程。虚拟机篇 - 编译过程的核心数据结构我们首先看下,Lua核心虚拟机实现的几个重要文件:llex.c 语义分割器、lparse.c 语法树解析器、lcode.c 可执行代码生成整个Lua代码编...
目录
上一章节,讲解了语法的解析功能luaX_next,这一章节主要讲解虚拟机代码编译成操作码的过程。
一、虚拟机篇 - 编译过程的核心数据结构
我们首先看下,Lua核心虚拟机实现的几个重要文件:llex.c 语义分割器、lparse.c 语法树解析器、lcode.c 可执行代码生成
整个Lua代码编译的过程,主要在lparse.c文件中实现,入口函数为:luaY_parser 。
Lua的代码是一边解析,一边编译,生成二进制字节码指令Opcode的,Opcode会放置在FuncState->Proto结构中的code数组上
Lua文件的解析通过LexState结构来管理整体的语法解析状态;语法块和函数的编译,则通过FuncState结构来管理;编译出来的二进制指令集,则保存在Proto结构中的code数组上:
-
LexState:语法分析上下文状态。该结构贯穿编译过程的始终,主要存储解析和编译过程中的语法树状态。
-
FuncState:函数编译状态。该结构主要存储语法块、函数等编译过程状态信息。
-
Proto:主要存放二进制的字节码指令集(Opcode)。字节码指令集主要存储在code的数组上;FuncState->pc指向下一个code的地址。Proto结构挂载在FuncState函数栈状态结构上。
/*
* 语法分析上下文状态
* state of the lexer plus state of the parser when shared by all
functions
*/
typedef struct LexState {
int current; /* 解析字符指针 current character (charint) */
int linenumber; /* 行数计数器 input line counter */
int lastline; /* 最后一行 line of last token 'consumed' */
Token t; /* 当前Token current token */
Token lookahead; /* 头部Token look ahead token */
struct FuncState *fs; /* 当前解析的方法 current function (parser) */
struct lua_State *L; //Lua栈
ZIO *z; /* io输入流 input stream */
Mbuffer *buff; /* buffer for tokens */
Table *h; /* to avoid collection/reuse strings */
struct Dyndata *dyd; /* dynamic structures used by the parser */
TString *source; /* 当前源名称 current source name */
TString *envn; /* 环境变量 environment variable name */
} LexState;
/**
* 存储函数编译状态
*/
typedef struct FuncState {
Proto *f; /* 存放Opcode current function header */
struct FuncState *prev; /* enclosing function */
struct LexState *ls; /* 词法状态 lexical state */
struct BlockCnt *bl; /* 当前块链 chain of current blocks */
int pc; /* 代码的下一个位置,指向Proto->code中的数组指针 next position to code (equivalent to 'ncode') */
int lasttarget; /* 'label' of last 'jump label' */
int jpc; /* 即将跳转的pc列表 list of pending jumps to 'pc' */
int nk; /* number of elements in 'k' */
int np; /* number of elements in 'p' */
int firstlocal; /* index of first local var (in Dyndata array) */
short nlocvars; /* number of elements in 'f->locvars' */
lu_byte nactvar; /* number of active local variables */
lu_byte nups; /* number of upvalues */
lu_byte freereg; /* first free register */
} FuncState;
/*
** Function Prototypes
** Proto主要存放二进制指令集Opcode
** Lua在解析函数的过程中,会将一条条语句逐个‘编译’成指令集
*/
typedef struct Proto {
CommonHeader;
lu_byte numparams; /* 定参个数 number of fixed parameters */
lu_byte is_vararg;
lu_byte maxstacksize; /* 栈个数 number of registers needed by this function */
int sizeupvalues; /* size of 'upvalues' */
int sizek; /* size of 'k' */
int sizecode; //code个数
int sizelineinfo;
int sizep; /* size of 'p' */
int sizelocvars;
int linedefined; /* debug information */
int lastlinedefined; /* debug information */
TValue *k; /* 常量表 constants used by the function */
Instruction *code; /* 存储指令集数组 opcodes */
struct Proto **p; /* functions defined inside the function */
int *lineinfo; /* map from opcodes to source lines (debug information) */
LocVar *locvars; /* information about local variables (debug information) */
Upvaldesc *upvalues; /* upvalue information */
struct LClosure *cache; /* last-created closure with this prototype */
TString *source; /* used for debug information */
GCObject *gclist;
} Proto;
二、虚拟机篇 - 指令集存储结构Instruction
Proto中的code主要是存储字节码指令集的数组。code的类型是Instruction,而Instruction在宏定义中是一个32位的unsigned int类型。
其中,前面6位放置Opcode操作指令,8位放置操作指令A,9位放置操作指令B,9位放置操作指令C
在lcode.c中,我们可以找到luaK_codeABC函数,主要封装了指令的生成函数。CREATE_ABC定义了宏生成操作指令。luaK_code函数将指令设置到Proto->code上。
/*
** type for virtual-machine instructions;
** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h)
*/
#if LUAI_BITSINT >= 32
typedef unsigned int Instruction;
#else
typedef unsigned long Instruction;
#endif
#define CREATE_ABC(o,a,b,c) ((cast(Instruction, o)<<POS_OP) \
| (cast(Instruction, a)<<POS_A) \
| (cast(Instruction, b)<<POS_B) \
| (cast(Instruction, c)<<POS_C))
/*
** Format and emit an 'iABC' instruction. (Assertions check consistency
** of parameters versus opcode.)
*/
int luaK_codeABC (FuncState *fs, OpCode o, int a, int b, int c) {
lua_assert(getOpMode(o) == iABC);
lua_assert(getBMode(o) != OpArgN || b == 0);
lua_assert(getCMode(o) != OpArgN || c == 0);
lua_assert(a <= MAXARG_A && b <= MAXARG_B && c <= MAXARG_C);
return luaK_code(fs, CREATE_ABC(o, a, b, c));
}
/*
** Emit instruction 'i', checking for array sizes and saving also its
** line information. Return 'i' position.
** Opcode存放在Proto结构上
** 其中f->code数组用于存放code
** fs->pc主要是计数器,标记code的个数及数组下标
*/
static int luaK_code (FuncState *fs, Instruction i) {
Proto *f = fs->f;
dischargejpc(fs); /* 'pc' will change */
/* put new instruction in code array */
luaM_growvector(fs->ls->L, f->code, fs->pc, f->sizecode, Instruction,
MAX_INT, "opcodes");
f->code[fs->pc] = i;
/* save corresponding line information */
luaM_growvector(fs->ls->L, f->lineinfo, fs->pc, f->sizelineinfo, int,
MAX_INT, "opcodes");
f->lineinfo[fs->pc] = fs->ls->lastline;
return fs->pc++;
}
三、虚拟机篇 - statlist状态机实现
Lua语言解析和编译过程的入口函数是luaY_parser,而真正实现状态机的是在mainfunc函数中的statlist。
statlist函数,主要通过while语句实现状态机的循环。通过luaX_next,逐个切割出语义Token,通过状态机循环,将语义转化成语法块,并逐个编译成二进制可执行指令Opcode。
statlist有几个关键要点:
- 状态机是通过语法块,然后逐块将代码解析成Opcode的。代码块如:if语句、for循环、function函数、表达式等。
- 语法块的判断是根据ls->t.token Token的值来确定的,根据不同的Token确定不同语法块的起始位置。
- 状态机中也会调用luaX_next函数,不断切割Lua的语法Token,并在状态机中进行解析 + 编译。
- 语法块中,经常会遇到嵌套的语法块,这个时候会回调statlist函数,进行递归遍历
statlist函数中,只有遇到return标识的时候,状态机循环才会中断,否则会持续运行到语法解析编译完毕。
/**
* 语法树解析
*/
static void statlist (LexState *ls) {
/* statlist -> { stat [';'] } */
while (!block_follow(ls, 1)) {
if (ls->t.token == TK_RETURN) {
statement(ls);
return; /* 最后一个语法块 'return' must be last statement */
}
statement(ls);
}
}
statement函数主要状态机的执行函数。通过switch方法,针对不同的Token值,执行不同的语法块处理。
Lua语言是有作用域的概念的。所以进入一个语法块的时候,会执行enterlevel,离开一个语法块的时候,调用leavelevel函数。
如果命中了某一个语法块,则进入对应的语法块处理逻辑(例如ifstat);默认情况下,会进入表达式的处理流程exprstat。
/**
* 解析语法树,按照块状分割
*/
static void statement (LexState *ls) {
int line = ls->linenumber; /* may be needed for error messages */
enterlevel(ls); //作用域
switch (ls->t.token) {
case ';': { /* stat -> ';' (empty statement) */
luaX_next(ls); /* skip ';' */
break;
}
case TK_IF: { /* stat -> ifstat */
ifstat(ls, line);
break;
}
case TK_WHILE: { /* stat -> whilestat */
whilestat(ls, line);
break;
}
......
case TK_RETURN: { /* stat -> retstat */
luaX_next(ls); /* skip RETURN */
retstat(ls);
break;
}
case TK_BREAK: /* stat -> breakstat */
case TK_GOTO: { /* stat -> 'goto' NAME */
gotostat(ls, luaK_jump(ls->fs));
break;
}
//表达式处理
default: { /* stat -> func | assignment */
exprstat(ls);
break;
}
}
lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg &&
ls->fs->freereg >= ls->fs->nactvar);
ls->fs->freereg = ls->fs->nactvar; /* free registers */
leavelevel(ls); //作用域
}
四、虚拟机篇 - 通过IF语句示例看执行过程
我们接下去通过一个if语句的示例,看Lua代码的解析编译的过程。
//Lua代码
if a > b then
age = a
else
age = b
end
//Opcode
0: LT 0,b,a
1: JMP 4
2: MOV age, a
3: JMP 5
4: MOV age, b
Lua文件解析后,拿到的第一个Token为TK_IF,所以在statement函数中,会调用ifstat。
/**
* 解析语法树,按照块状分割
*/
static void statement (LexState *ls) {
int line = ls->linenumber; /* may be needed for error messages */
enterlevel(ls); //作用域
switch (ls->t.token) {
case ';': { /* stat -> ';' (empty statement) */
luaX_next(ls); /* skip ';' */
break;
}
case TK_IF: { /* stat -> ifstat */
ifstat(ls, line);
break;
}
....
}
ifstat函数中,主要通过test_then_block函数,处理IF (cond) THEN block的语法问题。
通过while循环遍历elseif语句,elseif的处理方式也是采用test_then_block方法。
/**
* 解析if语句:
* IF (cond) THEN block
* {ELSEIF (cond) THEN block}
* [ELSE block]
* END
*/
static void ifstat (LexState *ls, int line) {
/* ifstat -> IF cond THEN block {ELSEIF cond THEN block} [ELSE block] END */
FuncState *fs = ls->fs; //获取函数栈
int escapelist = NO_JUMP; /* exit list for finished parts */
/* 首先解析 if (条件) then 逻辑块 end */
test_then_block(ls, &escapelist); /* IF cond THEN block */
/* 然后解析 elseif (条件) then 逻辑块 end*/
while (ls->t.token == TK_ELSEIF)
test_then_block(ls, &escapelist); /* ELSEIF cond THEN block */
/* 最后解析 else 逻辑块 end */
if (testnext(ls, TK_ELSE))
block(ls); /* 'else' part */
check_match(ls, TK_END, TK_IF, line);
luaK_patchtohere(fs, escapelist); /* patch escape list to 'if' end */
}
test_then_block函数中,通过luaX_next跳过IF/ELSEIF的token,然后调用expr方法处理条件问题。
条件语句最终会返回true/false的结果值,这个结果值会保存到expdesc结构中。expdesc结构贯穿整个Parse解析编译阶段,主要用于保存每次预发解析的详情信息的传递。
处理完条件语句后,会调用checknext,跳过THEN这个关键字Token,然后继续往下处理。
由于IF语句是一个判断语句,需要根据条件判断的情况,进行执行代码的跳转。这边是通过luaK_goiftrue函数,生成JMP类型的操作码,然后这个操作码最终也会调用luaK_codeABC函数,生成二进制代码,然后放到Proto上。
针对块的内容的解析,会递归回调statlist函数。因为块中的内容,有可能是简答的表达式赋值,也有可能是嵌套的函数、IF语句、FOR循环等。Lua也是有作用域的,所以进入一个块的回调,会执行enterblock函数,离开则执行leaveblock函数。我们看到,我们的IF语句中的块内容是一个表达式,调用statlist后,会进入表达式的处理函数exprstat中。
/**
* 解析:
* if (条件) then 逻辑块 end
* elseif (条件) then 逻辑块 end
*/
static void test_then_block (LexState *ls, int *escapelist) {
/* test_then_block -> [IF | ELSEIF] cond THEN block */
BlockCnt bl;
FuncState *fs = ls->fs;
expdesc v;
int jf; /* instruction to skip 'then' code (if condition is false) */
luaX_next(ls); /* skip IF or ELSEIF */
expr(ls, &v); /* 条件语句读取,返回结果值存储在v中 read condition */
checknext(ls, TK_THEN); //下一个Token
if (ls->t.token == TK_GOTO || ls->t.token == TK_BREAK) {
luaK_goiffalse(ls->fs, &v); /* will jump to label if condition is true */
enterblock(fs, &bl, 0); /* must enter block before 'goto' */
gotostat(ls, v.t); /* handle goto/break */
while (testnext(ls, ';')) {} /* skip colons */
if (block_follow(ls, 0)) { /* 'goto' is the entire block? */
leaveblock(fs);
return; /* and that is it */
}
else /* must skip over 'then' part if condition is false */
jf = luaK_jump(fs);
}
else { /* regular case (not goto/break) */
luaK_goiftrue(ls->fs, &v); /* skip over block if condition is false */
enterblock(fs, &bl, 0); //用于管理递归块管理
jf = v.f;
}
statlist(ls); /* 状态机继续解析IF语句块内语义 'then' part */
leaveblock(fs);
if (ls->t.token == TK_ELSE ||
ls->t.token == TK_ELSEIF) /* followed by 'else'/'elseif'? */
luaK_concat(fs, escapelist, luaK_jump(fs)); /* must jump over it */
luaK_patchtohere(fs, jf);
}
exprstat普通表达式处理逻辑相对比较清晰。主要俩步骤:1. 处理变量名称,可能有多个变量名(LHS_assign) 2. 进行变量赋值,生成Opcode
suffixedexp函数会将变量名信息存储在LHS_assign v结构上。assignment就是真正变量赋值操作,也就是生成Opcode操作。
assignment函数中,主要通过luaK_storevar对变量进行设置值。luaK_storevar函数底层也是调用的luaK_codeABC函数,当然不同类型的变量处理逻辑也是有一些不同的。
Lua的变量逻辑:
- Lua 中的变量全是全局变量,无论语句块或是函数里,除非用 local 显式声明为局部变量,变量默认值均为nil
- 使用local创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。
/**
* 普通表示式处理逻辑
*/
static void exprstat (LexState *ls) {
/* stat -> func | assignment */
FuncState *fs = ls->fs;
struct LHS_assign v; //处理多个值
suffixedexp(ls, &v.v); //处理变量名
/* 变量赋值处理 */
if (ls->t.token == '=' || ls->t.token == ',') { /* stat -> assignment ? */
v.prev = NULL;
assignment(ls, &v, 1); //赋值
}
else { /* stat -> func */
check_condition(ls, v.v.k == VCALL, "syntax error");
SETARG_C(getinstruction(fs, &v.v), 1); /* call statement uses no results */
}
}
/**
* 变量赋值操作
* ls:语法解析上下文状态
* lh:变量名称存储在expdesc结构中,链表形式,可以存储多个变量名
* nvars:值的个数
*/
static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) {
expdesc e;
check_condition(ls, vkisvar(lh->v.k), "syntax error");
if (testnext(ls, ',')) { /* assignment -> ',' suffixedexp assignment */
.........
}
else { /* assignment -> '=' explist */
int nexps;
checknext(ls, '='); //跳转到下一个Token
nexps = explist(ls, &e);
if (nexps != nvars)
adjust_assign(ls, nvars, nexps, &e); //调整 判断左边的变量数是否等于右边的值数
else {
luaK_setoneret(ls->fs, &e); /* close last expression */
luaK_storevar(ls->fs, &lh->v, &e);
return; /* avoid default */
}
}
init_exp(&e, VNONRELOC, ls->fs->freereg-1); /* default assignment */
luaK_storevar(ls->fs, &lh->v, &e);
}
更多推荐
所有评论(0)