2. 理解GCC的编译流程

任务目标

使用GCC的-E、-S、-c选项分别截停预处理、编译、汇编阶段,配合-v观察各阶段实际调用的子程序,最终通过链接生成可执行文件,从而建立对GCC四阶段编译流程的具象认知。

环境准备

仅需标准GCC工具链(含gcc、as、ld),Linux或macOS均可。创建实验目录并写入以下C源文件:

// hello.c
#include <stdio.h>
#define GREETING "Hello, GCC Pipeline!\n"

int main(void) {
    printf(GREETING);
    return 0;
}

分阶段截停与观察

在终端依次执行以下命令,每步观察输出文件类型与内容特征:

# 1. 预处理:展开宏、包含头文件,不检查语法
gcc -E hello.c -o hello.i

# 2. 编译:将预处理结果翻译为汇编(此时不做汇编与链接)
gcc -S hello.i -o hello.s

# 3. 汇编:将汇编代码转换为机器指令(目标文件,未链接)
gcc -c hello.s -o hello.o

# 4. 链接:解析符号引用,生成最终可执行文件
gcc hello.o -o hello

查看各阶段产物类型:

file hello.i hello.s hello.o hello

预期输出(示例):

hello.i: C source, ASCII text
hello.s: assembler source text
hello.o: ELF relocatable object file
hello:   ELF executable

用-v跟踪完整流程中GCC实际调用的子程序:

gcc -v hello.c -o hello_v 2>&1 | tee build_log.txt

在输出中定位以下关键行(不同版本路径略有差异):

cc1 -E -quiet hello.c -o hello.i    # 实际执行预处理的编译器前端
cc1 -quiet hello.i -o hello.s       # 实际执行编译的前端
as -o hello.o hello.s               # GNU汇编器
collect2 ... -o hello_v             # 链接器驱动(内部调用ld)

验证与判定标准

  1. 执行./hello,终端打印Hello, GCC Pipeline!,说明四阶段链路完整打通。
  2. hello.i中GREETING已被替换为字符串字面量,且stdio.h内容已展开,证明预处理生效。
  3. hello.s包含main标签及printf调用(call printf@PLT),证明编译阶段完成语法/语义分析并生成汇编。
  4. hello.o通过nm hello.o可见未定义符号printf(标记为U),证明尚未链接。
  5. build_log.txt中依次出现cc1、as、collect2进程,证明GCC是驱动各独立子程序完成工作的前端程序。

常见问题

  • 找不到stdio.h:确认未使用-nostdinc,且gcc安装完整。
  • 链接报错undefined reference to printf:检查是否跳过链接阶段直接执行了./hello.o——目标文件不可直接运行。
  • 想单步看预处理结果:使用gcc -E -P hello.c可去除行号标记,便于阅读。
预处理 gcc -E 编译 gcc -S 汇编 gcc -c 链接 gcc (无选项) hello.c hello.i hello.s hello.o hello cc1 -E cc1 as collect2/ld

上图展示了GCC将hello.c加工为可执行文件hello的完整流水线。每个阶段对应一个独立子程序,GCC本身仅扮演调度者角色。理解这一分层结构,有助于后续针对特定阶段(如仅预处理查看宏展开、仅编译检查内联汇编语法)进行精准调试。