ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

gcc编译器,以及源文件的翻译

2026/8/14 15:38:11 拓冰建站 浏览量
gcc编译器,以及源文件的翻译

gcc与翻译

  • 1、翻译的过程
    • 1.1、预处理
      • 1.1.1、两个小问题
    • 1.2、编译
    • 1.3、汇编
    • 1.4、链接
    • 1.5、一个小细节
  • 2、条件编译
    • 2.1、条件编译举例
    • 2.2、理解条件编译
    • 2.3、为什么要有条件编译
  • 3、翻译的整个过程的由来
    • 3.1、指令集简单理解
    • 3.2、开关
    • 3.3、二进制纸带编程
    • 3.4、汇编语言
    • 3.5、C语言等高级语言
  • 4、编译器与语言的自举过程
  • 5、链接的初步理解
    • 5.1、库的简单认识
    • 5.2、链接的简单认识
    • 5.3、感性认识:动态库 vs 静态库 & 动态链接 vs 静态链接
      • 5.3.1、第一个故事
      • 5.3.2、第二个故事
      • 5.3.3、总结
    • 5.4、两个小细节

编译器的作用,是把源文件翻译成可执行二进制文件。其中:

  • gcc:只能编译C程序
  • g++:可以编译C / C++程序

1、翻译的过程

翻译的过程,大致分为:

  • 预处理
  • 编译
  • 汇编
  • 链接

1.1、预处理

预处理阶段,编译器对源代码进行:

  • 头文件展开
  • 宏替换
  • 注释替换成空格
  • 条件编译(之后会提及)

当前我们以准备好的code.c源代码为例,code.c里有头文件、宏、注释:

预处理

执行指令:gcc -E code.c,就完成了code.c的预处理工作:


但是这样只能将编译结果放在打印屏幕上。
我们可以执行指令:gcc -E code.c -o code.i,将预处理后的内容放到新建的文件code.i中:


vim打开code.i文件,就可以看到预处理之后的结果。

这里我们用到的选项有:

  • -E:编译器开始翻译源文件,完成预处理就停下
  • -o:将预处理结果,写入code.i文件

1.1.1、两个小问题

头文件展开?

我们之前认为,预处理阶段编译器会将头文件展开,可“展开”这一行为到底有什么含义?

我们可以vim打开文件/usr/include/stdio.h,通过将/usr/include/stdio.h文件与code.i进行对比,我们可以发现有许多相似之处。

Linux下编译器进行头文件展开,本质上是将/usr/include目录下同名头文件内容,拷贝到源文件中,从而进行头源合并

其实学到后面我们就能体会到,只有预处理阶段才需要头文件。

预处理后的文件还是C语言文件吗?

答案是还是的,只不过更简洁了:注释被去掉了,宏被替换成了具体的内容,头文件也被替换拷贝上了具体的内容。

1.2、编译

编译的过程,是将预处理完的文件翻译成汇编语言的文件。

指令为:gcc -S code.i -o code.s

其中,

  • -S:编译器开始翻译源文件,完成编译就停止。意味着我们不仅可以对已经完成预处理的code.i使用-S选项完成编译,还可以对源文件code.c使用-S选项一键编译。
  • code.s:汇编语言文件的后缀一般是.s。

1.3、汇编

汇编的过程,是将汇编语言文件处理成目标文件(.o / .obj),目标文件又称为可重定位二进制目标文件

指令为:gcc -c code.c -o code.o

其中,

  • -c:编译器开始翻译文件,完成汇编就停止。

但是,我们无法执行code.o文件。比如在code.o文件中,我们想使用printf()函数,可是code.o文件里并没有printf()函数的定义。这时我们就需要将code.o与C标准库链接形成一个可执行文件。

1.4、链接

当我们执行当前目标文件需要第三方库的时候,我们就需要将当前文件与第三方库进行链接。

指令为:

  • gcc code.o -o code.exe

也可以一步到位:

  • gcc code.c -o code.exe
  • gcc -o code.exe code.c

这时code.exe就可以执行了。

1.5、一个小细节

-c选项

以后我们更不常使用预处理、编译指令,更常使用汇编指令生成目标文件。这是因为当我们有很多文件需要同时链接成一个可执行程序时,如果某一些文件需要修改,我们就可以针对那些文件专门进行修改、处理成目标文件,再链接在一起

这样对于单个文件的操作,就避免了多次链接,并且使得修改过程更清晰,从而提高效率。

2、条件编译

