脚本源码编译教程-千字排序| 深入理解编译系统

关于作者:

程雷,一线程序员,在一家手机公司兼职系统开发工程师。 他喜欢每天研究内核的基本原理。

2.编译系统的逻辑结构 3.编译原理介绍 4.静态链接和动态链接 5.总结与回顾 1.编译系统的产生和发展

什么是编译器? 为什么要有编译器? 编译器的作用是什么? 编译系统由哪些组件组成,它们之间有什么关系? 有一句很好的谚语:最好从它的历史来了​​解它。 为了对整个编译系统有一个全面透彻的了解,我们首先要仔细研究它的发展历史。 我们先来看看编译系统的发展历史。

1.1 手动硬件编程

当我们刚开始时,我们正在进行手动硬件编程。 请注意,这是手动硬件编程,而不是面向硬件的编程。 手动硬件编程是编程人员直接在计算机中手工改变尾纤连接的方法。 更换完所有尾纤连接后,插上电源,按下执行按钮,电脑就可以执行程序了。 其实这些编程方法都是非常原始、非常低级、非常麻烦的。 如果我们有两个程序A和B,并且我们想在执行A之后执行B,我们必须先中断A,然后在执行B之前手动将B编程到硬件中。上次需要执行A时,必须重新编程A到硬件。 这就好麻烦了,于是专家冯·诺依曼提出了一种叫做存储编程的思想,即我们把程序存储在内存中,每次计算机运行时,它都可以从内存中读取指令并执行。 。 存储编程是一个非常伟大的想法,也是计算机历史上的一大进步。 有一次,当我看到一本赞扬存储程序设计的书时,我感到非常困惑。 存储的程序,你说程序原来不是存储在硬盘上的吗? 这不是理所当然的事情吗? 哪些都是值得称赞的。 当我们习惯了某件事,司空见惯的时候,我们会认为它没什么,但其实这个东西的提出或者发明是非常伟大的。 就好像我们站着走着都觉得这没什么。 这不是应该的样子吗? 它就是这样儿的。 而事实上,直立行走是人类从植物到人类进化过程中非常重要的一步。 如果没有直立行走,人类今天可能仍然是生活在原始森林中的植物。 好吧,我们继续吧。 通过存储编程,我们已经从手动硬件编程发展到面向硬件的编程。

1.2 面向硬件的编程

第一个编译器从哪里来?

这里我们提前回答一个问题。 编译器也是一个程序,需要被编译。 那么第一个编译器是如何产生的以及由谁编译的。 第一个C语言编译器是用B语言编写的,并用B语言编译器编译。 有了这个盘古C语言编译器,我们就可以用C语言重写一个C语言编译器,这就是女娲C语言编译器。 使用二补C语言盘古编译器对女娲C语言编译器源代码进行编译,即可得到二补代码女娲C语言编译器。 然后女娲C语言编译器将二进制补码编译成源代码。 使用Nuwa C语言编译器,您可以实现类似于Nuwa汇编器的循环。 同样的,你可能会问第一个B语言编译器是怎么来的。 第一个B语言编译器是用BCPL语言编写的,并用BCPL语言编译器编译。 还在问脚本源码编译教程,第一个编译器是怎么实现的? 第一个编译器是用汇编语言编写的,并由汇编器进行汇编。 更进一步,第一个汇编器是手写的二进制补码。 因此,编译器的来源就是我们人类直接用机器语言编写的盘古汇编程序。 它不需要编译或汇编,可以直接在机器上运行。

源码脚本是什么意思_有源码怎么写脚本_脚本源码编译教程

1.3 中间语言编程

面向硬件的编程版本3.0——汇编编程,尽管已经很先进了,但仍然是面向硬件的编程。 程序员仍然需要了解很多硬件细节,无法专注于程序本身的逻辑。 事实上,大多数程序的功能实现与硬件无关,用汇编语言编写的程序也不具有跨平台性。 程序员必须为每个硬件平台编写程序或移植它。 很麻烦。 我们能否发明一种语言,屏蔽各种硬件细节,只专注于实现程序本身的逻辑,并在源代码层面实现跨平台功能? 于是中间语言就诞生了。 第一个中间语言可能是Algol,然后从Algol发展到BCPL、B、C、C++、JAVA、C#等。中间语言中没有各种硬件指令,没有寄存器,也没有复杂的轮询模式。 而是有各种数据类型的数据定义、对数据的直观操作以及if、for、while、switch等控制语句。 还有一些比较中间的句子结构如类、模板等,以接近人类自然语言的形式表达,可读性很强。 如果你想学习一些中间语言,比如C和C++,可以参考《深入理解C和C++》。

