将静态库链接到共享库并隐藏导出的符号

link a static library to a shared library and hide exported symbols

本文关键字:隐藏 符号 共享 静态 链接      更新时间:2023-10-16

链接器出现了一个烦人的问题。我想将共享库中的一些符号链接到静态库,但不导出其符号(即,我不能简单地合并库或与--whole-archive链接)。我想要的是将我的共享库链接到一个静态库,并删除未定义的符号。

我正在寻找的东西可能只是一个链接器选项,但我无法将我的手指放在它上

我会尽我所能描述这个问题(这并不容易),然后提供一个玩具的最小例子。

快速描述:

我想使用LD_PRELOAD技巧来捕获可执行文件中的一些函数调用。此可执行文件链接到第三方共享库,该库包含我想要捕获的函数的函数定义。

这个第三方库还包含另一个库中的符号,我也在库中使用该库,但使用了不同的(不兼容)版本。

我想做的是编译我的共享库,并在编译时将其与最后一个(静态)库的定义链接起来,而不导出符号,这样我的共享库可使用与我想要捕获的版本不同的版本。

简化的问题描述

我有一个名为libext.so的第三方库,我没有它的源代码。这定义了一个函数bar,并使用了另一个库中的函数foo,但符号都在那里定义:

$> nm libext.so
0000000000000a16 T bar
00000000000009e8 T foo

正如我提到的,foo是一个外部依赖项,我想使用更新的版本。我有一个更新的库,让我们称之为libfoo.a:

$> nm libfoo.a
0000000000000000 T foo

现在的问题是,我想创建一个重新定义bar的动态库,但我希望我的库使用libfoo.afoo的定义,并且我希望libext.so中的函数调用libext.so中的函数foo。换句话说,我想要我的库与libfoo.a的编译时链接。

我想要的是定义一个使用libfoo.a但不导出其符号的库。如果我将我的库链接到libfoo.a,我会得到:

$> nm libmine.so
0000000000000a78 T bar
0000000000000b2c T foo

这意味着我过载了foobar(我不想覆盖foo)。如果我不将我的库链接到libfoo.a,我会得到:

$> nm libmine.so
0000000000000a78 T bar
U foo

所以我的库将使用他们的foo版本,我也不想要。我想要的是:

$> nm libmine.so
0000000000000a78 T bar

其中foo在编译时被链接,并且其符号未导出。

最小示例

你不需要读这篇文章,但你可以用它来寻找解决方案。

bar.cpp:表示我没有代码的第三方应用程序:

#include <iostream>
extern "C" void foo(){ std::cerr << "old::foo" << std::endl; }
extern "C" void bar(){ std::cerr << "old::bar" << std::endl; foo(); }

foo.cpp:表示我的lib和第三方都使用的函数的更新版本:

#include <iostream>
extern "C" void foo(){ std::cerr << "new::foo" << std::endl; }

trap.cpp:我的库中的代码,它捕获bar,调用新的foo并转发:

#include <iostream>
extern "C" {
#include <dlfcn.h>
}
extern "C" void foo();
extern "C" void bar(){
std::cerr << "new::bar" << std::endl;
foo(); // Should be new::foo
void (*fwd)() = (void(*)())dlsym(RTLD_NEXT, "bar");
fwd(); // Should use old::foo
}

exec.cpp:调用bar:的伪可执行文件

extern "C" void bar();
int main(){
bar();
}

Makefile:仅Unix,抱歉

default:
# The third party library
g++ -c -o bar.o bar.cpp -fpic
gcc -shared -Wl,-soname,libext.so -o libext.so bar.o
# The updated library
g++ -c -o foo.o foo.cpp -fPIC
ar rcs libfoo.a foo.o
# My trapping library
g++ -c -o trap.o trap.cpp -fPIC
gcc -shared -Wl,-soname,libmine.so -o libmine.so trap.o -ldl -L. -lfoo
# The dummy executable
g++ -o test exec.cpp -L. libext.so

在这种情况下,bar调用foo;正常执行是:

$> ./test
old::bar
old::foo

预加载我的库拦截bar,调用我的foo并转发bar,当前执行为:

$> LD_PRELOAD=libmine.so ./test
new::bar
new::foo
old::bar
new::foo

最后一行错误,所需输出为:

$> LD_PRELOAD=libmine.so ./test
new::bar
new::foo
old::bar
old::foo

您想要使用链接器版本脚本,该脚本导出您想要的符号(此处为bar),并且隐藏其他所有内容。

此处为示例。

正如接受的答案中所指出的,我们可以使用链接器版本脚本将不需要的符号的范围从全局更改为本地:

BAR {
global: bar;
local: *;
};

使用链接器版本编译显示foo是本地的,程序现在的行为与预期一致:

$> gcc -shared -Wl,-soname,libmine.so -Wl,--version-script=libmine.version -o libmine.so trap.o -ldl -L. -lfoo
$> nm libmine.so
0000000000000978 T bar
0000000000000000 A BAR  
0000000000000a2c t foo
$> LD_PRELOAD=libmine.so ./test
new::bar
new::foo
old::bar
old::foo

另一种选择是使用属性-fvisibility=hidden重新编译libfoo.a,并与之链接。导出的符号的可见性也是本地的,其行为与上述相同。


注意:这个答案最初是OP(Thibaut)在问题中写的