C程序编译过程详解
示例程序
源代码
本文以一个简单的C程序为例,演示完整的编译过程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
while (1){
printf("hello wolrd\n");
sleep(1);
}
return 0;
}
程序说明
- 这是一个简单的无限循环程序,每秒输出一次”hello wolrd”
- 使用了三个标准库头文件:
stdio.h:提供printf函数unistd.h:提供sleep函数stdlib.h:提供标准库基础功能
问题记录
- 问题1:C程序编译的完整流程包含哪些阶段?每个阶段的主要作用是什么?
- 问题2:预编译(预处理)阶段会对源代码进行哪些处理?
- 问题3:编译阶段如何将预处理后的代码转换为汇编语言?
- 问题4:汇编阶段生成的目标文件和最终的可执行文件有什么区别?
- 问题5:链接阶段如何处理多个目标文件和库文件?
- 问题6:ELF文件格式的基本结构是什么?
关键概念
编译流程概述
C程序的编译过程分为四个主要阶段:
- 预编译(预处理):对源代码进行文本替换和处理
- 编译:将预处理后的代码转换为汇编语言
- 汇编:将汇编语言转换为机器码(目标文件)
- 链接:将目标文件和库文件组合成可执行文件
各阶段详细说明
1. 预编译阶段
- 命令:
gcc -E hello.c -o hello.i - 输入:
.c源文件 - 输出:
.i预编译文件(纯C代码,无预处理指令) - 主要处理:
- 头文件展开(
#include指令) - 宏定义替换(
#define指令) - 条件编译处理(
#if,#ifdef,#else,#endif等) - 注释删除
- 头文件展开(
执行过程:
$ gcc -E hello.c -o hello.i
$ file hello.i
hello.i: C source, UTF-8 Unicode text
$ ll hello.i
-rw-r--r--. 1 soveran soveran 64385 11月 13 15:29 hello.i
2. 编译阶段
- 命令:
gcc -S hello.i -o hello.s - 输入:
.i预编译文件 - 输出:
.s汇编语言文件 - 主要处理:
- 词法分析:将代码分解为标记(tokens)
- 语法分析:构建抽象语法树(AST)
- 语义分析:检查类型匹配等语义错误
- 优化:进行代码优化
- 代码生成:生成汇编语言代码
执行过程:
$ gcc -S hello.i -o hello.s
$ file hello.s
hello.s: assembler source, ASCII text
$ ll hello.s
-rw-r--r--. 1 soveran soveran 447 11月 13 15:30 hello.s
3. 汇编阶段
- 命令:
gcc -c hello.s -o hello.o - 输入:
.s汇编语言文件 - 输出:
.o目标文件(二进制文件) - 主要处理:
- 将汇编语言指令转换为机器码
- 生成ELF格式的可重定位目标文件
- 不解决外部符号引用
执行过程:
$ gcc -c hello.s -o hello.o
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ ll hello.o
-rw-r--r--. 1 soveran soveran 1560 11月 13 15:30 hello.o
4. 链接阶段
- 命令:
gcc hello.o -o hello - 输入:
.o目标文件和库文件 - 输出:可执行文件
- 主要处理:
- 符号解析:解析目标文件中的符号引用
- 重定位:将符号地址分配到最终的内存地址
- 合并段:将多个目标文件的相同段合并
- 链接库:链接所需的系统库和用户库
执行过程:
$ gcc hello.o -o hello
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f21b645cd1f9d8248ed713e84a7bfb34085e59c5, for GNU/Linux 3.2.0, not stripped
$ ll hello
-rwxr-xr-x. 1 soveran soveran 17496 11月 13 15:31 hello
程序运行与分析
运行程序
$ ./hello
hello wolrd
hello wolrd
hello wolrd
hello wolrd
hello wolrd
hello wolrd
hello wolrd
^C # 按下Ctrl+C终止程序
130 ✗ 15:01:01
查看依赖关系
$ ldd hello
linux-vdso.so.1 (0x00007ffc7e582000)
libc.so.6 => /lib64/libc.so.6 (0x00007f2c91c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2c91f41000)
查看ELF文件头部
$ hexdump -C hello | head -32
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 50 10 40 00 00 00 00 00 |..>.....P.@.....|
00000020 40 00 00 00 00 00 00 00 58 3c 00 00 00 00 00 00 |@.......X<......|
00000030 00 00 00 00 40 00 38 00 0d 00 40 00 20 00 1f 00 |....@.8...@. ...|
00000040 06 00 00 00 04 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....|
00000060 d8 02 00 00 00 00 00 00 d8 02 00 00 00 00 00 00 |................|
ELF文件格式
- 全称:Executable and Linkable Format
- 类型:
- 可重定位文件(.o):目标文件
- 可执行文件(无扩展名):可直接运行的程序
- 共享库文件(.so):动态链接库
- 内核模块文件(.ko):可加载的内核模块
- 基本结构:
- ELF头(ELF Header):包含文件类型、机器架构、入口地址等信息
- 程序头表(Program Header Table):描述程序运行时的内存布局
- 节(Sections):包含代码、数据等具体内容
- 节头表(Section Header Table):描述各个节的信息
共享库文件(Shared Libraries)
共享库是一种可被多个程序共享使用的代码库,在Linux系统中具有以下特点:
- 文件扩展名:
.so(Shared Object) - 存储位置:
/lib:系统核心共享库/usr/lib:用户共享库/usr/local/lib:本地编译安装的共享库
- 动态链接器:
- Linux:
ld.so(也称为ld-linux.so) - Windows:对应概念为动态链接库(
DLL文件)
- Linux:
- 作用:在程序运行时负责将需要的共享库加载到内存中,实现代码共享,减少可执行文件大小
查看共享库依赖
使用ldd命令可以查看可执行文件依赖的共享库:
$ ldd /usr/bin/ls
linux-vdso.so.1 (0x00007ffc773ad000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f8a8ea2c000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f8a8ea22000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8a8e800000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f8a8e764000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8a8ea89000)
查看系统共享库缓存
使用ldconfig -p命令可以查看系统缓存的共享库列表:
$ ldconfig -p
在缓存“/etc/ld.so.cache”中找到 824 个库
libzstd.so.1 (libc6,x86-64) => /lib64/libzstd.so.1
libz.so.1 (libc6,x86-64) => /lib64/libz.so.1
libyelp.so.0 (libc6,x86-64) => /lib64/libyelp.so.0
libyaml-0.so.2 (libc6,x86-64) => /lib64/libyaml-0.so.2
libyajl.so.2 (libc6,x86-64) => /lib64/libyajl.so.2
libxtables.so.12 (libc6,x86-64) => /lib64/libxtables.so.12
libxslt.so.1 (libc6,x86-64) => /lib64/libxslt.so.1
libxshmfence.so.1 (libc6,x86-64) => /lib64/libxshmfence.so.1
libxml2.so.2 (libc6,x86-64) => /lib64/libxml2.so.2
libxmlsec1.so.1 (libc6,x86-64) => /lib64/libxmlsec1.so.1
# ... 更多共享库
共享库管理命令
ldconfig:更新系统共享库缓存ldd:查看可执行文件的共享库依赖ldconfig -p:查看系统缓存的共享库列表
共享库依赖风险
重要注意事项:核心共享库(如/lib64/libc.so.6)是系统的基础组件,绝大多数系统命令(如ls、cp、mv、reboot、rm等)都依赖于它。
风险后果:如果不小心移除了核心共享库文件,将导致几乎所有系统命令无法执行,系统可能陷入瘫痪状态。
解决方法:
- 一旦发生这种情况,需要通过光盘救援模式(或其他启动媒介)启动系统
- 在救援模式下,挂载原系统分区
- 重新安装或恢复被移除的核心库文件
- 运行
ldconfig更新共享库缓存 - 重启系统恢复正常使用
这种情况充分说明了共享库在Linux系统中的核心地位,管理共享库时务必谨慎操作。
内核模块文件(Kernel Modules)
内核模块是可以动态加载到Linux内核中的代码,用于扩展内核功能:
- 文件扩展名:
.ko(Kernel Object):内核模块文件.o(Object):也可作为内核模块使用
- 存储位置:
- 位于
/lib/modules/$(uname -r)/kernel/目录下的子目录中 - 按照功能分类存放,如
arch/、crypto/、drivers/、fs/等
- 位于
- 管理命令:
insmod:加载内核模块rmmod:卸载内核模块modprobe:智能加载/卸载内核模块(自动处理依赖关系)lsmod:列出当前已加载的内核模块
内核模块目录结构示例:
$ ll /lib/modules/5.14.0-570.55.1.el9_6.x86_64/
总用量 28180
lrwxrwxrwx. 1 root root 45 10月 24 19:08 build -> /usr/src/kernels/5.14.0-570.55.1.el9_6.x86_64
-rw-r--r--. 1 root root 233922 10月 24 19:08 config
drwxr-xr-x. 12 root root 131 10月 31 22:29 kernel
-rw-r--r--. 1 root root 916496 10月 31 22:30 modules.alias
# ... 其他模块相关文件
$ ll /lib/modules/5.14.0-570.55.1.el9_6.x86_64/kernel/
总用量 20
drwxr-xr-x. 3 root root 17 10月 31 22:29 arch
drwxr-xr-x. 4 root root 4096 10月 31 22:29 crypto
drwxr-xr-x. 76 root root 4096 10月 31 22:29 drivers
drwxr-xr-x. 27 root root 4096 10月 31 22:29 fs
drwxr-xr-x. 6 root root 79 10月 31 22:29 kernel
drwxr-xr-x. 10 root root 4096 10月 31 22:29 lib
drwxr-xr-x. 2 root root 35 10月 31 22:29 mm
drwxr-xr-x. 39 root root 4096 10月 31 22:29 net
drwxr-xr-x. 3 root root 23 10月 31 22:29 samples
drwxr-xr-x. 13 root root 182 10月 31 22:29 sound
待深入研究
- 编译优化的具体技术和策略
- 静态链接与动态链接的区别和各自优缺点
- ELF文件格式的详细结构解析
- 编译过程中符号表的生成和使用机制
- 跨平台编译的实现原理
- 内核模块的编译、加载和卸载机制
- 共享库的版本管理和兼容性问题
- 动态链接器的工作原理和优化策略
编译过程总结
通过以上示例,我们可以清晰地看到C程序从源代码到可执行文件的完整编译流程:
- 源代码(hello.c) → [预编译] → 预编译文件(hello.i)
- 预编译文件(hello.i) → [编译] → 汇编文件(hello.s)
- 汇编文件(hello.s) → [汇编] → 目标文件(hello.o)
- 目标文件(hello.o) → [链接] → 可执行文件(hello)
每个阶段都有其特定的功能和输出文件类型,共同完成了从高级语言到机器可执行代码的转换过程。
参考资料
文档信息
- 本文作者:soveran zhong
- 本文链接:https://blog.clockwingsoar.cyou/fragment/c-compilation-process/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)