热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

编译和链接——读书笔记

文章目录编译和链接预编译编译汇编链接编译器做了什么词法分析语法分析语义分析中间代码生成目标代码生成与优化静态链接编译和链接对于下面的程序#includein

文章目录

  • 编译和链接
    • 预编译
    • 编译
    • 汇编
    • 链接
  • 编译器做了什么
    • 词法分析
    • 语法分析
    • 语义分析
    • 中间代码生成
    • 目标代码生成与优化
  • 静态链接


编译和链接

对于下面的程序

#include
int main()
{printf("Hello World\n");return 0;
}

当使用GCC来编译上面的程序时

gcc hello.c
./a.out
Hello World

上述过程可以分为四个步骤,分别是预处理,编译,汇编,链接。

在这里插入图片描述

预编译

首先是源代码文件hello.c和相关的头文件,如stdio.h等被编译器cpp预编译成一个.i文件。对于c++程序来说,它的源代码文件的扩展名可能是.cpp.cxx,头文件的扩展名可能是.hpp,而预编译后的文件扩展名是.ii

第一步预编译的过程相当于如下命令(-E表示只进行预编译)。

$gcc -E hello.c -o hello.i

预编译过程主要处理那些源代码文件中的以"#"开始的预编译指令。比如#include,#define

主要处理规则如下:

  • 将所有的#define删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 删除所有注释///**/
  • 添加行号和文件名表示,比如#2 "hello.c" 2,以便编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令,因为编译器要使用它们。

经过预编译后的.i文件不包含任何宏定义,因为所有的宏都已经被展开,而且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看与编译后的文件来确定问题。

编译

编译过程就是把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后产生相应的汇编代码文件

编译过程相当于下面的命令

$gcc -S hello.i -o hello.s
# 或者使用下面的指令
$gcc -S hello.c -o hello.s

现在版本的GCC把预编译和编译合并为一个步骤。使用一个叫做ccl的程序来完成这两个步骤。

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对简单,没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。可以使用汇编器as来完成。

$as hello.s -o hello.o
#或者
$gcc -c hello.c -o hello.o

生成目标文件。

链接


编译器做了什么

编译器就是将高级语言翻译成机器语言的一个工具。比如使用C/C++语言写的一个程序可以使用编译器将其翻译成机器可以执行的指令及数据。

编译过程一般可以分为6步:扫描,词法分析,语法分析,语义分析,源代码优化,代码生成和目标代码优化

整个过程如下所示

在这里插入图片描述

以下面的代码作为例子

//CompilerExpression.c
array[index] = (index + 4) * (2 + 6);

词法分析

首先源代码程序被输入到扫描器,扫描器只是简单地进行词法分析,运用一种有限状态机的算法可以很轻松地将源代码的字符序列分割成一系列的记号(token)

在这里插入图片描述
在这里插入图片描述
词法分析产生的记号一般可以分为如下几类:关键字,标识符,字面量(包含数字,字符串等)和特殊符号(加号,等号)。在识别记号的同时,扫描器也完成了其他工作,比如将标识符存放到符号表,将数字,字符串常量存放到文字表等,以备后面的步骤使用。

有个叫做lex的程序可以实现词法扫描,它会按照用户之前扫描好的词法规则将输入的字符串分割成一个个记号。

语法分析

语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用了上下文无关语法的分析手段。简单地说,由语法分析器生成的语法树就是以表达式为节点的树。C语言的一个语句是一个表达式,而复杂的语句是很多表达式的组合。上面的例子就是一个由赋值表达式,加法表达式,乘法表达式,数组表达式,括号表达式组成的复杂语句。

经过语法分析器以后形成的如下图所示的语法树。

在这里插入图片描述

从上图可以看出,整个语句被看成一个赋值表达式;赋值表达式的左边是一个数组表达式,右边是一个乘法表达式;数组表达式由两个符号表达式组成。符号和数字是最小的表达式,它们不是由其他的表达式来组成的,所以它们通常作为整个语法树的叶节点。在语法分析的同时,很多运算符号的优先级和含义也被确定下来。比如乘法表达式的优先级比加法高,而圆括号表达式的优先级比乘法高。另外有些符号具有多重含义,比如星号在C语言中可以表达乘法表达式,也可以表示对指针取内容的表达式,所以语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,比如各种括号不匹配,表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。

语义分析

语义分析器完成语义分析。语法分析仅仅完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如C语言中两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;比如同样一个指针和一个浮点数做乘法运算是否合法等。编译器所能分析的语义是静态语义,所谓静态语义是指在编译期间可以确定的语义,与之对应的动态语义就是只有在运行起才能确定的语义

静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程,语义分析过程中需要完成这个步骤。比如将一个浮点型赋值给一个指针的时候,语义分析程序会发现这个类型不匹配,编译器将会报错。动态语义一般指在运行期间出现的语义相关的问题,

