Skip to content

Phases of translation

c++语言是典型的Compiled language,如果我们站在c和c++语言的设计者的角度来思考,他们在设计这两门语言的时候,肯定也需要考虑程序在编译过程、链接过程、运行过程中可能涉及到的一些细节,所以这门语言需要提供各种各样的specifier来供programmer对这些过程进行控制(或者说对这些过程进行描述);后续在学习各种概念的时候,很多概念其实都是用来描述phase of translation的相关内容的。

另外需要注意的是:理论与实现,虽然原文描述多种phase of translation,实际的实现可能并没有严格地区分这些phase。对于programmer而言,区分compiler-time、link-time、run-time即可。

cppreference Phases of translation

creference Phases of translation

nickdesaulniers Object Files and Symbols

A quick review of the compilation+execution pipeline (for terminology):

  1. Lexing produces tokens
  2. Parsing produces an abstract syntax tree
  3. Analysis produces a code flow graph
  4. Optimization produces a reduced code flow graph
  5. Code gen produces object code
  6. Linkage produces a complete executable
  7. Loader instructs the OS how to start running the executable

一、GCC做了什么?

最近一段时间做了关于一些软件的交叉编译工作,由于觉得并没有学到许多东西,所以抽时间看了关于静态链接方面的内容,读了一部分《程序员的自我修养——链接、装载与库》,记录一些读书笔记及自己的总结。

我们在Linux中经常使用

gcc  源文件名.c  -o  目标可执行文件名

对一个编写好的C文件进行编译生成可执行文件,那么这之间的细节呢? 这里源文件名为 hello.c ,生成可执行文件名为 hello 为例子

图片

gcc一个C源文件时对过程进行了隐藏,实际为:预编译 cpp、编译 cc1、汇编 as、链接 ld,其中每一步后的英文名是所使用的工具的名称。

那么各自的步骤到底干了什么呢?

1、预编译

预编译:处理.c文件中以“#”开头的预编译指令。

这些**预编译指令**包括了**宏定义**、**条件预编译指令**等等,此步中也会进行删除掉所有的注释等操作。当无法判断一个**宏定义**是否正确或者**头文件**是否正确包含的话,可以直接打开此步生成的文件进行检查。

gcc  -E  hello.c  -o  hello.i

2、编译

编译: 进行词法分析、语法分析、语义分析等工作,此步所生成的文件是以汇编代码构成的。

这一步是由编译器cc1完成(gcc中),主要分为词法分析、语法分析、语义分析、中间语言生成、目标代码生成与优化几步。

a.词法分析:将所有的C语言源代码分割为一系列的token记号,这些token记号主要为关键字、标识符、常量及特殊符号,比如表达式 a+b 在这步中就会被拆分为 a 和 b 两个标识符及 + 这个特殊符号。

b.语法分析:产生**语法树**,关于这步需要有一些数理逻辑的知识,即生成以表达式为节点的树,对应上面 a+b 的情况是 + 为一个节点,而 a 和 b 分别为左右子树的节点。

c.语义分析:确定每个节点的类型,比如整型、字符型等。 可以理解为在前一步的树的基础上在每个节点上都标示好**类型**,对于一些隐式转换及强制类型转换都会在这步中进行处理。

d.中间语言生成:进行两步操作,首先将**语法树**转化为**中间代码**,然后在**中间代码**中对已经能够确定值的表达式进行求值。 其中中间代码一般为三地址码,即 x = y op z的形式,其中op代表特殊符号,然后如果有些表达式能够确定其值,比如 t1=5+6 这种两个常量相加的语句就直接进行计算。

e.目标代码生成与优化:如字面意思,进行目标代码的生成与优化。 关于目标代码的生成与具体的硬件平台有关,而优化部分有部分操作,比如合适的寻址方式、对于乘法运算使用位移进行代替,这些如果有接触汇编代码会比较了解。

编译器流程图:

这里写图片描述

此步中生成的文件如果懂得汇编代码的话仍然是可读的

gcc  -S  hello.c  -o  hello.s

3、汇编

汇编:将汇编代码转变成机器可以执行的指令。

此步中是根据**汇编指令**与**机器指令**的**对照表**进行一一翻译,基本上一个汇编语句对应一条机器指令。 此步生成的文件已经没法读了,打开后全部是乱码,因为已经全部机制指令了。

gcc  -c  hello.s  -o  hello.o    
或
gcc  -c  hello.c  -o hello.o      
或
as  hello.s  -o  hello.o

这一步生成的文件也叫做**relocatable file**

4、链接

链接:将几个输入的目标文件加工后合并为一个输出文件。

