第一章:温故而知新
1.1 从 Hello World 说起
程序从源代码到可执行文件的完整过程
让我们通过一个简单的 Hello World 程序来了解整个编译过程:
1 2 3 4 5 6
| #include <stdio.h>
int main() { printf("Hello, World!\n"); return 0; }
|
1. 预处理(Preprocessing)
预处理阶段主要处理以下内容:
- 处理所有的预处理指令(以 # 开头的指令)
- 展开所有的宏定义
- 处理条件编译指令
- 删除注释
可以使用以下命令查看预处理后的文件:
1
| gcc -E hello_world.c -o hello_world.i
|
2. 编译(Compilation)
编译阶段将预处理后的文件转换为汇编代码:
1
| gcc -S hello_world.i -o hello_world.s
|
生成的汇编代码大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| .section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 15 sdk_version 10, 15 .globl _main ## -- Begin function main .p2align 4, 0x90 _main: ## @main pushq %rbp movq %rsp, %rbp subq $16, %rsp leaq L_.str(%rip), %rdi movb $0, %al callq _printf xorl %ecx, %ecx movl %eax, -4(%rbp) movl %ecx, %eax addq $16, %rsp popq %rbp retq
|
3. 汇编(Assembly)
汇编阶段将汇编代码转换为机器码:
1
| gcc -c hello_world.s -o hello_world.o
|
4. 链接(Linking)
链接阶段将目标文件与所需的库文件链接,生成可执行文件:
1
| gcc hello_world.o -o hello_world
|
编译、链接、装载的基本概念
编译
编译是将高级语言转换为汇编语言的过程,主要包括:
- 词法分析:将源代码分解成标记(token)
- 语法分析:将标记组织成语法树
- 语义分析:检查语法树的语义正确性
- 中间代码生成:生成中间表示
- 目标代码生成:生成汇编代码
链接
链接是将多个目标文件合并成一个可执行文件的过程,主要包括:
装载
装载是将可执行文件加载到内存中运行的过程,主要包括:
- 创建进程
- 分配虚拟地址空间
- 加载可执行文件
- 设置程序入口点
1.2 万变不离其宗
计算机系统的基本组成
硬件系统
CPU(中央处理器)
内存
- RAM(随机访问存储器)
- ROM(只读存储器)
- 缓存
外设
软件系统
操作系统
应用程序
系统库
程序运行的基本原理
程序加载
可执行文件格式
- ELF(Linux)
- PE(Windows)
- Mach-O(macOS)
程序入口点
内存映射
程序执行
指令执行
数据访问
系统调用
程序终止
正常退出
- 返回主函数
- 调用 exit()
- 调用 _exit()
异常处理
资源释放
实践练习
- 编译过程实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| gcc -E hello_world.c -o hello_world.i
gcc -S hello_world.i -o hello_world.s
gcc -c hello_world.s -o hello_world.o
gcc hello_world.o -o hello_world
./hello_world
|
- 查看文件信息
1 2 3 4 5 6 7 8
| file hello_world
ls -l hello_world
ldd hello_world
|
思考题
- 为什么需要预处理阶段?预处理阶段主要处理哪些内容?
- 编译和汇编的区别是什么?
- 链接阶段的主要任务是什么?
- 程序加载到内存后,内存布局是怎样的?
- 系统调用和普通函数调用的区别是什么?
参考资料
- 《程序员的自我修养:链接、装载与库》
- 《深入理解计算机系统》
- 《编译原理》(龙书)
- GCC 在线文档
- ELF 文件格式规范