Skip to content

LV020-链接库路径参数

这一部分,来整理一下 GCC 编译的时候与库路径相关的几个参数的用法,其实静态库只要链接的时候都找到了,后面就不会再有问题了,下面要讨论的几个参数主要是针对动态库。

一、准备工作

1. 测试环境

  • gcc 版本如下:
shell
sumu@virtual-machine:~/workspace$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.5.0-3ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
  • ubuntu 版本如下
shell
sumu@virtual-machine:~/workspace$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.6 LTS
Release:        18.04
Codename:       bionic

2. 测试文件

2.1 三个源文件

这三个文件是 main.c 依赖 libA.c,而 libA.c 又依赖 libB.c。

c
/* libB.c */
#include <stdio.h>
void func_B(void) {
    printf("func_B called\n");
}
c
/* libA.c */
#include <stdio.h>
extern void func_B(void);
void func_A(void) {
    printf("func_A called\n");
    func_B();
}
c
/* main.c - 主程序 */
extern void func_A(void);
int main() {
    func_A();
    return 0;
}

2.2 Makefile

下面的 makefile 仅为生成静态库和动态库来测试。

makefile
CC = gcc
AR = ar

# 默认目标: 构建所有库
all: libs

libs: libA.a libB.a libA.so libB.so

# 静态库
libA.a: libA.o
	$(AR) rcs $@ $<

libB.a: libB.o
	$(AR) rcs $@ $<

# 动态库
libA.so: libA.o
	$(CC) -shared -fPIC -o $@ $<

libB.so: libB.o
	$(CC) -shared -fPIC -o $@ $<

# 清理
clean:
	rm -f *.o *.a *.so app_* *.out

.PHONY: all clean libs

2.3 生成动态库

我们生成一下动态库供后面使用:

shell
make libA.so
make libB.so

我们用 ldd 命令查看一下这两个库:

image-20260324200413604

会发现虽然 libA.so 依赖于 libB.so,但是并没有体现出来,这样做的话编译能通过(因为只是编译成位置无关代码),但链接器不会记录对 libB.so 的依赖关系,生成的 libA.soNEEDED 字段中 没有 libB.so

Tips:NEEDED 字段指的是程序运行时需要加载的动态库。可以用下面的命令查找这个字段:

shell
readelf -d libA.so | grep NEEDED

我们可以用下面的命令重新生成 libA.so:

shell
gcc -shared -fPIC -o libA.so libA.c -L. -lB

image-20260324200605974

后续使用这个库进行测试。这样的话,链接器会 解析并记录 对 libB.so 的依赖,生成的 libA.soNEEDED 字段中 包含 libB.so,创建了正确的动态库依赖关系。

3. 测试用的库

我们上面生成了两个库,当前库使用 ldd 查看如下:

image-20260325094327083

此时使用的 makefile 如下,和前面的区别就是,直接显式指定 libA.so 依赖于 libB.so

makefile
CC = gcc
AR = ar

# 默认目标: 构建所有库
all: libs

libs: libA.a libB.a libA.so libB.so

# 静态库
libA.a: libA.o
	$(AR) rcs $@ $<

libB.a: libB.o
	$(AR) rcs $@ $<

# 动态库
libA.so: libA.o libB.so
	$(CC) -shared -fPIC -o $@ $< -L. -lB

libB.so: libB.o
	$(CC) -shared -fPIC -o $@ $<

# 清理
clean:
	rm -f *.o *.a *.so app_* *.out

.PHONY: all clean libs

二、-L 参数

1. 测试过程

我们知道 main.c 依赖于 libA.so,直接用下面的命令编译:

shell
gcc main.c -L. -lA

发现直接报错:

