C/C++程序编译步骤以及如何生成可执行文件

引用别人的文章, 这里只用做备份, 原作者的文章出处: https://blog.csdn.net/qq_21842557/article/details/51886327

一、开篇

        许久不碰关于这方面的知识了,前几天同学开课提及到该部分,正好作为回顾吧。

        C/C++语言很多人都比较熟悉,这基本上是每位大学生必学的一门编程语言,通常还都是作为程序设计入门语言学的,并且课程大多安排在大一(反正我是混过来的)。刚上大学,学生们还都很乖,学习也比较认真、用心。所以,C/C++语言掌握地也都不错(说的是你么),不用说编译程序,就是写个上几百行的程序都不在话下,但是他们真的知道C/C++程序编译的步骤么?

        很多人都不是很清楚吧,如果接下来学过“编译原理”,也许能说个大概。VC的“舒适”开发环境屏蔽了很多编译的细节,这无疑降低了初学者的入门门槛,但是也“剥夺”了他们“知其所以然”的权利,致使很多东西只能“死记硬背”,遇到相关问题就“丈二”。国内教育,隐匿了关于程序代码变成计算机可执行的语言之间的一切过程,悲剧~~~所以,这部分只能自己查资料了,在此推荐两本书,一个是老外的《深入理解计算机系统》,另外一本就是国人写的非常优秀的一本关于底层介绍的书籍《程序员的自我修养》。

        本篇博客仅作为关于“C/C++程序编译步骤以及如何生成可执行文件”的简要介绍。

三、正文

1、写在前面

        关于学习编程的过程,一是刷各家公司的笔试题,各种奇葩的笔试题,挖了各种坑,这样才能让你快速进步;二是看了liutao的《囫囵C语言》系列,写的太精辟了,幽默的语言以及深入的理解。可以作者很久不更新了。应该是退出“神坛”了吧。

        电子计算机所使用的是由“0”和“1”组成的二进制数,二进制是计算机的语言的基础。计算机发明之初,人们只能降贵纡尊,用计算机的语言去命令计算机干这干那,一句话,就是写出一串串由“0”和“1”组成的指令序列交由计算机执行,这种语言,就是机器语言。想象一下老前辈们在打孔机面前数着一个一个孔的情景,嘘,小声点,你的惊吓可能使他们错过了一个孔,结果可能是导致一艘飞船飞离轨道。

        为了减轻使用机器语言编程的痛苦,人们进行了一种有益的改进:用一些简洁的英文字母、符号串来替代一个特定的指令的二进制串,比如,用“ADD”代表加法,“MOV”代表数据传递等等,这样一来,人们很容易读懂并理解程序在干什么,纠错及维护都变得方便了,这种程序设计语言就称为汇编语言,即第二代计算机语言。然而计算机是不认识这些符号的,这就需要一个专门的程序,专门负责将这些符号翻译成二进制数的机器语言,这种翻译程序被称为汇编程序。因为汇编指令和机器语言之间有着一一对应的关系,这可比英译汉或汉译英简单多了。

        高级语言是偏向人,按照人的思维方式设计的,机器对这些可是莫名奇妙,不知所谓。鱼与熊掌的故事在计算机语言中发生了。于是必须要有一个桥梁来衔接两者,造桥可不是一件简单的事情。当你越想方便,那桥就得越复杂。那高级语言是如何变成机器语言的呢,这个过程让我慢慢道来。

2、转换过程
        平时大家写代码然后编译即可生产计算机可以执行的指令,其实这个转换过程中有许多重要的过程,下面作详细介绍。     

        编译:将源代码转换为机器可认识代码的过程。编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。       

        C源程序->编译预处理->编译程序(生成*.s文件)->优化程序->汇编程序(生成*.o文件)->链接程序->可执行文件(*.out)

