如何用 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.c
和 test.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()
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)
如何查找操作码
我看到这个答案引起了一些兴趣,所以我想补充一些我一开始遇到困难的事情,即如何找到你想要执行的指令的操作码。网上有一些资源,最著名的是 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(它非常小,您甚至可以轻松地部署它)是一个很好的方式,可以在一个包中兼顾两全其美:)
这个:
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.c
和 test.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()
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)
如何查找操作码
我看到这个答案引起了一些兴趣,所以我想补充一些我一开始遇到困难的事情,即如何找到你想要执行的指令的操作码。网上有一些资源,最著名的是 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(它非常小,您甚至可以轻松地部署它)是一个很好的方式,可以在一个包中兼顾两全其美:)