LuaJIT
源码剖析
编译
词法分析+语法分析+字节码生成
Lua这三个阶段不是分开进行的,一个statement先进行词法分析(
lex_scan
)得到当前LexToken,然后根据当前LexToken和前置LexToken进行语法分析和字节码生成,这两步是同时进行的。所以语法分析一部分(例如函数的参数)代码的同时会将该部分的机器码emit到寄存器,不是语法分析一整个statment结束之后再统一emit字节码。注释及长注释在词法分析(
lex_scan
)获得LexToken时跳过的。
需求:屏蔽print函数调用
屏蔽程序代码中的print函数调用,想到了四个方案:
在静态代码检查时做一个print函数的检测,如果发现有print函数调用,则自动开单给责任人处理。
未采用原因:感觉这个方案治标不治本,而且开单给别人处理比较麻烦。
在词法分析时,如果检测到TK_name的值是"print",做一个词法分析的特殊逻辑,将整个函数调用的LexToken跳过。
未采用原因:该方案要对所有的TK_name进行
memcmp
,不太合理。而且分析函数调用的LexToken范围,属于语法分析的逻辑, 不应该在词法分析阶段做。在语法分析+字节码生成时,在
parse_call_assign
中如果检测到函数调用的函数名是"print",将expr_primary
替换成新写的语法分析逻辑,只保留语法分析部分,不生成函数调用的字节码。未采用原因:由于LuaJIT的
parse_call_assign
源码实现,该方案需要修改主表达式解析expr_primary
以及将函数调用是否为"print"的结果从expr_primary
透传到parse_call_assign
进行相关字节码处理,改变了luaJIT现有的函数定义、代码结构的一贯性。又考虑到保留print()
的字节码性能影响很小,因此没有采用该方方案。在语法分析+字节码生成时,如果检测到函数调用的函数名是"print",将
parse_args
替换成新写的语法分析逻辑,只保留语法分析部分,不生成参数的字节码(即最终print(...)
生成的字节码实际上是函数print()
)。最终采用该方案来完成需求。
实现:
在主表达式解析逻辑
expr_primary
中,如果LexToken是全局变量,则检测其是否为"print",如果表达式是一个函数调用且检测命中,则执行discard_args
分支替代parse_args
分支。实现了新的语法分析逻辑:
discard_args
,skip_args
,skip_expr_simple
,skip_expr_unop
,skip_expr
源码:
/* Forward declaration. */ static BinOpr skip_expr(LexState *ls); static void skip_args(LexState *ls) { const BCLine line = ls->linenumber; if (ls->tok == '(') { #if !LJ_52 if (line != ls->lastline) err_syntax(ls, LJ_ERR_XAMBIG); #endif lj_lex_next(ls); if (ls->tok != ')') { /* f(...). */ do { skip_expr(ls); } while (lex_opt(ls, ',')); } lex_match(ls, ')', '(', line); } else { err_syntax(ls, LJ_ERR_XFUNARG); return; /* Silence compiler. */ } } /* Discard function argument list. */ static void discard_args(LexState *ls, ExpDesc *e) { FuncState *fs = ls->fs; const BCLine line = ls->linenumber; skip_args(ls); lua_assert(e->k == VNONRELOC); const BCReg base = e->u.s.info; /* Base register for call. */ BCIns ins = BCINS_ABC(BC_CALL, base, 2, fs->freereg - base - LJ_FR2); expr_init(e, VCALL, bcemit_INS(fs, ins)); e->u.s.aux = base; fs->bcbase[fs->pc - 1].line = line; fs->freereg = base+1; /* Leave one result by default. */ } /* Parse primary expression. */ static void expr_primary(LexState *ls, ExpDesc *v) { FuncState *fs = ls->fs; uint8_t detect_print = 0; /* Parse prefix expression. */ if (ls->tok == '(') { BCLine line = ls->linenumber; lj_lex_next(ls); expr(ls, v); lex_match(ls, ')', '(', line); expr_discharge(ls->fs, v); } else if (ls->tok == TK_name || (!LJ_52 && ls->tok == TK_goto)) { var_lookup(ls, v); if (v->k == VGLOBAL) detect_print = v->u.sval->len == 5 && memcmp((const char*)(v->u.sval + 1), "print", 5) == 0; } else { err_syntax(ls, LJ_ERR_XSYMBOL); } for (;;) { /* Parse multiple expression suffixes. */ if (ls->tok == '.') { expr_field(ls, v); } else if (ls->tok == '[') { ExpDesc key; expr_toanyreg(fs, v); expr_bracket(ls, &key); expr_index(fs, v, &key); } else if (ls->tok == ':') { ExpDesc key; lj_lex_next(ls); expr_str(ls, &key); bcemit_method(fs, v, &key); parse_args(ls, v); } else if (ls->tok == '(' || ls->tok == TK_string || ls->tok == '{') { expr_tonextreg(fs, v); if (LJ_FR2) bcreg_reserve(fs, 1); if (detect_print && ls->tok == '(') /* Ignore args of print(...). */ discard_args(ls, v); else parse_args(ls, v); } else { break; } } } static void skip_expr_simple(LexState *ls) { switch (ls->tok) { case TK_number: case TK_string: case TK_nil: case TK_true: case TK_false: case TK_dots: break; case '{': /* Table constructor. */ BCLine line = ls->linenumber; lex_check(ls, '{'); while (ls->tok != '}') { if (ls->tok == '[') { lj_lex_next(ls); /* Skip '['. */ skip_expr(ls); lex_check(ls, ']'); lex_check(ls, '='); } else if ((ls->tok == TK_name || (!LJ_52 && ls->tok == TK_goto)) && lj_lex_lookahead(ls) == '=') { lj_lex_next(ls); lex_check(ls, '='); } skip_expr(ls); if (!lex_opt(ls, ',') && !lex_opt(ls, ';')) break; } lex_match(ls, '}', '{', line); return; case TK_function: err_syntax(ls, LJ_ERR_BADVAL); /* function define not supported. */ return; default: if (ls->tok == '(') { BCLine line = ls->linenumber; lj_lex_next(ls); skip_expr(ls); lex_match(ls, ')', '(', line); } else if (ls->tok == TK_name || (!LJ_52 && ls->tok == TK_goto)) { lj_lex_next(ls); } else { err_syntax(ls, LJ_ERR_XSYMBOL); } for (;;) { /* Parse multiple expression suffixes. */ if (ls->tok == '.') { lj_lex_next(ls); lj_lex_next(ls); } else if (ls->tok == '[') { lj_lex_next(ls); /* Skip '['. */ skip_expr(ls); lex_check(ls, ']'); } else if (ls->tok == ':') { lj_lex_next(ls); lj_lex_next(ls); skip_args(ls); } else if (ls->tok == '(' || ls->tok == TK_string || ls->tok == '{') { skip_args(ls); } else { break; } } return; } lj_lex_next(ls); } static void skip_expr_unop(LexState *ls) { if (ls->tok == TK_not || ls->tok == '-' || ls->tok == '#') { lj_lex_next(ls); skip_expr(ls); } else { skip_expr_simple(ls); } } static BinOpr skip_expr(LexState *ls) { skip_expr_unop(ls); BinOpr op = token2binop(ls->tok); while (op != OPR_NOBINOPR) { lj_lex_next(ls); op = skip_expr(ls); } return op; }
Sideeffect:
不支持在print参数中进行函数定义:由于在参数语法分析时,如果遇到了函数定义,需要调用
parse_stat
解析(最上层的语法分析入口),从parse_stat
到parse_args
中间一系列的处理都需要额外实现。而其他的语法都仅需要调用expr
进行表达式解析。考虑到print参数中进行函数定义虽然语法允许但没有实际意义,因此将该种情况实现为了语法报错LJ_ERR_BADVAL
。e.g.
部署:从项目稳定性的角度,并没有部署上述方案。原因:
print信息在内网是非常重要的调试测试手段,如果从编译参数上屏蔽了print,会导致内网外网运行不一样的LuaJIT代码分支,存在外网问题难以在内网测试暴露的隐患。而且,也不可能因为这个需求划分单独的内网服务器开启该屏蔽print的编译参数。
外网服务器也常常会收集print输出流用于调试。
评论
发表评论