3、细化
3.1、编译预处理

        编译预处理:读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。伪指令主要包括以下四个方面:

        (1)宏定义指令,如# define Name TokenString,#undef等。对于前一个伪指令,预编译所要作得的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。

        (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

        (3)头文件包含指令,如#include "FileName"或者#include<FileName>等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的主要目的是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>系统头文件)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号(""自定义头文件)。

        (4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输入而被翻译成为机器指令。删除所有注释“//”,“/*   */”以及添加行号,便于编译器编译时产生调试用的行号信息及用于编译时产生编译错误或警告时显示行号。

3.2、 编译阶段
        经过预编译得到的输出文件中,只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main、if、else、for、while、{,}、+、-、*、\等等。编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码(符号表)。

3.3、优化阶段

        优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。上图中,我们将优化阶段放在编译程序的后面,这是一种比较笼统的表示。

        对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除等等。后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

3.4、 汇编过程
        汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个汇编源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:

            代码段: 该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。 

            数据段: 主要存放程序中要用到的各种全局变量或静态的局部变量。(.rodata和 .data)

        UNIX环境下主要有三种类型的目标文件:

        (1)可重定位文件: 其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

        (2)共享的目标文件: 这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

        (3)可执行文件: 它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

3.5、 链接程序
        由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

        链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

        根据开发人员指定的库函数的链接方式的不同,链接处理可分为两种:

        (1)静态链接  在这种链接方式下,函数的代码(被应用程序引用的目标模块)将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。静态连接的劣势:浪费内存和磁盘空间,模块更新困难。

        (2)动态链接  在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射(优点:无拷贝环节,在内存中只有一份此共享代码,以节约存储器空间)到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

        动态连接解决了共享的目标文件多个副本浪费磁盘和内存空间的问题。在内存中共享一个目标文件模块的好处不仅仅是节省内存,还可以减少物理页面的换入换出,亦可以增加CPU的cache hit (关于这部分在《深入理解计算机系统》中有详细介绍,尤其是程序的局部性原理的应用,以前写代码都是瞎写,根本不知道还有这么个优势)。

        动态连接也有其缺点:很常见的一个问题是,当程序所依赖的某个模块更新后,由于新的模块与旧的模块之间接口不兼容,导致原有的程序无法运行。

        下面是一些对静态库的介绍,帮助理解。

        A static library is like abookstore, and a shared library is like... a library. With the former, you getyour own copy of the book/function to take home; with the latter you andeveryone else go to the library to use the same book/function. So anyone who wantsto use the (shared) library needs to know where it is, because you have to"go get" the book/function. With a static library, the book/functionis yours to own, and you keep it within your home/program, and once you have ityou don't care where or when you got it.

        The advantages of static libraries is thatthere are no dependancies required for the user running the application - e.g.they don't have to upgrade their DLL of whatever... The disadvantages is thatyour application is larger in size because you are shipping it with all the librariesit needs.

        Sharedlibraries are .so (or in Windows .dll, or in OS X .dylib) files. All the coderelating to the library is in this file, and it is referenced by programs usingit at run-time. A program using a shared library only makes reference to thecode that it uses in the shared library.

        Staticlibraries are .a (or in Windows .lib) files. All the code relating to thelibrary is in this file, and it is directly linked into the program at compiletime. A program using a static library takes copies of the code that it usesfrom the static library and makes it part of the program. [Windows also has.lib files which are used to reference .dll files, but they act the same way asthe first one].

        Sharedlibraries reduce the amount of code that is duplicated in each program thatmakes use of the library, keeping the binaries small. It also allows you toreplace the shared object with one that is functionally equivalent, but mayhave added performance benefits without needing to recompile the program thatmakes use of it. Shared libraries will, however have a small additional costfor the execution of the functions as well as a run-time loading cost as allthe symbols in the library need to be connected to the things they use.Additionally, shared libraries can be loaded into an application at run-time,which is the general mechanism for implementing binary plug-in systems.

        Staticlibraries increase the overall size of the binary, but it means that you don'tneed to carry along a copy of the library that is being used. As the code isconnected at compile time there are not any additional run-time loading costs.The code is simply there.

Personally,I prefer shared libraries, but use static libraries when needing to ensure thatthe binary does not have many external dependencies that may be difficult tomeet, such as specific versions of the C++ standard library or specificversions of the Boost C++ library.

        What some people have failed to mention is thatwith static libraries the compiler knows which functions your application needsand can then optimize it by only including those functions. This can cut downon library size massively, especially if you only use a really small subset ofa really large library!

4、可执行文件

         对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

        经过上述五个过程,C源程序就最终被转换成可执行文件了

四、总结
        看了那么多,是不是有了一个全新的认识呢?那么考察一下成果吧,下面是一个小测试。

File: hw.c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;//小检测:该语句可以省略吗?Why?
}

        为什么一个编译好的简单的Hello World程序也需要占据好几KB的内存空间呢?

原文链接: https://www.cnblogs.com/hzb462606/p/14605445.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C/C++程序编译步骤以及如何生成可执行文件

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/209224

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午11:46
下一篇 2023年2月12日 下午11:48

相关推荐