是否可以在包含对 C 函数的引用的 lua 字节码字符串上调用 loadstring?

Posted

技术标签:

【中文标题】是否可以在包含对 C 函数的引用的 lua 字节码字符串上调用 loadstring?【英文标题】:Is it possible to call loadstring on string of lua bytecode that contains a reference to a C function? 【发布时间】:2012-06-07 15:16:31 【问题描述】:

我们正在使用向 Lua 公开图形 API 的 Love2d Lua 游戏引擎。我们正在尝试序列化一个包含游戏世界所有保存游戏数据的巨型哈希表。此哈希包含一些函数,其中一些函数调用 Love2d C 函数。

为了序列化散列中的函数,我们使用 string.dump,并使用 loadstring 重新加载它们。这适用于纯 Lua 函数,但是当我们尝试序列化然后加载回一个调用包装 C 函数的函数(例如 Love2d api 中的函数)时,loadstring 返回 nil。

考虑以下通过 Love2d 的图形引擎将“hello, world”绘制到屏幕上的简单程序:

function love.load()
    draw = function()
        love.graphics.print('hello, world', 10, 10)
    end
end
function love.draw()
    draw()
end

我们希望能够做到这一点:

function love.load()
    draw_before_serialize = function()
        love.graphics.print('hello, world', 10, 10)
    end

    out = io.open("serialized.lua", "wb")
    out:write('draw = load([[' .. string.dump(draw_before_serialize) .. ']])')
    out:close()

    require "serialized"
end
function love.draw()
    draw()
end

执行此操作会写入磁盘上的 Lua 文件,该文件包含未编译的 Lua 和 Lua 字节码的混合,如下所示:

draw = load([[^[LJ^A^@      
       @main.lua2^@^@^B^@^B^@^D^E^B^B4^@^@^@%^A^A^@>^@^B^AG^@^A^@^Qhello, world 
       print^A^A^A^B^@^@]])

此方法适用于不调用 C 模块的 Lua 函数。我们认为这是问题所在,因为此示例确实有效:

function love.load()
    draw_before_serialize = function()
        print('hello, world')
    end

    out = io.open("serialized.lua", "wb")
    out:write('draw = load([[' .. string.dump(draw_before_serialize) .. ']])')
    out:close()

    require "serialized"
end
function love.draw()
    draw()
end

它不调用 Love2d 图形方法,而是打印到控制台。

经过更多测试,我们困惑地发现这个例子确实有效:

function love.load()
    draw_before_serialize = function()
        love.graphics.print('hello, world', 10, 10)
    end

    draw = load(string.dump(draw_before_serialize))
end
function love.draw()
    draw()
end

这里我们实际上并没有将函数写到磁盘上,而是直接转储它,然后立即将它加载回来。我们认为可能是罪魁祸首没有在设置了二进制写入模式标志 ("wb") 的情况下写出数据,但由于我们在 Linux 上,所以这个标志没有任何作用。

有什么想法吗?

【问题讨论】:

“打印到控制台”它打印什么?还有,你确定上面代码使用的全局环境和require使用的全局环境是一样的吗? [因为您依赖于在全局环境中定义的draw] 您应该被告知:[[' .. string.dump(draw_before_serialize) .. ']] 不一定会起作用。您获得的转储可能包含任何内容,包括]] 字符。这会提前终止字符串,从而破坏事情。 @NicolBolas 我曾经看到一个简单而聪明的解决方案,它只需检查字符串中的](=*)],然后用一个== 的最大数量多一个=找到匹配项。 【参考方案1】:

我认为问题在于字符串的格式。 Nicol Bolas 对围绕字节码转储的 [[]] 引号可能是正确的,但这指出了一个更大的问题;字节码实际上可以是任何东西,但您将其视为可以写入和读取文本文件的普通字符串。这个问题在您的上一个演示中得到了证明,您在其中加载转储的字符串而无需将其写入文件。

