编译与链接

在这里插入图片描述

编译与链接

嗨喽,大家好呀,今天给大家带来的是编译与链接的过程吗,这是一个偏底层的知识,理解有些许难度,不过大家不用担心,相信阅读完本博客你一定能更好的理解这个过程

一.翻译环境和运行环境

二.编译与链接

  1. 预处理
  2. 编译
  3. 汇编
  4. 链接

三.利用宏与调用函数的优劣

1.#define定义宏
2.宏和调用函数过程的优劣

1.翻译环境和运行环境

下面我们先来认识一下翻译环境和运行环境

在这里插入图片描述

同学们是否对我们的编辑器是如何工作的产生过疑问,为什么最后程序运行会产生一个.exe的文件呢?这其实是由于我们编辑器进行了编译与链接这个过程从而使得我们最终运行的程序是.exe

在这里插入图片描述
我们的文件会经过中间的编译环境,最后生成我们的可执行程序,而最后的一个箭头就是我们的运行环境

翻译环境生成计算机能看懂的机器指令,就是二进制命令
在这里插入图片描述

当我们用记事本打开.exe文件,会出现一堆乱码,这就是我们最终生成的机器指令

2.翻译环境

在这里插入图片描述

我们的翻译过程总共分为四步,分别是预处理,编译,汇编,链接
下面我将一一例举出各个过程

1.预处理:在这一步,预处理器会处理源代码中的预处理指令,如#include、#define等。虽然这一步并不直接处理函数调用,但它为后续的编译步骤准备了一个更清晰的代码基础。

2.编译过程可分为

词法分析:这是编译过程的基础,它的任务是输入源程序,对构成源程序的字符串进行扫描和分解,识别出一个个的单词(亦称单词符号或简称符号),如基本字(如编程语言中的关键字)、标识符、常数、运算符和界符(如标点符号、左右括号)。词法分析的结果是为后续的语法分析提供了输入和基础。此外,词法分析还可以在编译过程中发现源代码中的拼写错误和其他语法错误。

在这里插入图片描述

语法分析:在词法分析识别出单词符号串的基础上,语法分析的任务是分析并判定程序的语法结构是否符合语法规则。这通常通过构建语法树或抽象语法树(AST)来完成。语法分析是编译过程的核心部分,确保程序的结构正确性。

语义分析:在这一步中,编译器会检查源代码的语义,确保它们符合语言的定义。例如,它会检查变量是否在使用前已经被声明,函数调用的参数类型是否与函数定义匹配等。

中间代码生成:中间代码是介于源代码和目标代码之间的一种形式化表示。它具有比源代码更接近目标代码的抽象程度,但仍然保留了源代码的某些结构和语义。中间代码生成的主要目标是将源代码转化为一种与底层硬件无关的表示形式,以便能够进行优化和后续的目标代码生成。

代码优化:在中间代码生成后,编译器会进行一系列的优化操作,以提高生成的目标代码的执行效率。这些优化可以包括指令调度、数据流分析、循环优化等。

目标代码生成:最后,编译器将优化后的中间代码转换为特定于目标机器的机器代码,也就是最终的可执行文件。

在编译阶段,编译器将源代码转换成目标代码。在这个过程中,编译器会为源代码中的每个标识符(如变量、函数名等)分配一个唯一的地址,并将这些标识符及其对应的地址信息打包在符号表中。符号表是编译器内部使用的一种数据结构,用于在后续的编译和链接过程中解析和引用这些标识符。

3.汇编过程
在这里插入图片描述
(1).汇编器(Assembler)读取源程序文件,将其中的汇编语言指令和标识符转换为对应的二进制表示形式,即机器指令。这是汇编过程的核心任务,它确保了源程序中的每一条指令都能被计算机硬件正确理解和执行,在此过程中汇编器会参考符号表,以确保在生成的机器指令中正确地引用和使用这些标识符。但汇编过程本身并不负责生成或修改符号表。
(2).汇编器还会对源程序中的语法和语义错误进行检查。如果发现错误,汇编器会生成相应的错误提示,帮助程序员定位和修复问题。
(3).经过汇编的源代码将生成目标文件,其中包含了机器指令的二进制表示以及其他必要的信息(如数据段、代码段等)。目标文件通常具有特定的扩展名,如.obj、.o或.exe,具体取决于操作系统和编译器。

3.链接过程

链接过程通俗来说,就是把多个“零件”组合成一个完整的“机器”的过程。这些“零件”在编程中通常就是我们编译后得到的目标文件(比如.o文件),它们包含了程序的代码和数据,但还不能直接运行。

当链接器将编译后的目标文件链接成可执行文件时,它会合并各个目标文件的符号表,并解析符号之间的引用关系。这样,在最终的可执行文件中,每个函数的地址都被确定下来,并且可以通过符号表进行查找和引用。

链接器就像是一个“组装工人”,它的工作就是把这些目标文件“组装”起来,形成一个完整的可执行文件。在组装的过程中,链接器要做两件事情:

