如何用 LuaJIT 定义 C 函数?

How to define C functions with LuaJIT?

这个:

local ffi = require "ffi"

ffi.cdef[[
  int return_one_two_four(){
    return 124;
  }
]]

local function print124()
  print(ffi.C.return_one_two_four())
end

print124()

抛出错误:

Error: main.lua:10: cannot resolve symbol 'return_one_two_four': The specified procedure could not be found.

我对 C 有一定的了解,想利用它的一些优点来做一些事情,但我在 LuaJIT 的 FFI 库中找不到很多示例。似乎 cdef 仅用于函数声明而不用于定义。如何在 C 中创建函数然后在 Lua 中使用它们?

LuaJIT 包含 C 声明的识别器,但它不是成熟的 C 编译器。它的 FFI 系统的目的是能够定义特定 DLL 导出的 C 函数,以便它可以加载该 DLL(通过 ffi.load)并允许您从 Lua.

LuaJIT 可以通过基于 DLL C 的接口加载预编译代码,但不能编译 C 本身。

LuaJIT 是一个 Lua 编译器,但不是 C 编译器。您必须先将 C 代码编译到共享库中。例如

gcc -shared -fPIC -o libtest.so test.c
luajit test.lua

文件 test.ctest.lua 如下。

test.c

int return_one_two_four(){
    return 124;
}

test.lua

local ffi = require"ffi"

local ltest = ffi.load"./libtest.so"

ffi.cdef[[
int return_one_two_four();
]]

local function print124()
    print(ltest.return_one_two_four())
end

print124()

Live example on Wandbox

LuaJIT

中的 JIT

在问题下的评论中,有人提到了一种解决方法,即用机器代码编写函数并在 Windows 上的 LuaJIT 中执行它们。实际上,通过在 LuaJIT 中实现 JIT,在 Linux 中也是可能的。在 Windows 上,您可以将操作码插入字符串,将其转换为函数指针并调用它,但由于页面限制,在 Linux 上无法做到这一点。在Linux上,内存要么是可写的,要么是可执行的table,但不能同时两者,所以我们必须以读写模式分配一个页面,插入程序集,然后将模式更改为读取-执行。为此,只需使用 Linux 内核函数来获取页面大小和映射内存。但是,即使您犯了最微小的错误,例如其中一个操作码中的拼写错误,程序也会出现段错误。我使用的是 64 位汇编,因为我使用的是 64 位操作系统。

重要提示: 在您的机器上执行此操作之前,请检查 <bits/mman-linux.h> 中的幻数。它们在每个系统上都不相同。

local ffi = require"ffi"

ffi.cdef[[
typedef unsigned char uint8_t;
typedef long int off_t;

// from <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t length);
int mprotect(void *addr, size_t len, int prot);

// from <unistd.h>
int getpagesize(void);
]]

-- magic numbers from <bits/mman-linux.h>
local PROT_READ     = 0x1  -- Page can be read.
local PROT_WRITE    = 0x2  -- Page can be written.
local PROT_EXEC     = 0x4  -- Page can be executed.
local MAP_PRIVATE   = 0x02 -- Changes are private.
local MAP_ANONYMOUS = 0x20 -- Don't use a file.

local page_size = ffi.C.getpagesize()
local prot = bit.bor(PROT_READ, PROT_WRITE)
local flags = bit.bor(MAP_ANONYMOUS, MAP_PRIVATE)
local code = ffi.new("uint8_t *", ffi.C.mmap(ffi.NULL, page_size, prot, flags, -1, 0))

local count = 0
local asmins = function(...)
    for _,v in ipairs{ ... } do
        assert(count < page_size)
        code[count] = v
        count = count + 1
    end
end

asmins(0xb8, 0x7c, 0x00, 0x00, 0x00) -- mov rax, 124
asmins(0xc3) -- ret

ffi.C.mprotect(code, page_size, bit.bor(PROT_READ, PROT_EXEC))

local fun = ffi.cast("int(*)(void)", code)
print(fun())

ffi.C.munmap(code, page_size)

Live example on Wandbox

如何查找操作码

我看到这个答案引起了一些兴趣,所以我想补充一些我一开始遇到困难的事情,即如何找到你想要执行的指令的操作码。网上有一些资源,最著名的是 Intel® 64 and IA-32 Architectures Software Developer Manuals but nobody wants to go through thousands of PDF pages just to find out how to do mov rax, 124. Therefore some people have made tables which list instructions and corresponding opcodes, e.g. http://ref.x86asm.net/,但在 table 中查找操作码也很麻烦,因为即使 mov 也可能有许多不同的操作码,具体取决于目标和源操作数是什么.所以我做的是写一个简短的汇编文件,例如

mov rax, 124
ret

您可能想知道,为什么我的汇编文件中没有函数,也没有 segment .text 之类的东西。好吧,因为我不想 link 它,我可以把所有这些都省掉,省去一些打字。然后 assemble 它使用

$ nasm -felf64 -l test.lst test.s

-felf64 选项告诉 assembler 我正在使用 64 位语法,-l test.lst 选项告诉我想在文件 test.lst。清单看起来类似于:

$ cat test.lst
     1 00000000 B87C000000              mov rax, 124
     2 00000005 C3                      ret

第三列包含我感兴趣的操作码。只需将它们拆分为 1 个字节的单元并将它们插入到您的程序中,即 B87C000000 变为 0xb8, 0x7c, 0x00, 0x00, 0x00(幸运的是十六进制数- Lua 不敏感,我更喜欢小写)。

从技术上讲,您可以轻松完成您想做的各种事情(只要代码足够简单)。 使用这样的东西: https://github.com/nucular/tcclua 使用 tcc(它非常小,您甚至可以轻松地部署它)是一个很好的方式,可以在一个包中兼顾两全其美:)