dlopen动态加载共享库出现符号未定义错误

本文最后更新于:1 分钟前

dlopen动态加载共享库出现符号未定义错误

问题描述

有两个动态库,libfunc.so 和 libbase.so。其中 libfunc.so 依赖 libbase.so,在 libfunc.so 中有一个函数 void func() 调用了 libbase.so 中的函数 void base(),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// libbase.so
// base.h
#ifndef _base_h_
#define _base_h_

void base();

#endif // _base_h_

// base.c
#include "base.h"
#include <stdio.h>

void base() {
printf("base\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// libfunc.so
// func.h
#ifndef _func_h_
#define _func_h_

void func();

#endif // _func_h_

// func.c
#include "func.h"
#include <stdio.h>
#include "base.h"

void func() {
printf("func\n");
base();
}

在 main.c 中通过 dlopen 函数动态加载 libfunc.so,并调用其中的 func 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.c
#include <dlfcn.h>
#include <stdio.h>

int main() {
void *handle = dlopen("./libfunc.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return -1;
}

void (*func)() = dlsym(handle, "func");
if (!func) {
fprintf(stderr, "%s\n", dlerror());
return -1;
}

func();

dlclose(handle);

return 0;
}

但是在编译后提示 undefined symbol: base 错误:

1
2
3
4
code-server [14:53:36] dl  ➜ gcc -O0 -g main.c -o test -ldl                
code-server [15:01:03] dl ➜ ./test
func
./test: symbol lookup error: ./libfunc.so: undefined symbol: base

问题分析

由于 main.c 中并没有显式依赖 libfunc.so 和 libbase.so,所以在编译 main.c 的命令中没有进行显式链接这两个库。因此在运行时,dlopen 函数只加载了 libfunc.so,而没有加载 libbase.so,导致在 libfunc.so 中调用 libbase.so 中的函数时出现符号未定义错误。

那么我们尝试将这两个库都进行显式链接,看看能否解决问题:

1
2
3
4
code-server [15:03:42] dl  ➜ gcc -O0 -g main.c -o test -ldl -L. -lfunc -lbase
code-server [15:05:27] dl ➜ ./test
func
./test: symbol lookup error: ./libfunc.so: undefined symbol: base

可以看到仍然提示 base 符号未定义。

根因分析

由于 main.c 通过 dlopen() 加载 libfunc.so,所以不存在显式的依赖关系,我们甚至不需要在 main.c 中包含 func.h 这个头文件。因此在链接过程中,链接器会认为当前的 libfunc.so 和 main.o 无关,并将 libfunc.so 给优化掉。既然 libfunc.so 被视为无关项,那 libbase.so 更没有理由被留下来了。

也就是说,即使我们在命令中显式地去链接 -L. -lfunc -lbase 也是无效的。我们可以通过 readelf -d 命令查看 test 可执行文件的依赖:

1
2
3
code-server [15:13:40] dl  ➜ readelf -d test | grep -i 'share'
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

因此当执行 func() 函数时,由于 libbase.so 没有被加载,所以会提示 undefined symbol: base 错误。

解决方案

libfunc.so 可以通过 dlopen() 被加载,但是 libbase.so 本身与 main.c 无关,因此我们没有理由通过 dlopen() 来加载 libbase.so。

那有没有什么办法可以让 libbase.so 被加载进来呢?

1、 使用 LD_PRELOAD 强制预加载

LD_PRELOAD 是一个环境变量,用于在运行程序时预先加载指定的共享库。它的作用是在运行目标程序时优先加载指定的共享库,覆盖系统默认的库,从而实现对目标程序的功能增强、修改或者监控等目的。使用 LD_PRELOAD 可以在不修改目标程序源代码的情况下,对目标程序的行为进行改变。

我们可以通过设置 LD_PRELOAD 环境变量来强制预加载 libbase.so:

1
2
3
code-server [15:18:27] dl  ➜ LD_PRELOAD=./libbase.so ./test 
func
base

可以看到此时程序正常运行,base 函数也被调用了。

但是这样有个问题,如果 libfunc.so 依赖了多个共享库,那么我们就需要将所有的共享库都通过 LD_PRELOAD 预加载进来这样就会变得非常麻烦。

因此是否有其他的方法来解决这个问题呢?

2. -Wl,–no-as-needed 选项

在部分默认情况下,链接器会尽可能地链接被指定的库,但如果这个库的符号没有被使用到,链接器也不会链接它,可以认为,链接器在默认状态下使用了 --as-needed 选项对库进行链接。

这种情况导致了我们即使显式链接 libfunc.so 和 libbase.so,由于 main.c 并没有使用这两个库符号,所以链接器会将这两个库优化掉。

因此我们可以通过 -Wl,--no-as-needed 选项来关闭这个优化,使得可执行文件强制保持这个依赖关系:

1
2
3
4
code-server [15:46:29] dl  ➜ gcc -O0 -g main.c -o test -ldl -L. -Wl,--no-as-needed -lfunc -lbase 
code-server [15:46:34] dl ➜ ./test
func
base

此时程序正常运行,我们再通过 readelf -d 命令查看 test 可执行文件的依赖:

1
2
3
4
5
code-server [15:47:53] dl  ➜ readelf -d test | grep -i 'share'
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libfunc.so]
0x0000000000000001 (NEEDED) Shared library: [libbase.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

可以看到此时 test 可执行文件已经显式依赖了 libfunc.so 和 libbase.so 两个共享库。

此时还有一个问题,我们将这个依赖关系交给了应用去链接处理,当模块数量较多,且各个模块有各自的依赖时,应用的链接命令会变得非常复杂,并且应用本身并不想关心各个模块自己的依赖关系,它只想维护自己关心的模块即可。

是否还有其他的方式进行优化解决呢?

3. 构建 libfunc.so 时指定依赖

既然应用不想关心这些依赖关系,那么只能由 libfunc.so 自己来管理自己的依赖关系了。我们可以在构建 libfunc.so 时指定依赖的库,这样在链接 libfunc.so 时,链接器会自动将这些依赖的库链接进来。

我们只需要在编译 libfunc.so 时显式链接它的所有依赖库即可。我们先看看不链接依赖库时,libfunc.so 的信息:

1
2
code-server [15:58:46] dl  ➜ readelf -d libfunc.so | grep -i 'share'
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

可以看到此时 libfunc.so 只依赖了 libc.so.6 这个库。

我们在编译命令上加上 -lbase 选项,显式链接 libbase.so:

1
2
3
4
code-server [15:59:03] dl  ➜ gcc -fPIC -shared -O0 -g func.c  -I. -o libfunc.so -L. -lbase      
code-server [15:59:50] dl ➜ readelf -d libfunc.so | grep -i 'share'
0x0000000000000001 (NEEDED) Shared library: [libbase.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

此时 libfunc.so 显式依赖了 libbase.so 这个库。

我们再来编译运行一下 test 试试:

1
2
3
4
5
6
7
code-server [16:01:03] dl  ➜ gcc -O0 -g main.c -o test -ldl
code-server [16:01:07] dl ➜ readelf -d test | grep -i 'share'
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
code-server [16:01:13] dl ➜ ./test
func
base

可以看到此时可执行文件 test 并没有显式依赖 libfunc.so 和 libbase.so,通过 dlopen 加载 libfunc.so 时,libbase.so 也被加载进来了,因此程序正常运行。

总结

在使用 dlopen 函数动态加载共享库时,如果共享库之间存在依赖关系,需要注意以下几点:

  1. 如果共享库之间存在依赖关系,可以通过 LD_PRELOAD 环境变量来强制预加载依赖库。
  2. 如果共享库之间存在依赖关系,可以通过 -Wl,--no-as-needed 选项来关闭链接器的优化,强制保持这个依赖关系。
  3. 如果共享库之间存在依赖关系,可以在构建共享库时显式链接它的依赖库,这样在加载共享库时,依赖库也会被加载进来。

在模块化开发中,我们应该尽量使用第三种方法进行处理,将依赖关系交给模块自己去管理,而不是交给应用去处理。

以上就是本文的全部内容,希望对你有所帮助。


dlopen动态加载共享库出现符号未定义错误
https://ccccx159.github.io/2024/05/13/dlopen动态加载共享库出现符号未定义错误/
作者
Xu@n Ch3n
发布于
2024年5月13日
更新于
2024年5月13日
许可协议