Linux ELF格式与可执行程序加载全解析:从磁盘文件到运行进程

Linux ELF格式与可执行程序加载全解析:从磁盘文件到运行进程

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

《Git深度解析》:版本管理实战全解

🌟心向往之行必能至


🎥Cx330🌸的简介:


目录

前言:

一、ELF文件:Linux二进制的标准载体

1.1 ELF文件的四大类型

1.2 ELF文件的双重视角:Section与Segment

1.3 ELF核心结构:从头部到加载指引

(1)ELF Header(文件头)

(2)Program Header Table(程序头表)

(3)Section Header Table(节头表)

二. ELF 的生命周期:从源码到运行

2.1 编译链接(生成可执行 ELF,研究静态链接)

2.2 加载运行(可以暂时先不看,继续往下理解)

三. 进程虚拟地址空间

3.1 虚拟地址的核心作用

四、加载流程核心知识点总结

结语:


前言:

在Linux世界里,我们每天都在和各种可执行程序打交道:ls、gcc、自己编译的二进制文件……这些文件并非杂乱的机器码堆砌,而是遵循一套标准格式——ELF(Executable and Linkable Format,可执行与可链接格式)。它是Linux二进制文件的“身份证”,更是操作系统加载、运行程序的核心依据。

本文将带你吃透ELF文件结构,并一步步拆解可执行程序从触发执行到正式运行的完整加载流程,既有底层原理,也有实操验证,帮你彻底理解Linux程序的“诞生与启动”。

一、ELF文件:Linux二进制的标准载体

ELF并非只代表可执行程序,它是一套通用的二进制格式标准,覆盖了Linux编译、链接、运行全生命周期的文件类型。相比老旧的 a.out 格式,ELF具备跨架构、可扩展、双视角解析(链接/加载)的优势,成为Unix-like系统的主流二进制格式。

实战示例:生成并查看目标文件

// hello.c #include<stdio.h> void run(); // 声明外部函数 int main() { printf("hello world!\n"); run(); return 0; } // code.c #include<stdio.h> void run() { printf("running...\n"); } 

编译生成目标文件

# 编译源码生成目标文件(-c:只编译不链接) gcc -c hello.c code.c # 查看生成的目标文件 ls -l *.o # 验证文件类型(确认是ELF格式) file hello.o 
  • relocatable:表示该 ELF 文件是 “可重定位文件”(目标文件类型);
  • not stripped:表示文件保留了符号表等调试信息。

1.1 ELF文件的四大类型

我们日常接触的ELF文件主要分为四类,各司其职:

  • 可重定位文件(.o):编译阶段生成,包含独立机器码和重定位信息,无法直接运行,需经链接器合并为可执行文件/共享库;
  • 可执行文件(ET_EXEC):最终运行的程序,包含完整的代码、数据和加载指引,内核可直接加载执行;
  • 共享目标文件(.so,ET_DYN):动态链接库,运行时被加载到内存,多个进程可共享复用,节省内存;
  • 核心转储文件(core):程序崩溃时生成的内存镜像,用于调试定位崩溃原因。

1.2 ELF文件的双重视角:Section与Segment

ELF文件设计最精妙的点,在于同时支持链接视角加载视角,通过两套结构实现分工协作:

  • Section(节区,链接视角):供编译器、链接器使用,按功能拆分代码、数据、符号表、重定位信息等,比如.text(代码)、.data(初始化数据)、.bss(未初始化数据)、.symtab(符号表);
  • Segment(段,加载视角):供操作系统加载器使用,将多个相关Section打包为一个段,统一映射到内存,注重内存权限和加载地址,比如代码段、数据段。
✅️ 为什么要合并节为段?减少内存碎片(减少空间浪费):例如.text(4097 字节)和.init(512 字节),分开加载需 3 个 4KB 内存页,合并后仅需 2 个;统一权限管理:相同属性的节合并后,操作系统可一次性设置权限(如所有只读节合并为一个只读段)。

