第二章:编译和链接

第二章:编译和链接

2.1 被隐藏了的过程

预编译(Preprocessing)

预编译阶段主要处理以下内容:

  1. 处理所有的预处理指令
  2. 展开所有的宏定义
  3. 处理条件编译指令
  4. 删除注释

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

#define MAX 100
#define SQUARE(x) ((x) * (x))

#ifdef DEBUG
#define LOG(msg) printf("Debug: %s\n", msg)
#else
#define LOG(msg)
#endif

int main() {
int a = MAX;
int b = SQUARE(a);
LOG("Calculation completed");
return 0;
}

使用以下命令查看预处理后的文件:

1
gcc -E preprocessing.c -o preprocessing.i

编译(Compilation)

编译阶段将预处理后的文件转换为汇编代码。主要包括以下步骤:

  1. 词法分析

    • 将源代码分解成标记(token)
    • 识别关键字、标识符、运算符等
  2. 语法分析

    • 将标记组织成语法树
    • 检查语法正确性
  3. 语义分析

    • 检查类型匹配
    • 检查变量声明
    • 检查函数调用
  4. 中间代码生成

    • 生成中间表示(IR)
    • 优化中间代码
  5. 目标代码生成

    • 生成汇编代码
    • 寄存器分配
    • 指令选择

示例代码:

1
2
3
4
5
6
7
8
9
10
int add(int a, int b) {
return a + b;
}

int main() {
int x = 10;
int y = 20;
int result = add(x, y);
return result;
}

使用以下命令生成汇编代码:

1
gcc -S compilation.c -o compilation.s

汇编(Assembly)

汇编阶段将汇编代码转换为机器码。主要包括:

  1. 将汇编指令转换为机器指令
  2. 生成目标文件(.o文件)
  3. 生成符号表

使用以下命令生成目标文件:

1
gcc -c compilation.s -o compilation.o

链接(Linking)

链接阶段将多个目标文件合并成一个可执行文件。主要包括:

  1. 地址和空间分配
  2. 符号决议
  3. 重定位

示例代码(多文件):

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

extern int add(int a, int b);

int main() {
int x = 10;
int y = 20;
int result = add(x, y);
printf("Result: %d\n", result);
return 0;
}
1
2
3
int add(int a, int b) {
return a + b;
}

编译和链接命令:

1
2
3
gcc -c link_main.c -o link_main.o
gcc -c link_add.c -o link_add.o
gcc link_main.o link_add.o -o program

2.2 编译器做了什么

词法分析

词法分析器将源代码分解成标记(token)。例如:

1
int a = 10;

会被分解为:

  • 关键字:int
  • 标识符:a
  • 运算符:=
  • 常量:10
  • 分号:;

语法分析

语法分析器将标记组织成语法树。例如:

1
2
3
4
5
if (a > b) {
return a;
} else {
return b;
}

会生成类似这样的语法树:

1
2
3
4
5
6
7
    if
/ \
> else
/ \ / \
a b return return
| |
a b

语义分析

语义分析器检查程序的语义正确性。例如:

1
2
3
int a = "hello";  // 类型不匹配错误
int b = 10;
int c = a + b; // 类型不匹配错误

中间代码生成

中间代码生成器生成中间表示。例如:

1
2
3
int a = 10;
int b = 20;
int c = a + b;

可能生成类似这样的中间代码:

1
2
3
t1 = 10
t2 = 20
t3 = t1 + t2

目标代码生成

目标代码生成器生成最终的汇编代码。例如:

1
2
3
int add(int a, int b) {
return a + b;
}

可能生成类似这样的汇编代码:

1
2
3
4
5
6
7
8
9
10
add:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret

实践练习

  1. 预编译实验
1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建预编译示例文件
cat > preprocess.c << EOL
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
printf("%d\n", SQUARE(a));
return 0;
}
EOL

# 查看预编译结果
gcc -E preprocess.c -o preprocess.i
  1. 编译实验
1
2
3
4
5
# 生成汇编代码
gcc -S preprocess.c -o preprocess.s

# 查看生成的汇编代码
cat preprocess.s
  1. 链接实验
1
2
3
4
5
6
# 编译多个源文件
gcc -c preprocess.c -o preprocess.o
gcc preprocess.o -o preprocess

# 运行程序
./preprocess

思考题

  1. 预编译阶段为什么要处理条件编译指令?
  2. 词法分析和语法分析的区别是什么?
  3. 为什么需要中间代码生成阶段?
  4. 链接阶段的主要任务是什么?
  5. 编译器优化在哪个阶段进行?

参考资料

  1. 《程序员的自我修养:链接、装载与库》
  2. 《编译原理》(龙书)
  3. GCC 在线文档
  4. LLVM 文档
  5. GNU Binutils 文档