shell
sumu@virtual-machine:~/workspace$ gcc main.c -L. -lA
/usr/bin/ld: warning: libB.so, needed by ./libA.so, not found (try using -rpath or -rpath-link)
./libA.so: undefined reference to `func_B'
collect2: error: ld returned 1 exit status

这是因为 libA.so 还依赖于 libB.so,虽然他们都在同一目录下,但是没有办法自动找到 libB.so,我们直接把他们链接到一起:

shell
gcc main.c -L. -lA -lB

Tips:这里有一个地方可以注意一下,动态链接库其实也是有依赖顺序的:

当我们使用 gcc -shared -fPIC -o libA.so libA.cgcc -shared -fPIC -o libB.so libB.c 这两个命令编译出来的 libA.so 和 libB.so 的时候,用-lA -lB 不报错,反过来变成-lB -lA 会报找不到 func_B 的错。

当我们使用 gcc -shared -fPIC -o libB.so libB.cgcc -shared -fPIC -o libA.so libA.c -L. -lB 的时候,再编译 main.c 的时候,不管顺序怎样,都不会再报错了。

这样会发现,没有报错,成功生成 a.out,我们执行 a.out 并查看依赖:

shell
sumu@virtual-machine:~/workspace$ ./a.out 
./a.out: error while loading shared libraries: libA.so: cannot open shared object file: No such file or directory
sumu@virtual-machine:~/workspace$ ldd a.out 
        linux-vdso.so.1 (0x00007fffc6793000)
        libA.so => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f86ed8e7000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f86ededa000)

由上图可见,虽然使用-lB 参数将 libB.so 链接到了 a.out 中,但是上面只显示 a.out 依赖于 libA.so。由于找不到 libA.so(=> not found)的路径,因此还需要设置环境变量 LD_LIBRARY_PATH,我们来设置一下:

shell
export LD_LIBRARY_PATH=/home/sumu/workspace

然后我们重新执行 ldd 命令查看一下依赖情况:

shell
sumu@virtual-machine:~/workspace$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sumu/workspace
sumu@virtual-machine:~/workspace$ ./a.out 
func_A called
func_B called
sumu@virtual-machine:~/workspace$ ldd a.out 
        linux-vdso.so.1 (0x00007ffde91df000)
        libA.so (0x00007fc4901c0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc48fdcf000)
        libB.so (0x00007fc48fbcd000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc4905c4000)

由上图可见,libA.so 已经通过 LD_LIBRARY_PATH 环境变量找到,并且 libB.so 也出现在 a.out 的依赖中!

2. 结论

-L 指定的是链接时的库路径,生成的可执行文件在运行时库的路径由 LD_LIBRARY_PATH 环境变量指定。

三、-rpath

1. -rpath 简介

1.1 有什么用?

参考 Using LD, the GNU linker ,文档是这样写的:

Tips: 这里还有一个中文的手册可以作参考:GNU-LD 中文手册.pdf

text
Add a directory to the runtime library search path. This is used when linking an ELF executable with shared objects. All -rpath arguments are concatenated and passed to the runtime linker, which uses them to locate shared objects at runtime. The -rpath option is also used when locating shared objects which are needed by shared objects explicitly included in the link; see the description of the -rpath-link option. If -rpath is not used when linking an ELF executable, the contents of the environment variable LD_RUN_PATH will be used if it is defined. The -rpath option may also be used on SunOS. By default, on SunOS, the linker will form a runtime search patch out of all the -L options it is given. If a -rpath option is used, the runtime search path will be formed exclusively using the -rpath options, ignoring the -L options. This can be useful when using gcc, which adds many -L options which may be on NFS mounted filesystems. For compatibility with other ELF linkers, if the -R option is followed by a directory name, rather than a file name, it is treated as the -rpath option.

是 GCC 中用于指定运行时动态链接库搜索路径的选项。它允许开发者在编译和链接阶段将动态库的路径 硬编码 嵌入到生成的可执行文件中,从而在运行时无需额外设置环境变量 LD_LIBRARY_PATH。由于 -rpath 是传递给链接器(ld)的参数,所以必须通过 -Wl 前缀来使用。

1.2 语法格式

一般格式为

shell
-Wl,-rpath,<绝对路径> # 逗号后面不能有空格

例如:

shell
gcc main.c -o main -L/path/to/lib -Wl,-rpath,/path/to/lib -lmydynamiclib
  • -L/path/to/lib:指定动态库的搜索路径。
  • -Wl,-rpath,/path/to/lib:将 /path/to/lib 路径嵌入到可执行文件中。
  • -lmydynamiclib:链接名为 libmydynamiclib.so 的动态库。

如果有多个路径的话,可以使用多个 -Wl,-rpath,... 参数:

shell
# 示例:指定 /opt/libA 和 /home/user/libB 两个路径
gcc main.c \
    -L/opt/libA -L/home/user/libB \
    -lA -lB \
    -Wl,-rpath,/opt/libA \
    -Wl,-rpath,/home/user/libB \
    -o myprogram

1.3 $ORIGIN

在 Linux 系统中,$ORIGIN 是一个特殊的环境变量,它在链接(linking)和加载(loading)共享库时扮演着重要角色。主要用于定义库(如动态链接库*.so 文件)和可执行文件的运行时搜索路径。

当程序或者库被加载到内存执行时,Linux 动态链接器(如 ld.so 或 ld-linux.so)会负责寻找这些程序或者库所依赖的其它共享库。这个过程中,动态链接器会参考一系列的搜索路径来找到这些共享库。其中之一就是 $ORIGIN

$ORIGIN 代表了包含这个变量的可执行文件或者共享库的目录路径。当在编写共享库的时候,通过在链接时使用 -Wl,-rpath,'$ORIGIN' 选项(注意单引号是为了防止 Shell 对 $ORIGIN 进行解释;在 makefiles 或其他文件中直接使用时,通常需要写作 $$ORIGIN 来避免变量扩展),可以指定动态链接器在当前可执行文件或者库文件所在的目录下查找需要的共享库。

使用 $ORIGIN 的好处在于,它允许可移植性更高的应用程序部署,因为这意味着应用程序和它的依赖库可以被放置在文件系统中的任意位置,并且在运行时动态链接器仍然能正确找到它们,只要维持相对结构不变即可。

但是需要知道,并非所有的平台或构建系统都支持 $ORIGIN,因此在跨平台构建时需要注意。

2. 来尝试一下

2.1 清空 LD_LIBRARY_PATH

我们先清空前面设置的 LD_LIBRARY_PATH:

shell
export LD_LIBRARY_PATH=

2.2 只链接 A

前面我们执行 gcc main.c -L. -lA 的时候有如下的报错信息:

shell
/usr/bin/ld: warning: libB.so, needed by ./libA.so, not found (try using -rpath or -rpath-link)

这里其实有提示说可以用-rpath 或者-rpath-link 来指定。这里先使用-rpath:

shell
gcc main.c -L. -lA -Wl,-rpath,.

会发现是没有报错的,然后使用 ldd 命令查看:

image-20260325100444674

由上图可见,虽然没有明确指出链接 libB.so,但是 libB.so 还是出现在 a.out 的依赖中。但是是一个 not found 的状态,并且 a.out 也是无法执行的。

2.3 链接 A 和 B

我们这个时候配置一下按理说是要配置 LD_LIBRARY_PATH,但是这里我们暂时不这么做,先执行下面的命令,直接指定 a.out 依赖于 A 和 B 两个库:

shell
gcc main.c -L. -lA -lB -Wl,-rpath,.

会发现依然找不到:

image-20260325101102108

2.4 重新生成 libA.so

不配置 LD_LIBRARY_PATH 的话,我们现在只能重新生成一个 linA.so,我们同样使用 -rpath 参数指定 B 的路径:

shell
gcc    -c -o libA.o libA.c
gcc    -c -o libB.o libB.c
gcc -shared -fPIC -o libB.so libB.o
gcc -shared -fPIC -o libA.so libA.o -L. -lB -Wl,-rpath,.

gcc main.c -L. -lA -Wl,-rpath,.

会有如下输出信息:

image-20260325101532279

3. 改变库路径

3.1 测试 1:移动 libA.so

我们用上面重新生成的 libA.so,我们把它移动到到 lib_temp 目录去:

shell
mkdir -p lib_temp
mv libA.so lib_temp/

然后我们运行并查看库和可执行文件的依赖情况:

image-20260325102052671

发现 a.out 无法运行,找不到 libA.so。这个时候我们指定一下 LD_LIBRARY_PATH 的路径为 libA.so 的路径,看一下现象:

shell
export LD_LIBRARY_PATH=./lib_temp

然后再来看一下各个文件的依赖情况:

image-20260325102429682

一切又恢复了正常。此时,libA.so 是通过 LD_LIBRARY_PATH 找到的,而 libB.so 则是通过-rpath 指定的路径找到的。

3.2 测试 2:移动 A 和 B

我们还是用上面重新生成的 libA.so,我们它和 libB.so 移动到到 lib_temp 目录去:

shell
mkdir -p lib_temp
mv libA.so lib_temp/
mv libB.so lib_temp/

然后我们运行并查看库和可执行文件的依赖情况:

image-20260325102900265

发现 libA.so 依赖的 libB.so 的路径也找不到了。 a.out 无法运行,找不到 libA.so, 并且 libB.so 也不见了,原因就是要先找到 libA.so 再去找 libB.so,因为是 libA.so 依赖于 libB.so,而不是 a.out 依赖于 libB.so。

由此可见,使用了-rpath 参数指定库的路径后,生成的可执行文件的依赖库路径并非就固定不变了。而是执行时先从-rpath 指定的路径去找依赖库,如果找不到,还是会报 not fund。

这个时候我们指定一下 LD_LIBRARY_PATH 的路径为 libA.so 和 libB.so 所在的路径,看一下现象:

shell
export LD_LIBRARY_PATH=./lib_temp

然后再来看一下各个文件的依赖情况:

image-20260325103307950

一切又恢复了正常。此时,libA.so 和 libB.so 是通过 LD_LIBRARY_PATH 找到的。

3.3 小结

3.2 测试 2:移动 A 和 B 可知, LD_LIBRARY_PATH 不仅指定可执行文件的库路径,还指定了库所依赖于其它库的路径。

4. 结论

-rpath 可以在链接时指定库的路径,且指定的路径在运行时依然还有效(因为链接器已经将库的路径包含在可执行文件中了)。并非指定-rpath 参数后,就抛弃 LD_LIBRARY_PATH 环境变量,只是多了个可选的依赖库路径而已。

1.1 有什么用?

参考 Using LD, the GNU linker ,文档是这样写的:

Tips: 这里还有一个中文的手册可以作参考:GNU-LD 中文手册.pdf

text
When using ELF or SunOS, one shared library may require another. This happens when an `ld -shared` link includes a shared library as one of the input files. When the linker encounters such a dependency when doing a non-shared, non-relocateable link, it will automatically try to locate the required shared library and include it in the link, if it is not included explicitly. In such a case, the `-rpath-link` option specifies the first set of directories to search. The `-rpath-link` option may specify a sequence of directory names either by specifying a list of names separated by colons, or by appearing multiple times. The linker uses the following search paths to locate required shared libraries.

1. Any directories specified by `-rpath-link` options.
2. Any directories specified by `-rpath` options. The difference between `-rpath` and `-rpath-link` is that directories specified by `-rpath` options are included in the executable and used at runtime, whereas the `-rpath-link` option is only effective at link time.
3. On an ELF system, if the `-rpath` and `rpath-link` options were not used, search the contents of the environment variable `LD_RUN_PATH`.
4. On SunOS, if the `-rpath` option was not used, search any directories specified using `-L` options.
5. For a native linker, the contents of the environment variable `LD_LIBRARY_PATH`.
6. The default directories, normally ``/lib'` and ``/usr/lib'`.