符号解析:想象一下,如果我们有一个目标文件A,它里面引用了一个在目标文件B中定义的函数。链接器就需要找到这个函数在B中的位置,并告诉A:“你要调用的这个函数,在B的这个地方”。这样,当A想要调用这个函数时,就能找到正确的位置。

重定位:链接器还需要考虑这些“零件”在“机器”中的位置。也就是说,它要决定每个目标文件中的代码和数据应该放在最终的可执行文件的哪个位置。这样,当程序运行时,它就能知道去哪里找到这些代码和数据。

经过链接器这样一番“组装”之后,我们就得到了一个完整的、可以直接运行的可执行文件。这个过程就是链接过程,它确保了我们的程序中的各个部分都能正确地协作,从而实现预期的功能。
在这里插入图片描述

4.#define定义宏

#include<stdio.h>

#define SQUARE(x) (x)*(x)

int main()
{
	int a = 5;
	printf("%d\n", SQUARE(a + 1));

}
36

请大家注意一点的是,宏在预处理阶段被替换到源代码中,若括号内是一个表达式,那么必须要舍得加括号,因为如果不加括号,可能就会因为运算符优先级的问题导致出错

在这里插入图片描述

比如图中的情况,如果没有括号,宏替换就会变成a+1*a+1 = 2a+1。

下面我们来写一个宏,求两个整数的最大值

#include<stdio.h>  

#define MAX(x,y) ((x)>(y)?(x):(y))  

int main()
{
    int a = 0, b = 0;
    // 使用scanf_s读取整数,注意需要指定每个%c, %s和%[]格式说明符的大小  
    scanf_s("%d", &a);
    scanf_s("%d", &b);
    int tmp = MAX(a, b);
    printf("%d\n", tmp);
    return 0;
}
a,16
b,17
17

5.宏和调用函数的优劣

我们先再来回顾一下编译和链接
编译和链接是将源代码转换为可执行文件的过程,其中编译阶段会将源代码转换为汇编代码或机器码,而链接阶段则会将多个目标文件以及所需的库文件进行合并,生成最终的可执行文件。在这个过程中,函数调用、压栈、计算和函数返回等操作都会被转换为对应的机器指令。
当程序运行时,CPU会按照这些机器指令的顺序来执行相应的操作。例如,函数调用指令会告诉CPU跳转到函数体的起始地址并执行其中的代码;压栈和出栈指令则用于管理栈内存中的数据,确保函数参数、局部变量和返回地址的正确存储和访问;计算指令则用于执行函数体内的数学或逻辑运算;而函数返回指令则用于将控制权交还给调用函数,并恢复其执行环境。
因此,可以说这些过程都是紧密依赖于编译和链接生成的机器指令来执行的。这些指令确保了程序能够按照预期的逻辑和流程来运行,实现各种复杂的功能和操作。

(1)宏和函数调用的差别

预处理器确实在预处理阶段处理宏替换,而函数调用在这个阶段并没有任何操作。预处理器只是简单地查找源代码中的宏定义,并将其替换为宏体中的代码。如果宏定义很长或者宏在代码中多次使用,这确实会增加源代码的长度,从而可能间接增加最终生成的目标代码的长度
其次,关于函数调用,虽然函数调用本身不会在预处理阶段增加源代码的长度,但在编译过程中,编译器会为函数调用生成必要的指令。这些指令包括保存当前执行环境(如将局部变量和返回地址压入堆栈)、跳转到函数体执行、恢复执行环境(如从堆栈中弹出局部变量和返回地址)以及可能的参数传递和返回值处理。这些指令是编译器在编译过程中自动插入的,并且会增加最终生成的目标代码的长度。

下面我们引入两个概念
1.源代码:源代码长度:源代码是程序员编写的文本文件,它包含了程序的结构、逻辑和语法。
宏在预处理阶段被替换到源代码中,因此宏的使用会直接影响源代码的长度。如果宏定义较长或多次使用,源代码的长度会显著增加。
函数调用本身在源代码中只是文本上的引用,不会直接增加源代码的长度。
目标代码长度:目标代码(或机器代码)是编译器将源代码转换后的结果,它是CPU可以直接执行的指令序列。
函数调用在编译过程中会生成额外的指令,如保存和恢复上下文、参数传递等,这些指令会增加目标代码的长度。

所以我们可以得到这样一个结论,宏替换后的代码在编译时也会被转换为机器指令,但由于宏替换是在预处理阶段完成的,其影响主要体现在源代码层面,而非直接增加目标代码长度。然而,如果宏替换后的代码较长或复杂,它可能会间接导致目标代码中指令数量的增加。
运行阶段:最终的程序执行是基于目标代码的。CPU会按照目标代码中的指令序列来执行程序。
因此,虽然宏和函数调用在源代码层面有不同的影响,但最终的性能和行为是由目标代码决定的。
在讨论程序长度时,重要的是要明确我们是在谈论哪个阶段的长度:是源代码长度还是目标代码长度。因为这两个阶段有不同的特点和影响因素。
另外,需要注意的是,虽然宏和函数调用对代码长度有影响,但在实际编程中,我们更关注的是代码的可读性、可维护性和性能。因此,在选择使用宏还是函数时,除了考虑长度因素外,还需要综合考虑其他方面的权衡。

