Linux 底层深入:目标文件、ELF 格式与程序加载全解析

Linux 底层深入:目标文件、ELF 格式与程序加载全解析
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 Linux 开发中,我们每天都在和可执行程序打交道,但你是否好奇:gcc编译后生成的.o文件是什么?可执行程序为什么能直接运行?动态库又是如何被加载到内存中的?这些问题的答案,都藏在目标文件、ELF 格式和程序加载的底层逻辑里。本文从目标文件的本质入手,深入剖析 ELF 文件的结构、section 与 segment 的关系,最终讲透程序加载与进程虚拟地址空间的映射逻辑,帮你打通从源代码到运行程序的全链路认知。

一. 目标文件:编译后的 “半成品”

1.1 目标文件的本质

当我们用gcc -c编译 C/C++ 源代码时,编译器会将源码翻译成 CPU 能识别的机器码,但不会进行最终链接 —— 这个中间产物就是目标文件(后缀.o)。

  • 核心作用:作为程序的 “半成品”,存储单个模块的代码和数据,等待链接器组合成可执行程序;
  • 关键特性:修改单个源码文件后,只需重新编译对应的目标文件,无需全量编译,提升开发效率;
  • 文件格式:Linux 下目标文件遵循 ELF 格式(Executable and Linkable Format),是二进制文件的标准化封装。

1.2 目标文件的生成与验证

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

// hello.c#include<stdio.h>voidrun();// 声明外部函数intmain(){printf("hello world!\n");run();return0;}// code.c#include<stdio.h>voidrun(){printf("running...\n");}

编译生成目标文件

# 编译源码生成目标文件(-c:只编译不链接) gcc -c hello.c code.c # 查看生成的目标文件ls-l *.o # 验证文件类型(确认是ELF格式)file hello.o 
在这里插入图片描述
  • relocatable:表示该 ELF 文件是 “可重定位文件”(目标文件类型);

not stripped:表示文件保留了符号表等调试信息。

在这里插入图片描述

1.3 目标文件的核心问题:未解析的外部符号

目标文件是独立编译的,因此会存在 “不知道外部函数 / 变量地址” 的问题。例如:

  • hello.o中的printf(来自 C 标准库)和run(来自code.o),编译时无法确定其内存地址,编译器会暂时将跳转地址设为 0;
  • 这些未确定的地址,需要在链接阶段由链接器修正 —— 这就是 “重定位” 的核心目的。这个我们后面还会详细讲述的

二. ELF 文件:Linux 下的 “万能二进制格式”

ELF 是 Linux 系统中可执行程序、目标文件、动态库、核心转储文件(core dump)的统一格式标准。理解 ELF,就掌握了 Linux 二进制文件的 “通用语言”。

2.1 ELF 文件的四大类型

ELF 文件类型后缀用途示例
可重定位文件.o目标文件,用于链接生成可执行程序 / 动态库hello.o、code.o
可执行文件无(或 .out)可直接运行的程序/bin/ls、a.out
共享目标文件.so动态库,运行时加载/lib64/libc.so.6
核心转储文件.core进程崩溃时的内存快照,用于调试core.12345

2.2 ELF 文件的核心结构

无论哪种 ELF 文件,都由四大核心部分组成,从文件开头到末尾依次排列:

在这里插入图片描述

2.2.1 ELF 头:文件的 “身份证”

  • 核心作用:描述 ELF 文件的全局属性,定位其他部分的位置;
  • 关键信息
    • 魔数(Magic):7f 45 4c 46(ELF 文件的标识);
    • 文件类型(Type):可重定位 / 可执行 / 动态库等;
    • 目标架构(Machine):x86-64、ARM 等;
    • 入口点地址(Entry):可执行程序运行时的起始地址(目标文件为 0);
    • 程序头表偏移(e_phoff)和节头表偏移(e_shoff):定位程序头表和节头表的位置。
  • 实战查看 ELF 头
# 查看目标文件hello.o的ELF头 readelf -h hello.o # 查看可执行程序a.out的ELF头 gcc hello.o code.o -o a.out readelf -h a.out 
在这里插入图片描述

2.2.2 节(Section):文件的 “功能模块”

节是 ELF 文件的基本组成单位,按功能划分,核心节如下:
好的,已根据您提供的内容整理为表格。

节名称类型用途
.text代码节存储可执行机器指令(程序核心逻辑)
.data数据节存储已初始化的全局变量和局部静态变量
.bss未初始化数据节为未初始化的全局变量 / 静态变量预留空间(不占文件空间,加载时分配内存)
.symtab符号表存储函数名、变量名与地址的映射关系
.reloc重定位表记录需要链接时修正的地址(如外部函数调用)
.rodata只读数据节存储字符串常量等只读数据(如 printf 的字符串)
  • 实战查看节信息