实战查看段信息

# 查看a.out的程序头表(段信息) readelf -l a.out 

输出关键信息解读(主要是LOAD加载这个部分)

Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000744 0x0000000000000744 R E 200000 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000218 0x0000000000000220 RW 200000 
  • LOAD表示该段需要加载到内存;
  • R E只读可执行(对应.text、.rodata 等节);
  • RW可读可写(对应.data、.bss 等节);
  • VirtAddr段加载到内存后的虚拟地址
  • 下图中的A应该是R我这里就不改了

简单来说:链接看Section,加载看Segment

1.3 ELF核心结构:从头部到加载指引

一个标准的ELF文件,由三部分核心结构组成,层层递进指引程序加载:

(1)ELF Header(文件头)

位于文件最开头,是ELF的“总目录”,固定长度(32位52字节、64位64字节),内核加载时首先读取这里验证文件合法性。核心字段包括:

  • 魔数(Magic):前4字节固定为0x7f 45 4c 46(对应ASCII的DEL+ELF),是ELF文件的唯一标识,内核以此判断是否为合法ELF;
  • 文件类型/架构:标明是可执行文件、动态库,以及适配的CPU架构(x86_64、ARM等);
  • 程序入口地址(e_entry):程序加载后第一条指令的虚拟地址;
  • 程序头表/节头表偏移:指向Segment和Section的位置,是加载、链接的入口。

实操查看:执行 readelf -h 可执行文件 即可查看ELF文件头详情。

(2)Program Header Table(程序头表)

仅对可执行文件、动态库有效,是操作系统加载程序的“装载地图”。它是一个数组,每个元素描述一个Segment的信息:

  • 段类型(PT_LOAD:需加载到内存的段;PT_INTERP:动态链接器路径);
  • 文件偏移、虚拟内存地址、内存大小、文件大小;
  • 内存权限(可读R、可写W、可执行X)。

实操查看:执行 readelf -l 可执行文件 查看程序头表(段信息)。

(3)Section Header Table(节头表)

供链接器和调试器使用,描述每个Section的名称、类型、偏移、大小等,调试、反汇编时依赖该表。

实操查看:执行 readelf -S 可执行文件 查看节头表信息。


二. ELF 的生命周期:从源码到运行

ELF 文件的完整生命周期分为 “编译链接” 和 “加载运行” 两个阶段,每个阶段都有明确的核心操作:我们这里主要讲讲编译链接就可以了,运行可以继续往下看看虚拟地址空间先。

2.1 编译链接(生成可执行 ELF,研究静态链接)

无论是自己的 .o , 还是静态库中的 .o ,本质都是把.o文件进行连接的过程,所以:研究静态链接,本质就是研究 .o 是如何链接的,我们这里就不打包成静态库来研究了。
核心目标:将多个目标文件(.o)和库文件合并,修正未解析的符号地址,生成可执行 ELF。

关键步骤

  • 编译:gcc -c将源码(hello.c、code.c)翻译成目标文件(hello.o、code.o),每个目标文件包含独立的.text、.data 等节;
  • 合并节:通过链接,链接器将所有目标文件的同名节合并(如所有.text 节合并为一个大的.text 节,.data 节同理);
  • 符号解析与重定位:链接器通过符号表(.symtab)找到未解析的符号(如 hello.o 中的 run 函数),修正其地址(指向 code.o 中 run 函数的实际位置)
  • 生成程序头表:根据合并后的节的属性,划分段(如只读可执行段、可读可写段),写入程序头表。

实战验证重定位效果

# 反汇编目标文件hello.o,查看未重定位的call指令 objdump -d hello.o | grep callq # 反汇编可执行程序a.out,查看重定位后的call指令 objdump -d a.out | grep callq 

输出对比

  • 目标文件 hello.o 中,call 指令地址为e8 00 00 00 00(地址未修正);
  • 可执行程序 a.out 中,call 指令地址为e8 dc fe ff ff(地址已修正为实际函数地址)。

