找回密码
立即注册
搜索
发回帖 发新帖

6357

积分

0

好友

801

主题
发表于 前天 11:26 | 查看: 11| 回复: 0

你以为 Linux 运行程序,是把文件搬进内存,然后从 main() 开始?

实际上,文件中有些内容根本不需要加载;有些运行时需要的空间,在文件里并不保存对应的数据;而在 main() 之前,启动代码通常已经做了一轮准备。

前面讲过进程如何创建。这一次接着往下看:Linux 怎样知道哪些内容该加载、放到哪里,又该从哪里开始执行?

答案不是“猜文件内容”,而是读取文件里的描述信息。这套组织约定,就是 ELF。

这篇只抓一条主线: 代码编译成什么 → 图中的表和内容是什么关系 → 节为什么还要组成段 → 加载器怎样照着它们建立内存映像。

01 · ELF:让工具和系统读懂二进制文件

ELF 全称 Executable and Linkable Format,即“可执行与可链接格式”。除了机器指令和数据,文件里还记录了类型、架构、位置、大小和权限等信息。

只给你一串字节,你没法直接判断哪部分应该执行、哪部分应该可写。ELF 把这些内容及其描述按规则组织起来,链接器和加载器才能各取所需。

hello.c → 编译、汇编 → hello.o → 链接 → hello
            源代码         ELF 目标文件      ELF 可执行文件

普通 .o 文件已有机器代码,但可能还留着待解析的符号引用。链接器把目标文件等输入组合起来,处理符号和重定位,形成可执行文件或共享库。所以,ELF 不等于“可以直接执行”:

类型 常见用途
ET_REL .o 目标文件,供继续链接
ET_EXEC 非 PIE 可执行文件
ET_DYN 共享库,也用于 PIE 可执行文件
ET_CORE 核心转储,用于分析进程现场

PIE 是位置无关可执行文件,便于运行时选择加载位置。看到 DYN 不能直接认定“这是动态库”,更不能只靠文件扩展名判断。

02 · 对着这张图,先分清“内容”和“索引”

ELF 文件格式示意图:文件头、程序头表、节头表与段结构关系

图:ELF 布局与节、段关系示意。布局和段归属以实际文件为准。

图中间是同一个 ELF 文件。先别急着记字段,从上往下分清四部分:

图中部分 它的作用
ELF 文件头 说明文件是什么,并告诉你两张表在哪里
程序头表 一组描述条目,告诉加载器如何建立运行时映像
各节的内容 保存代码、数据、符号等;.bss 等有特殊规则
节头表 逐节记录类型、位置、大小、属性等信息

两张“头表”是描述信息,不是代码的副本。它们从不同角度描述文件:节头表按内容用途看,程序头表按运行时装载看。

文件头固定在文件开头,其他部分不能靠“上下紧挨着”定位,而要按文件头给出的偏移查找。不同 ELF 的布局和表是否存在,也可能不同。

03 · 节:把代码和数据按用途放好

先看一个小程序。无需逐行研究语法,只关注函数、字符串和两个全局变量最终放在哪里:

#include <stdio.h>

int initialized = 42;
int zeroed[1024];

int main(void)
{
    printf("ELF demo: %d, %d\n", initialized, zeroed[0]);
    return 0;
}
示例中的内容 常见所在节 为什么这样放
main 等函数的机器指令 .text 作为代码执行
printf 使用的格式字符串 .rodata 运行时读取,通常不需修改
initialized 的初始值 42 .data 需要把初始值保存在文件里
zeroed 数组的空间 .bss 初始值全为零,无需逐字节保存

这些是常见工具链布局,具体会受优化和链接方式影响。ELF 还可能有符号表、字符串表、重定位信息等节,帮助工具识别名称、解析引用;并不是所有节都需要进入进程内存。

节头表告诉工具各节的类型、文件偏移、大小和属性。节名本身通常保存在专门的字符串表中,节头通过索引找到名称。

04 · 段:加载器不需要逐节搬运

既然已经有节,为什么还要有段(Segment)?因为“链接时怎么组织内容”和“运行时怎么映射内存”,是两种需求。

链接器需要区分代码、数据、符号等内容;加载器则更关心哪段连续文件区域要映射到哪里,以及能不能读、写、执行。它不必逐个识别 .text、.data 这些名字。

图右侧把 .init、.plt、.text 等代码相关节放进一个可加载段,给它读、执行权限。其他可加载段可以承载只读数据,或可写数据。

节和段不是两份内容。一个段可以覆盖多个节;节头表与程序头表,是对同一文件的两种描述。

段的分组通常会考虑权限,但也受链接脚本、对齐等影响,不能简单认定“相同权限的节一定属于同一个段”。这也是参考图中需要按示意理解的地方。

程序头表每项描述一个段或相关运行信息。其中 PT_LOAD 指定可加载段;PT_INTERP 则指定解释器路径,通常指向动态链接器。并非每一种程序头条目都代表要映射的一块普通代码或数据。