# 查看a.out的所有节 readelf -S a.out 

2.2.3 节头表(Section Header Table):节的 “索引目录”

  • 核心作用:存储每个节的描述信息,包括节的名称、类型、大小、在文件中的偏移量等;
  • 链接器(如ld:通过节头表找到各个节,进行合并、重定位等操作。
在这里插入图片描述

2.2.4 程序头表(Program Header Table):加载的 “指南”

  • 核心作用:仅存在于可执行文件和动态库中,告诉操作系统如何将文件加载到内存,就像一个操作指南一样。

关键信息:描述 “段(Segment)” 的信息 —— 段是节的 “合并分组”(将属性相同的节合并,如只读可执行的.text 和.rodata 合并为一个段)。

在这里插入图片描述
在这里插入图片描述

2.3 关键概念:Section(节)与 Segment(段)的关系

很多初学者会混淆节和段,核心区别在于 “视角不同”:

  • 节(Section):链接视角(用于编译链接)—— 粒度细,按功能划分(如代码、数据、符号表),方便链接器处理;
  • 段(Segment):执行视角(用于加载运行)—— 粒度粗,按内存属性划分(如只读可执行、可读可写),方便操作系统加载和权限管理。
✅️ 为什么要合并节为段?减少内存碎片(减少空间浪费):例如.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我这里就不改了
在这里插入图片描述
‼️小补充:对于 程序头表节头表 又有什么用呢,其实 ELF 文件提供 2 个不同的视图/视角来让我们理解这两个部分:链接视图 (Linking view) - 对应节头表 Section header table文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解 ELF 文件中包含的各个部分的信息。为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如
4k),所以,链接器趁着链接就把小块们都合并了。执行视图 (execution view) - 对应程序头表 Program header table告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table。

说白了就是:一个在链接时作用,一个在运行加载时作用。

在这里插入图片描述

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

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

3.1 阶段 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进行合并,和上述过程一样,所以链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。
在这里插入图片描述
在这里插入图片描述


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

3.2 阶段 2:加载运行(可以暂时先不看,继续往下理解)

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

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

四. 进程虚拟地址空间:ELF 的 “运行舞台”

4.1 虚拟地址的核心作用

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

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


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

4.2 核心总结和补充

  • 统一格式:ELF 统一了目标文件、可执行程序、动态库等二进制文件的格式,让编译器、链接器、操作系统能无缝协作;
  • 分离视角:通过节(链接视角)和段(执行视角)的分离,兼顾了编译链接的灵活性和加载运行的高效性;
  • 虚拟地址抽象:ELF 编译时采用虚拟地址布局,屏蔽了物理内存的细节,让程序开发和加载更简单;
  • 模块化支持:目标文件的设计支持模块化开发,单个模块修改后无需全量编译,提升开发效率。
  • EIP -> mmu -> 通过CR3拿到页表物理地址 -> 查表 -> 去内存中找到物理地址
  • 页表初始化,数据从哪里来?ELF解决了虚拟地址(逻辑地址,Linux下两个都行),加载使具有物理地址
  • EIP怎么找到下一条地址,当前地址 + 当前指令长度 = 下一条指令地址
  • 虚拟地址空间 -> mm_struct <- 数据从哪里来?(ELF->section->segment)
  • 进程虚拟地址空间,不仅仅是需要OS支持,也需要CPU本身在硬件上支持,比如CR3 + MMU + 页表,也需要编译器在编译上支持,比如统一编址。

补充:如下图,结合最后的关键要点一起看

在这里插入图片描述


上图中关键要点


结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

【AI大模型前沿】阿里通义千问 Qwen3-Coder:开启智能代码生成与代理式编程新时代

【AI大模型前沿】阿里通义千问 Qwen3-Coder:开启智能代码生成与代理式编程新时代

