研究下 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选项指定链接的库文件名称,链接器会自动补充前缀lib为libfoo,并且优先链接动态库,所以会链接到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,指定标准库。默认是 GCC 的libstdc++,我们明确指定为 clang 的libc++。-fuse-ld,指定链接器,默认是 GCC 的ld,我们明确指定为 clang 的lld。
动态链接
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进行动态链接。可以使用dumpbin工具查看依赖项:
dumpbin.exe /dependents main.exe
在制作动态库.dll时,如果想隐式链接(像.so文件那样),还需要处理库的dllexport、dllimport,比较复杂,可以查看这里的文档了解,一个比较方便的方法是使用 xmake 的utils.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")