简单来说,可以看作拼**拼图**的过程,每个.o文件都是一块拼图碎片,链接就是将它们拼接成一幅图(可执行文件)的过程。

linux中可执行文件都称为ELF文件,Windows中为PE文件,之后都会这样称呼

总结:

经过上面的几步我们大致了解了编译的过程,尤其是gcc的编译器cc1对源文件的操作步骤。

下面是在32位的Ubuntu中对 hello.c 文件进行动态编译显示的详细信息,静态编译与之类似:

gcc  --verbose  -fno-builtin  hello.c   //--verbose 表示显示编译的详细信息,-fno-builtin表示关闭内置函数优化选项,因为hello.c文件中用printf只对一个字符串进行输出,此时GCC会将其自动替换为puts函数。
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/4.8/lto-wrapper
Target: i686-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.4-2ubuntu1~14.04.1' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libmudflap --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-i386/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-i386 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-i386 --with-arch-directory=i386 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-targets=all --enable-multiarch --disable-werror --with-arch-32=i686 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu
Thread model: posix
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.1) 
COLLECT_GCC_OPTIONS='-v' '-fno-builtin' '-mtune=generic' '-march=i686'
 /usr/lib/gcc/i686-linux-gnu/4.8/cc1 -quiet -v -imultiarch i386-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=i686 -auxbase hello -version -fno-builtin -fstack-protector -Wformat -Wformat-security -o /tmp/ccM5JRqF.s
GNU C (Ubuntu 4.8.4-2ubuntu1~14.04.1) version 4.8.4 (i686-linux-gnu)
    compiled by GNU C version 4.8.4, GMP version 5.1.3, MPFR version 3.1.2-p3, MPC version 1.0.1
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/i386-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/i686-linux-gnu/4.8/../../../../i686-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/i686-linux-gnu/4.8/include
 /usr/local/include
 /usr/lib/gcc/i686-linux-gnu/4.8/include-fixed
 /usr/include/i386-linux-gnu
 /usr/include
End of search list.
GNU C (Ubuntu 4.8.4-2ubuntu1~14.04.1) version 4.8.4 (i686-linux-gnu)
    compiled by GNU C version 4.8.4, GMP version 5.1.3, MPFR version 3.1.2-p3, MPC version 1.0.1
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: aaaa08c6fefb26a9d107c18441ada1ef
COLLECT_GCC_OPTIONS='-v' '-fno-builtin' '-mtune=generic' '-march=i686'
 as -v --32 -o /tmp/ccs3h630.o /tmp/ccM5JRqF.s
GNU汇编版本 2.24 (i686-linux-gnu) 使用BFD版本 (GNU Binutils for Ubuntu) 2.24
COMPILER_PATH=/usr/lib/gcc/i686-linux-gnu/4.8/:/usr/lib/gcc/i686-linux-gnu/4.8/:/usr/lib/gcc/i686-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.8/:/usr/lib/gcc/i686-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/i686-linux-gnu/4.8/:/usr/lib/gcc/i686-linux-gnu/4.8/../../../i386-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.8/../../../../lib/:/lib/i386-linux-gnu/:/lib/../lib/:/usr/lib/i386-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/i686-linux-gnu/4.8/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-fno-builtin' '-mtune=generic' '-march=i686'
 /usr/lib/gcc/i686-linux-gnu/4.8/collect2 --sysroot=/ --build-id --eh-frame-hdr -m elf_i386 --hash-style=gnu --as-needed -dynamic-linker /lib/ld-linux.so.2 -z relro /usr/lib/gcc/i686-linux-gnu/4.8/../../../i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/4.8/../../../i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/4.8/crtbegin.o -L/usr/lib/gcc/i686-linux-gnu/4.8 -L/usr/lib/gcc/i686-linux-gnu/4.8/../../../i386-linux-gnu -L/usr/lib/gcc/i686-linux-gnu/4.8/../../../../lib -L/lib/i386-linux-gnu -L/lib/../lib -L/usr/lib/i386-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/i686-linux-gnu/4.8/../../.. /tmp/ccs3h630.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-linux-gnu/4.8/crtend.o /usr/lib/gcc/i686-linux-gnu/4.8/../../../i386-linux-gnu/crtn.o

其中最重要的三句已经用红字表示了出来,可以看到一个最简单的helloworld程序都需要链接许多.o目标文件才能够得到ELF文件。 从上看出,gcc实际上是对**cpp、cc1、as、ld**几个后台程序的包装,执行它时会根据具体的参数要求去调用这几个后台程序,从而得到我们所需要编译出的ELF文件。