编译器的定义:好了,到这里,你应该已经清楚什么是编译器了。 让我们总结一下什么是编译器。 编译器是人类和计算机之间矛盾的产物。 这个矛盾就是计算机可以理解和执行二进制补码格式的程序,但不能理解和执行文本格式的程序。 人类则恰恰相反。 人类可以理解和编写文本格式的程序,但无法理解和编写二进制补码格式的程序。 于是编译器的出现就帮助人类解决了这个矛盾。 当人类以文本格式编写程序时,编译器将其翻译为二进制补码格式的程序,然后计算机执行它们。

1.4 编译系统的组成

之前我们已经解释清楚了什么是编译器,接下来我们继续按照这个思路来解释什么是链接、加载和建立。 一开始,程序很简单,只是一个C文件。 我们可以直接将C文件编译成可执行程序。 而且后来,随着程序越来越大,把整个程序放到一个C文件中就不太合适了。 C文件写几千行是合理的,写几万、几十万、甚至几百万行也是合理的。 这是不合理的。 所以我们将程序分成多个文件,分别编译每个文件,生成中间目标文件,然后将中间目标文件join合并为可执行程序。 这种连接和合并的过程称为链接,并且执行链接的程序。 称为链接器。 后来,随着程序变得更加复杂和庞大,这些链接形式也出现了问题。 由于公共程序有很多脚本源码编译教程,你将它们链接到每个程序中,这样每个程序都包含自己的库程序副本,这会造成很大的浪费。 c盘空间,而且运行时也是化学内存的浪费。 如果更新一个库程序,则必须重新链接每个可执行程序,这也很麻烦。 为了解决这些问题,动态链接诞生了,所以以前的链接形式被称为静态链接。 我们把一些公共库程序变成了动态链接库。 每个可执行程序链接动态库时,只是在程序内部做了一条链接记录,并没有将动态库复制到可执行程序本身,这样整个C盘就只有一份这个动态库。 在运行时,这个动态库只需要加载到进化显存一次,然后映射到不同进程的虚拟显存空间。 这个动作称为加载。 到目前为止我们了解了编译、链接、静态链接、动态链接和加载的概念。 编译器进行编译,链接器进行链接,加载器进行加载。

下面我们来谈谈真实情况。 假设我们有一个软件,由十几个文件夹和数百个C文件组成。 每次要编译的时候我们应该怎么做,将每个C文件一个一个的编译,然后将所有的中间目标文件进行静态链接和动态链接,生成最终的可执行程序。 每次都要手动输入这么多命令,多麻烦啊。 我们应该做什么? 最简单、最直接的方法之一就是将所有这些命令放入一个脚本中,每次只需执行该脚本即可。 这是个好主意,也解决了问题,但是还有一个问题,就是写这个脚本很麻烦,改这个脚本也很麻烦。 所以有一些方法可以手动生成这个脚本。 你写一些简单的配置和规则,然后用一个程序来处理规则,然后你可以手动生成脚本,然后执行脚本来编译整个程序。 后来你发现你的解析程序解析完你的规则文件后,可以直接在内部生成这个脚本并执行。 不需要显式地写下脚本然后执行它。 这套东西叫做创建系统。 建立系统由解析程序和规则文件两部分组成。 最知名的安装系统是make,它由make程序和Makefile两部分组成。 你根据Makefile的要求编写Makefile,然后执行make命令来编译整个程序。 后来随着程序越来越大、越来越复杂,Makefile也越来越大,越来越难写,于是就形成了元创建系统来帮助你生成Makefile文件。 元建系统也由解析程序和规则文件两部分组成。 你根据其要求编写规则文件,然后执行解析程序生成Makefile,然后执行make命令编译整个程序。 众所周知的make的元建立系统有CMake、autoconf/automake等。由于make在运行时需要包含各个文件夹中的Makefile,所以这对于小型程序来说是可以接受的,而当程序非常大且存在时,源代码下有很多文件夹,这个时候就变得难以忍受了。 ,所以人们开发了ninja构建系统,运行时只解析一个构建文件,运行效率比较高。 然而,手动编写ninja构建文件非常麻烦,对于小程序来说几乎不现实。 所以,建立忍者系统几乎是有必要的。 当只改变源代码内容时,ninja命令可以直接运行。 然而,当程序结构发生变化时,必须首先运行meta-setup系统来重新生成ninja构建文件。

现在我们看到整个编译系统由编译、链接、汇编三部分组成。 执行它们的是编译器、链接器和汇编系统。 当我们在命令行编译一个程序时,我们只需要调用一个setup命令,setup系统就会为我们编译和链接整个程序,同时还会做一些其他的辅助工作,比如打包、签名等。朋友可能会说我从来没有使用过任何编译器、链接器或系统构建系统。 我每次编程都是直接写程序,然后按一个按钮就可以了。 这个东西叫集成开发环境。 它将文本转换成编辑器、编译器、链接器、构建系统和调试器都集成在一起,这可以方便你的开发工作,但也让很多人不明白其背后的工作原理。 所以命令行看起来不太好用,不太好用意味着学习使用它比较麻烦。 然而,一旦你学会使用它,你就会发现它非常强大,但同时你也了解了它背后的原理。 深刻的。