预处理阶段编译器做的其中一件事是:处理条件编译。

2.1、条件编译举例

这里给出一段包含条件编译的C语言程序main.c:


我们可以简单解释上面出现的条件编译语句:

预处理阶段,编译器如果识别到宏V1,Version1()语句会被保留,Version2()语句会被裁剪;如果没有识别到宏V1,Version2()语句会被保留,Version1()语句会被裁剪。而#endif则会提醒编译器结束条件编译。

如何验证上面的解释?生成main.i文件查看即可。
main.c包含宏V1:


main.c不包含宏V1:

2.2、理解条件编译

通过上面的学习我们知道,条件编译的功能是在预处理阶段提示编译器进行代码裁剪

其实,井号#以及之后的内容,比如#ifdef #else #endif,都属于预处理符,是为了提醒编译器要进行代码裁剪,以及提醒编译器如何裁剪代码

2.3、为什么要有条件编译

我们之前使用的vs 2022,以及我们现在使用的Xshell,其实使用的是免费版 / 社区版,是不收费的。

而像vs 2022、Xshell及其它开发环境(软件),除了不收费的版本,还有付费版 / 企业版。

我们很容易想到,这些软件的免费版,相当于付费版去除了某些功能,只保留了基础的功能。

那么问题来了:这些软件大多数都是由C / C++语言编写的,那么这些设计并维护软件的厂商,到底是选择维护

  • 一份免费版代码
  • 一份付费版代码

还是选择只维护付费版代码,然后想办法给付费的用户开放所有功能,而给普通用户阉割掉一些高级功能?

当然是选择后者。因为选择前者意味着要维护两份代码(甚至多份代码),投入成本高;而选择后者,只需要维护好一份代码即可。

这时,条件编译裁剪代码的功能就派上用场了。

付费用户(不包含宏FREE)输出的结果:

注意到当前代码其实是没有宏FREE的。我们可以不通过vim改写代码,而是直接在Bash命令行上输入指令:gcc main.c -o main -DFREE=1,就可以添加宏FREE进入main.c:


这是免费用户(包含宏)的输出结果。

条件编译,不仅可以实现软件商业化的目的,还可以对源代码进行多功能开发,使之使用各种不同的场景。比如Linux源代码就使用了条件编译,所以Linux系统可以运作在服务器、车载系统、电视,以及"板子"上。

3、翻译的整个过程的由来

也许有人会好奇,源文件为什么要经过预处理、编译、汇编、链接这么多操作才能翻译成可执行文件(不可以直接由源文件直接一步到位翻译成可执行文件吗)?

这就需要我们稍微简单地回顾编译器的历史,以及编程语言的历史了。

3.1、指令集简单理解

首先我们可以来想象一个场景:

刚出生的小孩,什么也不会:不会站、不会坐、不会走路、不会抓握,也听不懂人说的话。经过父母一年的训练,这个小孩学会了基本的动作:坐、站、走路、抓握,以及语言的输入与输出。有一天,小孩的叔叔来到他家,对小孩说:小孩乖哦,给叔叔拿一杯水。小孩听到了叔叔的话,于是进行了一系列动作:站起来、走到桌前、拿起水杯、走到叔叔声旁,然后递水给叔叔。

在这个场景中,

  • 小孩对应计算机中的CPU
  • 父母给小孩进行训练,对应技术人员给一块裸的CPU硅片刻上指令集
  • 叔叔给小孩发号施令,对应向计算机输入指令
  • 小孩进行一系列动作,对应计算机CPU进行一系列计算操作

所以我们知道了,计算机CPU上存在指令集,指令集包含一系列操作指令。

由于计算机CPU只能读懂二进制代码,指令集包含的操作指令就是二进制代码

3.2、开关

计算机最开始出现时,使用的是"开关"这么一个使用的技术。我们可以理解为:程序员利用通电与断电,表示出一个个二进制序列,进而表示一个个操作指令;将这些操作指令(以及数据)输入计算机,计算机就会进行一系列计算操作,得出结果

但是,开关技术就将一些具体的功能写死了,换句话说,想要实现一种功能,就必须特别实现一种操作体系。这就导致,就算多种操作体系之间有相同的操作,我们也无法实现操作的复用,只能有几个问题就单独设计几个操作体系

3.3、二进制纸带编程

当时的科学家觉得开关太死板了,于是他们想出了另一种办法:二进制纸带编程。

