LuaJit分析(六)luajit -bl 命令分析

Luajit -bl命令用于将luajit字节码文件或者lua脚本文件反汇编,输出汇编指令,很好奇怎么将字节码文件和lua脚本文件放在一块处理的,下面一步步分析:

luajit虚拟机由luajit.c文件生成,首先定位到main函数,代码如下:

cpp 复制代码
int main(int argc, char **argv)
{
  int status;
  lua_State *L = lua_open();
  if (L == NULL) {
    l_message(argv[0], "cannot create state: not enough memory");
    return EXIT_FAILURE;
  }
  smain.argc = argc;
  smain.argv = argv;
  status = lua_cpcall(L, pmain, NULL);
  report(L, status);
  lua_close(L);
  return (status || smain.status > 0) ? EXIT_FAILURE : EXIT_SUCCESS;
}

进行一些初始化操作后,若没出现异常,则调用pmain函数,pmian函数代码如下:

cpp 复制代码
static int pmain(lua_State *L)
{
  struct Smain *s = &smain;
  char **argv = s->argv;
  int argn;
  int flags = 0;
  globalL = L;
  if (argv[0] && argv[0][0]) progname = argv[0];
  LUAJIT_VERSION_SYM();  /* Linker-enforced version check. */
  argn = collectargs(argv, &flags);
  if (argn < 0) {  /* Invalid args? */
    print_usage();
    s->status = 1;
    return 0;
  }
  if ((flags & FLAGS_NOENV)) {
    lua_pushboolean(L, 1);
    lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
  }
  /* Stop collector during library initialization. */
  lua_gc(L, LUA_GCSTOP, 0);
  luaL_openlibs(L);
  lua_gc(L, LUA_GCRESTART, -1);
  createargtable(L, argv, s->argc, argn);
  if (!(flags & FLAGS_NOENV)) {
    s->status = handle_luainit(L);
    if (s->status != LUA_OK) return 0;
  }
  if ((flags & FLAGS_VERSION)) print_version();
  s->status = runargs(L, argv, argn);
  if (s->status != LUA_OK){
    return 0;
  }
  if (s->argc > argn) {
    s->status = handle_script(L, argv + argn);
    if (s->status != LUA_OK) return 0;
  }
  if ((flags & FLAGS_INTERACTIVE)) {
    print_jit_status(L);
    dotty(L);
  } else if (s->argc == argn && !(flags & (FLAGS_EXEC|FLAGS_VERSION))) {
    if (lua_stdin_is_tty()) {
      print_version();
      print_jit_status(L);
      dotty(L);
    } else {
      dofile(L, NULL);  /* Executes stdin as a file. */
    }
  }
  return 0;
}

pmain函数中有两个关键调用:

cpp 复制代码
argn = collectargs(argv, &flags);
s->status = runargs(L, argv, argn);

分别用于获取输入的参数和获取参数后执行,runargs代码如下:

cpp 复制代码
static int runargs(lua_State *L, char **argv, int argn)
{
  int i;
  for (i = 1; i < argn; i++) {
    if (argv[i] == NULL) continue;
    lua_assert(argv[i][0] == '-');
    switch (argv[i][1]) {
    case 'e': {
      const char *chunk = argv[i] + 2;
      if (*chunk == '\0') chunk = argv[++i];
      lua_assert(chunk != NULL);
      if (dostring(L, chunk, "=(command line)") != 0)
  return 1;
      break;
      }
    case 'l': {
      const char *filename = argv[i] + 2;
      if (*filename == '\0') filename = argv[++i];
      lua_assert(filename != NULL);
      if (dolibrary(L, filename))
  return 1;
      break;
      }
    case 'j': {  /* LuaJIT extension. */
      const char *cmd = argv[i] + 2;
      if (*cmd == '\0') cmd = argv[++i];
      lua_assert(cmd != NULL);
      if (dojitcmd(L, cmd))
  return 1;
      break;
      }
    case 'O':  /* LuaJIT extension. */
      if (dojitopt(L, argv[i] + 2))
  return 1;
      break;
    case 'b':  /* LuaJIT extension. */
      return dobytecode(L, argv+i);
    default: break;
    }
  }
  return LUA_OK;
}

runarg函数根据获取的参数执行对应的操作,现在我们关心的是 -bl命令,可以看到只要命令的第一个字节为b时,进入dobytecode函数,这就包括了 -b -bl -bg等,该函数代码如下:

cpp 复制代码
/* Save or list bytecode. */
static int dobytecode(lua_State *L, char **argv)
{
  int narg = 0;
  lua_pushliteral(L, "bcsave");
  if (loadjitmodule(L))
    return 1;
  if (argv[0][2]) {      // -b 后面有其它的参数 如 -bl  -bg
    narg++;
    argv[0][1] = '-';
    lua_pushstring(L, argv[0]+1);
  }
  for (argv++; *argv != NULL; narg++, argv++){
    lua_pushstring(L, *argv);
  }
  report(L, lua_pcall(L, narg, 0, 0));
  return -1;
}