If the required shared library is not found, the linker will issue a warning and continue with the link.

它是 GCC 链接器(ld)的一个参数,专门用于解决 链接时(Compile/Link Time)的 间接依赖 查找问题。当我们的程序依赖库 A,而库 A 又依赖库 B,但编译程序时 没有显式链接库 B,链接器就需要到 -rpath-link 指定的路径来找到库 B,以便完成符号解析。

1.2 语法格式

它是 GCC 链接器(ld)的一个参数,需要使用 -Wl 前缀来使用,一般格式为

shell
-Wl,-rpath-link,/path/to/libs

例如:

shell
gcc main.c -o main -lmydynamiclib -L/path/to/lib -Wl,-rpath-link,/path/to/lib
  • -lmydynamiclib:链接名为 libmydynamiclib.so 的动态库。
  • -L/path/to/lib:指定动态库的搜索路径。
  • -Wl,-rpath-path,/path/to/lib:将 /path/to/lib 路径添加到链接器的搜索路径中,这 =, 都可以。多个路径的话,可以使用多个-rpath-link 分别指定也可以用冒号分隔,例如 -Wl,-rpath-link,/path/to/lib1:/path/to/lib2:/path/to/lib3

2. 不显示依赖 libB.so 的 libA.so

2.1 生成动态库

