编译过程
对于一个C/C++程序来说,在编写完代码之后到运行之间,需要完成编译的过程。我一直对这个过程有很多疑问,今天整理一下这个编译的大概流程。
选择一个编译器
计算机只能运行二进制代码,一个信息的组成,由信息本身的二进制串和解释方式构成。
我们需要将高级语言的代码,通过编译器编译成二进制代码之后,交给CPU进行解码和处理。这个编译的过程需要软件来进行,这种软件就是编译器
常用的C/C++编译器有,GCC,g++,clang等。基于Linux使用的常见的是GCC编译器,这篇文章也是基于这个编译器进行的
首先介绍一下GCC的基本使用方式:
GCC编译器是通过shell的方式进行使用的(就是在内个黑框框里面)。大概的格式是:
gcc [参数] [代码文件]
常见的参数有:
-E //进行预处理
-S //进行编译而不进行汇编和链接
-o //将文件输出到指定的文件里面(可以理解为,重命名)
-c //编译和汇编,但是不链接
这些参数大家也许现在不知道是干什么的,一会会结合例子讲解的
编译流程
我们选择好一个编译器之后,接下来就是进行正式的编译了。
我们基于单个文件的编译过程:
拥有一个c文件
让我们先拥有一个c文件:main.c
#include<stdio.h>
#define MAX 100
int main(void){
int i=MAX;
printf("HelloWorld\n");
return 0;
}
ok,这是最简单的c语言程序了。
编译的具体流程
GCC编译经过四个阶段:
预处理,编译,汇编,链接
我们一步一步的来:
预处理阶段:
预处理阶段可以理解为:文本处理的阶段。我们写过C语言的都知道,C语言中有宏这个说法,比如:
#define MAX 100
这个以#
开头的东西,就是宏定义,预处理阶段要完成的第一件事就是宏展开,将所有的宏替换。比如对于上面内个例子,预处理阶段就会把所有的MAX替换为100
预处理阶段还完成了另一见事情,对于#include<...>
这些语句的意思是将某个头文件引入,预处理阶段就会将这些引入的头文件也展开,导入我们的main.c文件当中
我们使用GCC进行第一步的处理:
gcc -E main.c -o main.i
得到的.i
文件就是预处理后的代码文本了,我们用vim来看一下main.i
中都有什么
大家如果熟悉stdio.h的话,这些函数就是定义在其中的内容,可以看到预处理确实是将头文件中的内容加入到了我们自己编写的c文件了
这里可以看到我们使用的printf函数了
在.i
文件的最下方我们找到了main.c的内容,我们可以发现,int i=MAX
已经被替换为了int i=100
,这就是进行了宏展开的过程。
编译阶段
在预处理阶段得到展开后的代码文本后,接下来我们就要正式开始将高级语言的代码向机器语言进行翻译了
这个翻译不是一蹴而就的,我们经过了这样的步骤:
编译阶段进行的就是将C/C++翻译为汇编语言的过程
我们使用GCC:
gcc -S main.i -o main.s
main.s
文件就是翻译后的汇编语言文件
同样的,使用vim查看一下main.s
的内容
可以看到翻译后的main函数
汇编阶段
汇编阶段就要将汇编文件,转换为目标文件:
什么是目标文件?Linux当中的.o
文件,在Windows’系统下则为.obj
文件,其实就是二进制文件,是未经过链接阶段的二进制文件
这里就简单的记住,目标文件就是二进制文件,但是没有经过链接过程
我们使用GCC
gcc -c main.s -o main.o
//注意使用-c,-c的意思是,编译和汇编,但是不链接
强行vim打开,发现很多奇怪的字符,但可以看到ELF这几个字母,这是一种文件格式,我们之后会讲到
链接阶段
到这里,二进制目标文件已经产生,接下来就要进行连接。
可是,什么是连接?为什么要进行连接呢?
我们用一个例子来解释一下这个问题:
假设,现在有三个文件,a.c
,b.h
和b.c
,其内容为:
//a.c
#include"b.h"
int main(void){
func();
}
//b.h
#ifndef BH
#define BH
void func();
#endif
//b.c
#include<stdio.h>
#include"b.h"
void func(){
;
}
得到这些文件之后,我们编译a.c
得到它的目标文件
gcc -c a.c -o a.o
然后,使用
readelf -s a.o
查看目标文件中的符号表
新的问题产生了,什么是符号表?通俗点来说,符号,就是函数和变量,符号表,就是一个表格,其中记录着符号的各种信息
那么未经过连接的a.o的符号表是什么样子的呢?
其他信息我们不关注,注意看第十一行:func前面的UND,就是undefined,虽然gcc编译了a.c,但是它并不知到func到底是个什么东西,因为,func的实现在b.c里面
我们再编译一下b.c
吧:
gcc -c b.c -o b.o
看第十行,func前不是UND而是1,因此说明了,func是存在于b.c中的
那么解决方法就很自然了:既然a的符号在b中,那么就把a,b合并了不就行了
因此:
ld a.o b.o -e main -o main
这句的意思是,链接a.o和b.o,入口函数是main函数,链接后文件名为 main
readelf -s main
这样一来,就链接上了,func符号也存在,也不是UND了
反汇编看一下:
objdump -d a.o
未链接前的a.o:
objdump -d main
注意看callq的那一行。最开始因为无法定位到func具体位置,因此编译器在a.o中定位在00 00 00 00
在main中,则是找到了具体的位置,偏移量未00 00 00 07
401012+00 00 00 07=401019,也就是func的位置。
好,回答main.c,使用gcc进行链接:
gcc main.o -o main
然后运行
./main
运行出了结果