1、c程序的编译过程
我们这里虽然介绍的是c程序的编译过程,但是实际上所有编译型语言的编译过程,大致是类似的。
1.1编译的四个过程
我们平时编译时,不管是通过IDE图形界面来编译的,还是通过命令行来编译的,我们感觉编译一下就完成了,然后就得到了你要的针对某os和某cpu的二进制可执行文件(机器指令的文件),但是实际上中间隐藏了四个过程,这四个过程被默默的处理了。
编译四个过程:预处理、编译、汇编、链接
四个过程中的"编译",特指其中的某个过程,这四个过程合在一起,我们也统称为编译,所以"编译"二字到底指的是第二个过程,还是全部过程的统称,这个就要看说话的"语境"了。
其实统称的"编译",完整的称法应该叫"编译链接”,只是简称为编译而已。
如果这四个过程是一次性编译完成的,这个四个过程分别会产生相应的文件,只不过中间产生的文件都是过渡性的临时文件,使用完成后就会被删除。
四个过程的总览图:
1.1.1预编译(预处理)
之所以叫预编译,表示为正式的编译做准备,预编译也被称为预处理。
预处理(预编译)
(1)***.c ————————> ***.i
如果编译过程是一次性完成的话,.i文件只是一个过渡性文件,.i被称为扩展后的c源码文件
为什么还叫c源码文件呢?
因为预处理后,只是宏定义等东西不见了,但是c源码依然还在,比如main函数,各种自己写的子函数,依然存在,所以还是被称为c源码文件。
打开.i文件后,我们是能够看的懂的,所以.i文件时ascii文件。后面会演示给大家看。
(2)预编译是以单个文件为单位来进行的
a.c ———> a.i
b.c ———> b.i
当然**.i的这个名字并不是固定的。
(3)预处理做了什么处理
1)宏替换:将宏替换为真实的宏体,比如
程序性中有使用NUM这个宏,这个宏的定义为#define NUM 100,程序中所有的NUM都会被替换为100。
2)包含头文件
将include包含的.h头文件内容,全部复制到.c文件中,因为头文件中定义了类型、宏、函数声明等等,这些都是函数调用会用到的,你调用某个函数时,就必须包含这个函数要的头文件。
疑问:头文件那么多内容,都包含进去的话,不会太多了吗?
编译时,编译器只使用要用的东西,用完后包含的内容都会被丢弃,实际上并不占空间。
3)条件编译
处理#if#endif 这类的东西
4)处理一些特殊的预处理关键宁
有关预处理的宏定义、头文件包含、条件编译、特殊预处理关键字,会在后面专门的《预处理》章节中讲到。
1.1.2编译
编译
( 1)***.i ————————> *** .s
s:汇编文件
(2)同样也是以单个文件为单位来进行的
(3)编译做了什么
将c语法的c源码,翻译为汇编语法的汇编源码。
(4) .s是ascii码文件
因为汇编也是人能看懂的文字编码形式,所以.s汇编文件也是ASCII码文件。
1.1.3 汇编
汇编
( 1)***s ————————> ***.o
.o文件是纯二进制文件
因为.o中放的是纯二进制的机器指令,所以我们打开后看不懂。
(2)同样也是以文件为单位来进行的
(3)汇编做什么
将AscII的汇编源码,翻译为能够被cPu执行的机器指令,,o文件中放的就是机器指令。但是.o文件还无法运行,需要链接后才能运行。
1.1.4链接
***1.o ————————\
***2.o —————————\
/ a.out (可执行文件)
. . . . —————————/
***n.o ————————/
(1)链接(连接)做了什么
1)将众多的.o合成一个完整的可执行文件
.o实现相互依赖的,比如a.o中调用的函数,被定义在了b.o中,如果不链接在一起的话,是无法工作的。
2)链接时,需要加入额外的启动代码
这个启动代码并不是我们自己写的,main函数是由启动代码调用的,我们的程序是从启动代码开始运行的,后面会介绍启动代码是怎么来的。
3)链接为一个可执行文件时,需要进行符号解析和地址重定位
后面会介绍什么事符号解析和地址重定位。
(2) Linux下可执行文件命名问题
在windows下,可执行的尾缀时.exe,但是在Linux下,可执行文件没有固定的尾缀。
(3)如果整个c工程就一个.c,最后得到的只有一个.o,此时还需要链接吗,可不可以直接执行呢?
同样的要链接后才能运行,因为链接后才有启动代码和重定位后的地址,否者无法运行。
1.2了解编译器集合
我们在前面就说过,编译器并不是一个单独的程序,而是一堆程序的集合。
为了更好的了解编译的四个过程,我们需要大概的弄清楚"编译器集合"组成。
对于"编译器集合"的组成,我们作为应用软件开发者了解到本章介绍的程度就可以了,至于更加深入的内容,那就是"编译器开发者"和"逆向破解者"所应该掌握的内容。
所以对于更多更深内容,如果是站在应用开发角度的话,我们不建议大家再去更加深入,作为应用开发来说,再深入的话意义不大。
1.2.1 codeblocks的编译器集合
(1) codeblocks安装目录
codeblocks的编译器集合就放在它的安装目录下。
(2)编译的四个过程,必须用到的基本程序
cpp、cc1、 as、collect2/ ld、 gcc
gcc编译时四个过程自动完成,我们既然已经知道了这四个过程,那么我们就自己一步一步实现这四个过程,然后得到最终的可执行文件。
我们讲这四个过程的主要目的是什么呢?
熟悉这四个编译过程,了解每个过程做了什么事,了解每个过程调用了编译器集合中什么程序
疑问:你怎么知道编译四过程使用的就是cpp、cc1、as、collect2、gcc这些程序呢?我们后面再来回答这个问题。
1) cpp(Mingw\bin\ )
(a)预编译程序(预编译器、预处理器):实现预编译
为了方便我们查看预编译后的结果,我们先在.c中加入宏、条件编译、头文件包含。
(b)演示
cpp helloworld.c -o helloworld.i
o选项用于指定目标文件,表示将预处理后的结果保存到.i文件中。
-验证.i文件是不是ascii文件。
–验证预处理后.c中的宏、 include、条件编译,在.i中还能否见到。
我们用一个helloworld.c文件做演示。
helloworld.c文件通过cpp命令预编译为helloworld.i文件。
我们打开.i文件观看,是一个我们能看懂的文件,里面包含了头部信息排除了判断为假的内容,宏定义也赋值到了变量上面。
2 ) cc1 (Mingw\libexec\gcc \mingw32\4.9.2\)
(a)编译程序(编译器):将c源码翻译为汇编源码(b)演示
..\libexec\gcc\mingw32\4.7.1\cc1 helloworld.i -o helloworld.s
验证.s文件是不是ASCII文件。
(c) cc1值得注意的地方
其实cc1本身也包含cpp预处理的功能,也就是说可以直接使用cc1将.c—>.s,cc1会完成之前的预处理的功能。
..\libezec\gcc\mingw32\4.7.1\cc1 helloworld.c -o helloworld.s
不过以上命令并不能被成功执行,因为还缺参数,他会提示找不到头文件,至于缺什么参数,我们这里就不关心了。
.s文件被编译为汇编文件。
3 ) as (Mingw\bin\ )
(a)汇编程序(汇编器):将汇编源码翻译为纯二进制的机器指令,放到.o文件中
(b)演示
as helloworld.s -o he1loworld.o
我们看一下汇编完成后的.o文件,已经变成我们看不懂的二进制文件了。
4) ld、 collect2
ld路径:Mingw\ binl. MinGw\mingw32\bin
collect2路径: Mingw\libexec\gcc\mingw32\4.7.1\collect2
(a)链接程序(链接器(静态链接器))
将所有的.o文件(自己的、编译器提供的)和库(动态库、静态库)链接在一起,得到可以运行的可执行文件。
(b) collect2 和ld之间的关系
collect2是对ld进一步封装得到的,这两个都可以用于链接。
(c)演示
实际上我们完全可以自己调用collect2和ld这两个程序(命令)来进行链接,但是链接并不是一件容易的事情,链接的时候需要跟大量的参数和选项,这些参数和选项我们自己并不清楚,所以我们自己调用eollect2和ld来链接的话,实际上操作起来比较困难。
所以链接的话,我们直接使用gcc程序来链接,gcc会自动调用collect2或者ld来链接,并且自动指定需要的各种的选项和参数,我们并不是需要关心。
gcc helloworld.o -o helloworld
或者
gcc helloworld.o
(如果不指定可执行文件名字的话,默认为a.exe)
我们打开helloworld.exe查看。
5 ) gcc/mingw32-gcc/g++/c++
其实gcc/mingwgcc/g++这几个都能编译c程序。
(a) gcc/mingw32-gcc/g++/C++关系
其中mingw32-gcc是对gcc继续做封装后得到的。
C++/g++是用来编译c++程序的,但是由于c++程序兼容c,所以c++/g+也能编译c程序。
正式因为编译集合中包含了g++,所以我们也能使用codeblocks来写c++程序的,而且codeblocks这个IDE本身好像就是c++写的。
(b) gcc/mingw32-gcc/g++/C++程序的作用
gcc/mingw32-gcc/g++/C++其实是总的调度程序,它按照需求去调用cpp/cc1/as/collect2等程序,完成对应四个过程。
通过前面的讲解知道,虽然我们能够自己调用cpp/cc1/as/collect2/ld来完成四个过程,得到最后的可执行文件,但是存在如下缺点。
-每个阶段的程序名都不一样,不方便记忆
第一阶段叫cpp,第二阶段叫cc1等,老实讲,时间久了我也忘了。
有了gcc这个总调度程序后,不管是哪个阶段,对于我们来说,只需要记住gcc这一个程序即可。你想实现那个阶段,通过gcc即可实现,通过给gcc指定不同的选项,gcc可以自动调用cpp/cc1/as/ collect2/1d中所需要的程序来实现对应的过程。
-如果每个阶段都我们自己亲自执行cpp/cc1/as/collect2/ld这些程序来编译的话,速度太慢了
有了gcc后,虽然可以通过指定选项,分别实现每个过程,但是实际上也可以调用gcc一次性快速完成四个过程,gcc会自动调用cpp/cc1/as/collect2/ld来完成。
次性完成时,中间产生的.i/.s/.o都是临时文件,编译后会被自动删除,这些文件我们并不关心,我们关心的只是最后的可执行文件。
使用gcc这个总调度程序,一次性完成所有过程时,编译速度非常快,用起来非常方便。
(c) gcc/mingw32-gcc/g++/c++的各种选项
它们几个的使用方式都是一样的,所以我们就以gcc为例来讲。
gcc的选项很多,先介绍常用的E/S/c/g选项,然后在介绍其它一些个不常用选项。至于其它的非常不常用的选项,我们这里不介绍,用到时大家自己研究搞定。
-E
只得到.i的扩展c源文件
演示
gcc -E helloworld.c -o helloworld.i
gcc会自动调用cpp或者cc1来进行预处理。如果不写目标文件,就直接输出到控制台。
疑问: gcc -o helloworld.i -E helloworld.c,这么写可以吗?
可以
-S
只编译到.s文件+演示
gcc -S helloworld.i -o helloworld.s
gcc会自动调用cc1,将.i编译为.s。
如果不写目标文件,会自动保存为同名的.s文件
疑问: gcc -s helloworld.c -o helloworld.s 可以吗?
可以,gcc自动调用cc1时, cc1先预编译,然后再编译。
-c
只编译得到.o文件
演示
1 )
gcc -c helloworld.s -o helloworld.o
自动调用as进行汇编,将.s中的汇编源码翻译为机器指令,并保存到.o
2 )
gcc -c helloworld.i -o helloworld.o
(a)调用cc1编译得到临时.s
(b)调用as将.s汇编得到.o
3 )
gcc -c helloworld.c -o helloworld.o
(a)调用cc1预编译、编译得到临时的.s‘’
(b)调用as将.s汇编得到.o
如果不写目标文件,会自动的保存为同名的.o文件
-直接得到可执行文件
+演示
gcc helloworld.c **.c -o helloworld.exe
gcc helloworl4.i **.i -o helloworld.exe
gcc helloworld.s **.s -o helloworld.exe
gcc helloworld.o **.o -o helloworld.exe
-g
如果要进行debug调试的话,通过指定-g选项,会加入调试信息,没有调试信息是无法进行调试的。debug调试:后面的课程再介绍。
其它不常用选项
o0/o1/o2/0s/o3:指定优化级别,00< o1 < 02< Os < 03
gcc hellowolrd.c -o helloworld.exe -o1
如果不指定有优化级别的话,默认就是o1级别,有关优化的更多情况,后面章节再介绍。
-Wall:
gcc hellowolrd.c -o helloworld -Wall
表示编译时将所有的警告信息都显示出来,如果不指定的话,默认只显示重要的警告,不重要的警告就不显示。
比如,有一个变量定义了但是没有使用,就是一个不重要的警告。如果指定了-Wall选项,会警告你没有使用,否者不提示这个警告。
警告真的不重要吗?
初学c的时候,来时会告诉你警告没关系,但是在实际开发中警告是不能有的,为什么?
对于程序的警告来说,虽然不是"编译链接"严重错误,但是在程序的运行过程中,这些警告可能会演变为威胁程序正常运行的错误,所以警告是程序的隐患,因此在实际开发中,编译时必须将警告排除。
-s
对可执行文件进行瘦身
gcc hellowolrd.c -o helloworld -s
不指定-s时,可执行文件都会包含调试等信息,用以实现程序的调试,但是当程序被调试没有bug后发布时,发布的程序就不再需要这些信息了,指定-s选项后,gcc编译时会对可执行文件进行瘦身,以去掉这些信息。
-std
指定编译时准守的语言标准,c的标准目前有c89、c90、c99、c11
gcc helloworld.c -o helloworld -std=c11//编译时,按照c11标准来解析c语法格式
语言在发展的过程中,每过一段时间就会修改、增加语法特性,然后重新指定语法标准,编译器在编译时就是按照标准来翻译语言的语法格式,c语言也是这样的。
如果gcc时指定某个c标准的话,就会使用该标准来编译,如果不指定的话,就使用gcc默认设置的标准来编译。
-股来说,新的标准都是兼容旧标准的,但是反过来就不行,如果你的程序使用了最新标准的语法特性,而在编译时指定的确是旧标准的话,就会编译出错,因为旧标准没有这些信的特性。
不过一般来说我们不用关心标准问题,因为我们使用的都是最常见c语法特性,不管哪个标准都是支持的,所以不用指定特定的标准,gcc设置的默认标准就支持。
-v
显示编译过程的详细信息
gcc helloworld.c -o helloworld -v//显示预处理、编译、汇编、链接,所有过程的详细信息。
回答之前的问题:我怎么知道编译时使用的是as、cc1、collect2、ld这些程序的?
通过加-v选项,阅读编译过程的详细信息知道的,后面会分析这些详细信息。
疑问:单个过程可以加-v吗?
可以,显示的就是单个过程的详细信息,比如
gcc -E helloworld.c -o helloworld.i -v//只显示预处理的详细信息。