剥离Linux共享库

43
我们最近被要求为我们的一个库发布Linux版本,之前我们在Linux下开发并发布了Windows版本,其中在Windows下部署库通常更容易。我们遇到的问题是将导出的符号剥离到仅包含公开接口中的符号。有三个很好的原因想要这样做:
  • 保护我们技术中专有的方面不通过导出符号泄露。
  • 防止用户出现冲突的符号名称问题。
  • 加快库的加载速度(至少我是这么说的)。
举个简单的例子:

test.cpp

#include <cmath>

float private_function(float f)
{
    return std::abs(f);
}

extern "C" float public_function(float f)
{
    return private_function(f);
}

使用(g++ 4.3.2,ld 2.18.93.20081009)编译

g++ -shared -o libtest.so test.cpp -s

使用{{ inspect }}检查符号

nm -DC libtest.so

提供

         w _Jv_RegisterClasses
0000047c T private_function(float)
000004ba W std::abs(float)
0000200c A __bss_start
         w __cxa_finalize
         w __gmon_start__
0000200c A _edata
00002014 A _end
00000508 T _fini
00000358 T _init
0000049b T public_function

显然不足。因此,接下来我们将重新声明公共函数为。
extern "C" float __attribute__ ((visibility ("default"))) 
    public_function(float f)

并编译使用

g++ -shared -o libtest.so test.cpp -s -fvisibility=hidden

这提供了

         w _Jv_RegisterClasses
0000047a W std::abs(float)
0000200c A __bss_start
         w __cxa_finalize
         w __gmon_start__
0000200c A _edata
00002014 A _end
000004c8 T _fini
00000320 T _init
0000045b T public_function

这很好,除了std::abs被暴露出来。更麻烦的是当我们开始链接其他(静态)库时,这些库中使用的所有符号都会被导出。此外,当我们开始使用STL容器时:

#include <vector>
struct private_struct
{
    float f;
};

void other_private_function()
{
    std::vector<private_struct> v;
}

我们最终会从C++库中获得许多额外的导出项。

00000b30 W __gnu_cxx::new_allocator<private_struct>::deallocate(private_struct*, unsigned int)
00000abe W __gnu_cxx::new_allocator<private_struct>::new_allocator()
00000a90 W __gnu_cxx::new_allocator<private_struct>::~new_allocator()
00000ac4 W std::allocator<private_struct>::allocator()
00000a96 W std::allocator<private_struct>::~allocator()
00000ad8 W std::_Vector_base<private_struct, std::allocator<private_struct> >::_Vector_impl::_Vector_impl()
00000aaa W std::_Vector_base<private_struct, std::allocator<private_struct> >::_Vector_impl::~_Vector_impl()
00000b44 W std::_Vector_base<private_struct, std::allocator<private_struct> >::_M_deallocate(private_struct*, unsigned int)
00000a68 W std::_Vector_base<private_struct, std::allocator<private_struct> >::_M_get_Tp_allocator()
00000b08 W std::_Vector_base<private_struct, std::allocator<private_struct> >::_Vector_base()
00000b6e W std::_Vector_base<private_struct, std::allocator<private_struct> >::~_Vector_base()
00000b1c W std::vector<private_struct, std::allocator<private_struct> >::vector()
00000bb2 W std::vector<private_struct, std::allocator<private_struct> >::~vector()

注意:启用优化后,您需要确保向量实际被使用,以便编译器不会将未使用的符号优化掉。
我相信我的同事已经成功构建了一个临时解决方案,涉及版本文件和修改STL头文件(!) ,它似乎可以工作,但我想问一下:
是否有一种干净的方法可以从Linux共享库中删除所有不必要的符号(即不属于公开库功能的符号)? 我尝试了很多g++和ld选项,但成功率很低,因此我更喜欢已知可行的答案而不是可能的答案。
特别是:
1.来自(闭源)静态库的符号没有被导出。
2.标准库的符号没有被导出。
3.来自对象文件的非公共符号没有被导出。
我们的导出接口是C。
我知道SO上的其他类似问题:
1.NOT sharing all classes with shared library 2.How to REALLY strip a binary in MacOs 3.GNU linker: alternative to --version-script to list exported symbols at the command line? 但是对答案的成功率很低。

1
关于系统库的静态链接:这是非法的。也就是说,由于GLIBCLGPL下许可,而该许可证适用于除动态链接之外的所有使用它的代码,因此通过静态链接,您的代码将被LGPL覆盖,并且需要提供源代码(对于任何您提供二进制文件并要求源代码的人)。但是,这不适用于libgcc和libstdc++,它们明确不适用于仅使用公共API的任何代码,无论如何链接。 - Jan Hudec
我知道这一点,并且并不是指glibc中的符号,上面所有的符号都是由C++标准库的模板实例化生成的,必须在我的目标文件中生成(因为模板实例化不能在库中!)。 - Adam Bowen
6个回答

9
我们现在的解决方案如下:

test.cpp

#include <cmath>
#include <vector>
#include <typeinfo>

struct private_struct
{
    float f;
};

float private_function(float f)
{
    return std::abs(f);
}