所以,链接过程中会涉及到对.o中外部符号进行地址重定位。

2.2 加载运行(可以暂时先不看,继续往下理解)

核心目标:操作系统根据 ELF 的程序头表,将文件加载到内存,创建进程并执行。
关键步骤

  • 创建进程:操作系统调用fork创建新进程,分配进程控制块(task_struct)和虚拟地址空间;
  • 解析程序头表:读取 ELF 的程序头表,识别需要加载的段(LOAD 类型);
  • 内存映射:通过mmap系统调用,将 ELF 文件中的段映射到进程虚拟地址空间的对应区域(如只读可执行段映射到 0x400000 开始的地址);
  • 初始化内存
    • 为.bss 节分配内存并清零;
    • 将.data 节的数据从文件复制到内存;
  • 设置程序入口:将 CPU 的程序计数器(PC)指向 ELF 头中的入口点地址(Entry),程序开始执行。
  • 注意:建议大家先往下看,这里我们可以暂时先不去理解,主要还是需要理解下面哪些图里面的一些逻辑过程。

三. 进程虚拟地址空间

3.1 虚拟地址的核心作用

现代操作系统都采用 “虚拟地址机制”,程序加载时使用的是虚拟地址,而非物理内存地址:

  • 隔离进程:每个进程有独立的虚拟地址空间,互不干扰;
  • 简化编程:程序编译时使用 “平坦地址空间”(从 0 开始的连续地址)(加载到内存之前在磁盘上我们系统叫它逻辑地址,加载到内存之后我们习惯叫虚拟地址(线性地址)),无需关心物理内存布局;
  • 高效利用内存:通过页表映射物理内存,支持内存共享(如动态库)和交换(Swap)。
  • 大家可以仔细看下下面的图示解析,有点多但都很重要

四、加载流程核心知识点总结

  • 内核只负责加载,不负责动态链接:动态链接由用户态ld-linux.so完成,内核仅做内存映射和权限管理;
  • 虚拟地址而非物理地址:加载时使用虚拟地址,通过页表映射到物理内存,实现内存隔离和共享;
  • 按需加载(懒加载):内核并非一次性将整个ELF加载到内存,而是通过缺页异常,按需加载代码和数据,节省内存;
  • 内存权限隔离:代码段不可写、数据段不可执行,防范内存篡改、栈溢出等安全风险。

结语:

ELF格式是Linux二进制程序的基石,而加载流程则是连接磁盘文件与运行进程的桥梁。理解ELF结构,能帮我们更好地排查程序崩溃、库依赖、内存异常等问题;吃透加载流程,才能真正掌握Linux程序运行的底层逻辑。

下一篇我们将深入动态链接PLT/GOT机制静态链接与动态链接的优劣对比,继续拆解Linux程序运行的底层细节,敬请关注。

🔥如果你在调试ELF文件、排查加载报错时遇到问题,欢迎留言交流,我会逐一解答。

Read more

Flutter 三方库 workiva_analysis_options 的鸿蒙化适配指南 - 实现工业级的代码质量审计与 Linter 规约对齐、支持端侧工程架构健康度自动检测实战

