云计算百科
云计算领域专业知识百科平台

【Linux指南】动静态库系列(六):编译、链接与 ELF:.o、.so、可执行文件到底是什么

文章目录

    • 一、先回顾编译和链接
    • 二、.o 文件是什么
    • 三、ELF 是什么
    • 四、ELF 的几种常见类型
      • 1. 可重定位文件 Relocatable File
      • 2. 可执行文件 Executable File
      • 3. 共享目标文件 Shared Object File
      • 4. Core dump
    • 五、ELF 文件整体由哪些部分组成
      • 1. ELF Header
      • 2. Section Header Table
      • 3. Program Header Table
      • 4. Sections
    • 六、常见 section 解释
      • 1. .text
      • 2. .data
      • 3. .bss
      • 4. .symtab
      • 5. .got 和 .plt
    • 七、链接视图和执行视图
      • 1. 链接视图
      • 2. 执行视图
    • 八、为什么 section 要合并成 segment
    • 九、动静态库与 ELF 的关系
      • 1. 静态库
      • 2. 动态库
      • 3. 可执行文件
    • 十、常用观察命令
      • 1. file
      • 2. readelf -h
      • 3. readelf -S
      • 4. readelf -l
      • 5. size
    • 十一、一个推荐实验
    • 十二、总结

前几篇我们已经会制作静态库和动态库,也解决了动态库运行期找不到的问题。但如果只停留在命令层面,很容易把 gcc、ar、ldd、readelf 当成一堆零散工具。

从这篇开始,我们往底层走一步:看看 .o、.a、.so、可执行文件背后到底是什么。你会发现,它们都绕不开一个核心格式:ELF。 image.png

一、先回顾编译和链接

一个 C 程序从源码变成可执行程序,大致经过这些阶段:

预处理 -> 编译 -> 汇编 -> 链接

平时我们一条命令:

gcc main.c -o main

看起来一步完成,实际上 gcc 帮我们串起了多个阶段。

如果拆开看,可以先生成目标文件:

gcc -c main.c -o main.o

这里的 -c 表示只编译,不链接。 image.png 此时 main.o 还不是可执行程序。它只是一个目标文件,里面有机器指令、数据、符号表、重定位信息等。

多个 .o 最后还要链接:

gcc main.o code.o -o main

链接阶段会把多个目标文件和用到的库组合成最终可执行程序。

二、.o 文件是什么

.o 是目标文件。它已经不是 C 源码,而是二进制文件。

可以用 file 查看:

file main.o

可能输出:

main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

这里最重要的信息是:

ELF
relocatable

这说明 .o 文件是 ELF 格式,而且属于可重定位文件。

可重定位意味着它还不能直接运行,需要和其他目标文件或库继续链接,才能形成可执行文件或共享库。

三、ELF 是什么

ELF 全称是 Executable and Linkable Format,即“可执行与可链接格式”。

Linux 下很多二进制文件都采用 ELF 格式,例如:

.o 目标文件
可执行程序
.so 动态库
core dump 文件

这也是为什么我们研究动静态库时必须理解 ELF。

因为:

.o 是 ELF;
.so 是 ELF;
可执行程序也是 ELF;
链接和加载都围绕 ELF 中的信息展开。

四、ELF 的几种常见类型

1. 可重定位文件 Relocatable File

也就是 .o 文件。

它包含代码和数据,但里面的很多地址还没有最终确定。链接器会根据这些信息继续生成可执行文件或动态库。

查看方式:

file main.o
readelf -h main.o

你会看到类型类似:

Type: REL (Relocatable file)

2. 可执行文件 Executable File

例如我们生成的 main:

gcc main.o code.o -o main
file main

可能输出:

ELF 64-bit LSB executable

它可以被操作系统加载运行。

3. 共享目标文件 Shared Object File

也就是动态库 .so:

file libmyc.so

可能输出:

ELF 64-bit LSB shared object

.so 可以被动态链接器加载到进程地址空间中。

4. Core dump

进程崩溃时产生的 core 文件,也属于 ELF 的一种,用于保存进程崩溃时的上下文,方便调试。

五、ELF 文件整体由哪些部分组成

一个 ELF 文件可以从四个关键部分理解:

ELF Header
Program Header Table
Section Header Table
Sections

image.png

1. ELF Header

ELF Header 位于文件开头,描述整个 ELF 文件的基本信息,比如:

  • 文件类型;
  • 目标机器架构;
  • 程序入口地址;
  • Program Header Table 的位置;
  • Section Header Table 的位置。

查看命令:

readelf -h main

你会看到类似字段:

Magic
Class
Data
Type
Machine
Entry point address
Start of program headers
Start of section headers

ELF Header 的作用可以理解为:

告诉工具和操作系统:这个 ELF 文件是什么,以及其他重要结构在哪里。

2. Section Header Table

Section Header Table 描述 ELF 中有哪些 section。

section 是链接视角下的组织单位,比如:

.text
.data
.bss
.symtab
.strtab
.rela.text
.got
.plt

查看命令:

readelf -S main.o

链接器在链接 .o 文件时,非常关心 section。比如多个 .o 的 .text 代码节会被合并,多个 .data 数据节也会被合并。

3. Program Header Table

Program Header Table 描述 ELF 如何被加载到内存。

查看命令:

readelf -l main

你会看到 LOAD、INTERP 等段信息。