经过语义分析阶段以后,整个语法树的表达式都被标识了类型。如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。上面的语法树在经过语义分析阶段后成为下图所示的形式。

在这里插入图片描述

可以看到,每个表达式(包括符号和数字)都被标识了类型。

中间代码生成

现代编译器有很多层次的优化,往往在源代码级别会有一个优化过程。源码级优化器在不同编译器中可能会有不同的定义或有一些其他的差异。源代码级优化器会在源代码级别进行优化。上面的例子中,(2+6)这个表达式可以被优化掉,因为它的值在编译期间就可以被确定。

经过优化的语法树如下图所示。

在这里插入图片描述
可以看到(2+6)这个表达式被优化成8。直接在语法树上做优化很困难,所以源代码优化器往往将整个语法树转换成中间代码,它是语法树的顺序表示,其实它已经非常接近目标代码了,但是它一般跟目标机器和运行时环境是无关的,比如它不包含数据的尺寸,变量地址和寄存器的名字等。

中间代码使得编译器可以被分成前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码

目标代码生成与优化

源代码级优化器产生中间代码标志着下面的过程属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长,寄存器,整数数据类型和浮点数数据类型等。对于上面的中间代码,代码生成器可能会生成下面的代码序列。

movl index, %ecx;
addl $4, %ecx;
mull $8, %ecx;
movl index, %eax;
movl %ecx,array(,eax,4);

最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式,使用位移来替代乘法运算,删除多余的指令等。上面的例子中,乘法由一条相对复杂的基址比例变址寻址的lea指令完成,虽有由一条mov指令完成最后的赋值操作,这条mov指令的寻址方式与lea是一样的。

movl index, %edx;
leal 32(,%edx,8), %eax;
movl %eax,array(,%edx,4);

经过这些扫描,语法分析,语义分析,源代码优化和目标代码优化,源代码被编译成了目标代码,但是这个目标代码有一些问题,就是indexarray的地址没有确定。如果要把目标代码使用汇编器编译成真正能够在机器上执行的指令,那么indexarray的地址应该从哪得到呢?如果indexarray定义在跟上面的源代码同一个编译单元里面,那么编译器可以为indexarray分配空间,确定它们的地址;如果定义在其他的程序模块呢?

事实上,定义在其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

静态链接

一个复杂的系统可以分成若干个小的系统,把每个源代码模块独立地编译,然后按照顺序将它们组装起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。从原理上来说,链接的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配,符号决议,重定位等这些步骤

符号决议有时候也被叫做符号绑定,名称绑定,名称决议,甚至还有叫做地址绑定,指令绑定,大体上它们的意思都一样,决议倾向于静态链接,而绑定倾向于动态链接。

最基本的静态链接如下图所示(Object File,一般扩展名为.o.obj),目标文件和库一起链接形成最终可执行文件。最常见的库就是运行时库,它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。

在这里插入图片描述
现代的编译和链接过程并没有那么难。比如我们在程序模块main.c中使用另外一个模块func.c中的函数foo()。在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接起曲江这些指令的目标地址修正。如果没有链接器,我们必须手工把每个调用foo的指令进行修正,则填入正确的foo函数地址。当func.c模块被重新编译,foo函数的地址有可能改变时,那么我们在main.c中所有使用foo的地址的指令将要全部重新调整。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时候,会根据所引用的符号foo,自动去相应的func.c模块查找foo的地址,然后将main.c模块中所有应用到foo的指令重新修正,让它们的目标地址为真正的foo函数的地址。这就是静态链接的最基本的过程和作用。

在链接过程中,对其他定义在目标文件中的函数调用的指令必须要重新调整,对使用其他定义在其他目标文件的标量来说,也存在同样的问题。可以结合具体的CPU指令来了解这个过程。

假设有一个全局变量叫做var,它在目标文件A里面,当在目标文件B里面要访问这个全局变量,比如在目标文件B里面有这么一条指令:

movl $0x2a,var;

这条指令就是给这个var变量赋值0x2a,相当于C语言中的var=42,然后编译目标文件B,得到这条指令机器码,如下图所示:
在这里插入图片描述
由于在编译目标文件B时,编译器并不知道变量var的目标地址,所以编译器在无法确定地址的情况下,将这条mov指令的目标地址置为0,等待链接器在将目标文件A和B链接起来的时候再将其修正。我们假设A和B链接后,变量var的地址确定下来为0x1000,那么链接器将会把这个指令的目标地址部分修改成0x1000。这个地址修正的过程也被叫做重定位,每个要被修正的地方叫一个重定位入口。重定位所做的就是给程序中每个这样的绝对地址引用的位置“打补丁”,使它们指向正确的地址。


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文为Codeforces 1294A题目的解析,主要讨论了Collecting Coins整除+不整除问题。文章详细介绍了题目的背景和要求,并给出了解题思路和代码实现。同时提供了在线测评地址和相关参考链接。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
author-avatar
盲人一块
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有