如何用LuaJIT定义C函数?

时间:2018-12-16 19:51:20

标签: c lua 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有一定程度的了解,并希望在某些方面使用C的一些优点,但是我在LuaJIT的FFI库中找不到很多示例。似乎cdef仅用于函数声明,而不用于定义。如何在C语言中创建函数,然后在Lua中使用它们?

3 个答案:

答案 0 :(得分:8)

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上,内存是可写的或可执行的,但不能同时存在,因此我们必须以读写模式分配页面,插入程序集,然后将模式更改为读写执行。为此,只需使用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,但是没有人愿意浏览成千上万的PDF页面,只是想知道如何做mov rax, 124。因此,有些人制作了列出指令和相应操作码的表格,例如http://ref.x86asm.net/,但在表中查找操作码也很麻烦,因为根据目标操作数和源操作数的不同,即使mov也会有许多不同的操作码。所以我要做的是例如编写一个简短的汇编文件

mov rax, 124
ret

您可能想知道,为什么我的汇编文件中没有函数,也没有segment .text之类的东西。好吧,由于我不想链接它,因此我可以将所有内容都保留下来并保存一些键入内容。然后只需使用

进行组装
$ nasm -felf64 -l test.lst test.s

-felf64选项告诉汇编程序我正在使用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和我更喜欢小写)。

答案 1 :(得分:1)

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

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

答案 2 :(得分:0)

从技术上讲,您可以做自己想做的事情而不会带来太多麻烦(只要代码足够简单)。 使用这样的东西: https://github.com/nucular/tcclua 使用tcc(非常小,您甚至可以轻松地部署它),这是一个很好的方法,可以在一个软件包中同时兼顾两个方面的优点:)