第五章:动态链接

第五章:动态链接

5.1 为什么要动态链接

静态链接的缺点

静态链接存在以下问题:

  1. 空间浪费
  2. 更新困难
  3. 内存占用大

示例代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

extern int add(int a, int b);
extern int subtract(int a, int b);

int main() {
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));
return 0;
}

编译和链接命令:

1
2
3
4
5
6
7
8
9
# 创建静态库
gcc -c static_lib.c -o static_lib.o
ar rcs libstatic.a static_lib.o

# 静态链接
gcc static_main.c -L. -lstatic -o static_program

# 查看文件大小
ls -l static_program

动态链接的优势

动态链接的优点:

  1. 节省空间
  2. 便于更新
  3. 共享内存

示例代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

extern int add(int a, int b);
extern int subtract(int a, int b);

int main() {
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));
return 0;
}

编译和链接命令:

1
2
3
4
5
6
7
8
# 创建动态库
gcc -shared -fPIC dynamic_lib.c -o libdynamic.so

# 动态链接
gcc dynamic_main.c -L. -ldynamic -o dynamic_program

# 查看文件大小
ls -l dynamic_program

5.2 动态链接的实现

动态链接的基本实现

动态链接的基本步骤:

  1. 加载动态库
  2. 符号解析
  3. 重定位

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <dlfcn.h>

int main() {
// 加载动态库
void *handle = dlopen("./libdynamic.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return 1;
}

// 获取函数指针
int (*add)(int, int) = dlsym(handle, "add");
int (*subtract)(int, int) = dlsym(handle, "subtract");

if (!add || !subtract) {
fprintf(stderr, "dlsym failed: %s\n", dlerror());
dlclose(handle);
return 1;
}

// 调用函数
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));

// 关闭动态库
dlclose(handle);
return 0;
}

编译和运行命令:

1
2
3
4
5
# 编译
gcc dl_demo.c -ldl -o dl_demo

# 运行
./dl_demo

动态链接的细节

动态链接的细节包括:

  1. 符号解析
  2. 重定位
  3. 延迟绑定

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

extern int add(int a, int b);
extern int subtract(int a, int b);

int main() {
// 第一次调用会触发延迟绑定
printf("First call: 10 + 5 = %d\n", add(10, 5));

// 第二次调用直接使用已解析的地址
printf("Second call: 10 + 5 = %d\n", add(10, 5));

return 0;
}

编译和运行命令:

1
2
3
4
5
# 编译
gcc lazy_binding.c -L. -ldynamic -o lazy_binding

# 运行
./lazy_binding

动态链接的优化

动态链接的优化技术:

  1. 符号预解析
  2. 共享库缓存
  3. 延迟加载

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <dlfcn.h>

int main() {
// 使用 RTLD_NOW 进行预解析
void *handle = dlopen("./libdynamic.so", RTLD_NOW);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return 1;
}

// 预解析所有符号
int (*add)(int, int) = dlsym(handle, "add");
int (*subtract)(int, int) = dlsym(handle, "subtract");

if (!add || !subtract) {
fprintf(stderr, "dlsym failed: %s\n", dlerror());
dlclose(handle);
return 1;
}

// 调用函数
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));

dlclose(handle);
return 0;
}

编译和运行命令:

1
2
3
4
5
# 编译
gcc optimize_dl.c -ldl -o optimize_dl

# 运行
./optimize_dl

实践练习

  1. 动态库创建实验
1
2
3
4
5
6
7
8
9
10
# 创建动态库
cat > libmath.c << EOL
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
EOL

gcc -shared -fPIC libmath.c -o libmath.so

# 查看动态库信息
readelf -d libmath.so
  1. 动态链接实验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建主程序
cat > main.c << EOL
#include <stdio.h>
extern int add(int a, int b);
extern int subtract(int a, int b);

int main() {
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));
return 0;
}
EOL

# 编译和链接
gcc main.c -L. -lmath -o math_program

# 运行程序
LD_LIBRARY_PATH=. ./math_program
  1. 延迟绑定实验
1
2
3
4
5
# 查看延迟绑定信息
objdump -R math_program

# 使用 strace 跟踪动态链接过程
strace ./math_program

思考题

  1. 动态链接相比静态链接有哪些优势?
  2. 动态链接的基本实现步骤是什么?
  3. 什么是延迟绑定?它有什么作用?
  4. 如何优化动态链接的性能?
  5. 动态链接可能带来哪些问题?

参考资料

  1. 《程序员的自我修养:链接、装载与库》
  2. 动态链接器文档
  3. ELF 文件格式规范
  4. GNU Binutils 文档
  5. System V ABI