我们通过下面的命令生成两个动态库:

shell
gcc    -c -o libA.o libA.c
gcc    -c -o libB.o libB.c
gcc -shared -fPIC -o libB.so libB.o
gcc -shared -fPIC -o libA.so libA.o

查看库的依赖信息如下:

image-20260325110818112

2.2 编译测试

我们执行下面的命令来编译 main.c:

shell
mkdir -p lib_temp && mv libB.so lib_temp # 库移动到其他目录
gcc main.c -L. -lA
gcc main.c -L. -lA -Wl,-rpath-link=./lib_temp

image-20260325115412667

2.3 结论

当 libA.so 中没有记录对 libB.so 的依赖的时候,即便加上了 -Wl,-rpath-link 也无法在链接的时候处理间接依赖。

3. 显示依赖 libB.so 的 libA.so

3.1 生成动态库

我们通过下面的命令生成两个动态库:

shell
gcc    -c -o libA.o libA.c
gcc    -c -o libB.o libB.c
gcc -shared -fPIC -o libB.so libB.o
gcc -shared -fPIC -o libA.so libA.o -L. -lB

查看库的依赖信息如下:

image-20260325115653310

3.2 编译测试

我们执行下面的命令来编译 main.c:

shell
mkdir -p lib_temp && mv libB.so lib_temp # 库移动到其他目录
gcc main.c -L. -lA
gcc main.c -L. -lA -Wl,-rpath-link=./lib_temp

