news 2026/9/30 9:08:03

从.o文件到可执行程序,搞懂ELF和静态链接

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从.o文件到可执行程序,搞懂ELF和静态链接

前面已经能够自己制作.a和.so了。

但是还有一个问题一直比较绕:

hello.c code.c

分别编译以后得到:

hello.o code.o

这两个.o文件到底是怎么变成最后那个可以直接执行的程序的?

这部分其实就是编译和链接。

再往下研究,还会碰到一个很重要的格式:

ELF


一、.o文件到底是什么?

先写两个简单的文件。

hello.c:

#include<stdio.h>voidrun();intmain(){printf("hello world!\n");run();return0;}

code.c:

#include<stdio.h>voidrun(){printf("running...\n");}

分别编译:

gcc-chello.c gcc-ccode.c

就得到:

hello.o code.o

.o就是目标文件。

这时候如果用:

filehello.o

可以看到它属于 ELF 格式,而且是可重定位文件。


二、为什么要先生成.o?

因为一个比较大的工程,不可能所有代码都写在一个文件里。

不同的源文件可以分别编译。

比如:

hello.c -> hello.o code.c -> code.o

如果之后只修改了code.c,就只需要重新编译:

gcc-ccode.c

不用把整个工程全部重新编译一遍。

这也是目标文件比较重要的一个原因。


三、为什么两个.o文件不能直接各自运行?

问题就在于:

hello.o

里面用了:

run();

但是run并没有定义在hello.c中。

它实际定义在:

code.o

同样,两个文件都使用了printf,但printf的实现也不在它们自己的源文件里面。

所以编译的时候,编译器只能先把这些还不知道的地址空出来。

例如反汇编:

objdump-dhello.o

会看到类似:

callq ...

后面的地址还没有确定。

原因很简单:

编译hello.c的时候,并不知道run最终会在什么位置。

所以这个地址只能先留着。


四、链接到底在干什么?

链接的时候,情况就不一样了。

此时:

hello.o code.o

都已经准备好了。

链接器会把它们组合起来,然后处理之前没有确定的符号地址。

简单来说就是:

hello.o code.o ↓ 链接 ↓ 合并各个section ↓ 统一安排地址 ↓ 修正原来没有确定的地址 ↓ 可执行程序

两个.o文件的.text最后就被合并到了最终程序的.text中,并重新进行了统一编址。


五、ELF是什么?

Linux 下很多二进制文件其实都是 ELF 格式。

这里主要接触四类:

.o -> 可重定位文件 可执行程序 -> Executable File .so -> Shared Object File core dump -> 内核转储

所以 ELF 并不是单独某一种文件,而是一种二进制文件格式。


六、ELF里面都有些什么?

从整体上看,一个 ELF 文件可以看到这些部分:

ELF头(ELF Header) 程序头表(Program Header Table) 节头表(Section Header Table) 节(Sections)

ELF Header 在文件开头。

它保存 ELF 文件的一些基本信息,同时还可以帮助定位文件的其他区域。

比如使用:

readelf-hhello.o

可以看到:

Class Data Version Type Machine Entry point Start of program headers Start of section headers ...

这些信息都可以帮助我们了解一个 ELF 文件到底是什么类型、面向什么平台,以及其他关键结构在哪里。


七、Section到底是什么?

Section 可以理解成 ELF 内部按照不同用途划分出来的一块块区域。

比较常见的有:

.text .data .rodata .bss .symtab .got .plt

其中:

.text

主要保存程序的机器指令。

.text ↓ 代码

.data

保存已经初始化的全局变量、静态变量等数据。

.data ↓ 已经初始化的数据

.rodata

保存只读数据,比如字符串常量。

.bss

给没有初始化的全局变量、静态变量预留空间。

可以通过:

readelf-Sa.out

查看一个 ELF 文件有哪些 Section。


八、那Segment又是什么?

前面看到的是 Section。

程序真正加载进内存的时候,又会涉及一个新的概念:

Segment

这两个东西不能混在一起。

ELF 中很多 Section 在加载到内存的时候,会按照属性重新组合成 Segment。比如:

可读 可写 可执行

具有相同属性的部分可以放到一起。

比如:

很多代码相关section ↓ 一个可执行segment
很多数据相关section ↓ 一个可读写segment

这样做可以减少内存页面的浪费。

比如一个 Section 有 4097 字节,单独放需要占两个页面;另一个很小的 Section 如果单独放,又可能额外占一个页面。

把属性相同的内容合并之后,就可以减少这种空间浪费。


九、Section Header Table和Program Header Table有什么区别?

这个地方刚开始很容易混。

可以直接这么理解:

Section Header Table ↓ 更关注 ELF 文件内部是怎么组织的
Program Header Table ↓ 更关注程序运行时应该怎么加载

也就是:

Section → 链接视图 → 更适合研究链接过程 Segment → 执行视图 → 更适合研究程序加载

一个主要服务于链接,一个主要服务于运行加载。


十、静态链接到底发生了什么?

回到最开始的:

hello.o code.o

执行:

gcc *.o-omain.exe

就可以得到最终程序。

这个过程就是静态链接的一个基本例子。

