文昌市网站建设_网站建设公司_电商网站_seo优化
2025/12/29 19:32:21 网站建设 项目流程

需要从Linux内核和ELF文件格式两个角度来回答。关于mm_struct和VMA,需要解释清楚VMA的概念、红黑树组织方式,以及它与页表的关系。关于PIC和GOT/PLT,需要说明-fPIC编译选项的作用、GOT在PIC中的特殊要求,以及整个过程的工作流程。

我们继续深入到这两个紧密相关的核心机制中。它们一个是操作系统管理进程内存的“地图册”,另一个是共享库能在内存任意位置运行的“自适应导航”

一、mm_struct与 VMA:进程虚拟内存的“地图绘制与管理”

每个进程的task_struct中都有一个mm_struct(内存描述符),它是内核管理该进程整个虚拟地址空间的总控结构。而虚拟内存区域(VMA)则是这张大地图中的一个个具体区块

核心数据结构关系

structtask_struct{// ...structmm_struct*mm;// 指向内存描述符// ...};structmm_struct{unsignedlongstart_code,end_code;// 代码段起止unsignedlongstart_data,end_data;// 数据段起止unsignedlongstart_brk,brk;// 堆的起止unsignedlongstart_stack;// 栈的起始pgd_t*pgd;// 指向页全局目录(顶级页表)structvm_area_struct*mmap;// 指向VMA链表的头structrb_rootmm_rb;// VMA红黑树的根// ...};structvm_area_struct{unsignedlongvm_start;// 区域的起始地址unsignedlongvm_end;// 区域的结束地址(后一字节)structvm_area_struct*vm_next;// 链表中的下一个VMAstructrb_nodevm_rb;// 红黑树中的节点unsignedlongvm_flags;// 区域的权限和属性(如读/写/执行)structfile*vm_file;// 如果映射自文件,指向file结构unsignedlongvm_pgoff;// 文件内的偏移(以页为单位)// ...};

下图清晰地展示了进程虚拟内存空间中VMA的组织方式及其与页表的关系:

渲染错误:Mermaid 渲染失败: Parse error on line 2: ...bgraph A[“进程的虚拟地址空间 (由 mm_struct 管理)”] -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

工作机制与实例

  1. 查找与判定:当进程访问一个地址(例如0x4000abcd)时,CPU的MMU会先查询页表。如果该地址对应的页表项无效(缺页),会触发缺页异常。内核的异常处理程序会查询该进程的mm_struct,通过红黑树快速找到包含该地址的VMA(例如,它属于libc.so的代码段VMA)。如果找不到,说明访问了非法地址,内核会发送SIGSEGV(段错误)信号终止进程。
  2. 分配物理内存:找到VMA后,内核检查vm_flags(例如,试图向一个只读的代码段VMA写入会触发段错误)。如果权限合法,内核会分配一个物理页帧,并建立或更新页表项,将虚拟地址映射到这个物理页帧。
  3. 文件映射:如果VMA的vm_file不为空(如映射了libc.so.text段),内核会在分配物理页帧后,从磁盘文件的对应偏移(vm_pgoff)处读取内容到内存,这个过程称为按需分页

查看实例
在Linux中,你可以通过/proc/<pid>/maps文件查看任意进程的VMA地图。

# 查看自身shell的内存映射cat/proc/$$/maps# 输出示例:# 00400000-00401000 r-xp 00000000 08:01 1048576 /bin/bash # 代码段# 00600000-00601000 rw-p 00000000 08:01 1048576 /bin/bash # 数据段# 7ffff7a0e000-7ffff7bd0000 r-xp 00000000 08:01 524313 /usr/lib/libc-2.33.so # libc代码# 7ffff7bd0000-7ffff7dd0000 ---p 001c2000 08:01 524313 /usr/lib/libc-2.33.so # 保护间隙# 7ffff7dd0000-7ffff7dd4000 r--p 001c2000 08:01 524313 /usr/lib/libc-2.33.so # libc只读数据# 7ffff7dd4000-7ffff7dd6000 rw-p 001c6000 08:01 524313 /usr/lib/libc-2.33.so # libc可写数据# 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] # 栈

每一行对应一个VMA,显示了其起止地址、权限(r读/w写/x执行/p私有或s共享)、文件偏移、设备号、inode和文件路径(或区域名)。

二、 PIC + GOT/PLT:共享库的“任意门”技术

位置无关代码是共享库(.so)能够被加载到任意虚拟地址而无需重定位其代码段的关键技术。它的核心思想是:代码中不包含任何绝对地址,所有对数据和函数(包括外部函数)的引用都通过一个“地址表”间接进行

PIC代码的生成:使用-fPIC-fpic编译器选项。

gcc -fPIC -c mylib.c -o mylib.o gcc -shared mylib.o -o libmylib.so

PIC如何工作(以x86-64为例)

  1. 访问全局数据:编译器无法知道数据最终在内存的哪个绝对地址。它利用了一个关键事实:任何一条指令与其所在ELF文件的某个部分(如GOT)之间的偏移,在链接时是固定的
    • 编译器会生成这样的代码来获取GOT的地址:
      ; x86-64 示例,获取_GLOBAL_OFFSET_TABLE_的地址 lea rdi, [rip + _GLOBAL_OFFSET_TABLE_] ; RIP相对寻址,获取GOT地址
    • 然后,通过GOT中固定的偏移量来访问数据:
      mov rax, [rdi + global_var_offset] ; 从GOT中加载global_var的地址 mov eax, [rax] ; 通过该地址访问真正的全局变量
  2. 调用外部函数:这正是PLT/GOT发挥作用的场景,我们之前已经详细讲过。在PIC模式下,call printf@plt中的printf@plt地址也是通过RIP相对寻址计算的,因此无论库被加载到何处,这条调用指令本身都是正确的。

PIC与GOT/PLT的配合流程
下图结合了PIC的编译特性,展示了从PIC代码编译到首次调用外部函数的完整流程:

渲染错误:Mermaid 渲染失败: Parse error on line 2: ... subgraph S1[“编译时 (生成PIC)”] C1 -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

为什么需要PIC?

  1. 节省物理内存:如果多个进程加载同一个非PIC库,操作系统虽然可以将其映射到各自虚拟空间的不同地址,但代码段中大量的绝对地址需要被重写(写时复制),导致每个进程的代码段物理页都不相同,无法共享。而PIC库的代码段是完全纯净、只读的,可以被所有进程共享同一份物理内存。
  2. 地址空间布局随机化:PIC是ASLR技术的基础。ASLR要求每次运行,栈、堆、库的加载地址都随机变化以增加安全性。非PIC的库无法在随机地址运行,而PIC库则毫无问题。

查看实例

# 对比编译非PIC和PIC的目标文件echo"extern int var; void func() { var = 1; }">test.c gcc -c test.c -o test_no_pic.o gcc -fPIC -c test.c -o test_pic.o objdump -d test_no_pic.o# 可能会看到 `movl $1, var` (绝对地址尝试)objdump -d test_pic.o# 会看到通过 `%rip` 和 GOT 进行间接访问的复杂指令# 查看共享库的GOTreadelf -S /usr/lib/libc.so.6|grepgot.plt objdump -d -j .plt /usr/lib/libc.so.6|head-20

🔗 总结:VMA与PIC/GOT/PLT的联系

  • mm_struct/VMA是操作系统内核视角的内存地图,它知道“库X的代码段被映射到了进程空间的地址Y,并且具有Z权限”。
  • PIC/GOT/PLT用户程序(共享库)视角的导航系统。当PIC代码需要访问数据或调用函数时,它通过精心设计的指令(RIP相对寻址)找到GOT这张“地址查询表”。
  • 连接点:这张“地址查询表”(GOT)本身,就是VMA管理的一个普通数据段,它被加载在内存映射区域的某个地址。PIC代码通过相对寻址找到它,然后通过它里面由动态链接器填写好的地址,最终跳转到另一个VMA(例如libc.so的代码段)去执行函数。

简单来说,VMA告诉了系统“库在哪里、能做什么”,而PIC/GOT/PLT则告诉库里的代码“你要的东西在哪里、怎么去”。两者结合,共同实现了灵活、高效、安全的共享库动态链接机制。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询