void other_private_function()
{
    std::vector<private_struct> f(1);
}

extern "C" void __attribute__ ((visibility ("default"))) public_function2()
{
    other_private_function();
}

extern "C" float __attribute__ ((visibility ("default"))) public_function1(float f)
{
    return private_function(f);
}

exports.version

LIBTEST 
{
global:
    public*;
local:
    *;
};

编译使用

g++ -shared test.cpp -o libtest.so -fvisibility=hidden -fvisibility-inlines-hidden -s -Wl,--version-script=exports.version

提供
00000000 A LIBTEST
         w _Jv_RegisterClasses
         U _Unwind_Resume
         U std::__throw_bad_alloc()
         U operator delete(void*)
         U operator new(unsigned int)
         w __cxa_finalize
         w __gmon_start__
         U __gxx_personality_v0
000005db T public_function1
00000676 T public_function2

这与我们正在寻找的相当接近。但是有一些需要注意的地方:

  • 我们必须确保在内部代码中不使用“exported”前缀(在这个简单的示例中为“public”,但在我们的情况下显然需要更有用的内容)。
  • 许多符号名称仍然出现在字符串表中,这似乎归因于RTTI,-fno-rtti使它们在我的简单测试中消失了,但这是一个相当彻底的解决方案。

如果有更好的解决方案,我很乐意接受任何人提出的建议!


我接受我们的最终解决方案,因为它最符合我们的需求,但是为了其他处于同样情况下的人的利益,我想补充说,如果你的情况有所不同,其他答案也都是完全可行的解决方案! - Adam Bowen
这个方法是否也会隐藏其他静态库中的符号?我遇到了完全相同的问题,许多来自各种外部依赖项的垃圾被导出。 - Soo Wei Tan
看起来是这样的。唯一的符号是我们的导出和一些在C/C++库中链接到的符号。 - Adam Bowen

8

您使用默认可见性属性和-fvisibility = hidden应该增加-fvisibility-inlines-hidden。

您还应该忘记尝试隐藏stdlib导出,参见此GCC错误的原因。

此外,如果您所有公共符号都在特定标头中,则可以将它们包装在#pragma GCC visibility push(default)#pragma GCC visibility pop中,而不是使用属性。虽然如果您正在创建跨平台库,请查看控制共享库的导出符号以统一Windows DLL和Linux DSO导出策略的技术。


感谢您抽出时间回答问题,提供的链接非常有趣。
  1. 我们公开了一个纯C接口(主要是为了兼容性),为什么我们要公开实现细节呢?仅仅因为我们可以在库边界上共享RTTI等内容,并不意味着我们会这样做。
  2. 在我的简单示例中,-fvisibility-inlines-hidden没有任何区别,我不认为它会对我们的接口产生影响(或解决我们的问题),但将来可能会有用。
  3. 不幸的是,所引用的文章似乎并没有提供任何解决我们问题的方案(除了我们已经拥有的)。
- Adam Bowen
我明白你所说的private_struct在被导出的向量实例化中的含义。你的同事对头文件做了什么改动,使它们消失了? - joshperry

7
值得注意的是,Ulrich Drepper撰写了一篇关于编写适用于Linux/Unix的共享库(所有?)方面的论文,其中涵盖了许多其他主题,包括控制导出符号等内容。这对于清楚地从共享库中仅导出白名单中的函数非常有帮助。请参考此处阅读。

6
如果您将私有成员包装在匿名命名空间中,则符号表中既看不到std::abs,也看不到private_function
namespace{
#include<cmath>
  float private_function(float f)
  {
    return std::abs(f);
  }
}
extern "C" float public_function(float f)
{
        return private_function(f);
}

编译(g++ 4.3.3):

g++ -shared -o libtest.so test.cpp -s

检查:

# nm -DC libtest.so
         w _Jv_RegisterClasses
0000200c A __bss_start
         w __cxa_finalize
         w __gmon_start__
0000200c A _edata
00002014 A _end
000004a8 T _fini
000002f4 T _init
00000445 T public_function

1
虽然它适用于简单的示例并且是隐藏本地构造的好方法,但私有命名空间不适用于完整项目的扩展。 - Adam Bowen

3
一般来说,在多个Linux和Unix系统上,这里在链接时没有答案。这是ld.so工作的基础原理。
这导致了一些相当费力的替代方案。例如,我们将STL重命名为_STL而不是std,以避免与STL发生冲突,并使用高、低和中间命名空间来使我们的符号远离可能与其他人的符号发生冲突。
下面是一个你可能不喜欢的解决方案:
1.创建一个只包含公开API的小型.so文件。 2.使用dlopen打开真正的实现,并使用dlsym进行链接。
只要不使用RTLD_GLOBAL,你现在就完全隔离了,虽然不是特别保密。-Bsymbolic也可能是可取的。

不幸的是,对于我们来说,隐藏算法的细节只是问题的一半。 - Adam Bowen
如果您使用我的两个共享对象方法,那么您可以进行全局符号重命名。@joshperry的东西可能能胜任这项工作。 - bmargulies

0

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。仅有链接的答案如果链接页面发生更改可能会变得无效。请添加一些解释和引用。 - Anna

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接