Flutter 三方库 workiva_analysis_options 的鸿蒙化适配指南 - 实现工业级的代码质量审计与 Linter 规约对齐、支持端侧工程架构健康度自动检测实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 workiva_analysis_options 的鸿蒙化适配指南 - 实现工业级的代码质量审计与 Linter 规约对齐、支持端侧工程架构健康度自动检测实战 前言 在进行 Flutter for OpenHarmony 的企业级大型分布式项目开发时,如何统一上百名开发者的代码风格?简单的 analysis_options.yaml 默认配置往往无法满足金融、工业等严苛领域对代码健壮性、可维护性的极致要求。workiva_analysis_options 合集了来自顶级工程实践的代码静态分析规约。本文将探讨如何在鸿蒙端构建一道坚不可摧的代码质量防线。 一、原直观解析 / 概念介绍 1.1 基础原理 该库本质上是一套高度严谨的 Linter 指令集。它通过对 Dart 核心分析引擎建议集的精妙筛选,强制开启了涉及内存安全(Avoid Unnecessary

By Ne0inhk
KWDB 硬核实战:30ms 写入千条轨迹,用 SQL 打造物流车队“天眼”系统

KWDB 硬核实战:30ms 写入千条轨迹,用 SQL 打造物流车队“天眼”系统

前言: 随着 5G 和物联网技术的普及,车联网 (Internet of Vehicles, IoV) 正成为数据爆发的新战场。与传统的静态传感器不同,车辆是移动的计算节点,它们每时每刻都在产生海量的时间序列数据:从 GPS 经纬度到发动机转速,从剩余油量到刹车踏板状态。 对于一家拥有数百辆货车的物流公司而言,这些数据就是金矿。通过实时监控,可以有效降低油耗、杜绝违规驾驶、优化配送路线。然而,传统的关系型数据库在面对车辆高频上报(例如每秒 10 次)的轨迹数据时,往往面临写入瓶颈;而单纯的时序数据库又难以处理复杂的车辆档案关联查询。 KWDB (KaiwuDB) 的“多模”特性恰好解决了这一痛点。今天,我们将实战构建一个物流车队实时监控平台,挑战如何在一个数据库内同时搞定“车辆档案管理”与“海量轨迹分析”。 场景设定:我们要为一个拥有 200 辆货车的物流车队构建监控系统。 核心挑战:高频写入:车辆每 10

By Ne0inhk
PostgreSQL动态分区裁剪技术:查询性能优化解析(2026年版)

PostgreSQL动态分区裁剪技术:查询性能优化解析(2026年版)

PostgreSQL动态分区裁剪技术:从原理到实战的查询性能优化 一、引言 1.1 研究背景与意义 随着企业数据量从TB级向PB级演进,数据库管理系统面临着严峻的挑战。PostgreSQL作为一款功能强大的开源关系型数据库,凭借其高度的可扩展性和标准兼容性,在金融、电商、物联网等领域得到了广泛应用。然而,在处理海量数据时,如何通过分区裁剪技术精准定位目标数据,避免无关分区的无效扫描,已成为查询性能优化的关键突破口。 在实际应用中,许多场景对查询性能有着极高要求。以电商行业为例,订单数据量庞大,每天可能产生数百万甚至数千万条订单记录。在进行订单查询、统计分析等操作时,如果不能有效利用分区裁剪技术,查询可能会耗费大量时间,严重影响用户体验。又如在金融领域,交易数据的实时查询对于风险控制至关重要,动态分区裁剪技术能够帮助金融机构快速获取所需数据。 1.2 研究目标与范围 本文旨在深入研究PostgreSQL声明式分区表的动态裁剪机制,通过结合源码分析与实际案例,系统地阐述其实现原理、优化策略及性能影响因素。研究目标包括: * 从源码层面深入剖析动态分区裁剪的实现原理 *

By Ne0inhk
你真的会打印日志吗?基于 Spring Boot 的全方位日志指南

你真的会打印日志吗?基于 Spring Boot 的全方位日志指南

—JavaEE专栏— 目录 * 一、日志概述:为什么它比 System.out.println 更重要? * 1.1 日志的核心用途 * 1.2 为什么弃用标准输出? * 二、日志框架体系:门面模式的深度解析 * 2.1 门面模式 (Facade Pattern) * 2.2 常见框架对比 * 三、实战:Spring Boot 日志的基本使用 * 3.1 传统方式获取日志对象 * 3.2 进阶方式:使用 Lombok (@Slf4j) * 四、深入理解日志级别 * 五、日志的高级配置 (application.yml) * 5.1 修改日志级别 * 5.

By Ne0inhk