当前位置: 首页 > news >正文

【Linux】库的制作与使用(2)ELF静态链接

目录

一 引出一个概念---ELF

二 ELF格式具体指什么

三 ELF可执行文件加载

四 理解链接与加载

1 静态链接

2 知识点总结

虚拟地址的生成过程

磁盘与内存中程序的地址表现

关键认知纠正

Linux 系统中的地址特殊性


我们上一篇博客还有一个内容点没有讲到:库的制作原理和加载原理---ELF,我们这篇博客就来介绍这部分

一 引出一个概念---ELF

我们以前学习,源代码写完是需要翻译的--->四步:预处理,编译,汇编,链接(把源文件编译成.o文件,和库一起打包并链接)

库是在链接的时候生效的

有些项目是通过C和汇编语言一起写出来的

我们把.o文件称为可重定位目标文件,本身是不可执行的

我们来看一份代码:

// 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"); } // 编译两个源⽂件 $ gcc -c hello.c $ gcc -c code.c $ ls code.c code.o hello.c hello.o

我们看到hello.c和code.c这两个程序,运行编译后形成.o文件,再通过gcc * .o合并形成可执行程序a.out (内部是2进制,但不只是二进制,内部以特定的规则,格式把写好的包含代码和数据的,逮捕好的二进制 ,他们是有固定的格式-->叫做ELF)

.o .a .exe .so的格式都是ELF格式(这里的格式指的是文件的内容所遵守的);因为大家的格式都相同,所以大家才能进行链接,形成可执行文件

所以,ELF是类 Unix 系统(Linux、Android、FreeBSD 等)中最主流的二进制文件标准格式,用于存放可执行程序、目标文件、动态库与核心转储文件

理解编译链链接的细节,我们需要先了解 ELF 文件。实际上,以下四类文件都属于 ELF 文件:

  • 可重定位文件(Relocatable File):即.o文件。包含适合与其他目标文件链接,用于生成可执行文件或共享目标文件的代码与数据。
  • 可执行文件(Executable File):即可以直接运行的程序文件。
  • 共享目标文件(Shared Object File):即.so文件。
  • 内核转储文件(core dumps):用于存放当前进程的执行上下文,由 dump 信号触发生成。

一个 ELF 文件主要由四部分组成:

  • ELF 头(ELF header):描述文件的核心特性,位于文件开头,主要作用是定位文件中的其他结构。
  • 程序头表(Program header table):描述所有有效的段(segments)及其属性。表中记录了每个段的起始位置、偏移量(offset)和长度。由于这些段在二进制文件中紧凑排布,需要通过程序头表的信息来区分和解析各个段。
  • 节头表(Section header table):包含对各个节(sections)的描述信息。
  • 节(Section):ELF 文件的基本组成单位,用于存放特定类型的数据。ELF 文件的各类信息与数据都存储在不同的节中,例如代码节存放可执行指令,数据节存放全局变量与静态数据等。

最常见的节包括:

  • 代码节(.text):用于存储机器指令,是程序的主要执行部分。
  • 数据节(.data):用于存储已初始化的全局变量和局部静态变量。

二 ELF格式具体指什么

ELF格式由四部分组成:

ELF header:是一个结构体变量,它可以直接写在磁盘上,决定了大小是固定的,可以描述整个ELF的格式

查看ELF header: 我们要学到一个新命令:readdelf

readelf -h 你的ELF文件

注意:文件经过gcc/g++编译之后,也还是文件(一直是文件,类型不变)

在ELF header中,有一个Magic,叫做魔数:它会生成一段没有意义,固定大小,彼此认识(编译器或......)的一段固定序列的随机值

你是怎么知道你要读取的文件是ELF格式的?

操作系统或工具判断一个文件是否为 ELF 格式,核心是读取文件开头的魔数
读取固定长度魔数
先读取文件最开头的 4 个字节,这部分属于 e_ident[0..3] 字段。
ELF 文件的魔数固定为:0x7F 0x45 0x4C 0x46,对应 ASCII 字符:\x7F E L F

如果这 4 个字节完全符合上述序列,就判定为 ELF 格式;否则为其他文件类型

所以魔数是用来在磁盘中判定文件类型的,当操作系统读取文件时,起始并不知道文件类型

如何看待磁盘文件的内容的:byte array[n]大数组(磁盘上的 ELF 文件,就是一个连续的字节数组:byte array[file_size])

byte array[n]中保存的内容:

byte array[0 ... 511] → ELF 文件头(最重要!) byte array[512 ... ...] → 程序头表(描述怎么加载进内存) byte array[...] → 代码段 .text byte array[...] → 数据段 .data byte array[...] → 重定位信息、符号表...

文件有自己的读写位置,就是文件下标

怎么知道可执行文件中每个区域开始位置+大小?

都被记录在固定的开头处(ELF header) 开始位置--->偏移量,大小--->区域的大小

Section Header Table:就是一个表结构,用来描述可执行程序中的一个一个section(数据节,保存未来形成的代码和数据);所以Section Header Table就是相当于宏观上统一管理section

