研究下 c++ 的动态链接和静态链接。

👋测试源码和环境

main.cpp

#include <string>

void Print(std::string &str);

int main()
{
    std::string str("hello!");
    Print(str);
    return 0;
}

libfoo.cpp

#include <iostream>
#include <string>

void Print(std::string &str)
{
    std::cout << str << std::endl;
}

环境

apt update && apt install -y lsb-release wget software-properties-common gnupg g++-multilib wget git make
bash <(curl -fsSL https://apt.llvm.org/llvm.sh) 18 all

## add global path
echo 'export PATH=/usr/lib/llvm-18/bin:$PATH' >> /etc/profile
echo 'export CC=clang' >> /etc/profile
echo 'export CXX=clang++' >> /etc/profile

👋libstdc++

先讨论libstdc++的情况。

libfoo

首先编译libfoo

## 生成目标文件 libfoo.o
clang++ -c libfoo.cpp -o libfoo.o

## 生成静态库 libfoo.a
ar rcs libfoo.a libfoo.o

## 生成动态库 libfoo.so
clang++ -shared libfoo.cpp -o libfoo.so
  • libfoo.o:目标文件。是源代码经过编译后生成的中间文件,包含了函数、变量和符号的实现,但不能独立运行,尚未链接成最终可执行程序。
  • libfoo.a:静态库。由多个.o打包而成的文件,在编译时会被嵌入到最终的可执行文件中,程序运行时不需要再查找该库文件。
  • libfoo.so:动态库或共享库。在程序运行时动态查找加载,不会在编译时被嵌入到可执行文件中。

在 windows 上:

  • .o对应.obj
  • .a对应.lib
  • .so对应.dll

动态链接

## 编译 main.cpp
clang++ main.cpp -L. -lfoo -o main

## 查看链接的库
ldd main

## 输出
linux-vdso.so.1 (0x00007fff69daf000)
libfoo.so => not found
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fe7a2000000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fe7a2301000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fe7a22e1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe7a1e1f000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe7a23ee000)

-L选项用于指定库文件的搜索路径,我们这里指定.为当前路径。-l选项指定链接的库文件名称,链接器会自动补充前缀liblibfoo,并且优先链接动态库,所以会链接到libfoo.so

注意到libfoo.so => not found,执行后会报错:

## 执行
./main

## 输出
./main: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

这是因为-L只是指定编译时库文件的搜索路径,而不会改变系统在运行时搜索库文件的路径,可通过环境变量LD_LIBRARY_PATH指定。

## 为LD_LIBRARY_PATH环境变量添加当前路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

## 再次执行
./main

## 输出
hello!

如果库文件前缀不以lib开头,也可以显式指定:

clang++ main.cpp ./libfoo.so -o main
clang++ main.cpp -L. -l:libfoo.so -o main
  • 第一种直接指定库文件完整路径。并且路径会嵌入可执行文件中,运行时无需关心LD_LIBRARY_PATH,但如果你将可执行文件或者动态库移动到了其他目录,则无法运行。
  • 第二种显式指定动态库名称。链接器不会自动追加lib前缀,如果libfoo.so不存在也不会尝试静态链接libfoo.a,但运行时仍然需要像上面那样确保库文件路径在LD_LIBRARY_PATH中。

静态链接

动态库无法静态链接,这里我们将libfoo.so重命名或者删掉,以下4种静态链接方式结果相同:

clang++ main.cpp -L. -lfoo -o main
clang++ main.cpp -L. -l:libfoo.a -o main
clang++ main.cpp ./libfoo.a -o main
clang++ main.cpp ./libfoo.o -o main

## 查看链接的库
ldd main

## 输出
linux-vdso.so.1 (0x00007ffc867f3000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f0810c00000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f0810ede000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f0810ebe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0810a1f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0810fcc000)
  • 第一种-l会优先查找libfoo.so,这里我们已经删掉,所以会链接到libfoo.a
  • 第二种显式指定静态库名称。
  • 第三种直接指定静态库完整路径。
  • 第四种直接指定目标文件。

另外还有一种更彻底的静态链接选项-static

彻底静态链接

clang++ main.cpp ./libfoo.a -static -o main

## 查看链接的库
ldd main

## 输出
## 没有任何依赖
not a dynamic executable

-static选项阻止与动态库进行链接,这要求所有库都需要提供.a文件进行静态链接,但好处就是没有任何额外依赖,c 标准库和 c++ 标准库也会嵌入到最终的可执行文件中。

尽可能静态链接

如果你有库必须动态链接,但想尽可能静态链接其他库,可以:

clang++ main.cpp ./libfoo.a -static-libstdc++ -static-libgcc -o main

## 查看链接的库
ldd main

## 输出
linux-vdso.so.1 (0x00007fff57960000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f417fcbd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f417fadc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f417fe98000)

