dlopen 加载 Wayfire plugin 时出现 undefined symbol
结论
修改 dlopen() 参数,或者随手补一个 dummy typeinfo,都不能让普通程序安全地充当 Wayfire plugin 的宿主。
libfiredecor.so 不是自包含的共享库。它依赖 Wayfire 主程序导出的 C++ 符号,例如:
_ZTIN2wf3txn20transaction_object_tE
反混淆后的名称是:
typeinfo for wf::txn::transaction_object_t
测试程序要正常加载这个 plugin,需要满足两个条件:
- 链接提供这些符号的 Wayfire 实现或相同的目标文件;
- 使用
-rdynamic或-Wl,--export-dynamic,把这些符号放入 executable 的动态符号表。
如果 Wayfire 没有提供可供外部程序链接的核心库,通常无法用一条简单的 g++ 命令完成这件事。手工伪造 RTTI 符号或使用 gold 的 --weak-unresolved-symbols,只能绕过部分加载检查,不能作为可靠的 ABI 测试方案。
为什么会出现这个错误
Wayfire plugin 原本由 Wayfire executable 加载。plugin 可以保留未定义符号,动态加载器会从主程序及其全局依赖中解析这些符号。
当前测试程序只链接了 libdl:
g++ test.cc -o test -Wall -ldl \
$(pkg-config --cflags pixman-1) \
-DWLR_USE_UNSTABLE
这里有几个问题:
pkg-config --cflags pixman-1只提供编译选项;- Wayfire header 只有声明,没有实现;
- 测试程序没有链接定义
wf::txn::transaction_object_tRTTI 的代码; - 主程序的动态符号表中没有对应符号。
可以先确认符号的含义:
c++filt _ZTIN2wf3txn20transaction_object_tE
输出应类似:
typeinfo for wf::txn::transaction_object_t
编译器会根据类定义、虚函数和 key function 的实现生成 RTTI。仅包含声明该类的 header,不能保证测试程序生成并导出完全相同的 RTTI 对象。
RTLD_GLOBAL 为什么没有作用
使用下面的参数:
dlopen(path, RTLD_NOW | RTLD_GLOBAL);
不会让缺失的符号变成可选符号。
RTLD_GLOBAL 只表示 plugin 加载成功后,其导出的符号可用于解析随后加载的其他共享库。它不会自动给主程序补上 Wayfire 的符号,也不会忽略 plugin 中的未定义引用。
RTLD_LAZY 可以推迟部分函数调用的 PLT 绑定,但 RTTI、全局对象、vtable 和其他数据重定位通常仍要在加载期间处理。即便错误被推迟到函数调用时才出现,依赖问题依然存在。
正确加载 plugin 的方法
如果能取得定义这些符号的 Wayfire library 或 object files,应把它们链接进测试程序,同时导出 executable 的符号:
g++ test.cc -o test \
-Wall \
-Wl,--export-dynamic \
<Wayfire 的 object files 或 link flags> \
-ldl \
$(pkg-config --cflags pixman-1) \
-DWLR_USE_UNSTABLE
也可以使用等价且更常见的 -rdynamic:
g++ test.cc -o test \
-Wall \
-rdynamic \
<Wayfire 的 object files 或 link flags> \
-ldl \
$(pkg-config --cflags pixman-1) \
-DWLR_USE_UNSTABLE
<Wayfire 的 object files 或 link flags> 没有一个通用的固定库名可以替代。这里必须使用实际 Wayfire 构建生成的库或目标文件。如果对应版本没有安装可链接的核心库,就要从同一版本的构建目录中获取。
-rdynamic 本身不会生成缺失的 RTTI。它只是把 executable 中已经存在的全局符号加入 .dynsym。因此,只增加下面的选项仍然不够:
g++ test.cc -o test -rdynamic -ldl ...
完成链接后,可以检查主程序是否确实导出了所需符号:
nm -D test | c++filt | grep 'typeinfo for wf::txn::transaction_object_t'
还需要检查 plugin 的全部未定义符号,不能只处理当前报出的第一个:
nm -D --undefined-only libfiredecor.so | c++filt
动态加载器一次只报告一个符号,这不表示 plugin 只依赖这一个 Wayfire 符号。
测试程序也应修正 dlsym() 的错误处理
dlsym() 失败时不应使用 perror()。它通过 dlerror() 报告错误,通常不会设置有意义的 errno。
#include <wayfire/plugin.hpp>
#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include <dlfcn.h>
int main(int argc, char **argv)
{
if (argc != 2)
{
std::fprintf(stderr, "usage: %s plugin.so\n", argv[0]);
return 2;
}
void *handle = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr)
{
std::fprintf(stderr, "dlopen failed: %s\n", dlerror());
return 1;
}
dlerror(); // 清除之前保存的错误
void *symbol = dlsym(handle, "getWayfireVersion");
const char *error = dlerror();
if (error != nullptr)
{
std::fprintf(stderr, "dlsym failed: %s\n", error);
dlclose(handle);
return 1;
}
using version_function = uint32_t (*)();
auto get_version = reinterpret_cast<version_function>(symbol);
const uint32_t plugin_version = get_version();
const uint32_t header_version = WAYFIRE_API_ABI_VERSION;
std::fprintf(stderr, "wayfire headers: %" PRIu32 "\n", header_version);
std::fprintf(stderr, "plugin: %" PRIu32 "\n", plugin_version);
dlclose(handle);
return plugin_version == header_version ? 0 : 1;
}
这里使用 RTLD_LOCAL 就够了,因为该程序不需要用 plugin 的符号解析后续加载的共享库。它不能解决未定义符号问题,但更符合这个检查程序的用途。
能否提供 dummy symbols
可以编写 shim library,导出指定名称的假符号,再通过链接或 LD_PRELOAD 让动态加载器找到它们。不过,这种方式用于 C++ RTTI 时很不可靠。
plugin 可能需要的不只是一个地址,还包括:
std::type_info对象的正确内存布局;- 对应类的 vtable;
- key virtual function;
- 基类 RTTI;
- 构造函数、析构函数和其他 Wayfire 符号。
此外,dlopen() 会执行 plugin 的 ELF 初始化代码,包括 .init_array 中的全局构造函数。即使程序只打算调用 getWayfireVersion(),也不能保证加载期间不会访问这些假对象。
如果 shim 只提供同名的字节数组,动态链接可能暂时成功。一旦 plugin 把它当作真正的 RTTI 对象使用,程序就可能崩溃或产生未定义行为。这种方法最多用于一次性诊断,不适合判断兼容性。
--weak-unresolved-symbols 是否是正确方案
不建议使用。
gold 的 --weak-unresolved-symbols 会把未解析引用按弱引用处理。运行时找不到弱符号时,其地址通常按零处理,因此 dlopen() 可能不会立即因该符号缺失而失败。
这种结果只能说明当前加载路径和 getWayfireVersion() 调用碰巧没有用到对应对象,无法证明 plugin 能在当前 Wayfire 中正常运行。该选项还有以下问题:
- 它会掩盖真实的强依赖;
- 全局构造函数仍可能访问空的弱符号;
- plugin 的其他功能可能在稍后崩溃;
- 必须重新链接 plugin,无法检查未经修改的现有 binary;
- 即使取得版本号,也没有验证其余 C++ ABI 依赖是否满足。
因此,--weak-unresolved-symbols 不能证明 plugin 与当前 Wayfire ABI 兼容,也不适合用作发行版的常规构建选项。
更适合长期使用的版本检查方式
如果可以调整 plugin 的构建方式,最好把 ABI version 保存为无需加载 DSO 就能读取的元数据,例如:
- plugin 旁边的 metadata 文件;
- package metadata;
- ELF note;
- 自定义 ELF section。
检查工具可以直接读取这些数据,无需执行 plugin 的初始化代码,也不用模拟 Wayfire 宿主。
单独导出一个数据变量仍不能彻底解决问题。通过 dlopen() 查找该变量时,加载器依然要处理整个 DSO 的重定位。要完全避开加载过程,可以使用 libelf、libdw 等工具直接读取专用 section。
对于已经存在且只包含 getWayfireVersion() 的 binary,也可以反汇编该函数,尝试提取其返回常量。但这种方法受 CPU 架构、编译器优化、LTO 和函数实现形式影响,不适合做通用检查器。
比较版本时,应遵循 Wayfire 对 WAYFIRE_API_ABI_VERSION 的定义。如果该值是必须完全一致的 ABI 标识,就应使用 ==,不能自行用 >= 判断版本是否“足够新”。当前 header 中的宏只表示测试程序编译时所用 header 的版本,未必等于机器上实际运行的 Wayfire executable 的 ABI version。
备注:内容仅供参考。
