joe 发表于 2022-6-20 15:59:40

Lua源码分析 - 虚拟机篇 - 语义解析之编译过程(16)

目录

一、虚拟机篇 - 编译过程的核心数据结构

二、虚拟机篇 - 指令集存储结构Instruction

三、虚拟机篇 - statlist状态机实现

四、虚拟机篇 - 通过IF语句示例看执行过程

上一章节,讲解了语法的解析功能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上。

https://img-blog.csdnimg.cn/20200402155152621.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2luaXRwaHA=,size_16,color_FFFFFF,t_70

/*
** 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 = i;
/* save corresponding line information */
luaM_growvector(fs->ls->L, f->lineinfo, fs->pc, f->sizelineinfo, int,
                  MAX_INT, "opcodes");
f->lineinfo = 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函数,进行递归遍历
https://img-blog.csdnimg.cn/20200401190901864.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2luaXRwaHA=,size_16,color_FFFFFF,t_70

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}
*
* END
*/
static void ifstat (LexState *ls, int line) {
/* ifstat -> IF cond THEN block {ELSEIF cond THEN 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 -> 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);
}

————————————————
版权声明:本文为CSDN博主「老码农zhuli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/initphp/article/details/105247775

页: [1]
查看完整版本: Lua源码分析 - 虚拟机篇 - 语义解析之编译过程(16)