静态链接与静态库


无论是动态库还是静态库都是为了解决代码重用的问题。静态库可以理解为一系列目标文件的集合,link的时候静态库中的symbol会和目标文件中的symbol一起链接,如上图所示。这种方式的好处是简单粗暴,静态库中的代码和目标工程中的代码一起编译连接,linker可以做全局的symbol级别的optimization,比如strip掉dead code等。使用静态库的劣势在于它会增加binary的大小,更重要的是如果静态库中的代码更新了,整个工程需要重新编译,不够灵活。

在UNIX操作系统中,静态库的表示方法为

lib + <library name> + .a

静态链接相对来说比较简单,如上图中展示了一个静态库被链接进一个executable的全过程

静态链接

–dead_strip

对于这种情况,binary中最终只会链接静态库中被用到的symbols,如下图所示

上图中,假设我们的binary只需要三角形和菱形两个symbol,这时候如果linker开启了优化模式,即使静态库中有多个symbol,最终被链接进来的也只有这两个。这也是为什么静态库的size很大,但最终的binary的size却很小的原因。为了加深理解,我们可以看下面的代码


//a.cpp
int __attribute__((noinline)) a_foo() {
    int buf[5000];
    return 1;
}
int __attribute__((noinline)) a_bar() {
    int buf[5000];
    return 1;
}


//main.cpp
extern int a_foo();
int main(){
    int x = a_foo();
    std::cout<<x<<std::endl;
    return 0;
}

上面代码中,a.cpp包含了两个函数,a_fooa_bar,而main.cpp中只用到了a_foo,而且a_foo是被声明的,main函数并不知道去哪里找这个函数。接着我们分别编译这两个文件,得到各自的目标文件

> clang -static -c main.cpp //main.o
> clang -static -c a.cpp //a.o

此时我们观察两个目标文件中的symbol


>  nm a.o | c++filt

00000050 T a_bar()
00000000 T a_foo()
                 U ___stack_chk_fail
                 U ___stack_chk_guard


> nm main.o | c++filt

000001e0 short GCC_except_table3
                 U __Unwind_Resume
                 U a_foo()
...
000000a0 T _main


观察目标文件中的symbol,其中main.o中的a_foo标记为U,符合我们的预期。

Static Linker - ld

接着我们手动的将这两个目标文件link起来产生最终的binary a.out,我们在MacOS下使用static linker - ld。它用来将目标文件链接成binary,与之对应的是所谓的dynamic linker - dyld,它的作用是用来加载动态库到内存中,我们后面会详细介绍dyld

ld -o a.out main.o a.o -lc++ -L/usr/local/lib -lSystem

Updated on macOS 11 and above, you need to pass -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib as well so that it locates the -lSystem library correctly. You can use -L$(xcode-select -p)/SDKs/MacOSX.sdk/usr/lib to evaluate the right path dynamically if required.

接着我们查看a.out中的符号

> nm a.out | c++filt

100000f68 short GCC_except_table3
                 ...
100000e70 T a_bar()
100000e20 T a_foo()
...
100000c40 T _main
                 U dyld_stub_binder

我们发现,a.o中没用的a_bar也被link进来了,这显然是我们不希望看到的,此时我们可以通过-dead_strip来告诉ld strip掉无用代码

ld -o a.out main.o a.o -lc++ -L/usr/local/lib -lSystem -dead_strip

也可以直接 clang++ main.cpp a.cpp -Wl,-dead_strip

此时我们再查看a.out的符号表则会发现a_bar()已经不在了。

ld64.lld

自然而然的我们会想否可以将上面的linker优化技术应用到静态库上,即给你一个很大的静态库,是否可以通过linker的帮助来裁剪掉无用的代码。为了回答这个问题我们要想一下-dead_strip是怎么工作的。显然对于每个executable都有一个main()函数,这个main函数是整个应用程序的entry point,也就是说我们可以从main函数中用到的symbols出发来trace所有用到的symbol并把他们记录下来,然后strip掉那些没有用的symbol。比如上面例子中main函数中发现了a_foo,是一个undefined symbol,这时linker再去寻找a_foo,而a_foo也是一个函数,它又用了别的symbol,通过这样的不断搜索,便可以找出所有用到的symbol。

