图解C语言的编译链接过程及过程过程图解(二)

C语言的编译链接过程就是将我们编写的C程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译链接。

● 编译是将文本形式的源代码翻译成机器语言形式的目标文件的过程。链接是组织目标文件、操作系统的启动代码和用于形成最终可执行代码的库文件的过程

从图中可以看出,整个代码的编译过程分为编译和链接两个过程。编译对应图中大括号括起来的部分,剩下的就是链接过程。

流程图如下:

编译过程可以分为编译和汇编两个阶段。

01

编译

编译就是读取源程序(字符流),进行词法和句法分析,将高级语言指令转换成功能等效的汇编代码。源文件的编译过程包括两个主要阶段:

第一个阶段是预处理阶段,发生在正式编译阶段之前。预处理阶段根据已放置在文件中的预处理指令修改源文件的内容。例如,#include 指令是一个预处理指令,它将头文件的内容添加到 .cpp 文件中。这种在编译前修改源文件的方式为适应不同计算机和操作系统环境的限制提供了很大的灵活性。一种环境所需的代码可能与另一种环境所需的代码不同,因为可用的硬件或操作系统不同。在许多情况下,可以将不同环境的代码放在同一个文件中,

主要处理以下几个方面:

(1) 宏定义指令,如#define ab

对于这个伪指令,所有预编译所做的就是用 b 替换程序中的所有 a,但作为字符串常量的 a 不是。还有#undef,它会取消宏的定义,这样以后出现的字符串就不会被替换。

(2) 条件编译指令,如#ifdef、#ifndef、#else、#elif、#endif 等。

这些指令的引入允许程序员通过定义不同的宏来决定编译器处理哪些代码。预编译器会根据相关文件过滤掉不必要的代码。

(3) 头文件包含#include “FileName” 或#include 等指令。

在头文件中,大量的宏(最常见的字符常量)一般用指令#define定义,包括各种外部符号的声明。使用头文件的主要目的是使某些定义可用于多个不同的 C 源程序。因为在需要使用这些定义的C源程序中,只需添加#include语句即可,无需在此文件中重复这些定义。预编译器会将头文件中的所有定义添加到它生成的输出文件中,以供编译器处理。c源程序中包含的头文件是系统可以提供的,这些头文件一般放在/usr/include目录下。使用尖括号 ( ) 将它们#include 到程序中。此外,开发者也可以定义自己的头文件。这些文件通常与 C 源程序放在同一目录中。在这种情况下,#include 中应使用双引号 (“”)。

(4)特殊符号,预编译器可以识别一些特殊符号。

例如一个c程序的执行是从什么开始的,源程序中出现的 LINE 标志将被解释为当前行号(十进制数),FILE 将被解释为当前编译的 C 源程序的名称。预编译器将为出现在源程序中的这些字符串替换适当的值。

预编译器所做的基本上是源程序的“替换”。在此替换之后,将生成一个没有宏定义、没有条件编译指令和特殊符号的输出文件。该文件的含义与未处理的源文件相同一个c程序的执行是从什么开始的,但内容不同。接下来,这个输出文件被翻译成机器指令作为编译器的输出。

第二阶段编译优化,预编译后得到的输出文件中只有常量;如C语言中数字、字符串、变量、关键字的定义,如main、if、else、for、while、{、}、+、-、*、\等。

编译器需要做的是在通过词法分析和语法分析确认所有指令符合语法规则后,将其翻译成等效的中间代码表示或汇编代码。

优化处理是编译系统中比较难的技术。它所涉及的问题不仅与编译技术本身有关,而且与机器的硬件环境也有很大关系。优化的一部分是中间代码的优化。这种优化不依赖于特定的计算机。另一个优化主要是针对目标代码的生成。

对于前一种优化,主要工作是去除常用表达式、循环优化(代码提取、强度弱化、改变循环控制条件、合并已知量等)、复制传播、去除无用赋值等.

