加载共享库(.so文件)到进程的虚拟内存空间是通过 mmap 系统调用实现的。以下是该过程的详细底层原理:
1. 加载共享库的过程
当一个进程需要加载一个共享库时,通常会调用 dlopen 函数。dlopen 函数会执行以下步骤:
- 打开文件:首先,
dlopen会使用open系统调用打开共享库文件。 - 读取文件头:然后,它会读取共享库文件的头部信息(例如ELF头),以确定文件的格式和内容。
- 映射文件到内存:接下来,
dlopen会使用mmap系统调用将共享库文件的各个段(如代码段、数据段)映射到进程的虚拟内存空间。 - 解析符号:最后,
dlopen会解析共享库中的符号,并将它们与进程中的符号表进行链接。
2. mmap 系统调用
mmap 是一个强大的系统调用,用于将文件或设备映射到进程的虚拟内存空间。其基本语法如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:指定映射的起始地址,通常为NULL,表示由内核选择地址。length:要映射的字节数。prot:内存保护标志,如PROT_READ、PROT_WRITE、PROT_EXEC。flags:映射标志,如MAP_PRIVATE、MAP_SHARED。fd:文件描述符,表示要映射的文件。offset:文件偏移量,表示从文件的哪个位置开始映射。
3. 共享库的内存映射
共享库文件通常包含多个段(sections),如代码段(.text)、数据段(.data)、只读数据段(.rodata)等。每个段会被分别映射到进程的虚拟内存空间。
示例
假设我们有一个共享库文件 libexample.so,其内存映射过程如下:
- 打开文件:
int fd = open("libexample.so", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
- 读取文件头:
读取ELF头部信息,确定文件格式和段信息。
- 映射各个段:
假设代码段的偏移量为 0x1000,长度为 0x2000,数据段的偏移量为 0x3000,长度为 0x1000。
void *text_segment = mmap(NULL, 0x2000, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0x1000);
if (text_segment == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
void *data_segment = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0x3000);
if (data_segment == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
- 解析符号:
解析共享库中的符号,并将它们与进程中的符号表进行链接。
4. 内存保护和共享
- 内存保护:通过
mmap的prot参数,可以设置内存段的保护属性。例如,代码段通常设置为PROT_READ | PROT_EXEC,数据段设置为PROT_READ | PROT_WRITE。 - 共享和私有映射:通过
mmap的flags参数,可以设置内存段的共享属性。例如,使用MAP_PRIVATE表示私有映射,使用MAP_SHARED表示共享映射。
5. 卸载共享库
当进程不再需要使用共享库时,可以调用 dlclose 函数卸载共享库。dlclose 会解除内存映射,并释放相关资源。
结论
加载共享库(.so文件)到进程的虚拟内存空间是通过 mmap 系统调用实现的。`m好的,继续深入探讨加载共享库(.so文件)到进程虚拟内存空间的底层原理。
6. 动态链接器的角色
在加载共享库时,动态链接器(如Linux上的ld.so或ld-linux.so)扮演了关键角色。动态链接器负责解析共享库中的符号,并将它们与进程中的符号表进行链接。
动态链接器的工作流程
- 加载共享库:动态链接器首先使用
mmap将共享库文件的各个段映射到进程的虚拟内存空间。 - 符号解析:动态链接器解析共享库中的符号,并将它们与进程中的符号表进行链接。这包括解析全局符号和重定位符号。
- 初始化:动态链接器调用共享库中的初始化函数(如
_init函数)进行初始化操作。
7. 重定位和符号解析
在加载共享库时,动态链接器需要处理重定位和符号解析。重定位是指将共享库中的符号地址调整为进程的实际地址。
重定位表
共享库文件中包含重定位表,记录了需要重定位的符号和地址。动态链接器会遍历重定位表,并根据进程的实际地址进行调整。
符号解析
动态链接器需要解析共享库中的符号,并将它们与进程中的符号表进行链接。这包括以下几种情况:
- 全局符号:全局符号在多个共享库之间共享,动态链接器需要确保全局符号的唯一性。
- 局部符号:局部符号仅在共享库内部使用,动态链接器只需在共享库内部解析。
- 未定义符号:未定义符号需要在其他共享库或主程序中解析,动态链接器需要查找并链接这些符号。
8. 延迟加载(Lazy Loading)
为了提高性能,动态链接器通常采用延迟加载(Lazy Loading)技术。延迟加载是指在共享库中的符号第一次被使用时才进行解析和链接,而不是在加载共享库时立即解析所有符号。
延迟加载的实现
延迟加载通过使用PLT(Procedure Linkage Table)和GOT(Global Offset Table)实现:
- PLT:PLT是一个跳转表,用于间接调用共享库中的函数。每个函数调用首先跳转到PLT表项,然后由PLT表项跳转到实际的函数地址。
- GOT:GOT是一个全局偏移表,存储了共享库中的全局变量和函数地址。PLT表项通过GOT表项获取实际的函数地址。
9. 示例代码
以下是一个简单的示例,展示如何使用dlopen和dlsym加载共享库并调用其中的函数:
共享库代码(libexample.c)
#include <stdio.h>
void hello() {
printf("Hello from shared library!\n");
}
编译共享库:
gcc -shared -fPIC -o libexample.so libexample.c
主程序代码(main.c)
#include <stdio.h>
#include <dlfcn.h>
int main() {
// 打开共享库
void *handle = dlopen("./libexample.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return 1;
}
// 获取函数指针
void (*hello)() = dlsym(handle, "hello");
if (!hello) {
fprintf(stderr, "dlsym failed: %s\n", dlerror());
dlclose(handle);
return 1;
}
// 调用函数
hello();
// 关闭共享库
dlclose(handle);
return 0;
}
编译主程序:
gcc -o main main.c -ldl
运行主程序:
./main
输出:
Hello from shared library!
结论
加载共享库(.so文件)到进程的虚拟内存空间是通过mmap系统调用实现的。动态链接器负责解析共享库中的符号,并将它们与进程中的符号表进行链接。通过使用延迟加载技术,可以提高共享库加载。
2910

被折叠的 条评论
为什么被折叠?



