读书随想:C 程序的编译过程

作为一个不写 C 的人,了解和理解这个过程,也会收获一点快乐和成就感呢。从中联系到自己熟悉的知识点,会有种知其一又知其二的感觉。

C 程序的编译过程

以下内容完全来自书中。

假设一个 C 程序,有两个文件 p1.c 和 p2.c。我们用 Unix 命令行编译代码:

1
gcc -Og -o p p1.c p2.c
  • 命令 gcc 指的就是 GCC C 编译器。这是 Linux 上默认的编译器,可以简单地用 cc 来启动它。
  • 编译选项 -Og 告诉编译器使用会生成符合原始 C 代码整体结构的机器代码的优化等级。使用较高级别优化产生的代码会严重变形,以至于产生的机器代码和初始源代码之间的关系非常难以理解。
  • 命令选项 -o p 指定产生的最终可执行文件为 p。

实际上 gcc 命令调用了一整套的程序,将源代码转化成可执行代码。

  1. 首先,C 预处理器扩展源代码,插入所有用 #include 命令指定的文件,并扩展所有用 #define 声明指定的宏。
  2. 其次,编译器产生两个源文件的汇编代码,名字分别为 p1.s 和 p2.s。
  3. 接下来,汇编器会将汇编代码转化成二进制目标代码文件 p1.o 和 p2.o。目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入全局值的地址。
  4. 最后,链接器将两个目标代码文件与实现库函数(例如 printf)的代码合并,并产生最终的可执行代码文件 p。可执行代码是机器代码的第二种形式,也就是处理器执行的代码格式。

过程图如下:

读书随想

  1. 个人很喜欢了解和理解这种过程,一方面是因为通过它们可以了解现代大型工程项目的构建工具作为一个大黑盒,内部可能做了什么,另一方面是因为如今回过头看,这个过程中的基本步骤作为一个小黑盒也不再是那么的“黑”。尽管仍然没有了解很多细节,但是对黑盒的边界似乎感知得更清楚了。
  2. 也很喜欢理解命令行中的各个选项的作用和产生的效果。尽管认为记忆不住它们不代表什么,但还是会有点心虚。如今 Chat GPT 对于解读命令的帮助真的很大。
  3. 喜欢源代码、汇编代码、目标代码、可执行代码这些对各个阶段产物的清晰命名。也喜欢可以拆分的过程,可以更直观地感受和验证每个阶段。这不是理所当然的,有些时候你知道某一个过程分成几个阶段,但是要观察它们却不容易。
  4. 在过程图的左侧,给出了各个阶段产物的文件类型。这部分涉及第 10 章系统级 I/O 的知识,应用程序常常要区分文本文件(text file)和二进制文件(binary file),但对于内核而言,它们没有区别,都属于普通文件(regular file)。我并不是一蹴而就建立起这种对文件本质的认识,因为曾经对文件格式、编码和存储认识不深,所以反而会有一些额外的感受。
  5. 过程图中的链接阶段,提到静态库。这部分涉及第 7 章链接的知识。有时候,会有点困惑为什么特别介绍了链接呢?

参考文章

  • 《深入理解计算机系统》