为什么OSX的strip不能去掉weak symbols?

Why OSX's strip can not remove weak symbols?

在尝试从链接的 executable 中删除不需要的东西时,我发现了一些奇怪的东西。 假设我们有一个简单直接的 C++ 程序:

class Foo {
public:
    template <typename T>
    char* getPtr() {
        static char c;
        return &c;
    }
};

char* bar() {
    Foo i;
    return i.getPtr<int>();
}

int main() {
    bar();
} 

clang++ t.cc 构建的二进制文件有下一个动态符号 table:

$ gobjdump -T ./a.out

./a.out:     file format mach-o-x86-64

DYNAMIC SYMBOL TABLE:
0000000100000f40 g       0f SECT   01 0000 [.text] __Z3barv
0000000100000f60 g       0f SECT   01 0080 [.text] __ZN3Foo6getPtrIiEEPcv
0000000100001020 g       0f SECT   08 0080 [.data] __ZZN3Foo6getPtrIiEEPcvE1c
0000000100000000 g       0f SECT   01 0010 [.text] __mh_execute_header
0000000100000f80 g       0f SECT   01 0000 [.text] _main
0000000000000000 g       01 UND    00 0200 dyld_stub_binder

考虑到它是一个 executable 而不是一个 dylib,我想去除除未定义符号之外的所有条目。理论上二进制仍然有效,因为有关所需的 dyld 绑定的信息仍然存在,并且入口点在 mach-o header 之后的某些加载命令中定义(因此与符号 table 无关)。

尝试 strip 得到一些奇怪的结果:

$ strip ./a.out
$ gobjdump -T ./a.out

./a.out:     file format mach-o-x86-64

DYNAMIC SYMBOL TABLE:
0000000005614542      d  3c OPT    00 0000 radr://5614542
0000000100000f60 g       0f SECT   01 0080 [.text] __ZN3Foo6getPtrIiEEPcv
0000000100001020 g       0f SECT   08 0080 [.data] __ZZN3Foo6getPtrIiEEPcvE1c
0000000100000000 g       0f SECT   01 0010 [.text] __mh_execute_header
0000000000000000 g       01 UND    00 0200 dyld_stub_binder

无论如何都不应该删除最后两个条目,因为 dyld 需要它们来处理此 executable。同时我们看到 _main__Z3barv 不见了。但是来自 class Foo 的符号仍然存在。它们与剥离的唯一区别是前者设置了 N_WEAK_DEF 标志 (0080)。以下是来自 <mach-o/nlist.h> 的一些关于该标志的少量信息:

 /*
 * The N_WEAK_DEF bit of the n_desc field indicates to the static and dynamic
 * linkers that the symbol definition is weak, allowing a non-weak symbol to
 * also be used which causes the weak definition to be discared.  Currently this
 * is only supported for symbols in coalesed sections.
 */
#define N_WEAK_DEF  0x0080 /* coalesed symbol is a weak definition */

不幸的是,它没有解释为什么 strip 忽略带有该标志的符号。

所以问题是 - 如何教 strip 删除甚至 N_WEAK_DEF 符号,以防用户不想导出它们。

P.S。我查看了命令选项,但没有发现任何有用的东西(-N 也删除了未定义的符号,所以它不是一个选项)。用 visibility("hidden") 声明 class 是个难题,但不幸的是,在实际项目中做起来并不容易。

我的第一个结论是跳转到它是雷达错误 5614542 的结果,因此那个奇怪的符号,但它与它无关。

我会根据您使用的似乎是 nlist 重定位而不是新的基于字节码的重定位这一事实得出一些假设和猜测(您可以通过查找 dyld 信息加载命令来检查) ,这要么是用一个古老的工具链构建的,要么是一个 MH_OBJECT 文件,用于未经过最后 linking 步骤的主要可执行文件。我不是 100% 确定这里是否是这种情况 - 但无论如何,

抱歉我的上述假设,但最初的答案仍然适用,除非您真的想选择退出符号合并,在这种情况下使用私有 linkage 构建您的应用程序,但此模板实例化再次强制符号由于一个很好的理由,它很弱,它有一个静态构造函数和一个隐式实例化的模板,它更喜欢安全,所以它保留了符号。你根本不能将它导出到可执行文件之外,虽然这里有一个小案例,但 C++ 程序倾向于使用诸如 boost 之类的东西,或者依赖于其他 C++ 库的 C++ 库,它们都会创建链,最终你会得到多个共享名称空间中的定义只是因为 C++ 语义。在你的小测试用例中,你可以摆脱它,在一个更大的应用程序中,除非你真的知道你在做什么并检查诸如 dylibs 的依赖树之类的东西,让 dyld 完成它的工作。我认为我原来的答案仍然适用于主要部分,因为它解释了为什么你的符号被标记为弱(ODR 是一个 C++ 特定的概念,但不同的静态 linkers 处理不同):


更详细的解释 - 它与 C++ 语义有关,即 一个定义规则 (ODR) 这是一个接近但与无法拥有的概念不同的概念在同一个命名空间中重复强符号(我的意思是 link 命名空间,而不是 C++ 命名空间,这很快就会造成混淆)。

如果您想知道为什么它被标记为弱,那是为了让 dyld 能够在 dynamic linking 期间合并它,因为重用该模板会实例化它再次出现(导致 ODR 违规并根据上下文 link 时间错误),因为它是一个隐式实例,可能需要也可能不需要合并(直到静态甚至动态 link 才知道)时间,除非你当然将它定义为隐藏,在这种情况下你必须非常小心,因为语义会根据它是否是模块化构建等因素而有很大差异(我的意思是 LLVM "modules",而不是 Modules TS对于 C++)。

如果它不弱,您将通过将其定义为隐藏在超过 1 个翻译单元中(如果您重复使用该模板,例如在模块中的 header 中,就会导致违反 C++ 规则的 ODR ,你会得到重复的符号错误)。您可能会违反 ODR,因为它实际上并未强制执行,但要为一些令人讨厌的意外做好准备(即使用非模块化构建,也就是 "every translation unit is a module")。

通过将其定义为弱的,dyld 能够 select 更正每个最终 linked 的定义 object 是共享库还是可执行文件(并且不要忘记共享缓存)在 运行 时间 和 bind/relocate 它们适当地位于其他平面命名空间中。

编译器可以在没有任何形式提示的情况下推断出以上内容,隐藏 linkage 是一个非常糟糕的主意,除非你理解其中的含义,你想要 internal可见性,如果你真的想 re-instantiate 并每次都复制模板。 OSX 总体上有一个相当复杂的 linking 模型,可能会踩到很多地雷。

如果我关于 object 文件的事情是正确的,你真的不应该 运行 在 object 文件之前删除它们被送入静态 linker.

感谢 Apple 开源项目,我终于找到了答案。看起来 strip 从不删除全局弱 def 符号:

    ...
    *
    * In 64-bit applications, we only need to save coalesced
    * symbols that are used as weak definitions.
    */
    ...

(可惜网站没有逐行导航,也可能是我看的不够仔细) 这解释了我的情况,因为我的二进制文件是 mach-o-x86-64

尽管如此,这种行为背后的动机仍不清楚。

编辑:请参阅 Kristina 的回答以了解为什么 strip 更喜欢保留全局弱定义。