为什么动态链接不合并数据段和代码段?
动态链接(dynamic linking)不包含合并代码段(code segment)和数据段(data segment)的过程,主要原因在于动态链接的设计目标和工作方式。以下是更详细的解释:
-
设计目标:
- 动态链接的目标是将可执行文件在运行时与共享库(如DLL或SO文件)进行链接,以实现代码重用、节省内存和简化更新。动态链接旨在提供一个运行时的机制,允许不同程序共享相同的库,而无需在编译时将这些库合并到每个可执行文件中。
-
工作方式:
- 动态链接的工作方式是通过延迟绑定(lazy binding)和在运行时解析符号。可执行文件在加载时并不会立即将库的代码段和数据段合并到自身的地址空间,而是通过动态链接器(dynamic linker)在需要访问库的函数或数据时才解析和加载相应的符号。
- 这种方式避免了在编译时和链接时合并库代码和数据段,减少了可执行文件的大小,并允许不同的进程共享同一份库的内存映像(memory image),节省系统内存。
-
共享内存:
- 共享库的代码段通常是只读的,可以被多个进程共享,而数据段通常是每个进程私有的。如果在动态链接时将代码段和数据段合并,则会失去这种共享内存的优势,导致内存使用效率降低。
- 代码段的共享使得多个进程可以使用同一份库的代码,而无需为每个进程加载一份独立的副本。这对于大型库(如标准C库或图形库)尤为重要。
-
更新和维护:
- 动态链接库的独立性允许库的更新和维护更加简便。因为库是独立于可执行文件的,所以更新库时不需要重新编译和链接所有依赖该库的可执行文件。这在操作系统和大型应用程序的维护中非常重要。
- 如果在链接时就将代码段和数据段合并,更新库就需要重新生成所有使用该库的可执行文件,增加了维护的复杂性和工作量。
-
性能考虑:
- 延迟绑定的机制允许在程序运行时仅加载和解析实际需要的符号,而不是在启动时一次性加载所有库。这可以减少程序启动时间,并根据需要动态加载库,提高性能和灵活性。
综上所述,动态链接不包含合并代码段和数据段的过程,主要是为了实现代码重用、节省内存、简化更新和维护以及提高性能和灵活性。通过在运行时动态加载和链接库,系统可以更高效地管理资源,并提供更大的灵活性。
能用精干的语言讲解一下动态链接的过程吗?
Linux 动态链接的过程可以分为以下几个阶段,每个阶段都涉及特定的步骤和机制。下面是一个详细的分解:
1. 编译和链接阶段
在编译和链接阶段,程序员将源代码编译成目标文件,并将这些目标文件与动态库链接生成可执行文件。
编译
gcc -c main.c -o main.o
这条命令将 main.c
编译成目标文件 main.o
。
链接
gcc -o main main.o -L. -lmylib
这条命令将目标文件 main.o
与动态库 libmylib.so
链接生成可执行文件 main
。
在这个阶段,链接器(如 ld
)不会将动态库的代码和数据合并到可执行文件中,而是记录动态库的名字和一些符号信息。
2. 加载阶段
当用户运行可执行文件时,Linux 内核会加载这个文件并启动执行。加载阶段涉及以下步骤:
加载可执行文件
内核将可执行文件加载到内存中,包括其代码段、数据段等。然后,将控制权交给程序的入口点(通常是 main
函数)。
调用动态链接器
在 ELF(Executable and Linkable Format)可执行文件中,有一个名为 INTERP
的段,它指定了动态链接器的位置,例如 /lib64/ld-linux-x86-64.so.2
。内核会加载这个动态链接器,并将控制权转交给它。
3. 动态链接器的工作
动态链接器负责解析可执行文件中未解析的符号,并将共享库加载到进程地址空间。主要步骤如下:
解析动态库依赖
动态链接器读取可执行文件的 .dynamic
段,找到需要加载的共享库,并解析这些库的依赖关系。常用的共享库路径包括 /lib
和 /usr/lib
,也可以通过 LD_LIBRARY_PATH
环境变量指定。
加载共享库
动态链接器使用 mmap
将共享库映射到进程地址空间。共享库的代码段通常是只读的,可以被多个进程共享,而数据段是每个进程私有的。
解析符号
动态链接器解析可执行文件和共享库中未解析的符号。这包括全局变量和函数调用。它会在共享库的符号表中查找这些符号,并更新可执行文件和共享库中的重定位条目,使它们指向正确的内存地址。
运行初始化代码
一些共享库可能有初始化代码(如 C++ 静态构造函数),这些代码会在库加载后立即执行。动态链接器会运行这些初始化代码。
4. 程序执行
一旦动态链接器完成所有符号解析和重定位工作,它将控制权交还给程序的入口点,程序开始执行。
5. 延迟绑定(Lazy Binding)
为了提高性能,动态链接器可能会使用延迟绑定技术。即,只有在函数第一次被调用时才解析符号。这是通过一种称为 PLT(Procedure Linkage Table)和 GOT(Global Offset Table)的机制实现的。
延迟绑定步骤
- 初始调用:
- 当程序第一次调用一个动态库中的函数时,它会跳转到 PLT 条目。
- PLT 处理:
- PLT 条目会将控制权交给动态链接器的一个特殊入口点(
_dl_runtime_resolve
)。
- PLT 条目会将控制权交给动态链接器的一个特殊入口点(
- 符号解析:
- 动态链接器解析符号并更新 GOT 条目,使得后续对该函数的调用直接跳转到函数的实际地址。
6. 结束和清理
当程序结束时,动态链接器会执行共享库的析构函数或清理代码,释放相关资源。
总结
Linux 动态链接的过程涉及编译和链接阶段、加载阶段、动态链接器的工作、延迟绑定以及程序结束和清理。每个阶段都有特定的任务和机制,确保程序可以正确加载和执行,并有效地共享系统资源。
什么是未解析的符号?
未解析的符号(unresolved symbols)是在编译和链接过程中遇到但尚未找到其定义的符号。符号可以是变量、函数、对象等,它们需要在某个地方有明确的定义。未解析的符号通常出现在链接阶段,具体体现在以下几个方面:
1. 编译阶段的符号表
在编译阶段,每个源文件被编译成一个目标文件(object file)。目标文件包含了符号表(symbol table),其中列出了该文件定义的符号和该文件引用但未定义的符号。
2. 链接阶段的符号解析
在链接阶段,链接器(如 ld
)将多个目标文件和库文件链接在一起,生成一个可执行文件或共享库。在这个过程中,链接器需要解析所有符号,也就是说,找到每个符号的定义并将其引用替换为具体的地址。
3. 动态链接和未解析的符号
在动态链接过程中,一些符号可能在编译时没有定义,而是在运行时由动态链接器解析。这些符号在静态链接阶段被标记为未解析符号,需要在程序运行时由动态链接器(如 ld-linux.so
)解析。
未解析符号的来源
未解析符号可能来自以下几种情况:
1. 外部库
程序可能依赖于外部库(如动态链接库),这些库中的符号在编译时无法解析。只有在运行时,动态链接器才能将这些符号解析到具体的库函数或变量。
2. 未定义的函数或变量
编写代码时,可能会引用尚未定义的函数或变量。如果在链接阶段没有提供相应的定义,就会产生未解析符号错误。
3. 链接顺序错误
链接器在解析符号时,顺序很重要。如果库文件的链接顺序不正确,可能导致未解析符号。
动态链接器处理未解析符号
在动态链接过程中,未解析符号会在运行时通过以下步骤解析:
-
查找动态库: 动态链接器会查找所需的动态库,这些库可能在系统的标准库路径中,或者在
LD_LIBRARY_PATH
中指定的路径中。 -
加载动态库: 动态链接器将找到的动态库加载到内存中。
-
解析符号: 动态链接器会在加载的动态库中查找未解析的符号,并将其解析为具体的内存地址。
通过这些步骤,动态链接器能够在程序运行时解析所有未解析的符号,使程序正确执行。
动态链接的程序也要经过静态链接吗?
是的,动态链接的程序在生成最终可执行文件之前也要经过静态链接。以下是解释为什么和如何进行这两个链接过程的详细说明:
静态链接和动态链接概述
- 静态链接:在编译过程中,将所有需要的库函数和目标文件合并到一个可执行文件中。静态链接后,可执行文件独立于外部库。
- 动态链接:在编译时并不将所有库函数合并到可执行文件中,而是记录库的名称和符号信息,实际的符号解析和库加载在运行时完成。
静态链接阶段
在编写和编译源代码时,每个源文件会被编译成目标文件(object file)。这些目标文件包含了各自的符号表,其中列出了定义的符号和引用但未定义的符号。静态链接的过程如下:
-
编译:
将源代码编译成目标文件。
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o -
静态链接:
将多个目标文件和静态库文件链接成一个可执行文件。gcc main.o foo.o -o myprogram
在这个静态链接过程中,链接器(如ld
)会解析符号引用,将每个符号引用替换为相应的内存地址或函数入口点.
动态链接的结合
当程序依赖于动态链接库时,静态链接阶段不会将动态库的代码和数据合并到可执行文件中,而是记录库的名字和一些符号信息。这个过程仍然称为静态链接,但它处理动态库的方式有所不同。
动态链接的步骤
-
编译阶段:
和静态链接一样,首先将源文件编译成目标文件。gcc -c main.c -o main.o
gcc -c foo.c -o foo.o -
静态链接阶段:
在链接过程中,链接器将生成可执行文件,但不会将动态库的代码合并到文件中。相反,它会记录动态库的路径和未解析的符号。gcc main.o foo.o -o myprogram -L. -lmylib
其中
-L.
指定库搜索路径,-lmylib
指定要链接的动态库libmylib.so
。 -
生成的可执行文件:
生成的可执行文件会包含对动态库的引用,但实际的符号解析会留到运行时。
运行时的动态链接
当用户运行可执行文件时,动态链接器(例如 /lib64/ld-linux-x86-64.so.2
)会执行以下操作:
-
加载可执行文件:
- 内核加载可执行文件,并将控制权交给动态链接器。
-
查找并加载动态库:
- 动态链接器读取可执行文件中的动态段(
.dynamic
),找到需要的动态库,并将这些库加载到进程地址空间。
- 动态链接器读取可执行文件中的动态段(
-
解析符号:
- 动态链接器解析未解析的符号,将它们绑定到正确的地址。
-
执行初始化代码:
- 如果动态库有初始化代码(例如 C++ 的静态构造函数),动态链接器会在符号解析后运行这些代码。
-
执行程序:
- 最后,动态链接器将控制权交还给程序的入口点,程序开始执行。
总结
- 静态链接阶段:在编译生成可执行文件时,即使是动态链接程序,也需要进行静态链接,以解析和合并各个目标文件以及静态库中的符号。对于动态库,只记录库的名字和一些符号信息,而不合并实际代码。
- 动态链接阶段:在程序运行时,动态链接器加载动态库,并解析未解析的符号。
静态链接和动态链接相互补充,共同确保程序的符号解析和依赖库管理。静态链接在编译时解决了目标文件和静态库的符号问题,而动态链接在运行时处理动态库的符号解析和加载。
共享库的数据段是私有的,这些数据段都是什么?
共享库中的数据段确实源自源文件中的定义,但在编译和链接过程中,这些数据会被组织和存储到共享库的特定段中。这是一个从源代码到最终生成共享库的过程,包括编译、链接和加载等步骤。让我们详细解释这一过程。
1. 源文件中的定义
在源文件中,你定义了全局变量、静态变量和常量。例如:
int global_initialized_var = 10; // 已初始化的全局变量
int global_uninitialized_var; // 未初始化的全局变量
const char *message = "Hello, World!"; // 只读常量
2. 编译阶段
当你编译源文件时,编译器将这些变量和常量的信息放入目标文件的相应段中:
gcc -fPIC -c example.c -o example.o
global_initialized_var
被放入.data
段,因为它是已初始化的。global_uninitialized_var
被放入.bss
段,因为它是未初始化的。message
被放入.rodata
段,因为它是只读常量。
3. 链接阶段
当你将目标文件链接成共享库时,链接器会将这些段组织到共享库中:
gcc -shared -o libexample.so example.o
在这个阶段,.data
、.bss
和 .rodata
段被写入共享库文件 libexample.so
中。
4. 共享库的结构
共享库文件中包含多个段,这些段由链接器在链接阶段组织和创建。主要的段包括:
.text
段:包含代码(函数定义)。.data
段:包含已初始化的全局变量和静态变量。.bss
段:包含未初始化的全局变量和静态变量。.rodata
段:包含只读数据,如常量字符串。
5. 加载和运行时
当一个可执行文件使用共享库时,动态链接器负责加载共享库并将其映射到进程的地址空间。
运行时加载
当你运行程序时:
gcc main.c -L. -lexample -o myprogram
./myprogram
动态链接器(如 ld-linux.so
)会:
- 加载共享库:将
libexample.so
的各个段映射到进程的地址空间。 - 初始化数据段:
.data
段中的数据会被复制到内存中,并保留它们的初始值。.bss
段会被清零,确保未初始化变量的值为零。.rodata
段会被映射为只读,以防止常量数据被修改。
例子详解
让我们详细看看这个过程:
源文件 example.c
:
int global_initialized_var = 10; // 初始化的全局变量 (.data 段)
int global_uninitialized_var; // 未初始化的全局变量 (.bss 段)
const char *message = "Hello, World!"; // 只读数据 (.rodata 段)
编译生成目标文件 example.o
:
gcc -fPIC -c example.c -o example.o
链接生成共享库 libexample.so
:
gcc -shared -o libexample.so example.o
此时,libexample.so
包含了 .data
段、.bss
段和 .rodata
段,分别存储 global_initialized_var
、global_uninitialized_var
和 message
。
可执行文件 main.c
:
#include <stdio.h>
extern int global_initialized_var;
extern int global_uninitialized_var;
extern const char *message;
int main() {
printf("global_initialized_var: %d\n", global_initialized_var);
printf("global_uninitialized_var: %d\n", global_uninitialized_var);
printf("message: %s\n", message);
return 0;
}
编译和链接生成可执行文件 myprogram
:
gcc main.c -L. -lexample -o myprogram
运行 myprogram
:
./myprogram
在运行时,动态链接器会:
- 加载
libexample.so
,将其段映射到进程的地址空间。 - 初始化
.data
段中的global_initialized_var
为 10。 - 清零
.bss
段中的global_uninitialized_var
。 - 将
.rodata
段中的message
设置为只读。
总结
共享库中的数据段包含初始化和未初始化数据,它们在源文件中定义,但在编译和链接过程中被组织到共享库的特定段中。在运行时,动态链接器负责将这些段加载到内存中,并正确初始化它们。这种机制确保了共享库可以有效地管理全局和静态变量,同时支持多个进程共享库的代码。