.so文件会将该文件通过mmap映射到虚拟内存

加载共享库(.so文件)到进程的虚拟内存空间是通过 mmap 系统调用实现的。以下是该过程的详细底层原理:

1. 加载共享库的过程

当一个进程需要加载一个共享库时,通常会调用 dlopen 函数。dlopen 函数会执行以下步骤:

  1. 打开文件:首先,dlopen 会使用 open 系统调用打开共享库文件。
  2. 读取文件头:然后,它会读取共享库文件的头部信息(例如ELF头),以确定文件的格式和内容。
  3. 映射文件到内存:接下来,dlopen 会使用 mmap 系统调用将共享库文件的各个段(如代码段、数据段)映射到进程的虚拟内存空间。
  4. 解析符号:最后,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_READPROT_WRITEPROT_EXEC
  • flags:映射标志,如 MAP_PRIVATEMAP_SHARED
  • fd:文件描述符,表示要映射的文件。
  • offset:文件偏移量,表示从文件的哪个位置开始映射。

3. 共享库的内存映射

共享库文件通常包含多个段(sections),如代码段(.text)、数据段(.data)、只读数据段(.rodata)等。每个段会被分别映射到进程的虚拟内存空间。

示例

假设我们有一个共享库文件 libexample.so,其内存映射过程如下:

  1. 打开文件
int fd = open("libexample.so", O_RDONLY);
if (fd == -1) {
    perror("open");
    return 1;
}
  1. 读取文件头

读取ELF头部信息,确定文件格式和段信息。

  1. 映射各个段

假设代码段的偏移量为 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;
}
  1. 解析符号

解析共享库中的符号,并将它们与进程中的符号表进行链接。

4. 内存保护和共享

  • 内存保护:通过 mmapprot 参数,可以设置内存段的保护属性。例如,代码段通常设置为 PROT_READ | PROT_EXEC,数据段设置为 PROT_READ | PROT_WRITE
  • 共享和私有映射:通过 mmapflags 参数,可以设置内存段的共享属性。例如,使用 MAP_PRIVATE 表示私有映射,使用 MAP_SHARED 表示共享映射。

5. 卸载共享库

当进程不再需要使用共享库时,可以调用 dlclose 函数卸载共享库。dlclose 会解除内存映射,并释放相关资源。

结论

加载共享库(.so文件)到进程的虚拟内存空间是通过 mmap 系统调用实现的。`m好的,继续深入探讨加载共享库(.so文件)到进程虚拟内存空间的底层原理。

6. 动态链接器的角色

在加载共享库时,动态链接器(如Linux上的ld.sold-linux.so)扮演了关键角色。动态链接器负责解析共享库中的符号,并将它们与进程中的符号表进行链接。

动态链接器的工作流程
  1. 加载共享库:动态链接器首先使用mmap将共享库文件的各个段映射到进程的虚拟内存空间。
  2. 符号解析:动态链接器解析共享库中的符号,并将它们与进程中的符号表进行链接。这包括解析全局符号和重定位符号。
  3. 初始化:动态链接器调用共享库中的初始化函数(如_init函数)进行初始化操作。

7. 重定位和符号解析

在加载共享库时,动态链接器需要处理重定位和符号解析。重定位是指将共享库中的符号地址调整为进程的实际地址。

重定位表

共享库文件中包含重定位表,记录了需要重定位的符号和地址。动态链接器会遍历重定位表,并根据进程的实际地址进行调整。

符号解析

动态链接器需要解析共享库中的符号,并将它们与进程中的符号表进行链接。这包括以下几种情况:

  1. 全局符号:全局符号在多个共享库之间共享,动态链接器需要确保全局符号的唯一性。
  2. 局部符号:局部符号仅在共享库内部使用,动态链接器只需在共享库内部解析。
  3. 未定义符号:未定义符号需要在其他共享库或主程序中解析,动态链接器需要查找并链接这些符号。

8. 延迟加载(Lazy Loading)

为了提高性能,动态链接器通常采用延迟加载(Lazy Loading)技术。延迟加载是指在共享库中的符号第一次被使用时才进行解析和链接,而不是在加载共享库时立即解析所有符号。

延迟加载的实现

延迟加载通过使用PLT(Procedure Linkage Table)和GOT(Global Offset Table)实现:

  1. PLT:PLT是一个跳转表,用于间接调用共享库中的函数。每个函数调用首先跳转到PLT表项,然后由PLT表项跳转到实际的函数地址。
  2. GOT:GOT是一个全局偏移表,存储了共享库中的全局变量和函数地址。PLT表项通过GOT表项获取实际的函数地址。

9. 示例代码

以下是一个简单的示例,展示如何使用dlopendlsym加载共享库并调用其中的函数:

共享库代码(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系统调用实现的。动态链接器负责解析共享库中的符号,并将它们与进程中的符号表进行链接。通过使用延迟加载技术,可以提高共享库加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值