当前位置: 首页 > news >正文

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

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

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

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函数代码如下:

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函数中有两个关键调用:

argn = collectargs(argv, &flags);
s->status = runargs(L, argv, argn);

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

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等,该函数代码如下:

/* 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  -bgnarg++;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压入参数,argv[0][2]为真表示 -b后面还有字符, 如 -bl命令,并将 -bl替换成 -l再压入参数。通过循环压入了文件名参数,先不管luajit内部怎么获取到bcsave脚本的,我们直接进入bcsave.lua查看源码:

-- Public module functions.
return {start = docmd -- Process -b command line option.
}

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

local function docmd(...)local arg = {...}local n = 1local list = falselocal ctx = {strip = true, arch = jit.arch, os = string.lower(jit.os),type = false, modname = false,}while n <= #arg dolocal a = arg[n]if type(a) == "string" and string.sub(a, 1, 1) == "-" and a ~= "-" thentable.remove(arg, n)if a == "--" then break endfor m=2,#a dolocal opt = string.sub(a, m, m)if opt == "l" thenlist = trueelseif opt == "s" thenctx.strip = trueelseif opt == "g" thenctx.strip = falseelseif arg[n] == nil or m ~= #a then usage() endif opt == "e" thenif n ~= 1 then usage() endarg[1] = check(loadstring(arg[1]))elseif opt == "n" thenctx.modname = checkmodname(table.remove(arg, n))elseif opt == "t" thenctx.type = checkarg(table.remove(arg, n), map_type, "file type")elseif opt == "a" thenctx.arch = checkarg(table.remove(arg, n), map_arch, "architecture")elseif opt == "o" thenctx.os = checkarg(table.remove(arg, n), map_os, "OS name")elseusage()endendendelsen = n + 1endendif list thenif #arg == 0 or #arg > 2 then usage() endbclist(arg[1], arg[2] or "-")elseif #arg ~= 2 then usage() endbcsave(ctx, arg[1], arg[2])end
end

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

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函数:

local function readfile(input)if type(input) == "function" then return input endif input == "-" then input = nil endreturn check(loadfile(input))
end

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

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函数定义如下:

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函数的代码:

local function savefile(name, mode)if name == "-" then return io.stdout endreturn check(io.open(name, mode))
end

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

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

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

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

-- Return one bytecode line.
local function bcline(func, pc, prefix)local ins, m = funcbc(func, pc)if not ins then return endlocal 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 -- BCMjumpreturn format("%s=> %04d\n", s, pc+d-0x7fff)endif mb ~= 0 thend = band(d, 0xff)elseif mc == 0 thenreturn s.."\n"endlocal kcif mc == 10*128 then -- BCMstrkc = funck(func, -d-1)kc = format(#kc > 40 and '"%.40s"~' or '"%s"', gsub(kc, "%c", ctlsub))elseif mc == 9*128 then -- BCMnumkc = funck(func, d)if op == "TSETM " then kc = kc - 2^52 endelseif mc == 12*128 then -- BCMfunclocal fi = funcinfo(funck(func, -d-1))if fi.ffid thenkc = vmdef.ffnames[fi.ffid]elsekc = fi.locendelseif mc == 5*128 then -- BCMuvkc = funcuvname(func, d)endif ma == 5 then -- BCMuvlocal ka = funcuvname(func, a)if kc then kc = ka.." ; "..kc else kc = ka endendif mb ~= 0 thenlocal b = shr(ins, 24)if kc then return format("%s%3d %3d  ; %s\n", s, b, d, kc) endreturn format("%s%3d %3d\n", s, b, d)endif kc then return format("%s%3d      ; %s\n", s, d, kc) endif mc == 7*128 and d > 32767 then d = d - 65536 end -- BCMlitsreturn 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符号名的转换。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Java 入门指南:Java 并发编程 —— JMM Java内存模型
  • sql-labs靶场(41-50)
  • Linux主机网络参数的设置—IP地址的作用和类型
  • 十一头像红旗怎么弄的?3个方法轻松教会你!
  • 贴梗海棠T2T基因组-文献精读40
  • 网优学习干货:2.6G仿真操作(2)
  • PTA - C语言接口题集2
  • 算法练习题06:leetcode793每日温度
  • 04:布局规划
  • 一个php快速项目搭建框架源码,带一键CURD等功能
  • DRF——serializer中获取嵌套评论
  • 力扣135-分发糖果(java详细题解)
  • 《黑神话:悟空》:30%抽成真相
  • 使用ffmpeg+node-media-server实现从rtsp服务器拉流再推送至rtmp服务器,实现http+flv进行web播放
  • 线性查找表的应用:用户登录注册程序
  • [数据结构]链表的实现在PHP中
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • 【剑指offer】让抽象问题具体化
  • AHK 中 = 和 == 等比较运算符的用法
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • cookie和session
  • Flex布局到底解决了什么问题
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • javascript从右向左截取指定位数字符的3种方法
  • jQuery(一)
  • OSS Web直传 (文件图片)
  • Vue2 SSR 的优化之旅
  • 闭包,sync使用细节
  • 跨域
  • 浅谈web中前端模板引擎的使用
  • 用mpvue开发微信小程序
  • ​插件化DPI在商用WIFI中的价值
  • ​七周四次课(5月9日)iptables filter表案例、iptables nat表应用
  • ​如何在iOS手机上查看应用日志
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • (2)STL算法之元素计数
  • (2)空速传感器
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (C语言版)链表(三)——实现双向链表创建、删除、插入、释放内存等简单操作...
  • (k8s)Kubernetes本地存储接入
  • (pytorch进阶之路)扩散概率模型
  • (vue)el-cascader级联选择器按勾选的顺序传值,摆脱层级约束
  • (二开)Flink 修改源码拓展 SQL 语法
  • (附源码)springboot美食分享系统 毕业设计 612231
  • (三)Honghu Cloud云架构一定时调度平台
  • (三十)Flask之wtforms库【剖析源码上篇】
  • (微服务实战)预付卡平台支付交易系统卡充值业务流程设计
  • (未解决)jmeter报错之“请在微信客户端打开链接”
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • (一)硬件制作--从零开始自制linux掌上电脑(F1C200S) <嵌入式项目>
  • (原創) 人會胖會瘦,都是自我要求的結果 (日記)
  • ***汇编语言 实验16 编写包含多个功能子程序的中断例程
  • .[hudsonL@cock.li].mkp勒索加密数据库完美恢复---惜分飞
  • .L0CK3D来袭:如何保护您的数据免受致命攻击
  • .net 7和core版 SignalR