后一种优化与机器的硬件结构密切相关,最重要的是要考虑如何充分利用机器各个硬件寄存器中存储的相关变量的值来减少访问内存。此外,如何根据机器硬件执行指令(如流水线、RISC、CISC、VLIW等)的特点,对指令进行一些调整,使目标代码更短,执行效率更高,也是一项重要研究。话题。

02

图片[1]-图解C语言的编译链接过程及过程过程图解(二)-老王博客

汇编

汇编实际上是指将汇编语言代码翻译成目标机器指令的过程。对于翻译系统处理的每一个C语言源程序,都会通过这个过程最终得到对应的目标文件。对象文件中存储的是与源程序等效的对象的机器语言代码。目标文件由段组成。通常在一个目标文件中至少有两个部分:

代码段:该段主要包含程序指令。

该段通常可读和可执行,但通常不可写。

数据段:主要存放程序中使用的各种全局变量或静态数据。通用数据段是可读、可写和可执行的。

在 UNIX 环境中存在三种主要类型的目标文件:

(1)可重定位文件

它包含适合与其他目标文件链接以创建可执行或共享目标文件的代码和数据。

(2)共享对象文件

该文件包含适合在两种上下文中链接的代码和数据。第一个是链接器可以将它与其他可重定位文件和共享对象一起处理以创建另一个对象;二是动态链接器可以处理它与另一个可执行文件和其他共享对象结合在一起,创建一个进程映像。

(3)可执行文件

它包含一个可由操作系统创建以执行进程的文件。汇编器产生的实际上是第一类目标文件。对于后两者,需要一些其他的处理才能获得,这就是链接器的工作。

链接过程

汇编器生成的目标文件不能立即执行,可能还有很多未解决的问题。

例如,源文件中的函数可能会引用另一个源文件中定义的符号(如变量或函数调用等);库文件中的函数可以在程序中调用,等等。所有这些问题都需要通过链接器来处理才能解决。

链接器的主要工作是将相关的目标文件相互连接起来,即将一个文件中引用的符号与另一个文件中符号的定义连接起来,使所有这些目标文件成为一个系统,可以由操作系统安装。成一个统一的整体。

根据开发者指定的同一个库函数的不同链接方式,链接过程可以分为两种:

(1)静态链接

在这种链接方式下,函数的代码会从它所在的静态链接库中复制到最终的可执行程序中。当程序执行时,这段代码将被加载到进程的虚拟地址空间中。静态链接库实际上是目标文件的集合,每个目标文件都包含库中一个或一组相关函数的代码。

(2) 动态链接

通过这种方式,函数的代码被放置在一个称为动态链接库或共享对象的对象文件中。链接器此时所做的就是在最终的可执行程序中记录共享对象的名称和少量的注册信息。当这个可执行文件被执行时,动态链接库的全部内容会在运行时映射到对应进程的虚拟地址空间。动态链接器会根据可执行程序中记录的信息找到对应的功能代码。

对于可执行文件中的函数调用,可以分别采用动态链接或静态链接的方法。当共享对象被多个进程使用时,使用动态链接可以使最终的可执行文件更短,并节省一些内存,因为只需要在内存中保存一份共享对象的代码。但不使用动态链接必然比使用静态链接好。在某些情况下,动态链接可能会带来一些性能损失。

我们在 Linux 中使用的 gcc 编译器将上述过程捆绑在一起,用户只需一条命令即可完成编译工作。这对于编译工作来说确实很方便,但是对于初学者理解编译过程是非常不利的。下图展示了gcc代理的编译过程:

从上图可以看出:

综上所述,编译过程就是以上四个过程:预编译、编译、汇编、链接。了解这四个过程中所做的工作有助于我们了解头文件、库等的工作过程,而对编译和链接过程的清晰了解也可以帮助我们在编程时定位错误,并尽量调动编程时的编译器。检测错误会有很大帮助。

-结尾-

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论