那么函数指令是如何形成?函数调用是如何调用的,分为那几步呢?
函数指令是在编译过程中就已经形成的,与函数的地址一起被确定下来,并在汇编阶段被转换为适应目标机器的汇编代码。
一般如果只有函数的声明那么可以通过汇编前的过程,汇编过程确实是将函数的指令转换成机器指令,而找到这些函数指令的前提是知道这些函数的地址或位置。
函数的地址可以通过多种方式获得,中一种常见的方式是通过编译后的符号表或映射文件。这些文件包含了程序中所有符号(包括函数)的地址信息。在编译和链接过程中,编译器和链接器会生成这些文件,以便在运行时能够定位到函数的具体地址。符号表是在汇编后获得的有了函数的地址就是有了函数的定义,就能生成一堆汇编指令
汇编器需要函数的定义或声明,以便能够将其指令转换为对应的机器码。
如果在汇编阶段找不到某个函数的定义,汇编器通常会报错。这是因为汇编器需要确切的函数定义来生成正确的机器指令。如果函数定义缺失,汇编器无法继续处理相关的指令,因为它无法将这些指令转换为机器码。
报错后,汇编过程将停止,不会继续进行到链接阶段。这是因为链接器的工作是基于汇编器生成的目标文件,如果目标文件中有错误或缺失的引用,链接器也无法正确地进行链接操作。
因此,在汇编前,必须确保所有的函数都有正确的定义,并且这些定义在汇编过程中是可访问的。这样,汇编器才能正确地将函数指令转换为机器指令,并生成可用于链接的目标文件。如果汇编阶段出现错误,通常需要在修正这些错误之后,才能继续进行后续的链接过程。

在这里插入图片描述

如上图所示,函数调用、压栈、计算和函数返回是程序执行中的关键步骤,下面是对这些过程的详细解释函数调用是程序执行中用于调用一个已定义的函数的过程。当调用一个函数时,程序会跳转到该函数的定义处并执行其中的代码。在调用函数之前,程序会保存当前的执行环境,以便在函数执行完毕后能够恢复并继续执行后续的代码。
1.压栈
压栈是函数调用过程中的一个重要步骤,它涉及到将相关的数据和信息保存到程序的栈内存中。栈是一种后进先出(LIFO)的数据结构,用于存储局部变量、函数调用的返回地址等信息。
在函数调用时,压栈操作主要包括以下几个部分:
保存返回地址:在调用指令执行前,当前函数的返回地址会被压入栈中,以便在函数执行完毕后能够返回到正确的位置继续执行。
参数压栈:函数的参数也会按照一定的顺序(通常是从右至左)压入栈中,以便在函数体内使用。
局部变量压栈:函数内部定义的局部变量也会在栈上分配空间,以确保它们在函数执行期间能够正确访问。
2.计算
计算通常指的是在函数体内执行的一系列数学或逻辑运算。这涉及到对函数参数、局部变量以及其他相关数据的处理。计算过程可以根据具体的函数实现和算法设计而有所不同,但它通常是在函数调用的上下文中进行的。
3.函数返回
函数返回是函数执行完毕后的一个关键步骤,它涉及到将函数的结果返回给调用者,并恢复调用函数的执行环境。
函数返回的过程大致如下:
4.返回值存储:如果函数有返回值,该值会被存储在事先约定好的位置(通常是寄存器或栈上的某个位置)。
恢复调用环境:函数返回前,会恢复调用函数的栈帧,包括返回地址和其他寄存器值。这确保了函数返回后能够正确地继续执行调用函数的后续代码。
跳转到返回地址:程序会跳转到保存的返回地址,将控制权交还给调用函数。此时,调用函数可以继续执行后续的代码,并可以使用函数返回的结果(如果有的话)。
在整个过程中,栈起到了关键的作用,它管理着函数的局部变量、参数和调用栈帧,确保函数调用的正确执行和返回。
通过这些步骤,函数调用、压栈、计算和函数返回共同构成了程序执行中的关键流程,实现了代码的模块化设计和复用。

经过上面一长串的分析,宏比函数在程序的规模和速度方面更胜一筹,并且宏的参数是类型无关的
但如果宏比较长,就会使得源代码长度变长,宏也没有办法进行调试,所以,对于一些复杂或关键的功能,最好使用函数或其他可调试的编程结构来实现,以提高代码的可维护性和可调试性。

好的,今天我们的博客就分享到这了,希望大家能有所得,拜拜!

  • 33
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值