有如下打印信息:

image-20260325125123945

3.3 结论

当 libA.so 中记录了对 libB.so 的时候,只链接 libA.so,再加上 -Wl,-rpath-link ,就可以在链接的时候处理间接依赖。

4. 运行库路径

3. 显示依赖 libB.so 的 libA.so 这部分编译测试部分就可以看出,其实 -rpath-link 指定的路径在运行时不再有效,我们执行 ldd 的时候就是 libA.so 就是 not found,所以执行肯定是报错的。那么我们依次执行:

shell
export LD_LIBRARY_PATH=.
./a.out

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./lib_temp
./a.out

来看一下结论:

image-20260325135405367

4. 结论

-rpath-link 可以在链接时指定库的路径,可以解决间隔依赖的问题,但是要求库中必须有被依赖库的记录。运行可执行文件时,-rpath-link 指定的路径就不再有效(链接器没有将库的路径包含进可执行文件中)。

五、直接使用路径

1. 两个动态库

我们通过下面的命令生成两个动态库:

shell
gcc    -c -o libA.o libA.c
gcc    -c -o libB.o libB.c
gcc -shared -fPIC -o libB.so libB.o
gcc -shared -fPIC -o libA.so libA.o

查看库的依赖信息如下:

image-20260325110818112

2. 尝试一下

我们直接用下面的命令编译:

shell
gcc main.c ./libA.so ./libB.so

这样直接写死路径是可以正常编译并执行的:

image-20260325111015639

3. 改变库的路径

我们将 libA.so 移动到 lib_temp 下:

shell
mkdir -p lib_temp
mv libA.so lib_temp

此时查看可执行文件依赖信息:

image-20260325111207006

发现 libA.so 路径找不到,并且无法执行,此时我们配置一下 LD_LIBRARY_PATH:

shell
export LD_LIBRARY_PATH=./lib_temp

然后再查看依赖情况:

image-20260325111357957

4. 结论

使用路径直接链接 .so 文件会将该路径硬编码到可执行文件中,程序运行时会严格按照编译时指定的路径查找动态库,从上面的现象来看,即便是配置了 LD_LIBRARY_PATH,也未能正确找到对应的动态库。

六、总结

总结一下-L-rpath-rpath-link

参数用途作用时机使用场景特点
-L指定链接时的库搜索路径仅在编译/链接时编译时指定动态库的搜索目录生成的可执行文件在运行时需要通过LD_LIBRARY_PATH环境变量指定库路径
-rpath指定运行时动态链接库搜索路径链接时 + 运行时将库路径硬编码到可执行文件中,运行时无需设置LD_LIBRARY_PATH路径会被嵌入到可执行文件的DT_RPATH中,支持$ORIGIN表示可执行文件所在目录
-rpath-link解决链接时的间接依赖查找问题仅在链接时程序依赖库A,库A又依赖库B,但编译时没有显式链接库B的情况仅在链接时有效,运行时无效。要求库A的NEEDED字段中已记录对库B的依赖

关键区别总结

对比项-L-rpath-rpath-link
链接时有效
运行时有效
处理间接依赖
路径嵌入可执行文件

参考资料:

GCC 中 -L、-rpath 和-rpath-link 的区别 - lsgxeva - 博客园

Linux 动态库路径_-rpath-CSDN 博客

Linux 使用$ORIGIN 为共享库设置相对路径实现可移植部署-开发者社区-阿里云