很多时候我们在工作的时候经常会谈到编译这个词,在不同的场景下它的含义也是不同的。 我们来做一些名词分析,谈谈编译的四种不同含义:

编译原理 上面所说的编译是指最狭义的编译。 通常没有汇编过程,直接生成目标文件。 如果有组装工序,则不包括组装工序。 许多编译器采用汇编过程的形式。 的。

为什么这里需要进行名词分析呢? 因为在工作和一些讨论中,会因为“编译”这个词的含义不同而产生一些不清楚的争议。 比如我们编译整个程序的时候,只会讲编译程序,而不会讲程序的设置,也不会讲程序的编译、链接、打包。 这样就会变得很尴尬,所以很多时候会因为编译的含义不同而产生不必要的争议。 比如有一次服务器编译出现错误。 我说编译有错误。 有朋友说编译没有问题,但是打包有错误。 还有一次,一个朋友编译出错了,让我帮他看看到底是怎么回事。 我看了一下,说编译过程没有问题,但是链接阶段出错,找不到符号。 他一脸心疼的说道,这个编译是不是出问题了? 也有一些朋友不明白编译原理上的编译和工作中编译的区别,会问一些特别有趣的问题。 例如,有人问我为什么预处理时不能及早发现句型错误。 我说的预处理还没有到编译阶段。 他说预处理不是编译。 如果我们理解了四种不同编译广度的含义,我们就可以更好地传达我们想要表达的内容。

2、编译系统的逻辑结构

在下一章中,我们已经对编译的概念和编译系统的组成部分有了基本的了解。 本章我们将详细讲一下它们之间的结构关系以及各个组件的功能作用。

2.1 狭义编译

我们先看一下狭义编译:

狭义编译是一个源文件和n个头文件的编译。 它包括三个过程:预处理、最窄编译和汇编。 预处理是对源文件中的预处理指令进行处理的过程。 预处理指令包括头文件包含指令(#include)、宏定义指令(#define)、取消宏定义指令(#undef)、条件编译指令(#if#ifdef#ifndef#else#elif#endif)等编译指令。 预处理过程会将所有包含的头文件插入到源文件中,处理其他预处理指令,最终生成编译unit.i文件。 那么编译单元就是最狭义的编译,也就是上面编译原理中提到的编译。 上面提到了编译的原理。 最狭义的编译可以最终生成汇编文件或者直接生成目标文件。 没有汇编过程,书上通常讲的是直接生成目标文件,但实际上,编译器通常会生成汇编文件。 最后,它会经历一个编译过程。 汇编是将汇编文件生成为目标文件的过程。 目标文件包含直接可执行的机器指令,并且整个文件不是可执行格式。

2.2 最狭义的编译

最狭义的编译是编译原理上的编译。 编译原理是一门非常复杂的课程。 它是计算机科学中技术最密集的领域之一。 这是一门在理论和实践上都非常难以理解的科学。 这里我们不打算详细讲解编译原理的知识,只是粗略的介绍一下它的框架。 我们先看一张图:

我们说的是直接生成目标文件,因为整个编译器结构是比较完整的。 如果生成的是汇编文件,则机器码生成部分不存在。 编译器通常分为两个部分:后端和前端。 后端负责解析语言本身,前端负责生成机器代码。 为什么分为后端和前端两部分? 因为后端和前端没有必然的联系,所以分离后可以有更大的灵活性。 灵活性主要有两个方面。 对于同一种语言,我们只需要实现一次句型解析。 当语言需要移植到另一个CPU架构时,我们只需要实现前端即可。 对于不同的语言,我们不需要为同一个CPU架构实现多个前端。 所有语言都可以共享相同的前端。 当编译器需要支持新语言时,只需要实现后端即可。 后端包括词法分析、语法分析(复杂句分析)、语义分析。 前端包括中间代码生成、中间代码优化、机器代码生成、机器代码优化。 下一章将详细介绍它们。

2.3 链接流程

经过窄编译后,我们得到了目标文件,而目标文件并不是最终的可执行程序。 我们需要链接目标文件来生成可执行程序。 程序执行时会产生一个进程。 该进程由一个exe主程序和n个so库程序组成。 主程序和库程序是通过目标文件和静态库文件的静态链接生成的。 静态链接分为隐式静态链接和显式静态链接。 隐式静态链接就是目标文件和目标文件直接合并在一起。 显式静态链接是将目标文件与静态库进行链接,本质上与静态库相同。 目标文件已链接。 隐式静态链接和显式静态链接之间没有太大区别。 本质上没有区别。 只是编译命令不同。 exe动态链接到so,so也动态链接到so。 动态链接分为半动态链接和全动态链接。 两者的本质是一样的,都是动态链接的,也就是说,没有复制文件的过程,而且两者的实现方式有很大不同。 半动态链接就是我们通常所说的动态链接。 编程时需要包含so的头文件,编译时需要指定so所在的路径。 这个so的相关信息会记录在链接后产生的文件中。 当进程启动时,加载器会加载这个so,并在程序运行时调用这个so的函数时动态解析符号。 全动态链接是指通过函数dlopen加载代码中对应的so,通过函数dlsym找到要调用的函数的符号,然后调用该函数。 使用完后可以通过dlclose卸载so。 完全动态链接不需要包含相应的头文件,编译时也不需要指定so的路径。 如果运行时找不到,dlopen将返回NULL,程序不会崩溃。 使用后可以卸载。 相反,对于半动态链接来说,如果编译时没有找到so,就会编译失败,如果运行时没有找到so,程序就会启动失败。 也不可能在程序运行时卸载so并将其移出进程的显存空间。

我们看一下编译时链接过程:

编译时链接除了静态链接之外,还将执行半动态链接的第一步。 我们看一下半动态链接和全动态链接的启动加载:

进程启动时,首先fork创建进程的shell,然后exec加载进程的主程序hello.exe和loader ld.so。 之后进程返回用户空间,执行权交给ld。 ld会根据主程序文件中的动态段信息加载所有so文件,完成重定位工作,最终将执行权转移给主程序。 主程序首先执行的不是main函数,而是_start函数。 _start 函数将调用 __libc_start_main 函数。 该函数将完成进程环境的初始化并最终调用main函数。 进入main函数就是程序执行的主体。 如果程序执行dlopen函数,也会加载相应的so文件。

2.4 建立体系

构建系统的法语单词是buildsystem,又译为构建系统。 编制制度是什么? 如果每次编译程序都要执行很多gccld命令,那就太麻烦了。 通过建立系统,每次只需要执行一个简单的命令就可以编译整个程序。 下面我们以最常用的setup系统make为例简单说一下。 make命令会在同一目录下查找Makefile文件,然后解析并执行它。 Makefile的内容包含5部分,1.变量定义,2.显示规则,3.隐式规则,4.文件说明,5条注释。 变量定义相当于C语言中的宏,使用时会在相应位置进行替换。 显示规则描述了如何生成文件。 显示规则包含三部分:目标、依赖和命令。 由于许多规则模式非常常用,因此 make 有一些外部规则。 我们不需要编写所有规则,make 会手动推断它们。 文件指令类似于C语言中的预处理命令,例如#include,可以包含上层makefile。 注释是以 # 开头的行。

显示规则的格式如下。 隐式规则没有命令部分。

3.编译原理介绍

后面我们讲最狭义的编译,即编译原理中的编译。 这里简单介绍一下编译原理,不做详细讨论。 由于编译原理太深入了,想要深入学习的朋友可以看参考资料中推荐的书籍。 我们先看一下编译的整体流程:

3.1 词法分析

什么是词法分析,为什么需要进行词法分析? 词法分析就是将每个字符变成一个短语(Token)。 短语,英文是Token,中文翻译成令牌或者符号。 它是源代码的最小逻辑单元。 词法分析的作用就相当于我们小时候学过的:如何分句,把一个句子一个一个的分割成单词。 单词包括名词、动词、不定式、形容词、词尾、动词、连词等,构成句子的主语、谓语、宾语、状语补语。 同样,在词法分析中,从源代码字符流中一一生成短语,并对短语的属性进行分类,以方便进一步分析:复杂句分析(句型分析),将短语组合成短语。 词法分析的基本原理也比较简单。 它们主要是基于空格和换行符来定义的,但情况也不完全如此。 有一些特殊情况。 比如a=b+c,即使没有空格,整体上也不能被视为一个短语。 相反,它应该被视为五个短语:a、=、b、+、c。 另一个例子是 s="helloworld"。 虽然“helloworld”中间有一个空格,但它不能被视为两个短语,而是一个短语。

词法分析器分析的短语有哪些类别? 总共有5类,分别是关键字、标识符、常量、运算符和分隔符。 虽然关键字也可以被视为标识符,但它们只是特殊的标识符。 它们是计算机语言标准规定的具有特殊含义的标识符。 主要有两种类型。 一种类型表示数据类型,如int、long、float等,另一种类型表示过程,如if、while、for。 标识符是普通标识符,是程序员给程序中的实体赋予的名称。 它们主要分为两类,即变量名和函数名。 常量是程序中的文字常量,包括整型常量、浮点常量、字符常量、字符串常量、布尔常量等。运算符是用来执行某些运算的符号,例如+-x/%等用于算术运算的运算符和 >>=< 用于比较操作。