研究下 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")