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_argsskip_argsskip_expr_simpleskip_expr_unopskip_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_statparse_args中间一系列的处理都需要额外实现。而其他的语法都仅需要调用expr进行表达式解析。考虑到print参数中进行函数定义虽然语法允许但没有实际意义,因此将该种情况实现为了语法报错LJ_ERR_BADVAL

    • e.g.


  • 部署:从项目稳定性的角度,并没有部署上述方案。原因:

    • print信息在内网是非常重要的调试测试手段,如果从编译参数上屏蔽了print,会导致内网外网运行不一样的LuaJIT代码分支,存在外网问题难以在内网测试暴露的隐患。而且,也不可能因为这个需求划分单独的内网服务器开启该屏蔽print的编译参数。

    • 外网服务器也常常会收集print输出流用于调试。

参考资料:

评论

此博客中的热门博文

Lua Gems