动态加载带虚表的异常类致链接错误,如何让RTTI正常工作?
1. 错误的根本原因
这个问题的核心是运行时符号解析的时机不匹配,结合你的编译链接流程来看:
关键编译链回顾
- 你先编译
lib.cc生成libf.so,这个库包含myexception的vtable、typeinfo以及f()的实现。 - 编译
main.cc时链接这个libf.so生成a.out:此时main.o中的catch(myexception)语句会直接引用myexception的typeinfo和vtable符号,链接器在libf.so中找到了这些符号,所以链接成功,但a.out的动态依赖被记录为libf.so。 - 你将原始的
libf.so重命名为libreal.so,再用stub.cc编译出新的libf.so——这个新库完全没有myexception相关符号,只有一个构造函数会在库加载时调用dlopen("libreal.so")。
运行时符号解析失败的原因
当a.out启动时,动态链接器的执行顺序是:
- 先加载
a.out本身,然后加载它依赖的libf.so(即stub版本) - 执行
libf.so的构造函数(此时才会dlopen加载libreal.so)
但a.out在启动阶段就需要解析myexception的typeinfo/vtable符号(因为这些是main代码直接引用的符号),而此时libreal.so还未被加载,动态链接器在已加载的库中找不到这些符号,就会立刻抛出"undefined symbol"错误,程序根本没机会执行到dlopen那一步。
不同编译环境的差异
- GCC/clang+libstdc++:报错
typeinfo for myexception,因为libstdc++将类的typeinfo作为独立的全局符号,catch语句会直接引用这个符号。 - clang+libc++/GCC用-fPIC编译main:报错
vtable for myexception,因为libc++的RTTI实现直接关联到vtable,或者-fPIC的编译方式改变了符号的引用策略,让vtable成为必须在启动时解析的符号。
2. 让RTTI在dlopen加载的类上正常工作的方案
当然可以,这里提供几种可靠的解决思路:
方案1:延迟符号解析,让符号在实际使用时再查找
编译main.cc时添加-Wl,-z,lazy选项,强制动态链接器使用延迟绑定(默认情况下,PIE可执行文件可能会启用立即绑定)。这样myexception的typeinfo/vtable符号会直到catch语句执行时才被解析,而此时libreal.so已经被dlopen加载完成,符号就能被找到。
编译命令修改为:
g++ main.cc -Wall -Wextra libf.so -fPIE -g -Wl,-z,lazy
方案2:让main不直接引用库中的符号,改为动态加载函数
修改main.cc,让它自己负责dlopen加载libreal.so并获取f()的地址,而不是直接调用f()。这样main不会在启动时引用myexception的任何符号,直到实际调用f()抛出异常时,libreal.so已经加载完成,RTTI就能正常工作。
修改后的main.cc:
#include <dlfcn.h> #include <stdlib.h> #include "lib.h" int main() { void* handle = dlopen("./libreal.so", RTLD_NOW | RTLD_GLOBAL); if (!handle) { abort(); } typedef void (*f_func)(); f_func f = reinterpret_cast<f_func>(dlsym(handle, "f")); if (!f) { abort(); } try { f(); } catch(myexception) {} dlclose(handle); return 0; }
方案3:将异常类定义移到公共共享库中
创建一个包含myexception定义的公共共享库,让a.out和libreal.so都链接这个库。这样a.out启动时,公共库会提供myexception的typeinfo/vtable弱定义,当libreal.so被加载后,会覆盖为实际的实现,RTTI就能正常工作。
步骤示例:
- 创建
libcommon.h(内容和原lib.h一致) - 创建
libcommon.cc:#include "libcommon.h" void myexception::info() {} // 空实现作为弱定义 - 编译公共库:
g++ libcommon.cc -Wall -Wextra -shared -o libcommon.so -fPIC -g - 编译
libreal.so时链接公共库:g++ lib.cc -Wall -Wextra libcommon.so -shared -o libreal.so -fPIC -g - 编译
main.cc时链接公共库:g++ main.cc -Wall -Wextra libcommon.so -fPIE -g - 编译stub库的命令不变
内容的提问来源于stack exchange,提问作者Roland Schulz

