【Linux 进程管理】Linux 可执行程序运行机制深度解析

Linux 可执行程序运行机制深度解析

本文档从内核实现、二进制格式解析、内存管理等维度,详细剖析 Linux 系统下 ELF 可执行程序的运行机制。

1. 可执行文件格式解析 (ELF)

ELF (Executable and Linkable Format) 是 Linux 的标准二进制格式。它提供了两种视图:链接视图 (用于编译链接)和执行视图(用于加载运行)。

1.1 ELF 核心结构

ELF Header (文件头)

位于文件开头,定义了全局属性。

  • 魔数 (Magic) : 7F 45 4c 46 (.ELF),用于内核识别文件格式。
  • Entry Point : 程序入口虚拟地址 (_start)。
  • Program Header Table Offset: 描述段表在文件中的位置。
Program Header Table (程序头表)

核心作用 : 告诉内核如何将文件映射到内存。

关键字段:

  • p_type: 段类型。PT_LOAD 表示该段需要被加载到内存;PT_INTERP 表示动态链接器路径。
  • p_vaddr: 内存中的虚拟地址。
  • p_filesz / p_memsz: 文件中大小与内存中大小(.bss 段通常 memsz > filesz)。
Section Header Table (节头表)

核心作用: 描述各个节区的属性,主要用于链接期和调试。

  • p_type: 节区类型(如 PROGBITS, NOBITS)。
  • p_flags: 权限标志(Alloc, Write, Exec)。

1.2 关键节区 (Sections) 详解

节区名称 说明 内存权限 加载行为
.text 代码段,存放 CPU 机器指令 R-X (读/执行) 从文件直接映射
.data 数据段,存放已初始化的全局/静态变量 RW- (读/写) 从文件直接映射 (COW)
.bss Block Started by Symbol,存放未初始化变量 RW- (读/写) 匿名映射,初始化为 0
.rodata 只读数据段,存放字符串常量等 R-- (只读) 从文件直接映射
.plt 过程链接表 (Procedure Linkage Table) R-X 用于动态链接延迟绑定
.got 全局偏移表 (Global Offset Table) RW- 存放外部符号的运行时地址

2. 内存加载过程

2.1 从磁盘到内存的完整流程

当执行 ./demo 时,内核经过以下步骤将其加载:

  1. 系统调用 : 用户态 execve -> 内核态 sys_execve -> do_execveat_common
  2. 读取文件头: 内核读取文件前 128 字节,检查魔数。
  3. 寻找处理函数 : search_binary_handler 发现是 ELF 格式,调用 load_elf_binary
  4. 处理解释器 : 检查是否存在 PT_INTERP 段。若存在(如 /lib64/ld-linux-x86-64.so.2),则也需要加载该解释器。
  5. 映射 Segment :
    • 遍历 Program Headers。
    • PT_LOAD 类型的段,调用 elf_map (底层为 mmap) 将文件内容映射到虚拟地址空间。
    • 注意: 此时并未真正读取物理内存(Lazy Loading),仅建立了虚存映射。
  6. 初始化栈 : 分配物理页作为用户栈,并将 argc, argv, envp 压栈。
  7. 移交控制权 :
    • 动态链接:PC 指向解释器的入口地址。
    • 静态链接:PC 指向程序的 e_entry

2.2 内存空间布局

下图展示了 64 位 Linux 进程的经典内存布局:

2.3 页表机制与地址转换

Linux 使用多级页表(x86_64 通常为 4 级或 5 级)完成虚拟地址 (VA) 到物理地址 (PA) 的转换。

  • CR3 寄存器: 存储顶级页表(PGD)的物理基地址。
  • MMU 查找: CPU 访问虚拟地址时,MMU 硬件自动遍历 PGD -> PUD -> PMD -> PTE。
  • PTE (Page Table Entry): 最终指向物理页帧 (Page Frame),并包含权限位 (R/W, User/Supervisor, NX)。
  • TLB (Translation Lookaside Buffer): 缓存最近的转换结果,加速访问。

3. 动态链接过程

3.1 动态链接器 (ld.so) 机制

内核完成映射后,ld.so 接管运行:

  1. 自举 : ld.so 先重定位自身(因为它本身也是动态库)。
  2. 加载依赖 : 读取 ELF 的 .dynamic 段,递归加载 DT_NEEDED 标记的所有共享库。
  3. 符号解析: 解决主程序和共享库之间的符号引用。

3.2 延迟绑定 (Lazy Binding)

Linux 默认不解析所有函数地址,而是推迟到第一次调用时。

原理 (GOT + PLT):

  • GOT: 存放绝对地址的数据表。
  • PLT: 一小段跳转代码。