从注释可以看出,该函数用于保存或反汇编字节码,它通过bcsave.lua脚本执行操作,先通过lua_pushstring压入参数,argv02为真表示 -b后面还有字符, 如 -bl命令,并将 -bl替换成 -l再压入参数。通过循环压入了文件名参数,先不管luajit内部怎么获取到bcsave脚本的,我们直接进入bcsave.lua查看源码:

Lua 复制代码
-- Public module functions.
return {
  start = docmd -- Process -b command line option.
}

这个语句说明先执行docmd函数,从注释可以看出 改文件也是处理 -b类命令的,docmd代码如下:

Lua 复制代码
local function docmd(...)
  local arg = {...}
  local n = 1
  local list = false
  local ctx = {
    strip = true, arch = jit.arch, os = string.lower(jit.os),
    type = false, modname = false,
  }
  while n <= #arg do
    local a = arg[n]
    if type(a) == "string" and string.sub(a, 1, 1) == "-" and a ~= "-" then
        table.remove(arg, n)
        if a == "--" then break end
        for m=2,#a do
            local opt = string.sub(a, m, m)
            if opt == "l" then
              list = true
            elseif opt == "s" then
              ctx.strip = true
            elseif opt == "g" then
              ctx.strip = false
            else
              if arg[n] == nil or m ~= #a then usage() end
                  if opt == "e" then
                      if n ~= 1 then usage() end
                      arg[1] = check(loadstring(arg[1]))
                  elseif opt == "n" then
                    ctx.modname = checkmodname(table.remove(arg, n))
                  elseif opt == "t" then
                    ctx.type = checkarg(table.remove(arg, n), map_type, "file type")
                  elseif opt == "a" then
                    ctx.arch = checkarg(table.remove(arg, n), map_arch, "architecture")
                  elseif opt == "o" then
                    ctx.os = checkarg(table.remove(arg, n), map_os, "OS name")
                  else
                    usage()
                  end
            end
        end
    else
      n = n + 1
    end
  end
  if list then
    if #arg == 0 or #arg > 2 then usage() end
    bclist(arg[1], arg[2] or "-")
  else
    if #arg ~= 2 then usage() end
    bcsave(ctx, arg[1], arg[2])
  end
end

从中可以看出,当命令为 -l时,执行bclist函数,否则执行 bcsave函数。这篇文章我们只关心 -bl的处理过程,此时arg1为 -bl后的参数,即文件名,arg2为空时,输入 -,bclist代码如下:

Lua 复制代码
local function bclist(input, output)
  local f = readfile(input)
  require("jit.bc").dump(f, savefile(output, "w"), true)
end

它调用了jit/bc.lua中的dump函数,其中参数为readfile 和savefile返回的结果。先看readfile函数:

Lua 复制代码
local function readfile(input)
  if type(input) == "function" then return input end
  if input == "-" then input = nil end
  return check(loadfile(input))
end

readfile函数中,参数是function类型时,直接返回本身,lua中"-"和 nil都可以表示空,否则返回库函数 loadfile的结果。因此将源码和字节码统一处理的关键在于loadfile函数。loadfile库函数的定义在lib_base.c中,代码如下:

cpp 复制代码
LJLIB_CF(loadfile)
{
  GCstr *fname = lj_lib_optstr(L, 1);
  GCstr *mode = lj_lib_optstr(L, 2);
  int status;
  lua_settop(L, 3);  /* Ensure env arg exists. */
  status = luaL_loadfilex(L, fname ? strdata(fname) : NULL,
        mode ? strdata(mode) : NULL);
  return load_aux(L, status, 3);
}
Loadfile函数调动了luaL_loadfilex函数,该函数定义如下:
LUALIB_API int luaL_loadfilex(lua_State *L, const char *filename,
            const char *mode)
{
  FileReaderCtx ctx;
  int status;
  const char *chunkname;
  if (filename) {
    ctx.fp = fopen(filename, "rb");
    if (ctx.fp == NULL) {
      lua_pushfstring(L, "cannot open %s: %s", filename, strerror(errno));
      return LUA_ERRFILE;
    }
    chunkname = lua_pushfstring(L, "@%s", filename);
  } else {
    ctx.fp = stdin;
    chunkname = "=stdin";
  }
  status = lua_loadx(L, reader_file, &ctx, chunkname, mode);
  if (ferror(ctx.fp)) {
    L->top -= filename ? 2 : 1;
    lua_pushfstring(L, "cannot read %s: %s", chunkname+1, strerror(errno));
    if (filename)
      fclose(ctx.fp);
    return LUA_ERRFILE;
  }
  if (filename) {
    L->top--;
    copyTV(L, L->top-1, L->top);
    fclose(ctx.fp);
  }
  return status;
}

luaL_loadfilex根据传进来的filename读取文件后,实际调用lua_loadx函数,lua_loadx函数定义如下:

cpp 复制代码
LUA_API int lua_loadx(lua_State *L, lua_Reader reader, void *data,
          const char *chunkname, const char *mode)
{
  LexState ls;
  int status;
  ls.rfunc = reader;
  ls.rdata = data;
  ls.chunkarg = chunkname ? chunkname : "?";
  ls.mode = mode;
  lj_buf_init(L, &ls.sb);
  status = lj_vm_cpcall(L, NULL, &ls, cpparser);
  lj_lex_cleanup(L, &ls);
  lj_gc_check(L);
  return status;
}

lua_loadx实际是执行了cpparser函数,该函数判断读入的函数是lua脚本时,执行lj_parse函数进行词法转换。是luajit字节码文件时,执行lj_bcread读取字节码函数信息,因此最终返回的都是编译后的函数。cpparser的进一步跟踪参考字节码自定义的内容:

luajit自定义修改

下面是savefile函数的代码:

Lua 复制代码
local function savefile(name, mode)
  if name == "-" then return io.stdout end
  return check(io.open(name, mode))
end

当指定的参数为 "-"时,也就是 -bl后没有参数时,返回 stdout,即最后反汇编是在控制台输出,否则返回一个打开的文件标识符。

bc.lua中定义了 dump = bcdump,bcdump函数代码如下:

Lua 复制代码
-- Dump bytecode instructions of a function.
local function bcdump(func, out, all)
  if not out then out = stdout end
  local fi = funcinfo(func)
  if all and fi.children then
    for n=-1,-1000000000,-1 do
      local k = funck(func, n)
      if not k then break end
      if type(k) == "proto" then bcdump(k, out, true) end
    end
  end
  out:write(format("-- BYTECODE -- %s-%d\n", fi.loc, fi.lastlinedefined))
  local target = bctargets(func)
  for pc=1,1000000000 do
    local s = bcline(func, pc, target[pc] and "=>")
    if not s then break end
    out:write(s)
  end
  out:write("\n")
  out:flush()
end

从备注可以看出就是输出字节码指令,这是一个递归函数,先根据传入的第一个参数,使用funcinfo函数获取函数信息,funcinfo是一个库函数,位于jit.util. funcinfo,如果它有孩子,则递归调用全部为原型类型的孩子。它通过循环获取所有的指令,并调用bcline函数输出每一行的指令,bcline函数代码如下:

Lua 复制代码
-- Return one bytecode line.
local function bcline(func, pc, prefix)
  local ins, m = funcbc(func, pc)
  if not ins then return end
  local ma, mb, mc = band(m, 7), band(m, 15*8), band(m, 15*128)
  local a = band(shr(ins, 8), 0xff)
  local oidx = 6*band(ins, 0xff)
  local op = sub(bcnames, oidx+1, oidx+6)
  local s = format("%04d %s %-6s %3s ",
    pc, prefix or "  ", op, ma == 0 and "" or a)
  local d = shr(ins, 16)
  if mc == 13*128 then -- BCMjump
    return format("%s=> %04d\n", s, pc+d-0x7fff)
  end
  if mb ~= 0 then
    d = band(d, 0xff)
  elseif mc == 0 then
    return s.."\n"
  end
  local kc
  if mc == 10*128 then -- BCMstr
    kc = funck(func, -d-1)
    kc = format(#kc > 40 and '"%.40s"~' or '"%s"', gsub(kc, "%c", ctlsub))
  elseif mc == 9*128 then -- BCMnum
    kc = funck(func, d)
    if op == "TSETM " then kc = kc - 2^52 end
  elseif mc == 12*128 then -- BCMfunc
    local fi = funcinfo(funck(func, -d-1))
    if fi.ffid then
      kc = vmdef.ffnames[fi.ffid]
    else
      kc = fi.loc
    end
  elseif mc == 5*128 then -- BCMuv
    kc = funcuvname(func, d)
  end
  if ma == 5 then -- BCMuv
    local ka = funcuvname(func, a)
    if kc then kc = ka.." ; "..kc else kc = ka end
  end
  if mb ~= 0 then
    local b = shr(ins, 24)
    if kc then return format("%s%3d %3d  ; %s\n", s, b, d, kc) end
    return format("%s%3d %3d\n", s, b, d)
  end
  if kc then return format("%s%3d      ; %s\n", s, d, kc) end
  if mc == 7*128 and d > 32767 then d = d - 65536 end -- BCMlits
  return format("%s%3d\n", s, d)
end

从函数注释即可以看出是输出字节码指令的一行,它通过bcnames得到opcode的具体值,通过vmdef.ffnames得到调用的库函数的函数名bcnames和ffnames都在vmdef.lua中定义,同时该文件由buildvm.c自动生成,vmdef.lua中完整定义了各个指令下标的opcode名和各个库函数的符号名

**总结:**当执行luajit -bl命令时,实际上是调用的 jit/bcsave.lua文件,通过loadfile库函数加载文件,经过luajit处理后,得到原型数据。接着调用bc.lua中的bcdump函数,递归解析各个原型,通过bcline函数,解析各个指令,bcline函数中用到了vmdef.lua中定义的opcode和各个库函数的符号名,这些符号名由buildvm.c在编译过程中自动生成。因此vmdef.lua是在反汇编时用于opcode下标到opcode符号名的转换。

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json