This 为包含函数的表实现序列化程序可以满足您的需求,但我也认为它已损坏(好吧,无论如何我都无法正常工作......)。无论如何,它在正确的轨道上。您需要格式化字节码并然后将其写入文件。

我确信有更好的方法可以做到这一点,但这很有效:

1.    binary = string.dump(some_function)
2.    formatted_binary = ""
3.    for i = 1, string.len(binary) do
4.        dec, _ = ("\\%3d"):format(binary:sub(i, i):byte()):gsub(' ', '0')
5.        formatted_binary = formatted_binary .. dec
6.    end

这将遍历字节码中的每个字符,将它们格式化为转义字节(每个字符都是包含类似“\097”的代码的字符串,在插值时会转义为“a”)。

此示例的第 4 行有点密集,因此我将对其进行分解。首先,

binary:sub(i, i)

从字符串中拉出第 i 个字符。那么

binary:sub(i, i):byte()

返回第 i 个字符的 ascii 整数表示。然后我们将其格式化为

("\\%3d"):format(binary:sub(i, i):byte())

它给了我们一个像“\ 97”这样的字符串,例如,如果字符是“a”。但这不会正确转义,因为我们需要“\097”,所以我们用“0”替换“”。 gsub 返回结果字符串和执行的替换次数,所以我们只取第一个返回值并将其放入“dec”中。我不确定为什么默认情况下“%3d”格式不会用“0”替换空格......哦,好吧。

然后为了执行格式化的二进制字符串,我们需要对其进行转义并将结果传递给“load”。 Lua 中的怪异 [[]] 引号不会像 "" 那样进行转义......事实上,我不确定他们是否会进行任何转义。因此,为了创建一个可执行的 Lua 字符串,该字符串将返回一个函数,该函数将执行“some_function”中的任何操作,我们这样做:

executable_string = 'load("' .. formatted_binary .. '")'

好的 - 将所有这些放在一起,我认为我们可以让您的测试用例像这样工作:

  1 function love.load()
  2     draw_before_serialize = function()
  3         love.graphics.print('hello, world', 10, 10)
  4     end
  5 
  6     binary = string.dump(draw_before_serialize)
  7     formatted_binary = ""
  8     for i = 1, string.len(binary) do
  9         dec, _ = ("\\%3d"):format(binary:sub(i, i):byte()):gsub(' ', '0')
 10         formatted_binary = formatted_binary .. dec
 11     end
 12     
 13     out = io.open("serialized.lua", "wb")
 14     out:write('draw = load("' .. formatted_binary .. '")')
 15     out:close()
 16     
 17     require "serialized"
 18 end 
 19 function love.draw()
 20     draw()
 21 end

当我用 Love 运行它时,我得到一个 OpenGL 屏幕,角落里印有“hello world”。生成的文件“serialized.lua”包含以下内容:

draw = load("\027\076\074\001\000\009\064\109\097\105\110\046\108\117\097\084\000\000\004\000\004\000\008\009\002\002\052\000\000\000\055\000\001\000\055\000\002\000\037\001\003\000\039\002\010\000\039\003\010\000\062\000\004\001\071\000\001\000\017\104\101\108\108\111\044\032\119\111\114\108\100\010\112\114\105\110\116\013\103\114\097\112\104\105\099\115\009\108\111\118\101\001\001\001\001\001\001\001\002\000\000")

【讨论】:

反对有这么难吗?我有一个序列化字符串,几乎与您发布的这个字符串类似,我想将其更改为基于人类的语言,以便对该字符串生成的脚本进行一些小的更改。

以上是关于是否可以在包含对 C 函数的引用的 lua 字节码字符串上调用 loadstring?的主要内容,如果未能解决你的问题,请参考以下文章

Dalvik字节码

字节码引用检测原理与实战

编译lua代码,存储字节码然后加载并执行

Java字节码的执行是由啥完成的?

Lua,处理非ascii字节流,字节序变化

Lua中调用C函数(lua-5.2.3)