回到静态库问题上,通常对于静态库,我们会提供public APIs,这些API即可作为我们的entry point,作为trace的起点。接下来的问题是,我们需要一个linker来帮我们完成trace + dead_code strip。不幸的是,MacOS上默认的ld不能strip目标文件,-dead_strip只对executable或者动态库有效。由于静态库只是目标文件的合集,我们需要一种linker可以帮我们strip 目标文件 - ld64.lld

需要注意的是ld64.lld目前已经处于不被维护的状态,请慎重使用

ld64.lld是LLVM toolchain里的一种linker,使用它我们需要自行编译LLVM

> git clone https://github.com/llvm/llvm-project.git
> mkdir build-release && cd build-release
> cmake -G Ninja ../llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;lld"
> ninjia

有了ld64.lld之后,我们需要先为其提供一个symbol list,里面包含我们要保留的symbol。还是上面的例子,假如我们要保留a_foo,则我们可以用下面命令

> echo "__Z5a_foov" > exported.syms
> LD=~/LLVM/build-release/bin/ld64.lld
> $LD -dead_strip -o a_lite.o -exported_symbols_list ./exported.syms -r a.o

此时得到的a_lite.o只保留了a_foo。可以看到ld64.lldld不同的地方在于,它支持-r,即可以将目标文件作为-dead_strip的输入,同时输出可以是一个monolithic 目标文件。

Resources

Appendix #1

vtable与-dead_strip

虽然-dead_strip可以帮我们strip掉无用代码,但它却不是万能的,对于虚函数,它貌似无能为力

//a.cpp
#include <iostream>
class A{
public:
    virtual void bark(){
        std::cout << "from A" << std::endl;
    }
    void print();
};
//main.cpp
#include "./a.h"
int main(){
    A b;
    b.print();
    return 0;
}

上述代码中,我们创建了一个A对象,并调用了它的成员方法print()。如果我们编译上述代码,并查看a.out的符号表,我们会发现A的虚函数bark()依然存在,其原因是-dead_strip貌似无法strip掉类的virtual table,进而strip不掉虚方法

> clang++ main.cpp a.cpp -Wl,-dead_strip -o a.out
> nm a.out | c++filt

...
0000000100000e40 unsigned short A::bark()
0000000100001b70 T A::print()
0000000100000e00 unsigned short A::A()
0000000100000e20 unsigned short A::A()
...
00000001000020e8 short vtable for A
               U vtable for __cxxabiv1::__class_type_info
...

Appendix #2

The C++ Registry Pattern

C++中有一种很常见的Registry Pattern,即通过定义一个无用的全局变量来执行一段初始化代码,我们还是来看一个具体的例子

//main.cpp
class AA {
public:
    AA(){}
    void foo(){}
};
class BB {
public:
    void bar(){}
}
auto REG_a = AA();

int main(){
    BB b = BB();
    b.bar();
    return 0;
}

上述例子中我们的main()函数定义了b并调用了bar(),按照我们对-dead_strip的理解,AA应该会被strip掉,因为除了auto REG_a = AA()这句之外没有其它的Call Site,而REG_a也没有在main函数中出现,因此应该同样被strip掉,我们可以编译一下看看结果是否符合预期

> clang++ main.cpp -Wl,-dead_strip
> nm a.out | c++filt | grep AA

0000000100000db0 unsigned short AA::AA()
0000000100000f10 unsigned short AA::AA()

我们发现AAREG_a并没有被strip掉,也就是说对于这种情况,linker会保留AA的构造函数,以及构造函数的transitive closure。但是对于AA::foo()却不会保留。