操作系统加载可执行程序时,更关心 Program Header Table,因为它告诉系统:

哪些内容要加载到内存;
加载到什么虚拟地址;
内存区域权限是什么;
哪些段可读、可写、可执行。

4. Sections

Sections 是 ELF 中真正存放代码、数据、符号等内容的区域。

比如:

.text 存放机器指令
.data 存放已初始化全局变量和静态变量
.bss 描述未初始化全局变量和静态变量
.symtab 存放符号表
.got 全局偏移表
.plt 过程链接表

六、常见 section 解释

1. .text

.text 保存程序的机器指令,也就是函数代码。

例如 main、add、my_strlen 最终都会对应 .text 中的一段指令。

2. .data

.data 保存已初始化的全局变量或静态变量。

例如:

int g_val = 10;

这种变量通常会进入 .data。

3. .bss

.bss 对应未初始化或初始化为 0 的全局变量、静态变量。

例如:

int g_count;
static int s_flag;

.bss 很特别:它通常不在文件中保存大量实际的 0 数据,而是记录大小。程序加载时再在内存中清零。

这能节省磁盘空间。

4. .symtab

.symtab 是符号表,记录函数名、变量名和相关地址信息。

后面讲符号解析和重定位时会重点用到它。

5. .got 和 .plt

.got 是全局偏移表,.plt 是过程链接表。

它们和动态链接密切相关,后面会用两篇文章专门讲。

现在先记住:

GOT/PLT 是动态库函数调用的重要基础设施。

七、链接视图和执行视图

ELF 可以从两个视角理解。

1. 链接视图

链接视图关注 section。

链接器处理目标文件时,会关心:

.text 在哪里
.data 在哪里
符号表在哪里
重定位表在哪里

所以链接视图主要对应:

Section Header Table

2. 执行视图

执行视图关注 segment。

操作系统加载程序时,不会一个 section 一个 section 地零散加载,而是按 segment 加载。

执行视图主要对应:

Program Header Table

可以简单总结:

链接器看 section;
操作系统加载器看 segment。

八、为什么 section 要合并成 segment

一个 ELF 文件里可能有很多 section。如果加载时每个 section 都单独映射,会浪费内存页,也不利于权限管理。

现代系统以页为单位管理内存,常见页大小是 4KB。很多 section 很小,如果每个都单独占页,会造成大量碎片。

所以在生成可执行文件时,链接器会把属性相同或相近的 section 合并到 segment 中。

例如:

可读 + 可执行的 section -> 合并到代码相关 LOAD segment
可读 + 可写的 section -> 合并到数据相关 LOAD segment
只读数据 -> 合并到只读 segment

这样操作系统加载时就可以按 segment 建立内存区域,并设置权限:

代码段:可读、可执行
数据段:可读、可写
栈:可读、可写

这就是 readelf -l 中 LOAD 段的重要意义。

九、动静态库与 ELF 的关系

现在回头看前面几篇的内容:

1. 静态库

静态库 .a 是多个 .o 的归档包,而 .o 是 ELF 可重定位文件。

所以静态库本质上是在组织一批 ELF 目标文件。

2. 动态库

动态库 .so 本身就是 ELF 共享目标文件。

它可以被链接器引用,也可以在运行时被动态链接器加载。

3. 可执行文件

最终程序也是 ELF。它的 Program Header Table 会告诉操作系统如何加载它。

所以整个链条可以写成:

.c -> .o(ELF REL) -> .a 或 .so -> 可执行文件(ELF EXEC/DYN)

image.png

十、常用观察命令

1. file

查看文件大类:

file main.o
file libmyc.so
file main

2. readelf -h

查看 ELF Header:

readelf -h main

3. readelf -S

查看 section:

readelf -S main.o
readelf -S main

4. readelf -l

查看 segment:

readelf -l main

5. size

查看各段大致大小:

size main

输出中常见:

text data bss

这能帮助你观察程序中代码、已初始化数据、未初始化数据分别占多少空间。

十一、一个推荐实验

准备两个文件。

hello.c:

#include <stdio.h>

extern void run(void);

int main()
{
printf("hello\\n");
run();
return 0;
}

code.c:

#include <stdio.h>

void run(void)
{
printf("run\\n");
}

编译成 .o:

gcc -c hello.c
gcc -c code.c

观察:

file hello.o
readelf -h hello.o
readelf -S hello.o

链接:

gcc hello.o code.o -o main

观察可执行文件:

file main
readelf -h main
readelf -l main
readelf -S main

你会看到:

.o 是可重定位 ELF;
main 是可执行 ELF;
main 有 Program Header Table;
section 会映射到 segment。

十二、总结

ELF 是理解 Linux 编译、链接、库和加载的关键。

.o、.so、可执行程序都和 ELF 有关:

.o:ELF 可重定位文件
.so:ELF 共享目标文件
可执行程序:ELF 可执行文件或 PIE 形式共享对象

链接器主要关注 section,操作系统加载程序主要关注 segment。理解这一点,后面讲符号表、重定位、动态库映射、GOT/PLT 时就不会迷路。

下一篇,我们继续深入链接器最核心的工作:符号解析与重定位。也就是链接器到底如何把一个文件里的函数调用,接到另一个文件里的函数实现上。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Linux指南】动静态库系列(六):编译、链接与 ELF:.o、.so、可执行文件到底是什么
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!