二进制纸带编程,就是在纸带上"打洞",将二进制指令刻在纸带上,然后通过传感器扫描纸带,将指令输入计算机进行操作,得出结果。

这意味着,操作指令的设计在纸带上完成就可以了,计算机只管对输入的指令进行操作。二进制纸带编程一定程度上提高了效率。

可是另一个不可忽视的问题日渐凸显:纸带上指令的设计,需要我们清楚操作对应的二进制指令。二进制指令都是01组成的序列,抽象无比,难以记忆。

有没有一种方法,能够将操作的二进制指令与操作本身的一些特性对应起来,以方便理解,比如用操作的单词代表操作指令?

3.4、汇编语言

后来人们又发明了汇编语言。汇编语言,就利用了指令的英文单词替代了指令的二进制序列。比如:

  • mov:移动数据
  • add:相加
  • push:压栈
  • call:调用函数

英文单词可比01序列方便记忆多了。汇编语言相当于对二进制指令集做了一层包装

但是汇编语言还是麻烦了:我们可能需要几十条指令,才能完成一个简单的操作,比如变量的创建。

还有没有方法,使得程序的设计更贴合人类的语言习惯?

所以,C语言应运而生。

3.5、C语言等高级语言

C语言的使用更符合人类的语言习惯。然而C语言也有缺点与不足。

随着时代的发展,C++、java、python、PHP等一些高级语言也逐渐登上计算机编程历史舞台。

所以,C语言源文件的翻译并没有直接一步到位。我们可以将C语言翻译成汇编语言,然后借助之前积累的有关汇编语言的技术,最终完成源文件向可执行程序的转变。

4、编译器与语言的自举过程

汇编语言,本质上是一套规则。我们只有借助规则,才能设计出程序乃至项目,从而完成我们预期的目标。所以是先有汇编语言

然而,当汇编语言刚出现的时候,由于还没有针对汇编语言的编译器,我们还无法对编写的汇编语言进行翻译,得到可执行文件。

这时,我们可以用低一级的语言,即二进制代码,设计出第一代汇编语言编译器

以后,第一代汇编语言编译器逐渐完善,支持新功能,我们就可以按照新功能设计出第二代汇编语言;第二代汇编语言就可以设计出第二代汇编语言编译器;第二代汇编语言编译器又支持了新功能,就可以设计出第三代汇编语言…

也就是说,编译器支持新功能,汇编语言就可以增加新语法;增加了新语法的汇编语言,又可以反过来设计新的编译器。这就是编译器与语言的自举过程

同理,C语言的第一代编译器是由汇编语言编写的,接下来我们就可以通过C语言与C语言编译器的自举,来进行C语言与C语言编译器的迭代更新。

5、链接的初步理解

源文件翻译的链接阶段中,需要将众多.o文件与链接库链接。那么这个库到底是什么?

5.1、库的简单认识

我们之前就提到过:我们在源文件代码中调用了printf()函数,而我们没有在源文件定义printf()函数,所以我们必须链接当前环境提供的C语言标准库,在标准库中有printf()的定义。

这个printf()的定义,也许是曾经某个参与C语言开发的顶级大佬设计的。现如今全世界的编写程序的人都在使用这个大佬写的printf(),而不是自己定义一个printf()。

所以,库本质上是程序员之间的一种协作方式。库能提高编程效率

那么库在哪里?


其中,libc.so就属于C标准库。其实libc.so是C语言的动态库。

windows下也有库。以下是Linux与windows的库后缀的区分:

系统动态库静态库
Linux.so.a
windows.ddl.lib

我们也可以分析出Linux下库命名的规则:

lib + [真正名称] + .[a / so]

5.2、链接的简单认识

当我们创建了一个C语言文件soft.c,编译后对可执行文件执行:ldd soft,我们会观察到:

ldd指令的作用是查看当前程序依赖的库。其实,有不少指令都依赖C / C++标准库。

我们之前提到过,库分为动态库和静态库,其中:

  • 动态库:通常与程序进行动态链接
  • 静态库:通常与程序进行静态链接

目前的知识,依旧无法支持我们较为全面地了解动态库 / 动态链接,与静态库 / 静态链接,我们就来感性认识它们:

5.3、感性认识:动态库 vs 静态库 & 动态链接 vs 静态链接

5.3.1、第一个故事

你是小于,你以优异的成绩考入了临川二中。中考之后的暑假,你既惊喜,又担忧,担忧什么呢:临川二中以半军事化管理方式、考试很多闻名当地,学校是肯定不能带手机的;而你又想很喜欢上网,很喜欢玩游戏。

