预处理

第一步要进行的处理就是预处理,C语言中”#”作用是作为预处理命令,在编译器进行编译之前对源码做某些转换。
展开头文件/宏替换/去掉注释/条件编译

  1. 库文件使用
  2. #include<***.h>
  3. 自定义头文件使用
  4. #include"***.h"

(1) 替换#define
(2)处理所有的条件编译指令,#ifdef #ifndef #endif等
(3)处理#include,将#include指向的文件插入到该行处
(4)删除所有注释
(5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行
(6)保留#pragma编译器指令,因为编译器需要使用它们。

image.png

编译

编译器检查语法错误,编译成汇编语言

汇编

汇编器把汇编语言编译成机器码

链接

我们写的代码中有部分的函数的具体执行步骤,但是函数对应的机器码在我们的源码里是没有的。单纯只是将我们的源码编译成机器码无法完整的执行所有功能,需要于库文件或者其他源码进行连接才能生成一个完整的可执行文件。

静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL(动态链接库)中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。
image.png

链接

C++ 是以 .cpp 为编译单元,也就是一个 .cpp 生成一个临时目标文件,linux下是生成.o文件,Windows 下生成 .obj 文件,以 linux 为例,在非特别指定的情况下 a.cpp 生成 a.o,每个编译单元是独立的,并不关心其它编译单元,正是如此我们也容易做出分布式编译工具。

我们将生成的一堆.o文件里面的函数与变量提取出来,放到对应的二进制区段里面,该替换的替换,该决议的决议,解决符号与替换问题,就完成链接与绑定,生成最终目标文件,它可以是执行文件,也可以动静态库。

我们用objdump -t c1.o命令来查看一下c1.o的符号表

  1. c1.o: file format elf64-x86-64
  2. SYMBOL TABLE:
  3. 0000000000000000 l df *ABS* 0000000000000000 c1.cpp
  4. 0000000000000000 l d .text 0000000000000000 .text
  5. 0000000000000000 l d .rodata.str1.1 0000000000000000 .rodata.str1.1
  6. 0000000000000000 *UND* 0000000000000000 _Z12PrintStudent7student
  7. 0000000000000000 *UND* 0000000000000000 _ZN7student8ShowInfoEv
  8. 0000000000000000 g F .text 0000000000000084 main
  9. 0000000000000000 *UND* 0000000000000000 printf
  10. 0000000000000000 *UND* 0000000000000000 s1

这里直接看出来,我们用到c2.cpp中的一个普通函数,一个成员函数,一个全局变量对应的符号student::ShowInfo,PrintStudent,s1都是UND,也就是未明确,在链接的过程中会进行寻找裁决替换。

gcc

  • 预处理 gcc -E hello.c -o hello.i

  • 编译 gcc -S hello.i -o hello.s

  • 汇编 gcc -c hello.s -o hello.o

  • 链接 gcc hello.o -o hello.exe