查看具体的section:

# objdump -S main

我们以前所写的代码,全局变量,类.....,编译后都会以一个节一个节的形式存在某一个section中

链接的本质就是形成ELF

三 ELF可执行文件加载

ELF格式是在磁盘中保存的,在对程序双击等操作时,可执行程序要被加载到内存

磁盘在进行IO交互的时候,基本单位是4KB;但是在读取section时可能不足4KB,可能是128字节,但是申请了4KB,只有128个字节的空间是有效数据;如此反复,很多空间就被浪费了

所以我们可以把section合并起来

程序一到内存,就变成了进程,进程中含有页表,页表中记录了可读,可写,可执行的信息,为什么页表中会有这些信息?

因为每个数据是带有权限信息的,例如可读,可写.....,这些权限信息ELF都写好了,可以从ELF中提取这些权限信息

我们合并section的原则是什么?

相同权限的section进行加载时合并,合并之后的内容叫做segment---段!(不用在ELF中存在,边合并,边加载)

怎么知道哪些section进行合并?

Program HeadersTable中把section合并成segment的操作手册


四 理解链接与加载

1 静态链接

静态链接,就是在程序运行之前,把所有需要的代码和库文件,打包合并成一个独立的可执行文件的过程

静态链接 = 链接器(ld)把多个 .o 可重定位文件 + 静态库(.a/.lib)合并成一个完整的、可直接运行的可执行文件,所有依赖都打包进去,运行时不再需要外部文件

代码会被CPU运行,但是CPU只认二进制文件,所有我们就需要有指令集;那什么是指令集呢?

我们用一个故事来理解:

我们给刚出生的婴儿说:站起来,别哭了....这样的指令,婴儿是听不懂的;但是在我们长时间的说,长时间的训练,婴儿就能听懂一些简单的指令。如果连续发布四五条认识的指令,它完成了把婴儿从什么都不知道,到训练到它知道,这个过程叫做给小孩写入指令集的过程。

婴儿就相当于CPU,刚出生就相当于一块刚生成出来的二氧化硅板子,我们训练婴儿的过程,就相当于向CPU刻录指令集,这个过程由CPU的供应商向光片机通过高频率刻录形成电路

CPU有了指令集,只能识别简单的指令;用户可以把指令集做集合,形成复杂命令--->就能完成具体工作

静态函数重定向是静态链接的核心步骤:链接器定位到被调用函数的真实地址,将指令中预留的占位符地址(00000)替换为该实际地址,完成地址修正,最终生成可直接运行的程序

一个程序没有加载到内存时,变量或代码会不会有地址?

一定会有!(和内存地址的关系后面讲)

静态链接会整合所有模块,将用到的函数代码拷贝到可执行文件中(exe文件),并把 .o 文件里预留的函数占位地址替换为真实地址。这个过程也正是 .o 文件被称为可重定位目标文件的原因。

观念与可执行程序地址问题:

有如下几种地址:

(1)物理地址:实际硬件内存的物理存储位置,是真实的内存单元编号。
(2)虚拟地址(线性地址):由操作系统的内存管理单元(MMU)映射到物理地址的地址,程序运行时使用的地址,需通过编址方案转换为物理地址。
(3)逻辑地址:由基址(固定位置)+ 偏移量组成,和虚拟地址的区别是:虚拟地址不直接知道对应的物理内存位置,而逻辑地址通过基址和偏移能定位到程序内部的指令 / 数据

虚拟地址就是直接知道200m的位置找到家

逻辑地址就是已知一个固定位置(庙)+偏移量(20m)

在现实生活中,处处是地址:例如学生,老师的编号,门牌号....

给学生,老师编号--->本质就是编址

一个可执行程序,有很多行代码,是一个函数代码的集合;里面的每一行代码,都必须有地址,有地址就需编址,就需选择编制方案

程序编译阶段:会用平坦模式编址(起始地址设为 0000…),结合绝对编址、相对编址两种方法,最终生成虚拟地址(由编址方案确定)

可执行程序还没加载到内存时,它有没有地址?

答案是有,因为编译阶段已经分配了虚拟地址(如call指令会对应目标地址),加载时操作系统再将虚拟地址映射到物理地址。

CPU 执行程序时会从程序的入口地址(Entry Point)开始读取指令,这个地址是编译阶段确定的虚拟地址,并非物理地址。程序运行时,操作系统会将这个虚拟起始地址映射到物理内存,CPU 通过这个映射后的地址开始取指执行

虚拟地址表的形成与数据来源
虚拟地址表的形成:
虚拟地址表(页表 / 段表,取决于内存管理方式)是操作系统在程序加载时创建的。当可执行程序被加载到内存时,操作系统会为程序分配物理内存页 / 段,同时建立虚拟地址与物理地址的映射关系,这个映射关系就存储在虚拟地址表中。
表内数据的来源:
虚拟地址部分:来自程序编译时生成的虚拟地址(由编译器的编址方案确定);
物理地址部分:来自操作系统为程序分配的物理内存单元(由内存管理模块根据内存空闲情况分配)

