如何在不修改源码时链接含同名同签名函数的目标文件?
场景
存在两个目标文件A.o和B.o,它们对应的.cpp文件中都定义了签名相同的sayHi函数。希望在项目中同时使用这两个实现,但链接时出现重定义错误,请问能否在不修改源码的情况下实现这一需求?
我尝试为其中一个目标文件创建共享对象包装,但未成功,推测是因为最终符号表中B.o的数据会与A.o冲突,即使打包进Wrapper.so也无法解决。
更新
我尝试创建了名为test1.so和test2.so的共享库,用包装器封装冲突符号:test1.so中创建Wrapper命名空间,内部函数调用A.o的sayHi;test2.so中创建Wrapper2命名空间,内部函数调用B.o的sayHi。链接生成可执行文件时,链接器未报重定义错误,但调用包装函数:
Wrapper2::sayHi(); Wrapper::sayHi();
时,两者调用的是同一个符号。请问如何让它们调用正确的符号?
我还尝试了运行时动态加载的方式,代码如下,这种方式可以得到预期结果,但想知道是否可以通过类似g++ helloworld.cpp test1.so test2.so的方式静态链接实现,而非使用以下代码:
void* libraryHandle = dlopen("./test1.so", RTLD_LAZY); if (!libraryHandle) { std::cerr << "Error loading library: " << dlerror() << std::endl; return 1; } // 获取函数指针 typedef void (*WrappedFunction)(); WrappedFunction wrappedFunction = (WrappedFunction)dlsym(libraryHandle, "_ZN8Wrapper27sayHiEv"); if (!wrappedFunction) { std::cerr << "Error loading symbol: " << dlerror() << std::endl; dlclose(libraryHandle); return 1; } // 调用包装函数 wrappedFunction(); // 卸载共享库 dlclose(libraryHandle); libraryHandle = dlopen("./test2.so", RTLD_LAZY); if (!libraryHandle) { std::cerr << "Error loading library: " << dlerror() << std::endl; return 1; } wrappedFunction = (WrappedFunction)dlsym(libraryHandle, "_ZN7Wrapper7sayHiEv"); if (!wrappedFunction) { std::cerr << "Error loading symbol: " << dlerror() << std::endl; dlclose(libraryHandle); return 1; } // 调用包装函数 wrappedFunction(); // 卸载共享库 dlclose(libraryHandle);
1. 为什么包装后调用的是同一个符号?
问题出在共享库的符号可见性上。默认情况下,A.o和B.o中的sayHi是全局可见符号,链接可执行文件时,链接器会优先绑定第一个遇到的sayHi,导致两个包装函数最终指向同一个实现。解决核心是隐藏共享库内的原始sayHi符号,只暴露包装后的命名空间函数,有两种实现方式:
方法一:编译时控制符号可见性
使用-fvisibility=hidden编译选项将默认符号设为隐藏,再显式导出包装函数:
# 编译A.o时隐藏所有符号 g++ -c A.cpp -fvisibility=hidden -o A.o # 编译wrapper1.cpp,显式导出Wrapper::sayHi g++ -c wrapper1.cpp -fvisibility=hidden -o wrapper1.o # 链接生成test1.so g++ -shared A.o wrapper1.o -o test1.so
wrapper1.cpp中需要给包装函数添加可见性属性:
namespace Wrapper { __attribute__((visibility("default"))) void sayHi() { ::sayHi(); // 调用A.o的sayHi } }
同理处理test2.so,这样原始sayHi会被限制在各自共享库内,不会全局冲突。
方法二:链接脚本控制符号可见性
创建链接脚本hide_symbols.lds:
{ global: Wrapper::sayHi; Wrapper2::sayHi; # 仅暴露这两个包装符号 local: *; # 其余符号设为局部 };
链接共享库时指定该脚本:
g++ -shared A.o wrapper1.o -o test1.so -Wl,--version-script=hide_symbols.lds g++ -shared B.o wrapper2.o -o test2.so -Wl,--version-script=hide_symbols.lds
2. 能否通过静态链接方式实现?
可以,有两种可靠方案:
方案一:归档库+局部符号
- 将
A.o、B.o分别打包成归档库,编译时隐藏内部符号:
g++ -c A.cpp -fvisibility=hidden -o A.o ar rcs libA.a A.o g++ -c B.cpp -fvisibility=hidden -o B.o ar rcs libB.a B.o
- 编写包装文件分别调用各自归档库的
sayHi:wrapperA.cpp:
namespace Wrapper { void sayHi(); } extern "C" void sayHi(); // 声明A.o中的sayHi void Wrapper::sayHi() { sayHi(); }
wrapperB.cpp同理。
3. 编译并链接:
g++ -c wrapperA.cpp -o wrapperA.o g++ -c wrapperB.cpp -o wrapperB.o g++ helloworld.cpp wrapperA.o wrapperB.o libA.a libB.a -o main
方案二:符号重命名
用objcopy直接修改目标文件的符号名,无需包装:
objcopy --redefine-sym sayHi=sayHi_A A.o A_renamed.o objcopy --redefine-sym sayHi=sayHi_B B.o B_renamed.o
之后直接链接重命名后的目标文件,代码中调用sayHi_A和sayHi_B即可。
内容的提问来源于stack exchange,提问作者Satrofu

