第一章:温故而知新

第一章:温故而知新

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 万变不离其宗

计算机系统的基本组成

硬件系统

  1. CPU(中央处理器)

    • 控制单元
    • 运算单元
    • 寄存器组
  2. 内存

    • RAM(随机访问存储器)
    • ROM(只读存储器)
    • 缓存
  3. 外设

    • 输入设备
    • 输出设备
    • 存储设备

软件系统

  1. 操作系统

    • 进程管理
    • 内存管理
    • 文件系统
    • 设备驱动
  2. 应用程序

    • 系统程序
    • 用户程序
  3. 系统库

    • 标准库
    • 运行时库
    • 第三方库

程序运行的基本原理

程序加载

  1. 可执行文件格式

    • ELF(Linux)
    • PE(Windows)
    • Mach-O(macOS)
  2. 程序入口点

    • 主函数入口
    • 启动代码
    • 初始化代码
  3. 内存映射

    • 代码段
    • 数据段

程序执行

  1. 指令执行

    • 取指令
    • 译码
    • 执行
    • 写回
  2. 数据访问

    • 寄存器访问
    • 内存访问
    • 外设访问
  3. 系统调用

    • 进程控制
    • 文件操作
    • 设备操作
    • 网络通信

程序终止

  1. 正常退出

    • 返回主函数
    • 调用 exit()
    • 调用 _exit()
  2. 异常处理

    • 信号处理
    • 异常处理
    • 错误处理
  3. 资源释放

    • 内存释放
    • 文件关闭
    • 设备释放

实践练习

  1. 编译过程实验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 预处理
gcc -E hello_world.c -o hello_world.i

# 2. 编译
gcc -S hello_world.i -o hello_world.s

# 3. 汇编
gcc -c hello_world.s -o hello_world.o

# 4. 链接
gcc hello_world.o -o hello_world

# 5. 运行
./hello_world
  1. 查看文件信息
1
2
3
4
5
6
7
8
# 查看可执行文件信息
file hello_world

# 查看文件大小
ls -l hello_world

# 查看文件依赖
ldd hello_world

思考题

  1. 为什么需要预处理阶段?预处理阶段主要处理哪些内容?
  2. 编译和汇编的区别是什么?
  3. 链接阶段的主要任务是什么?
  4. 程序加载到内存后,内存布局是怎样的?
  5. 系统调用和普通函数调用的区别是什么?

参考资料

  1. 《程序员的自我修养:链接、装载与库》
  2. 《深入理解计算机系统》
  3. 《编译原理》(龙书)
  4. GCC 在线文档
  5. ELF 文件格式规范