程序是如何被编译的?以gcc为例

编译过程

​ 对于一个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文件了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ivm7LdL2-1656325117873)(编译过程.assets/tmp1FC.png)]

​ 这里可以看到我们使用的printf函数了

在这里插入图片描述

​ 在.i文件的最下方我们找到了main.c的内容,我们可以发现,int i=MAX已经被替换为了int i=100,这就是进行了宏展开的过程。

在这里插入图片描述

编译阶段

​ 在预处理阶段得到展开后的代码文本后,接下来我们就要正式开始将高级语言的代码向机器语言进行翻译了

​ 这个翻译不是一蹴而就的,我们经过了这样的步骤:

C/C++
汇编语言
机器语言

​ 编译阶段进行的就是将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.hb.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

在这里插入图片描述

运行出了结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值