在这个过程中,主要发生两件事情。

第一件:

把多个模块组合起来。

第二件:

把那些原来不知道的地址补完整。

比如hello.o里面调用:

run

编译的时候不知道run在哪里,所以地址暂时不能确定。

链接以后:

hello.o ↓ 找到 run 的定义 ↓ 确定 run 的地址 ↓ 修改原来的调用位置

这就是重定位。


十一、怎么查看这些东西?

Linux 下有几个命令特别适合拿来观察 ELF:

file

看文件类型:

filehello.o

readelf

查看 ELF 内部信息:

readelf-hhello.o readelf-Shello.o readelf-shello.o readelf-lmain.exe

objdump

查看机器指令:

objdump-dhello.o objdump-dmain.exe

例如:

objdump-dhello.o

可以发现call后面的地址在.o文件里还没有真正确定。

而链接完成以后:

objdump-dmain.exe

就可以看到类似:

callq 1149 <run>

这样的实际跳转位置。

这个变化其实就很直观地把“重定位”表现出来了。


十二、符号表有什么用?

除了机器指令,还可以看看:

readelf-shello.o

这里能看到符号表。

比如hello.o里面的:

main run puts

会以符号的形式出现。

其中如果某个符号显示:

UND

可以理解成这个符号在当前.o中没有定义。

比如:

hello.o

里用到了:

run

但run实际上定义在:

code.o

所以在hello.o的符号表里,run就属于未定义符号。

等两个.o进行链接之后,最终程序里就能够找到真正的run。


十三、把编译、链接整个串起来

现在整个过程可以写成:

hello.c ↓ hello.o
code.c ↓ code.o

然后:

hello.o + code.o + 静态库 ↓ 链接 ↓ 地址重定位 ↓ ELF可执行程序

所以以前我看到:

gcc hello.c code.c-omain

感觉就是一句命令。

现在再拆开看,其实里面经历了:

源代码 ↓ 编译 ↓ .o ↓ 链接 ↓ 地址重定位 ↓ 可执行ELF

真正把几个相互独立的模块拼起来的,就是链接阶段。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 9:07:00

SpringBoot+Vue医院后台管理系统设计与全栈实现

前阵子帮人从头搭了一版医院后台管理系统&#xff0c;从数据库建模、后端接口、前端页面到最终部署&#xff0c;全程走了一遍。做这类系统的最大感受是&#xff1a;它看起来就是个“信息管理系统”&#xff0c;但真把挂号、门诊、收费、药房、床位这些环节串起来之后&#xff0…

作者头像 李华
网站建设 2026/9/30 9:05:55

钙钛矿硅叠层 34.0%,MPPT 2000h保持84%:氧化锆颗粒改造埋底界面

钙钛矿/硅叠层太阳电池把宽带隙钙钛矿顶电池与硅底电池叠在一起&#xff0c;大幅压制热化损失&#xff0c;效率越过单结Shockley–Queisser极限&#xff0c;认证值已达35.2%。理想结构受两点制约&#xff1a;制绒硅表面起伏剧烈&#xff0c;钙钛矿沉积不均匀&#xff1b;空穴传…

作者头像 李华
网站建设 2026/9/30 9:05:10

AI工程从零开始:搭建最小闭环的实战路线图

如果你是在逛技术社区的时候刷到“ai-engineering-from-scratch”这种仓库名&#xff0c;大概率第一反应和我一样&#xff1a;这年头还有人从零开始学AI工程&#xff1f;我真正把“从零开始”这四个字当回事&#xff0c;是因为一次内部评审会——一个面试者谈起接口调用、模型微…

作者头像 李华
网站建设 2026/9/30 9:04:17

医院设备报修管理系统实战:微信小程序+Flask全流程开发

上个月去一家二甲医院办事&#xff0c;碰巧看到设备科老师还在用手工台账登记设备维修&#xff1a;一台心电监护仪报修&#xff0c;电话打到设备科&#xff0c;值班员先在纸上记一笔&#xff0c;再翻通讯录找负责的维修工程师&#xff0c;修完之后补一张三联单。整个过程全靠人…

作者头像 李华
网站建设 2026/9/30 9:03:48

破解save的三重身份:从按钮文案到文件解析的实战指南

前几天朋友塞给我一个本地化的活&#xff1a;一套中文界面的小工具要回填英文文案&#xff0c;交付前再整体校对一遍。做到一个按钮时我停住了&#xff0c;按钮上写着“确定”&#xff0c;同事扫了一眼说这不就是OK吗&#xff0c;直接填Ok就行。我没急着动手&#xff0c;翻了一…

作者头像 李华
网站建设 2026/9/30 9:02:56

Mac mini本地AI实战指南:Llama3+Ollama+llama.cpp高效部署

1. 别被“AI时代”四个字吓住&#xff1a;Mac mini不是服务器&#xff0c;但比你想象中更能打 很多人看到“AI时代”就下意识觉得——得配个RTX 4090、32GB显存、双路Xeon&#xff0c;还得搭个液氮散热。结果一查价格&#xff0c;心凉了半截。再一看自己那台放在电视柜底下吃灰…

作者头像 李华