节布局与映射可参考 GNU ld 的节布局说明;加载条目见 ELF 程序加载规范。

05 · 读懂 LOAD,就能解释 .bss

对于一个 LOAD 条目,先看这几个字段:

字段 含义
p_offset 对应内容在文件中的起始偏移
p_vaddr 对应虚拟地址;PIE 等需结合加载基址理解
p_filesz 这个段在文件中的字节数
p_memsz 这个段在内存中的字节数
p_flags 读 R、写 W、执行 X 权限

假设某个段的 p_filesz = 4 KB、p_memsz = 12 KB。它要求前 4 KB 内容来自文件,后面多出的 8 KB 内存在运行时清零。

这就解释了 .bss:它描述零初始化对象所需的空间,不在文件中占用对应大小的数据字节。 因此,加一个很大的未初始化全局数组,通常会增加运行时内存需求,却不会让可执行文件按相同大小增长。

注意,.bss 的节头描述仍需要元数据空间;“不占文件数据字节”不等于文件里完全没有它的信息。

06 · 用 readelf,把图与真实文件对起来

将上面的程序保存为 hello.c,在 Linux 中执行。需要 GCC、file 和 binutils;这里显式生成 PIE,避免依赖工具链的默认设置:

gcc -O0 -fPIE -pie hello.c -o /tmp/elf_hello
file /tmp/elf_hello
readelf -h /tmp/elf_hello
readelf -W -S /tmp/elf_hello
readelf -W -l /tmp/elf_hello

先看 -h:认清文件身份

主要看 Magic(ELF 以 7f 45 4c 46 开头)、Class(ELF32 / ELF64)、Machine(目标架构)、Type,以及 Entry。再看两张头表的偏移和条目数量,就知道图中的索引从哪里开始。

再看 -S:找到代码与变量

找到 .text、.rodata、.data 和 .bss。示例中,.bss 的类型是 NOBITS,这正对应“需要空间,但没有相同大小的文件内容”。

最后看 -l:观察加载范围

下面摘取本机 x86-64 构建结果中的一个可写 LOAD 条目,只保留关键字段。数值为十六进制,不同环境可能不同:

Type Offset VirtAddr FileSiz MemSiz Flags
LOAD 0x2db8 0x3db8 0x25c 0x1288 RW

逐项读:文件内容从偏移 0x2db8 开始;对应虚拟地址为加载基址加 0x3db8;文件提供 0x25c 字节,但内存需要 0x1288 字节,超出的尾部要清零;RW 表示读写。

继续看输出末尾的 Section to Segment mapping。本机这个段覆盖 .init_array、.fini_array、.dynamic、.got、.data 和 .bss,能直接看到“一个段包含多个节”。差值还涉及其他对象及对齐,不应直接当作示例数组的大小。

想对比目标文件,可以执行 gcc -c hello.c -o /tmp/elf_hello.o,再运行 readelf -h 和 readelf -l 查看它:类型是 REL,普通 .o 通常没有程序头表,因为它还不是最终装载映像。

07 · 从 ELF 到 main,中间还有什么?

运行程序时,execve() 替换当前进程的程序映像。对 ELF 的典型处理,可以分成四步理解:

  1. 检查文件头: 确认格式、架构等信息,并定位程序头表。
  2. 建立内存映射: 按 LOAD 条目准备地址空间、权限和零填充区域;Linux 通常通过映射和按需调页提供文件内容,并非一次性把整个文件读入物理内存。
  3. 准备启动环境: 设置用户栈、参数、环境变量和辅助向量等。
  4. 交出控制权: 若有 PT_INTERP,先进入动态链接器;由它完成所需依赖加载、重定位等工作,再转入程序入口。没有解释器时则进入程序自身入口。

程序入口常见为 _start,启动代码经运行库初始化后才调用 main()。所以,main 是你写业务代码的入口,却通常不是进程开始执行的第一条指令。

文件头的 e_entry 描述程序入口;带解释器时,内核最先交出控制的位置可以是解释器入口,两者要区分。

加载与解释器行为见 execve(2);Linux ELF 定义见 elf(5)。

写在最后

回头看这张图,就不必把 ELF 当成一堆难记的字段:

  • 文件头: 它是什么,索引在哪里?
  • 节与节头表: 内容各有什么用途,方便链接和分析。
  • 段与程序头表: 运行时如何装载,加载器据此建立内存映像。

前面的 task_struct 描述“谁在运行”,ELF 则为“运行什么、怎样装载”提供信息。下次看 readelf 输出,先辨认文件,再找节,最后对照 LOAD 和节到段的映射,整条链就能串起来。如果你也在折腾 Linux 系统编程,可以到 云栈社区 一起交流。

参考:ELF 文件头规范、节规范、GNU readelf 文档、GCC 编译阶段说明。




上一篇:Linux进程与线程内核图解:task_struct、线程组与资源共享
下一篇:一图看懂 Linux 进程加载:从 execve 到 main 完整链路
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-25 03:32 , Processed in 0.070605 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表