Entry point address 写的是程序加载到内存的入口地址”:
入口地址是程序运行时 CPU 开始执行的第一个指令的虚拟地址,由编译器在生成可执行文件时写入文件头(如 PE/ELF 文件格式)。程序加载时,操作系统读取这个入口地址,将其映射到物理内存后,CPU 就从该地址开始执行程序。

CPU进来的是虚拟地址,出去的是物理地址

2 知识点总结

虚拟地址的生成过程

  1. Section 合并程序编译、链接阶段,编译器会将代码段(.text)、数据段(.data)、BSS 段等不同section(段)进行合并,形成连续的指令和数据块,这是地址分配的基础。
  2. 统一编址合并后的可执行程序会进行统一编址,包含两种方式:
    • 绝对编址:直接给指令 / 数据分配固定的虚拟地址(如起始地址设为 0x00000000);
    • 相对编址:以某个基准点(如函数起始地址)为参考,用偏移量表示地址,笔记中 “以一开始作为参照点” 就是这个意思。这一步完成后,虚拟地址的雏形就已形成,后续加载时仅需映射到物理地址即可。

磁盘与内存中程序的地址表现

  1. 磁盘中:可执行程序对应的地址一般以逻辑地址表示(基址 + 偏移),此时还未关联物理内存,只有虚拟 / 逻辑地址的定义。
  2. 内存中:程序加载到内存后,操作系统会立刻为其建立物理地址虚拟地址的映射,CPU 通过虚拟地址访问物理内存。

关键认知纠正

  • 不要认为 “只有把代码加载到内存才有地址”:地址在编译链接阶段就已分配(虚拟 / 逻辑地址),加载只是完成虚拟地址到物理地址的映射,并非地址的首次生成。
  • 可执行程序采用同一编址:指编译链接时为整个程序分配统一的虚拟地址空间,保证指令 / 数据的地址连贯性。

Linux 系统中的地址特殊性

在 Linux 中,虚拟地址和逻辑地址本质是同一个地址,只是在不同场景下的叫法不同:

  • 编译链接阶段称逻辑地址(基址 + 偏移);
  • 程序运行、内存管理阶段称虚拟地址(需 MMU 映射到物理地址)。而其他系统(如 Windows)中,逻辑地址和虚拟地址可能有更明确的分层(如分段 + 分页),Linux 因以分页管理为主,二者概念趋于统一。
http://www.jsqmd.com/news/579155/

相关文章:

  • 什么是精益生产管理八大浪费?精益生产管理八大浪费详解
  • 电机轴承异响?5分钟教你用振动分析仪定位故障(附实测案例)
  • AI浪潮下的核心密码:Token如何重塑智能经济与未来竞争格局?
  • 本源量子开发工具链全解析:从QPanda到VQNet,构建量子计算生态
  • ReactNative项目OpenHarmony三方库集成实战:react-native-render-html
  • OpenClaw+Qwen2.5-VL-7B:3种方法提升图文任务成功率
  • 星图平台快速体验:OpenClaw镜像+Qwen3.5-9B完成图片分析沙盒测试
  • 嵌入式开发中的位操作技巧与实战应用
  • 关于eclipse2019中导入克隆的web项目
  • OpenClaw+Qwen3.5-9B办公自动化:3类图片处理场景实测
  • Spring IoC 与 DI 核心详解 —— 基于 XML 配置:Bean 创建、依赖注入与生命周期全解析(Spring系列1)
  • Sodaq_LSM303AGR库深度解析:六轴IMU嵌入式驱动设计与低功耗实践
  • 美胸-年美-造相Z-Turbo案例分享:从简单描述到精美成图的全过程展示
  • 3 个高级思路,让你的 AI 绘画 / 视频从此充满想象力
  • 新手福音:用快马平台理解openclaw架构图并生成你的第一个应用
  • 初识Maven
  • 2026年矿山煤矿电力电缆生产厂家推荐:中低压、低压、中压、变频等电缆厂家 - 品牌2026
  • OpenClaw+百川2-13B-4bits量化模型:个人周报自动化生成与整理方案
  • 阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
  • OpenClaw飞书机器人配置:Qwen3.5-9B-AWQ-4bit对话触发全流程
  • Diablo Edit2实战解决方案:从存档修复到角色定制的完整指南
  • 2026年国内阻燃防火电缆推荐!阻燃防火电缆国内一线品牌精选 - 品牌2026
  • “title“: “从Java全栈开发视角看现代Web应用架构设计“,
  • 什么是Token?一文读懂Token是什么,影响你的使用成本!
  • TI SAR ADC模型(Matlab) 包含各类非理想因素,时钟偏差,增益偏差
  • Swift学习笔记13-可选链
  • 从手动挡到自动驾驶:Infoseek如何用工程思维重构媒体发布
  • OpenClaw镜像体验报告:千问3.5-35B-A3B-FP8多模态任务云端实测
  • 状态机中的人物状态
  • 50万行代码泄露后,开发者们凌晨4点冲进Rust:这次真的撕开了