0%

LuaJIT ffi与dlsym的姻缘

近日在静态编译sysbench时遇到了一个问题,运行编译好的静态可执行文件时报错:

1
2
3
4
5
6
root@test:~/sysbench-1.0.20/src# ./sysbench
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

Reading the script from the standard input:

PANIC: unprotected error in call to Lua API ([string "sysbench.sql.lua"]:207: sysbench: undefined symbol: db_destroy)

sysbench内置了LuaJIT解释器用于执行SQL性能测试,动态编译时运行正常,按提示得知错误为符号db_destroy未找到,首先使用readelf打印可执行文件sysbench的符号表看看有没有名叫db_destory的函数:

1
2
root@test:~/sysbench-1.0.20/src# readelf -s sysbench | grep db_destroy
15617: 0000000000407420 35 FUNC GLOBAL DEFAULT 6 db_destroy

结果显示该函数存在,从错误提示可见该错误是由Lua抛出,而非C代码,遂定位Lua代码:

1
2
root@test:~/sysbench-1.0.20/src# find . -type f -name "sysbench.sql.lua"
./src/lua/internal/sysbench.sql.lua

该Lua文件以C String的形式被编译进最终可执行文件内,在当前目录内的Makefile.am内可见以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BUILT_SOURCES = sysbench.lua.h sysbench.rand.lua.h sysbench.sql.lua.h \
sysbench.compat.lua.h sysbench.cmdline.lua.h \
sysbench.histogram.lua.h

CLEANFILES = $(BUILT_SOURCES)

EXTRA_DIST = $(BUILT_SOURCES:.h=)

SUFFIXES = .lua .lua.h

.lua.lua.h:
@echo "Creating $@ from $<"
@var=$$(echo $< | sed 's/\./_/g') && \
( echo "unsigned char $${var}[] =" && \
sed -e 's/\\/\\\\/g' \
-e 's/"/\\"/g' \
-e 's/^/ "/g' \
-e 's/$$/\\n"/g' $< && \
echo ";" && \
echo "size_t $${var}_len = sizeof($${var}) - 1;" ) > $@

定位到错误行:

1
2
3
4
5
6
7
-- sql_driver metatable
local driver_mt = {
__index = driver_methods,
__gc = ffi.C.db_destroy, --<---------------------------line 207
__tostring = function() return '<sql_driver>' end,
}
ffi.metatype("sql_driver", driver_mt)

可知对象driver_mt的垃圾回收方法被指向了一个ffi方法ffi.C.db_destroy,尝试定位它:

1
2
3
4
5
6
7
8
9
ffi = require("ffi")

sysbench.sql = {}

ffi.cdef[[
...
int db_destroy(sql_driver *drv);
...
]]

可见db_destroy为一个C函数,尝试定位:

1
2
3
root@test:~/sysbench-1.0.20/src# find . -name "*.c" | xargs grep db_destroy
./src/sb_lua.c: db_destroy(ctxt->driver);
./src/db_driver.c:int db_destroy(db_driver_t *drv)
1
2
3
4
5
6
7
int db_destroy(db_driver_t *drv)
{
if (drv->ops.thread_done != NULL)
return drv->ops.thread_done(sb_tls_thread_id);

return 0;
}

至此可以确定db_destroy函数存在并被连接到了sysbench可执行文件内,为什么LuaJIT不能透过ffi.C在静态编译时调用它呢?通过ffi.C为关键词在LuaJIT的文档里找到了这段文字:

ffi.C
This is the default C library namespace — note the uppercase ‘C’. It binds to the default set of symbols or libraries on the target system. These are more or less the same as a C compiler would offer by default, without specifying extra link libraries.

On POSIX systems, this binds to symbols in the default or global namespace. This includes all exported symbols from the executable and any libraries loaded into the global namespace. This includes at least libc, libm, libdl (on Linux), libgcc (if compiled with GCC), as well as any exported symbols from the Lua/C API provided by LuaJIT itself.

可以得知在POSIX系统上,可以通过ffi.C访问当前可执行文件的符号表,以及透过dlopen()加载的动态链接库中的符号表。由此基于以下结论:

  • Linux下可执行文件的符号表可以被strip掉并不影响执行,C函数相互调用所需的地址信息已被硬编码在C代码内。
  • sysbench.sql.lua作为Lua脚本,在C的编译时以字符串形态进入可执行文件内,在运行时方被LuaJIT解释执行。
  • 被dlopen()引入的动态链接库中的函数需要以dlsym()函数导出函数地址方可被调用。

所以,当LuaJIT解释执行j脚本以ffi.C呼叫db_destroy()时,LuaJIT才知道它需要去寻找一个名db_destroy的符号,此时只有dlsym可以做到这一点,dlsym的实现如下:

1
2
3
4
5
6
7
8
9
10
11
const char *strtab = ... /* locate .dynstr */
const ElfW(Sym) *sym = ... /* locate .dynsym */

for (; sym < end_of_dynsym; ++sym) {
if (strcmp(strtab + sym->st_name, "foo") == 0) {
/* found it */
return load_address + sym->st_value;
}
}
/* symbol not found */
return NULL;

如代码所示,dlsym通过读取动态链接库的.dynstr与.dynsym两个ELF Sections 来搜索符号并取得符号对应地址,但sysbench可执行文件经包含了db_destroy()函数且它并不是一个动态库[黑人问号脸]。

于是我再次link了一个非静态的sysbench看看和静态的有什么区别,首先还是看一下符号表:

1
2
3
root@test:~/sysbench-1.0.20/src# readelf -s sysbench_shared | grep db_destroy
418: 0000000000013060 35 FUNC GLOBAL DEFAULT 14 db_destroy
1830: 0000000000013060 35 FUNC GLOBAL DEFAULT 14 db_destroy

可以看到出来了两个地址相同的符号,通过万能的谷歌得知ELF可执行文件可以同时拥有两个符号表,分别名为.dynsym和.symtab,.dynsym需要通过gcc参数-rdynamic或连接器参数-export-dynamic获取,且不会被strip掉,Luajit的ffi设计依赖于.dynsym来进行运行时符号查找。

-export-dynamic

Allow symbols from output-file to be resolved with dlsym (see Dlopened modules).

而静态编译参数-static会让gcc参数-rdynamic无效化,缺少了.dynsym自然ffi.C无法正常工作,从而导致sysbench报错,我尝试直接以-export-dynamic参数连接,但因为libc.a没有用位置无关代码参数(-fPIE)编译而失败。

解决思路:

  • 从ld方向看能不能给静态编译目标文件也加上.dynsym,看了几小时binutils代码看得头昏脑胀的,也没一点眉目,改日再深究。
  • 替换系统libdl,自己写一个dlsym()返回.symtab中记录的符号地址,是否可行有待验证。(已完成)
  • 将Lua脚本以LuaJIT编译后链接进sysbench,工作量巨大,不太现实。