于是你向老爸求助,老爸装模做样地说:“想玩游戏那怎么行~我给你介绍隔壁小帅,小帅今年高考,考入了清北大学。你向他取取经!”

于是小于你与小帅学长见了面。你对小帅学长说,你不担心自己的成绩,你只担心自己上学期间上不了网玩不了游戏。小帅学长说那好办,就直接给了你条详细的地址,那里是学校附近一所网吧。

开学后,小于你平时照常上课,周日时,你给自己制定了一个计划:

  • 9:00 ~ 10:00 写作文
  • 10:00 ~ 11:00 学习化学
  • 11:00 ~ 12:00 学习数学
  • 12:00 ~ 12:30 吃饭
  • 12:30 ~ 14:30玩游戏
  • 14:30 ~ 16:30 睡觉
  • ······

问题是,学校不让带手机,学校里就玩不了游戏。你想起了小帅学长给的地址。你按照地址来到了网吧。你跟老板说你要上网,老板就领你到号码为1234的电脑前,让你开始上网。于是你度过了一个愉快的下午。

渐渐地,班上同学,以及其他班上同学都知道了这家网吧,纷纷都偷偷来这家网吧上网玩游戏.。后来的某一次月考,不少班级的成绩一落千丈,包括小于你所在的班级。临川二中的老师们都很生气,一问才发现成绩变差的学生都去过那家网吧频繁上网。于是老师们联名举报,帽子大院的帽子叔叔们就将网吧查封了。最终,同学们,包括小于,都上不了忘了。

在上面的故事中,

  • 小于和每一个同学就是目标文件
  • 临川二中就是内存
  • 小于周天的一个个行程就是目标文件对应源文件的要执行的代码
  • 网吧就是动态库;号码1234的电脑就是执行代码需要的库函数
  • 小帅学长就是编译器中的链接器;小帅学长向小于提供网吧的地址,就是链接器对目标文件进行动态链接

其实,以这个故事类比动态链接,还是不太严谨。后面我们对于动、静态库会有更详细的讲解。

在这个故事中,我们也会发现动态链接的一个很突出的缺点:如果网吧被查封了,就上不了网;同理,如果动态库丢失,编译成的程序会无法执行

5.3.2、第二个故事

网吧被查封了,小于你很苦恼:别人上网吧成绩掉了,我成绩又没掉,网吧没了我还怎么上网?

于是小于你告诉老爸,叫老爸想想办法。给力的老爸直接找到校长,软磨硬泡,让校长答应了在你的宿舍桌上安装电脑;与此同时,老爸联系了之前网吧被查封了的网吧老板,将你之前用过的1234号电脑直接买了过来,装在了你的宿舍。这样你就有了一台自己的电脑。

其他同学听说了,也想效仿小于你在宿舍装一台电脑。渐渐地,所有同学的桌前都有了一台电脑。

在这个故事中,

  • 电脑就是静态库,一台台被需要的电脑,组成了静态库
  • 老爸和网吧老板,就是(使用静态库的)链接器
  • 给小于和其他每一个同学装电脑,就是在给目标文件进行静态链接

5.3.3、总结

讲到这,我们也许就能对动态库与静态库、动态链接与静态链接有了一个大概的认识:

  • 动态链接,就是将库函数的地址写入到目标文件,然后与动态库链接到一起做成可执行程序。其中相同的库函数,在不同的程序中,是可以共享的。
  • 静态链接,就是将库函数直接拷贝到目标文件,做成可执行程序。若不同的程序需要相同的库函数,那么连接后每一个程序都有独属于自己的库函数,尽管这些库函数都是完全一样的。

那么动、静态链接也就可以简单做一个比较:

连接方式动态链接静态链接
依赖库的程度动态库丢失或者出问题,链接后的程序无法执行静态库中库函数拷贝到了程序中
文件大小较小较大

5.4、两个小细节

如果删除C动态库,会怎样?

结果是很可能大部分指令用不了。因为大部分指令都是C / C++编写的。

gcc默认使用的是动态链接

我们可以理解为,系统认为动态链接形成的可执行程序体积更小,存储使用效率更高,于是系统默认就用动态链接。


强制进行静态链接需要加上选项-static

有人可能静态链接完毕后,发现C / C++程序无法执行。大概率是系统没有静态C / C++库,可以搜索指令安装静态库。