系列篇章💥 No.文章1【AI大模型前沿】深度剖析瑞智病理大模型 RuiPath:如何革新癌症病理诊断技术2【AI大模型前沿】清华大学 CLAMP-3:多模态技术引领音乐检索新潮流3【AI大模型前沿】浙大携手阿里推出HealthGPT:医学视觉语言大模型助力智能医疗新突破4【AI大模型前沿】阿里 QwQ-32B:320 亿参数推理大模型,性能比肩 DeepSeek-R1,免费开源5【AI大模型前沿】TRELLIS:微软、清华、中科大联合推出的高质量3D生成模型6【AI大模型前沿】Migician:清华、北大、华科联手打造的多图像定位大模型,一键解决安防监控与自动驾驶难题7【AI大模型前沿】DeepSeek-V3-0324:AI 模型的全面升级与技术突破8【AI大模型前沿】BioMedGPT-R1:清华联合水木分子打造的多模态生物医药大模型,开启智能研发新纪元9【AI大模型前沿】DiffRhythm:西北工业大学打造的10秒铸就完整歌曲的AI歌曲生成模型10【AI大模型前沿】R1-Omni:阿里开源全模态情感识别与强化学习的创新结合11【AI大模型前沿】Qwen2.5-Omni:

By Ne0inhk
手把手教你 Openclaw 在 Mac 上本地化部署,保姆级教程!接入飞书打造私人 AI 助手

手把手教你 Openclaw 在 Mac 上本地化部署,保姆级教程!接入飞书打造私人 AI 助手

AppOS:始于 Mac,却远不止于 Mac。跟随 AppOS一起探索更广阔的 AI 数字生活。 OpenClaw 是 Moltbot/Clawdbot 的最新正式名称。经过版本迭代与改名后,2026年统一以「OpenClaw」作为官方名称,核心定位是通过自然语言指令,替代人工完成流程化、重复性工作,无需用户掌握编程技能,适配多场景自动化需求。 该项目经历了多次更名,Clawdbot → Moltbot → OpenClaw(当前名称) # OpenClaw 是什么? OpenClaw 是一个开源的个人 AI 助手平台。 简单来说,它是一个可以将你自己的 AI 助手接入你已经在用的即时通讯工具(Telegram、WhatsApp、飞书等)的系统。你可以自己挑选 AI 模型进行连接,添加各种工具和技能(如飞书等),构建专属工作流。说白了如果应用的够好,它就是一个能帮你干活的“

By Ne0inhk
亚马逊AI编程工具Kiro工具初体验——人工实测,非AI生成

亚马逊AI编程工具Kiro工具初体验——人工实测,非AI生成

背景 先前尝试过多款国内免费的AI编程工具,其中使用频次最高的是字节跳动的TRAE,全程免费无门槛,也是我体验下来最顺手的一款;其次是百度的文心快码,我仅少量使用过,这两款工具均推出了独立的IDE,实际视觉和功能底层来看,本质上都是基于VS Code改造而来,保留了基础操作逻辑,上手难度不高,对熟悉VS Code的开发者很友好。 除此之外,腾讯的CodeBuddy和阿里的千问AI编程助手,则是以插件形式安装在现有主流IDE(如IDEA、VS Code)上。实际使用中能明显感受到,受限于插件的权限边界,它们的使用体验远不如独立IDE便捷——这类插件大多只能实现基础的编程问答、代码修订功能,无法与操作系统进行交互,比起能让程序跑起来、抓取报错后自动分析并修复BUG的全流程操作,其自动化程度明显偏低,难以满足实际开发的高效需求。 相比于市面上的宣传造势,这些国内AI编程工具的实际使用体验,远低于我的预期。处理简单业务逻辑这类简单的重复性编程体力活,它们还算够用;但一旦遇到稍微复杂的业务逻辑、算法实现或框架深层适配问题,就显得力不从心,甚至会输出误导性代码——这些代码看似逻辑通顺,实际编

By Ne0inhk
人工智能:自然语言处理在法律领域的应用与实战

人工智能:自然语言处理在法律领域的应用与实战

自然语言处理在法律领域的应用与实战 学习目标 💡 理解自然语言处理(NLP)在法律领域的应用场景和重要性 💡 掌握法律领域NLP应用的核心技术(如法律文本分类、实体识别、合同分析) 💡 学会使用前沿模型(如LegalBERT、LexGLUE)进行法律文本分析 💡 理解法律领域的特殊挑战(如专业术语、法律规范、数据稀缺) 💡 通过实战项目,开发一个合同分析应用 重点内容 * 法律领域NLP应用的主要场景 * 核心技术(法律文本分类、实体识别、合同分析) * 前沿模型(LegalBERT、LexGLUE)在法律领域的使用 * 法律领域的特殊挑战 * 实战项目:合同分析应用开发 一、法律领域NLP应用的主要场景 1.1 法律文本分类 1.1.1 法律文本分类的基本概念 法律文本分类是将法律文本划分到预定义类别的过程。在法律领域,法律文本分类的主要应用场景包括: * 判例分类:将判例分为不同的类别(如民事、刑事、行政) * 法律文件分类:

By Ne0inhk