贡献者: addis
事实上,ubuntu 系统中用 apt
安装 lib*-dev
就是在系统的默认搜索路径添加头文件和 lib*.a
,lib*.so
文件,以及它依赖的 package 中的这些文件。这些文件在同一系统版本和同一 cpu 架构都是通用的(运气好的话也可能在不同系统中通用)。
在 C/C++
中,如果一个函数声明时前面使用了 static
,那么它将只在当前 translation unit 中可见,也就是无法被的文件在 link 时使用。
.a
文件是 static library, 在编译的时候一起编入可执行文件. 下面举一个例子
// lib1.cpp
#include <iostream>
using namespace std;
int f1() { cout << "In library 1" << endl; }
再编一个主文件
// main.cpp
#include <iostream>
using namespace std;
void f1();
int main()
{
f1();
cout << "In main" << endl;
}
如果将这两个文件用 g++ 正常编译 g++ main.cpp lib1.cpp
执行结果为
In library 1
In main
但现在我们把 lib1.cpp
先编译成 .o
文件
g++ -static -c -o lib1.o lib1.cpp
(其实 -o lib1.o
可以省略)(-static
用于静态编译), 再从 .o
文件生成 .a
文件 .a
文件的命名规则一般是前面加 (lib*.a
)
ar rcs lib1.a lib1.o
可以将多个 .o
文件封装到 .a
里面, 在后面添加 lib2.o, lib3.o
等即可。.a
就是 .o
的压缩文件(archive),ar
和 tar
差不多。(其中 rcs
选项中的 r
选项是添加并替换旧文件(如果有同名), c
选项是 create archive, s
选项是 write out an index, 虽然还是不明白什么意思)。若要打印文件内容,用 ar p lib1.a
或者 ar pv lib1.a
(verbose)。若要取出所有文件,用 ar x lib1.a [文件1] [文件2]
。
再来将 lib1.a
和主程序文件一块编译
g++ main.cpp -o main.x -L./ -l 1
其中 -o
的作用是给生成的可执行文件命名, -L
的作用是声明 .a
所在的目录, -l
是指明所用的 .a 文件, (将 lib*.a
写成 *
即可).
现在就可以运行 main.x 了
./main.x
.a
就是 archive 的 .o
文件。在 link 阶段使用。
g++
在(静态)link 阶段 .o
或 .a
的顺序是非常重要的,某个 o
文件只能调用在它后面列出的 o
文件,否则会提示找不到 symbol。要让 g++
忽略这个顺序,可以使用
g++
进行动态链接,可以用 -l
的另一种形式 -l :libXXX.so
。其中 :文件名
直接搜索 文件名
,而不是 lib文件名.so或a
。
-Wl,--start-group 文件1 文件2 ... -Wl,--end-group
,这样 linker 找不到 symbol 时就会在 group 内的文件中反复搜索(编译速度会降低)。
ldd
仅列出直接依赖的库文件(不包含用 dlopen()
函数直接从代码中加载的,因为很有可能会根据用户输入来加载,无法预判)。
lddtree
(apt install pax-utils
)
stdc++.so.6
即使同名,也不一定是正确的版本,见这里。如果不同路径都有这个文件,似乎会自动加载正确的版本。
1创建动态库:
g++ -shared -fPIC -o lib1.so lib1.cpp
使用方法:
把 cpp 编译成 .o 文件时不需要声明动态链接库和所在目录, -c
选项(不链接)普通编译即可.
g++ -c main.cpp
把 .o
文件链接成可执行文件时, 在最后 (注意必须是在最后) 加上
-Wl,-rpath,<library path> -L<library path> -l <libname1> -l <libname2>
其中 <library path>
可以是多个路径,如 path1:path2:...
其中 -Wl,aaa,bbb
命令是将 aaa bbb
选项传给 linker,剩下的 -L<library path> -l <libname1> -l <libname2>
的用法和上述 .a
中的一样.
g++ -o main.x main.o -l1 -L./ -Wl,-rpath,./
Linux 程序运行时搜索动态链接库的顺序(详见这里以及 man8):
rpath
(一般不推荐)
LD_LIBRARY_PATH
runpath
/lib/
和 /usr/lib/
中的,以及 /etc/ld.so.conf
中的文件。在 ubuntu 中,该文件实际上 include
了 /etc/ld.so.conf.d
路径中的所有 *.conf
文件。
-Wl,-rpath
设置的变为 runpath
而不是 rpath
。二者唯一区别在于动态库搜索路径顺序。一般并不推荐使用 rpath
。
rpath
,用 -Wl,--disable-new-dtags,-rpath
,如果要强制旧编译器使用 runpath
,也可以用 -Wl,--enable-new-dtags,-rpath
。
-L
和 rpath
里面的路径可以是相对路径,但如果 rpath
用相对路径,那么就是相对于执行程序时的 pwd
。如果要相对于执行程序,那么用 $ORIGIN/相对路径
可以用 ldd main.x
查看动态链接库,会发现其中有 lib1.so
。如果不用 rpath
,也可以在执行可执行文件以前把路径加入到环境变量 LD_LIBRARY_PATH
中。rpath
和 LD_LIBRARY_PATH
可以是相对于可执行文件的相对路径,也可以是绝对路径。也可以用 $ORIGIN
表示可执行文件的路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/custom/path/
g++
中的 -L
选项(make 的时候搜索静态或动态库的路径)可以用环境变量 LIBRARY_PATH
设置,-I
选项(搜索头文件的路径)可以用环境变量 CPATH
设置。
rpath
只能设定当前可执行文件的的路径,如果可执行文件依赖的 .so
文件所需要的 .so
文件不在默认路径,就只能通过修改 LD_LIBRARY_PATH
或者用 patchelf
(见下文)对其修改才可以。
LD_LIBRARY_PATH
中不可以有关键的系统 .so
文件(如 libc
),否则如果不兼容,很多系统命令像 ls, touch, echo
等都会用不了。
LIBRARY_PATH
的顺序很重要,如果同名 .a
的路径出现在同名 .so
路径的前面,那么就会链接 .a
。如果二者的路径相同,那么还是会有限链接 .so
。
rpath
,设置环境变量 LD_RUN_PATH
也是等效的。链接的程序是 ld
,而动态链接库是 ld-linux.so
DT_SONAME
entry 会保存本来的文件名给运行时的 linker 确认。用 patchelf --print-soname 文件
可以显示(通常来说,lib*.so.*.*.*
内部的 soname
可能会省略最后若干个版本编号。所以把 so 文件重命名以后,还要用 patchelf --set-soname 新文件名 新文件名
。
chrpath
(不推荐)或者 patchelf --set-rpath '路径' XXX.so
(其实设置的也是 runpath
而不是 rpath
)其中如果要设置相对路径如 '$ORIGIN/相对路径'
参考这里。用这种方法,加上 $ORIGIN
功能,似乎可以像 AppImage 或者 snap 一样把任何动态链接的程序打包到同一个文件夹中,包括所有依赖,也就是所谓的绿色软件。
patchelf --print-rpath 文件
可以查看 rpath。返回的多个路径使用冒号隔开,可以在命令后面加 | tr : \\n
把冒号替换为换行。
rpath
(不推荐)而不是 runpath
,先用 patchelf --remove-rpath 文件
移除所有 runpath
或 rpath
,然后 patchelf --force-rpath --set-rpath '路径1:路径2:..' 文件
即可。
patchelf --print-needed 文件
可以显示直接依赖的动态库(不包含依赖的依赖)。这应该和 lddtree 文件
的第一层是一样的。
patchelf --remove-needed lib依赖库.so 文件
可以删除指定的依赖库。
readelf -a 文件 | grep NEEDED
。
patchelf --add-needed lib依赖库.so 文件
添加指定依赖库。注意多次添加会出现重复的依赖。
patchelf
可以同时指定多个 文件
。
patchelf --print-interpreter main.x
可以显示动态链接程序的链接器,一般是 /lib64/ld-linux-x86-64.so.2
。也可以用 patchelf --set-interpreter 链接器 文件
指定。
-l
既能匹配 lib***.a
也能匹配 lib***.so
,那么 gcc 会默认选 .so
(貌似有时候二者都需要)。如果想要静态链接,要么用 -static
选项(禁止链接到任何动态 lib),要么直接指定 .a
的地址和文件名,如 g++ -o main.x f1.o /some/path/lib***.a another/path/lib***.a
。
-l ***
不会自动匹配到带版本号的 lib***.so.x.x
,一般存在一个名为 lib***.so
的软链指向某个具体的版本。
-l:lib库.so或a
。不能有空格。
rpath
是相对路径,那么它相对的是 pwd
而不是可执行文件的位置,定义时可以使用 ${ORIGIN}
来表示可执行文件的位置。
elf文件
的 runpath 或 rpath 可以用 readelf elf文件 -d | grep PATH
或者 objdump -x elf文件 | grep PATH
。
readelf -s 链接库.so
可以检查里面的 symbols。函数的类型是 FUNC
,输出有两个表,看 .dynsym
就可以,它是 .symtab
中 allocable 的一个子集(详见这里)。如果是 c++ 函数,那么函数名是 mangle 的,用 --demangle
来显示 c++ 中的函数名(会包括 namespace)。
.a
或 .so
文件里面是否有某个函数,用例如 nm -A /usr/lib/x86_64-linux-gnu/libflint.a | grep fmpz_set
类似地也有 nm xxx.so
,同样也可以用 --demangle
so
库中没有 debug 信息,那么除了函数名外并不能查看输入输出变量的个数和类型。如果有 debug 信息,可以用 readelf -wi
查看。
-l
的顺序就是搜索 symbol 的顺序,如果多个依赖的 so
中有相同的 symbol,会使用第一个搜到的。如果函数 prototype 不对就会运行出错。注意不会搜索间接依赖库中的 symbol。
linux-vdso.so.1
不存在于文件系统,开机时自动加载到内存。libc.so.6
是 GNU C library,里面有 system call。ld-linux-x86-64.so.2
是 linker,它的路径是专门由可执行文件的 interpreter path 指定的,不归 rpath 那些管。
libc.so
,那么一定要用匹配版本的 ld-*.so
并且专门指定器路径。事实上 dpkg -L libc6
中列出的所有库都似乎需要和 libc.so
的版本匹配。常见的有 libm, libpthread, librt
等。
-Wl,--dynamic-linker=/绝对路径/ld-linux.so.X
可以指定 dynamic linker。
其他笔记:
ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\n
可以查看 linker
的默认搜索路径
sudo ldconfig
可以更新动态链接库的搜索
ldconfig -p | grep 库名
可以查看除了 LD_LIBRARY_PATH
之外的所有动态搜索路径中有没有某个 .so
。
ldconfig -N -v $(sed 's/:/ /g' <<< $LD_LIBRARY_PATH) 2>&1 | grep 库名
可以包括 LD_LIBRARY_PATH
动态库本身也可以依赖于其他动态库,例如再添加一个程序
// lib0.cpp
#include <iostream>
using namespace std;
void f0()
{
cout << "In library 0" << endl;
}
然后修改 lib1
// lib1.cpp
#include <iostream>
using namespace std;
void f1()
{
cout << "In library 1" << endl;
void f0();
f0();
}
制作库
g++ -c lib0.cpp lib1.cpp // 生成 lib0.o lib1.o
g++ -shared -fPIC -o lib0.so lib0.cpp
g++ -shared -fPIC -o lib1.so lib1.cpp -l0 -L./ -Wl,-rpath,./
编译主程序,使用库,注意只需要链接 lib1
g++ -c main.cpp
g++ -o main.x main.o -l1 -L./ -Wl,-rpath,./
用 ldd main.x
检查所有依赖的动态库,会发现 lib0, lib1
都在。
main.x
依赖 -l test1, -l test2
,而它们又分别依赖 -l:libbase.so.1
和 -l:libbase.so.2
,而这两个库里面有同名的 symbol,那么 libbase.so.1和2
中的所有同名 symbol 会使用前者中的,而不可能分别调用不同的版本。这之和链接的顺序有关,和 main.x
中调用函数的顺序无关。
conflict()
。注意 main.x
并不直接依赖 libbase.so.x
而是间接依赖,无法调用其中的函数。所以使用的 conflict()
会是最先链接的 libtest
中的。如果 main.x 直接依赖所有四个库,那么编译的时候会警告可能发生冲突,运行时会调用最先链接的库中的 conflict()
。
main()
无法直接调用 base()
,因为不是直接依赖 libbase
。链接阶段错误:undefined reference to symbol '_Z4basev', error adding symbols: DSO missing from command line, ld returned 1 exit status
。
export LD_LIBRARY_PATH=$PWD
g++ -shared -fPIC -o libbase.so.1 libbase.1.cpp
g++ -shared -fPIC -o libbase.so.2 libbase.2.cpp
g++ -shared -fPIC -o libtest1.so libtest1.cpp -L . -l:libbase.so.1
g++ -shared -fPIC -o libtest2.so libtest2.cpp -L . -l:libbase.so.2
g++ -o main.x main.cpp -L . -l:libtest1.so -l:libtest2.so
void test1(); void test2();
void conflict();
int main() { test1(); test2(); conflict(); }
#include <iostream>
using namespace std;
void base();
void test1() { base(); }
void conflict() { cout << "conflict() in libtest1.cpp" << endl; }
#include <iostream>
using namespace std;
void base();
void test2() { base(); }
void conflict() { cout << "conflict() in libtest2.cpp" << endl; }
#include <iostream>
using namespace std;
void base() { cout << "base() in libbase.1" << endl; }
void conflict() { cout << "conflict() in libbase1" << endl; }
#include <iostream>
using namespace std;
void base() { cout << "base() in libbase.2" << endl; }
void conflict() { cout << "conflict() in libbase2" << endl; }
ldd
命令查看。
dlopen()
加载库文件,dlsym()
加载具体函数,不需要函数声明,但首先要知道函数指针的类型。
#include <iostream>
#include <dlfcn.h> // Required for dlopen, dlsym, and dlclose
#include <cstdlib> // For exit()
typedef int (*processFunc)(double, const char*);
int main() {
// Load the shared library
void* handle = dlopen("./libexample.so", RTLD_LAZY);
if (!handle) {
std::cerr << "Error: " << dlerror() << std::endl;
exit(EXIT_FAILURE);
}
// Reset errors
dlerror();
// Get the function symbol from the library
processFunc process = (processFunc) dlsym(handle, "process");
const char* dlsym_error = dlerror();
if (dlsym_error) {
std::cerr << "Error: " << dlsym_error << std::endl;
dlclose(handle);
exit(EXIT_FAILURE);
}
// Call the function with sample arguments
double num = 42.5;
const char* str = "Hello, world!";
int result = process(num, str);
std::cout << "Result: " << result << std::endl;
// Close the library
dlclose(handle);
return 0;
}
libc.so
这样的东西可能会和系统本身的发生冲突,所以这个方法并不总是 work,应该只对相同发行版的相同版本相同 cpu 架构有效。然而静态链接的程序就没有这个问题。