栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

编译过程

​ 对于一个C/C++程序来说,在编写完代码之后到运行之间,需要完成编译的过程。我一直对这个过程有很多疑问,今天整理一下这个编译的大概流程。

选择一个编译器

​ 计算机只能运行二进制代码,一个信息的组成,由信息本身的二进制串和解释方式构成。

​ 我们需要将高级语言的代码,通过编译器编译成二进制代码之后,交给CPU进行解码和处理。这个编译的过程需要软件来进行,这种软件就是编译器

​ 常用的C/C++编译器有,GCC,g++,clang等。基于Linux使用的常见的是GCC编译器,这篇文章也是基于这个编译器进行的

​ 首先介绍一下GCC的基本使用方式:

​ GCC编译器是通过shell的方式进行使用的(就是在内个黑框框里面)。大概的格式是:

gcc [参数] [代码文件]

​ 常见的参数有:

-E //进行预处理
-S //进行编译而不进行汇编和链接
-o //将文件输出到指定的文件里面(可以理解为,重命名)
-c //编译和汇编,但是不链接

​ 这些参数大家也许现在不知道是干什么的,一会会结合例子讲解的

编译流程

​ 我们选择好一个编译器之后,接下来就是进行正式的编译了。

​ 我们基于单个文件的编译过程:

拥有一个c文件

​ 让我们先拥有一个c文件:main.c

#include
#define MAX 100
int main(void){
    int i=MAX;
    printf("HelloWorldn");
    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
#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

运行出了结果

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/993769.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号