这将静态链接 c++ 标准库,但遗憾的是没有-static-libc来静态链接 c 标准库。一个折中的方法是低版本的系统上编译程序,使其链接低版本的libc,由于 c 标准库向后兼容,在高版本系统上也能正常运行。

如果你仍然想静态链接 c/c++ 标准库,但动态链接其他库,可以尝试摆弄-Wl,-Bstatic-Wl,-Bdynamic,然后找到所有依赖逐个-l:xx.a,你需要克服无数个链接错误。。

👋libc++

在 Linux 系统上,使用 clang 编译默认是链接libstdc++,即 GCC 的 c++ 标准库实现。而libc++clang/llvm 的 c++ 标准库实现。

选择libstdc++还是libc++,我想在大部分项目中是没啥差别的,libc++理论上和 clang 配合更好,但在 Linux 上大多是链接libstdc++,而 macOS 上则是libc++

重新编译libfoo

## 生成目标文件 libfoo.o
clang++ -c -stdlib=libc++ libfoo.cpp -o libfoo.o

## 生成静态库 libfoo.a
ar rcs libfoo.a libfoo.o

## 生成动态库 libfoo.so
clang++ -shared -stdlib=libc++ -fuse-ld=lld libfoo.cpp -o libfoo.so
  • -stdlib,指定标准库。默认是 GCClibstdc++,我们明确指定为 clanglibc++
  • -fuse-ld,指定链接器,默认是 GCCld,我们明确指定为 clanglld

动态链接

clang++ main.cpp ./libfoo.so -stdlib=libc++ -fuse-ld=lld -o main

## 查看链接的库
ldd main

## 输出
linux-vdso.so.1 (0x00007ffdc1ce5000)
./libfoo.so (0x00007f63905c2000)
libc++.so.1 => /lib/x86_64-linux-gnu/libc++.so.1 (0x00007f63904b9000)
libc++abi.so.1 => /lib/x86_64-linux-gnu/libc++abi.so.1 (0x00007f639047d000)
libunwind.so.1 => /lib/x86_64-linux-gnu/libunwind.so.1 (0x00007f639046f000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f6390390000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f639036e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f639018d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f63905cf000)

静态链接

clang++ main.cpp ./libfoo.a -stdlib=libc++ -fuse-ld=lld -static -o main

## 查看链接的库
ldd main

## 输出
not a dynamic executable

需要注意的是当使用-stdlib=libc++时,没有对应的-static-libc++,即无法单独静态链接libc++,只能-static全部静态链接,或者尝试手动逐个链接.a文件。

👋x86-32

当目标架构是32位时,加上-m32编译选项即可,但需要注意有安装相应的i386包,文章开头我们安装了g++-multilib,很方便的提供了各种架构的库文件、头文件支持。

但 clang 的官方包仅为 debian 提供i386的软件包,并且不支持多个架构共存安装。

👋MSVC

在 Windows 上情况有所不同,一个简单的归纳概括:

GCC Clang MSVC
c 标准库 动态:libc.so
静态:libc.a
动态:libc.so
静态:libc.a
动态:ucrtbase.dll
静态:libcmt.lib
c++ 标准库 动态:libstdc++.so
静态:libstdc++.a
动态:libc++.so
静态:libc++.a
动态:msvcprt.dll
静态:libcpmt.lib

libc通常特指glibc,即 GUN 的 c 标准实现,同时也有musl等其他 c 标准库实现。

而 msvc 的 c 库又称C 运行时库,msvc 自2015起默认链接UCRT,即通用 c 运行时库。UCRT 除了实现 C 标准库,还包括程序启动、错误处理、内存管理等与 Windows 系统深度耦合的运行时功能,这些功能超出了纯粹的 c 标准库范围,因此称为"C 运行时库"更贴合其用途。

在 msvc 中.lib文件的语义和 Linux 的.a文件并不完全相同,.lib文件不仅可以作为静态库使用,还可以作为动态库的“导入库”。导入库是一个特殊的.lib 文件,它不包含函数的实现,而是只包含指向动态库中函数的符号信息等。

可以使用/MT编译器选项静态链接 c 运行时库和 c++ 标准库。与之对应的是/MD进行动态链接。

在制作动态库.dll时,如果想隐式链接(像.so文件那样),还需要处理库的dllexportdllimport,比较复杂,可以查看这里的文档了解,一个比较方便的方法是使用 xmakeutils.symbols.export_all,会自动帮我们处理:

set_toolchains("msvc")

target("foo")
    set_kind("shared")
    add_files("libfoo.cpp")
    add_rules("utils.symbols.export_all", {export_classes = true})

target("main")
    set_kind("binary")
    add_deps("foo")
    add_files("main.cpp")