在C语言中,动态链接指的是在程序运行时动态加载和链接库的过程,而不是在编译或链接时。这种链接方式的优势在于,可以在不重新编译程序的情况下,更新或替换库。此外,多个应用程序可以共享同一个库的单一副本,从而节省内存。
以下是动态链接涉及的基本概念:
-
动态链接库:这些库在程序运行时被加载。在Unix-like系统中,这些库的扩展名通常为
.so
(共享对象,shared object);在Windows上,扩展名为.dll
(动态链接库,dynamic-link library)。 -
静态链接:与动态链接相反,静态链接将库的所有代码都嵌入到最终的可执行文件中。这使得可执行文件更大,但也更独立,因为它不依赖于外部的库文件。
-
延迟加载:某些系统允许程序在实际需要时才加载和链接动态库,而不是在程序启动时。
如何在C中使用动态链接:
-
编写代码:我们可以像通常那样编写C代码,但要确保使用的是库中的函数和数据结构。
-
编译:使用GCC或其他编译器编译代码。例如:
gcc -Wall -c myprogram.c
-
链接:链接时,使用
-l
选项指定库的名字,并使用-L
选项指定库的路径(如果库不在标准路径下)。例如:gcc myprogram.o -L/path/to/library -lmylibrary -o myprogram
-
运行:在运行程序之前,确保动态链接器知道如何找到我们的库。这通常通过设置
LD_LIBRARY_PATH
环境变量来完成(在Unix-like系统上)。
在C语言中使用动态链接需要对链接过程有深入的了解,以及对库版本和依赖性的管理。但是,当正确使用时,它提供了强大的灵活性,允许我们更新库而不必重新编译或链接应用程序。
下面,我们来看一个实际的例子。
// child.h
#ifndef HELLO_H
#define HELLO_H
int hello();
#endif
// main.c
#include "child.h"
int main(){
hello();
return 0;
}
// hello.c
#include <stdio.h>
int hello(){
printf("hello, world\n");
return 0;
}
// danger.c
extern int puts(char *);
int hello(){
puts("you are going to output: ");
// do something;
// ...;
puts("\nyou are hacked...\n");
return 0;
}
执行下列指令:
gcc -c main.c -o main.o
gcc -c -fPIC hello.c -o hello.o
gcc -shared hello.o -o libhello.so
# compile-time linking:
gcc main.o -L./ -lhello -o a.out
# runtime linking:
#export LD_LIBRARY_PATH=./
./a.out
-
gcc -c main.c -o main.o
:- 这条命令将
main.c
源文件编译为目标文件,但不进行链接(由于有-c
参数)。 - 输出的结果是一个名为
main.o
的目标文件。
- 这条命令将
-
gcc -c -fPIC hello.c -o hello.o
:- 这条命令将
hello.c
源文件编译为目标文件,但不进行链接。 -fPIC
参数代表“位置无关代码”。当构建共享库时,这是非常关键的,因为它允许代码被加载到任何内存地址,无需重新定位。简单来说,它确保库中的代码无论在内存中的哪个位置都可以工作。- 输出的结果是一个名为
hello.o
的目标文件。
- 这条命令将
-
gcc -shared hello.o -o libhello.so
:- 这条命令将
hello.o
目标文件链接为名为libhello.so
的共享库。 -shared
参数告诉gcc
生成一个可以与其他对象链接的共享对象,形成一个可执行文件。
- 这条命令将
-
gcc main.o -L./ -lhello -o a.out
:- 这条命令将
main.o
目标文件与libhello.so
共享库链接,生成名为a.out
的可执行文件。 -L./
告诉链接器在当前目录中查找库。-lhello
告诉链接器与libhello.so
进行链接。按照惯例,使用-l
参数指定库时,会删除lib
前缀和.so
扩展名。
- 这条命令将
-
export LD_LIBRARY_PATH=./
(已被注释):- 如果取消注释并运行此命令,它会将
LD_LIBRARY_PATH
环境变量设置为当前目录(./
)。这告诉操作系统在运行可执行文件时在哪里查找共享库。如果我们不设置这个,并且库不在标准位置,那么可能无法运行该可执行文件,因为它找不到所需的库。
- 如果取消注释并运行此命令,它会将
-
./a.out
:- 这条命令运行编译好的
a.out
可执行文件。
- 这条命令运行编译好的
总结:
- 将
main.c
编译成目标文件。 - 将
hello.c
编译成目标文件,并打算将其转化为共享库。 - 从
hello.o
目标文件创建了libhello.so
共享库。 - 将
main.o
与libhello.so
链接,创建了可执行文件。 - 然后,我们(可选地)设置了库搜索路径,最后运行了可执行文件。
执行下列指令:
gcc -c -fno-builtin main.c -o main.o
gcc -c -fPIC hello.c -o hello.o
gcc -shared hello.o -o libhello.so
# compile-time linking:
gcc main.o -L./ -lhello -o a.out
./a.out
# prepare a fake lib:
gcc -c -fPIC danger.c -o danger.o
gcc -shared danger.o -o libhello.so
# run the executable
./a.out
这个例子涉及到共享库的替换。接下来我们详细看看这些步骤:
-
gcc -c -fno-builtin main.c -o main.o
:- 与之前的
gcc -c
类似,这条命令编译main.c
,输出目标文件为main.o
。 -fno-builtin
参数指示 GCC 不要替换代码中的任何可能的内建函数。例如,通常 GCC 可能会用更优化的版本替换一些如strcpy
这样的函数。使用这个参数可以禁止这样的替换。
- 与之前的
-
gcc -c -fPIC hello.c -o hello.o
和gcc -shared hello.o -o libhello.so
:- 这两个命令与我们之前提到的命令类似:首先,编译
hello.c
文件为一个位置无关的目标文件hello.o
,然后,创建一个名为libhello.so
的共享库。
- 这两个命令与我们之前提到的命令类似:首先,编译
-
gcc main.o -L./ -lhello -o a.out
:- 这条命令将
main.o
与当前目录下的libhello.so
链接,生成一个名为a.out
的可执行文件。
- 这条命令将
-
./a.out
:- 运行已链接的可执行文件
a.out
。
- 运行已链接的可执行文件
-
gcc -c -fPIC danger.c -o danger.o
和gcc -shared danger.o -o libhello.so
:- 这两步编译和链接一个新的共享库。首先,编译
danger.c
为一个位置无关的目标文件danger.o
。 - 然后,创建一个名为
libhello.so
的共享库,这将覆盖或替换前面创建的同名共享库。
- 这两步编译和链接一个新的共享库。首先,编译
-
./a.out
:- 再次运行可执行文件
a.out
。但此时,由于libhello.so
已经被替换,a.out
将使用新的、可能是恶意的共享库。
- 再次运行可执行文件
关键要点:这个流程展示了共享库替换的潜在危险。如果某个应用期望使用一个特定的共享库,但由于某种原因(例如攻击者替换了共享库或错误的配置),它加载了一个不同的库,那么应用的行为可能会完全不同,甚至可能是恶意的。
这就是为什么应用程序和系统管理员必须确保共享库的完整性和来源,并使用如 AppArmor、SELinux 或其他MAC (Mandatory Access Control) 系统,以及数字签名来确保只加载和执行受信任的代码。