发布一款应用程序比较耗时,尤其是手机游戏应用还需要各种审查。一种简单方便的热更新,可以满足上述需求。静态编程语言生成框架,动态语言完成其他逻辑,这样可以达到热更新。lua由于其性能及简洁,是许多项目热更新时选择的动态开发语言。此文主要讲解C/C++和Lua的混合编程,主要针对Lua5.2及之后的版本(之前的版本接口略有调整)。
直接在指定目录执行以下命令即可完成编译,会生成liblua.a(静态库),lua(解释器),luac(编译器)。
curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
tar zxf lua-5.4.4.tar.gz
cd lua-5.4.4
make all test
从官方路径https://www.lua.org/versions.html,下载相应Lua版本的源代码,解压到指定目录。
打开Visual Studio,新建一个C++静态库工程,将onlua.c文件(见附件)添加到工程中,并在C/C+±>Additional Include Directores框中指定Lua源代码目录。然后在C/C+±>Preprocessor->Preprocessor Definitions框中填入MAKE_LIB,编译生成liblua.lib静态库。
打开Visual Studio,新建一个C++静态库工程,将onlua.c文件(见附件)添加到工程中,并在C/C+±>Additional Include Directores框中指定Lua源代码目录。然后在C/C+±>Preprocessor->Preprocessor Definitions框中填入MAKE_LUA,编译生成lua.exe解释器。
打开Visual Studio,新建一个C++静态库工程,将onlua.c文件(见附件)添加到工程中,并在C/C+±>Additional Include Directores框中指定Lua源代码目录。然后在C/C+±>Preprocessor->Preprocessor Definitions框中填入MAKE_LUAC,编译生成luac.exe编译器。
lua代码中添加my = require(“XXXX”),会按照搜索路径先搜索lua模块,再搜索名为XXXX的动态库。
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
int Add(lua_State* L)
{
lua_Integer a = lua_tointeger(L, 1);
lua_Integer b = lua_tointeger(L, 2);
lua_pushinteger(L, a + b);
return 1;
}
int Sub(lua_State* L)
{
lua_Integer a = lua_tointeger(L, 1);
lua_Integer b = lua_tointeger(L, 2);
lua_pushinteger(L, a - b);
return 1;
}
static luaL_Reg CustomLib[] =
{
{"Add", Add},
{"Sub", Sub},
{NULL, NULL}
};
// Custom必须和动态库名字保持一致,必须导出C风格
extern "C" int luaopen_Custom(lua_State* L)
{
luaL_newlib(L, CustomLib);
return 1;
}
mylib = require("Custom")
print(mylib.Add(1, 2))
print(mylib.Sub(3, 2))
指定动态库目录
-- windows
package.cpath = "dir/?.dll;"..package.cpath
-- linux
package.cpath = "dir\?.dll;"..package.cpath
lua_State* L = luaL_newstate();
luaL_openlibs(L);
if (0 != luaL_loadfile(L, strFilePath.c_str()))
{
printf("%s\n", lua_tostring(L,-1));
return 1;
}
int bRet = lua_pcall(L, 0, 0, 0);
if (0 != bRet)
{
printf("%s\n", lua_tostring(L, -1));
return 2;
}
char* pLua = "print(123)";
if (0 != luaL_dostring(L, pLua))
{
printf("%s\n", lua_tostring(L,-1));
return 3;
}
// 待Lua调用的C注册函数。
int Add(lua_State* L)
{
// 检查栈中的参数是否合法,1表示Lua调用时的第一个参数(从左到右),依此类推。
// 如果Lua代码在调用时传递的参数不为number,该函数将报错并终止程序的执行。
INT64 op1 = luaL_checkinteger(L,1);
INT64 op2 = luaL_checkinteger(L,2);
// 将函数的结果压入栈中。如果有多个返回值,可以在这里多次压入栈中。
lua_pushinteger(L,op1 + op2);
// 返回值用于提示该C函数的返回值数量,即压入栈中的返回值数量。
return 1;
}
int Trace(lua_State *L)
{
int n = lua_gettop(L); /* number of arguments */
int i;
for (i = 1; i <= n; i++) { /* for each argument */
size_t l;
const char *s = luaL_tolstring(L, i, &l); /* convert it to string */
if (i > 1) /* not the first element? */
OutputDebugString(" ");
OutputDebugString(s);
lua_pop(L, 1); /* pop result */
}
return 0;
}
// 在luaL_openlibs后调用
lua_register(L, "Add", Add);
lua_register(L, "Trace", Trace);
print(Add(1, 2))
print(Sub(2, 1))
void SetValueOfVar(LPCSTR _lpcVarName, INT64 _nVal)
{
lua_getglobal(L, _lpcVarName);
lua_pop(L, -1);
lua_pushinteger(L, _nVal);
lua_setglobal(L, _lpcVarName);
}
void CLuaEngine::WriteItemOfTable(LPCSTR _lpcTableName, LPCSTR _lpcItemName, LPCSTR _lpcItem)
{
lua_getglobal(L, _lpcTableName);
lua_getfield(L, -1, _lpcItemName);
lua_pushstring(L, _lpcItem);
lua_setfield(L, 1, _lpcItemName);
lua_pop(L, 1);
}
CString ReadItemOfTable(LPCSTR _lpcTableName, LPCSTR _lpcItemName)
{
lua_getglobal(m_lState, _lpcTableName);
lua_getfield(m_lState, -1, _lpcItemName);
const char* pName = lua_tostring(m_lState, -1);
return pName;
}
void traverse_table(lua_State *L, int index)
{
lua_pushnil(L);
// 现在的栈:-1 => nil; index => table
while (lua_next(L, index))
{
// 现在的栈:-1 => value; -2 => key; index => table
// 拷贝一份 key 到栈顶,然后对它做 lua_tostring 就不会改变原始的 key 值了
lua_pushvalue(L, -2);
// 现在的栈:-1 => key; -2 => value; -3 => key; index => table
const char* key = lua_tostring(L, -1);
const char* value = lua_tostring(L, -2);
printf("%s => %s\n", key, value);
// 弹出 value 和拷贝的 key,留下原始的 key 作为下一次 lua_next 的参数
lua_pop(L, 2);
// 现在的栈:-1 => key; index => table
}
// 现在的栈:index => table (最后 lua_next 返回 0 的时候它已经把上一次留下的 key 给弹出了)
// 所以栈已经恢复到进入这个函数时的状态
}
static luaL_Reg CustomLib[] =
{
{"Add", Add},
{"Sub", Sub},
{NULL, NULL}
};
static int luaopen_algorithm(lua_State* L)
{
// 创建导出库函数
luaL_newlib(L, CustomLib);
return 1;
}
void pre_loadlibs(lua_State* L)
{
// 预加载扩展静态库
luaL_getsubtable(L, LUA_REGISTRYINDEX, "_PRELOAD");
lua_pushcfunction(L, luaopen_algorithm);
lua_setfield(L, -2, "algo"); // algo为库名
lua_pop(L, 1);
}
my = require("algo")
print(my.Add(3, 8))
function AddEx(x,y)
return x+y
end
lua_getglobal(L, "AddEx"); //lua_getglobal函数负责从全局表中找到那个“AddEx”字段对应的数据,并把它送到代码块的栈顶
lua_pushnumber(L, 5); // 把参数x压入到我们L虚拟机的栈中,至此栈顶数据是x,接着是function,也就是我们的“AddEx”函数
lua_pushnumber(L, 7); // 同上,把参数y压入L虚拟机栈中,栈顶y,接着x,再往下就是function,也就是“AddEx”
lua_call(L, 2, 1); // 2个参数,1个返回值
// 取出返回值
std::cout <<"返回的值是:"<< (lua_tointeger(L,-1)) << std::endl;
if (0 != luaL_dostring(L, "AddEx(3, 4)"))
{
printf("%s\n", lua_tostring(L,-1));
return 3;
}
传递一个byte类型的Buff,并且提供下标操作。
typedef struct _BYTE_ARRAY
{
unsigned int nSize;
char* pBuff;
}BYTE_ARRAY;
static int ByteArrayConstructor(lua_State * l)
{
unsigned int size = static_cast<unsigned int>(luaL_checkinteger(l, 1));
// We could actually allocate Foo itself as a user data but
// since user data can be GC'ed and we gain unity by using CRT's heap
// all along.
BYTE_ARRAY * udata = (BYTE_ARRAY *)lua_newuserdata(l, sizeof(BYTE_ARRAY));
(udata)->nSize = size;
(udata)->pBuff = new char[size]();
// Usually, we'll just use "Foo" as the second parameter, but I
// say luaL_Foo here to distinguish the difference:
//
// This 2nd parameter here is an _internal label_ for luaL, it is
// _not_ exposed to Lua by default.
//
// Effectively, this metatable is not accessible by Lua by default.
luaL_getmetatable(l, "luaL_ByteArray");
// The Lua stack at this point looks like this:
//
// 3| metatable "luaL_foo" |-1
// 2| userdata |-2
// 1| string parameter |-3
//
// So the following line sets the metatable for the user data to the luaL_Foo
// metatable
//
// We must set the metatable here because Lua prohibits setting
// the metatable of a userdata in Lua. The only way to set a metatable
// of a userdata is to do it in C.
lua_setmetatable(l, -2);
// The Lua stack at this point looks like this:
//
// 2| userdata |-1
// 1| string parameter |-2
//
// We return 1 so Lua callsite will get the user data and
// Lua will clean the stack after that.
return 1;
}
BYTE_ARRAY * ByteArrayCheck(lua_State * l, int n)
{
// This checks that the argument is a userdata
// with the metatable "luaL_Foo"
return (BYTE_ARRAY *)luaL_checkudata(l, n, "luaL_ByteArray");
}
static int ByteArraySet(lua_State * l)
{
BYTE_ARRAY * foo = ByteArrayCheck(l, 1);
int nIdx = (int)luaL_checkinteger(l, 2);
int nVal = (int)luaL_checkinteger(l, 3);
luaL_argcheck(l, nIdx <= foo->nSize, 1, "index out of range");
foo->pBuff[nIdx-1] = char(nVal);
// The Lua stack at this point looks like this:
//
// 4| result string |-1
// 3| metatable "luaL_foo" |-2
// 2| userdata |-3
// 1| string parameter |-4
//
// Return 1 to return the result string to Lua callsite.
return 0;
}
static int ByteArrayGet(lua_State * l)
{
BYTE_ARRAY * foo = ByteArrayCheck(l, 1);
int nIdx = (int)luaL_checkinteger(l, 2);
luaL_argcheck(l, nIdx <= foo->nSize, 1, "index out of range");
lua_pushinteger(l, foo->pBuff[nIdx-1]);
// The Lua stack at this point looks like this:
//
// 4| result string |-1
// 3| metatable "luaL_foo" |-2
// 2| userdata |-3
// 1| string parameter |-4
//
// Return 1 to return the result string to Lua callsite.
return 1;
}
static int ByteArrayDestructor(lua_State * l)
{
BYTE_ARRAY * foo = ByteArrayCheck(l, 1);
delete[] foo->pBuff;
return 0;
}
luaL_Reg sFooRegs[] =
{
{ "new", ByteArrayConstructor },
{ "set", ByteArraySet },
{ "get", ByteArrayGet },
{ "__gc", ByteArrayDestructor },
{ NULL, NULL }
};
int luaopen_my(lua_State *L) {
luaL_newlib(L, sFooRegs);
return 1;
}
static void RegisterFoo(lua_State * l)
{
// Create a luaL metatable. This metatable is not
// exposed to Lua. The "luaL_Foo" label is used by luaL
// internally to identity things.
luaL_newmetatable(l, "luaL_ByteArray");
// Register the C functions _into_ the metatable we just created.
luaL_setfuncs (l, sFooRegs, 0);
// The Lua stack at this point looks like this:
//
// 1| metatable "luaL_Foo" |-1
lua_pushvalue(l, -1);
// The Lua stack at this point looks like this:
//
// 2| metatable "luaL_Foo" |-1
// 1| metatable "luaL_Foo" |-2
// Set the "__index" field of the metatable to point to itself
// This pops the stack
//lua_setfield(l, -1, "__index");
// The Lua stack at this point looks like this:
//
// 1| metatable "luaL_Foo" |-1
// The luaL_Foo metatable now has the following fields
// - __gc
// - __index
// - add
// - new
// 那么现在metatable在栈底,array表在其上的位置
// metatable.__index = array.get
lua_pushliteral(l, "__index");
lua_pushliteral(l, "get");
lua_gettable(l, 2);
lua_settable(l, 1);
// metatable.__index = array.set
lua_pushliteral(l, "__newindex");
lua_pushliteral(l, "set");
lua_gettable(l, 2);
lua_settable(l, 1);
// Now we use setglobal to officially expose the luaL_Foo metatable
// to Lua. And we use the name "Foo".
//
// This allows Lua scripts to _override_ the metatable of Foo.
// For high security code this may not be called for but
// we'll do this to get greater flexibility.
lua_setglobal(l, "ByteArray");
}
local byteArray = ByteArray.new(100);
byteArray[2] = 33;
print(byteArray[2])
应用程序内嵌入lua虚拟机执行lua代码时,没有太好的方法调试lua代码。通过打印信息来查看代码运行的效果,调试效率不高。有没有一种方法,可以单步调试lua代码呢?腾讯开源了一款基于VS Code调试的插件luapanda,使用简单方便。
// 如果文件加密,可以在此处解密文件Buff(szLuaBuff)
if (0 != luaL_loadbuffer(L, szLuaBuff, nFileSize, NULL))
{
printf("%s\n", lua_tostring(L,-1));
return 1;
}
对源代码的加密,通过OD追踪到luaL_loadbuffer,然后dump出所有Buff,即为代码明文。为了更进一步加强反破解,可以采用运行字节码,然后用修改OpCode来加强代码安全。