调用流程:

  1. 程序调用 func@plt
  2. func@plt 跳转到 GOT[func]
  3. 首次调用 : GOT[func] 存的是 PLT 的下一条指令地址(即"没找到,回去")。
  4. PLT 继续执行:压入符号 ID,跳转到 _dl_runtime_resolve
  5. 解析 : ld.so 查找 func 真实地址,填入 GOT[func]
  6. 后续调用 : GOT[func] 已是真实地址,直接跳转,无额外开销。

4. 进程执行流程

4.1 execve 内部调用链

execve 1. 映射PT_LOAD 2. 加载解释器 3. 设置栈 4. 修改寄存器 返回用户态 用户空间: ./program 系统调用入口 fs/exec.c: do_execve do_execveat_common prepare_binprm

  1. 读前128字节
  2. 检查权限 search_binary_handler fs/binfmt_elf.c: load_elf_binary 内存映射 加载 ld.so create_elf_tables start_thread 跳转到 _start

4.2 进程控制块 (PCB) 初始化

execve 过程中,当前进程的 task_struct 发生关键变化:

  • mm_struct : 释放旧的内存描述符,分配新的 mm
  • comm: 进程名更新为新程序名。
  • signal: 重置信号处理函数(因为旧代码已不存在)。
  • files : 继承打开的文件描述符(除非设置了 FD_CLOEXEC)。

4.3 调用链分析

  1. _start (汇编入口): 清零 rbp,从栈获取 argc
  2. __libc_start_main (glibc):
    • 初始化线程环境。
    • 调用 .init 段和 .init_array (全局构造函数)。
  3. main(): 用户代码。
  4. exit() : 调用 .fini_array (全局析构),结束进程。

5. 内存管理机制

5.1 缺页异常 (Page Fault)

由于 mmap 是懒加载,程序开始执行时,物理内存中并没有代码。

  1. CPU 尝试取指,发现页表项无效 (Present=0)。
  2. 触发缺页异常,陷入内核 do_page_fault
  3. 内核检查 vm_area_struct,确认是合法映射。
  4. 分配物理页 -> 从磁盘读取文件内容 -> 更新页表
  5. 恢复执行,CPU 重新取指成功。

5.2 写时复制 (COW)

对于 .data 段,父子进程或多个运行实例初始共享同一物理页(只读)。

  • 当进程尝试数据时,触发异常。
  • 内核检测到是 COW 页,复制一份物理页给当前进程,权限改为可写。
  • 各进程随后拥有独立的数据副本。

6. 实际案例分析

编写 demo.c 并编译,查看反汇编。

源代码:

c 复制代码
// 调用动态库函数 free
free(heap_var);

反汇编 (.plt.sec):

asm 复制代码
0000000000001070 <free@plt>:
    1070:       f3 0f 1e fa             endbr64 
    1074:       f2 ff 25 45 2f 00 00    bnd jmp *0x2f45(%rip)        # 3fc0 <free@GLIBC_2.2.5>
    107b:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

解析:

  1. 指令 jmp *0x2f45(%rip) 是一个间接跳转。
  2. 目标地址 = 当前指令地址 0x1074 + 指令长度 7 + 偏移 0x2f45 = 0x3fc0
  3. 0x3fc0 位于 .got 段。
  4. 运行前 : 0x3fc0 处填的是 0x107b (下一条指令)。
  5. 运行后 : 0x3fc0 处被 ld.so 修改为 free 函数在 libc.so 中的真实物理内存地址。
相关推荐
casdfxx2 小时前
配置v3s支持8188eu、8192cu网卡(三)-openssh不能登录linux开发板。
linux·服务器·网络
遇见火星2 小时前
Linux 服务器被入侵后,如何通过登录日志排查入侵源?【实战指南】
linux·运维·服务器·入侵·日志排查
凤凰战士芭比Q2 小时前
(一)zabbix7.0(安装、自定义监控、告警)
linux
gis分享者2 小时前
如何在 Shell 脚本中使用管道(pipeline)实现数据传递?(容易)
linux·pipeline·shell·脚本·管道·数据传递
model20053 小时前
Alibaba linux 3安装LAMP(2)
linux·运维·服务器
喵了meme3 小时前
Linux学习日记16:守护进程
linux·服务器·学习
一匹电信狗3 小时前
【Linux我做主】进程实践:手动实现Shell
linux·运维·服务器·c++·ubuntu·小程序·开源
BUG_MeDe3 小时前
Linux 下VRF的简单应用
linux·运维·服务器
Sleepy MargulisItG3 小时前
Linux 基础指令详解(常用)
linux