编译过程
? 对于一个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 ,其内容为:
#include"b.h"
int main(void){
func();
}
#ifndef BH
#define BH
void func();
#endif
#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
运行出了结果
|