编译器之自举

article/2025/9/15 21:52:30

**要阅读本文,不需要太高深的编译原理知识,甚至不需要编译相关的知识。但是本文也不是面向对电脑一无所知的读者的,你至少要知道:

  1. 不管是exe可执行文件还是Linux下的程序,都是一些二进制代码,我们称之为机器语言。这些代码的执行和系统以及CPU都有关。

  2. 大部分情况下,编译器是一种将高级语言翻译成机器语言的程序。而任何程序本身也是一些机器语言的代码。

  3. 无论是高级语言、汇编语言还是机器语言,实际上都是等价的,唯一有区别的是,越高级的语言写起来越容易(所以我们倾向于尽量多书写高级语言代码);同时,机器语言是可以直接运行的。如果你觉得上面三条可以理解或者可以理解一部分,那么请继续阅读。另外,本文中提到的无论是编译的过程还是语言的分类甚至是一些例子,都可以看做是一个简化的模型。实际上,很少有一个exe程序就可以运行的编译器,现代的编译器都是十分复杂的。不过,考虑这些简化模型对我们没有任何坏处,它们和实际情况相差并不很大。**

1233356-bad894b36a15dc6a.jpg

首先我来详细解释一下高级语言和机器语言。
语言的所谓“高级”,实际上界定不是那么明确。不过我们可以确定的是:C++、Java、Python之流比汇编语言更高级,而汇编语言比机器语言更高级。这里提到一个汇编语言和机器语言的区分。可能有些读者不明白这两个概念的区别,实际上很简单,汇编语言是我们会看到的那些MOV、JMP等命令组成的语言,而机器语言则纯粹是是各种01串。我想没有人会愿意写一个机器语言的程序——实际上,我们学校的计算机组成实验都是用Verilog HDL这种高级语言来完成的,而嵌入式原理实验也是用一种汇编语言完成的;即使是做处理器和单片机,也不会有人愿意去写机器语言,毕竟一大堆01串太坑爹了。试想,让你用C++写一个从1输出到100的程序,几行代码就可以搞定;而汇编语言则可能需要几十行;机器语言呢,Oh my god,一大堆01000101110110101011111001……,看都看不懂,还写啥啊。
但是上面也提到了,机器语言和高级语言的区别是,它可以直接运行。比如exe程序,实际上它内部存储的就是一些机器语言的代码,机器可以直接阅读这些代码并在处理器中运行它们(这里说的不是完全准确,比如.NET编译出的exe程序实际上是一段中间代码,由CLR解释成机器代码才能运行——不过这可以暂且忽略,就当作我说的是一个简化的模型)。当然程序的运行是依赖机器架构和系统的,不然Wine什么的也就没有用了(什么是Wine?WINE = WINE Is Not an Emulator!有趣的名称递归定义还有很多,不过与本文无关,请自行Google)。而实际上,是机器架构不同还是系统不同,并不是我们考虑的问题。我们考虑的问题只是,一段代码在A机器和X系统下能否运行,换到B机器和Y系统下又能否运行。就算在A机器和X系统下能运行,如果换成了A机器和Y系统之后不能运行了,那对于我们这也可以看做两台不同的机器(也就是说,A机器和Y系统实际上就可以看做一个新的机器B)。所以之后的描述中,我们不考虑操作系统的情况,而是只考虑机器,我们编号为a、b等,而它们上面可以运行的机器语言我们编号为A、B等。
以上几段的关键点是:无论是机器语言还是高级语言,实际上都是代码。在之后的讨论中,我们不应该将它们区别对待。虽然机器语言代码和高级语言略有不同,它可以直接在机器上运行,但是对于编译过程,它们没有任何区别。

上面的问题了解了之后,我再来简单说一下编译器。那编译器是什么?这要从两个方面来解释。
首先,编译器本身的功能是,将一种语言S的代码转化为一种语言T的代码。这里稍微了解编译器的读者可能有疑问了:我用的gcc之类的编译器,明明是把C语言代码编译成了一个exe程序,并不是把S语言的代码编译成了T语言的代码啊?如果你也有这个疑问,请重新阅读以上几段——我在前面已经提过了,“任何程序本身也是一些机器语言的代码”。也就是说,我们这里把机器语言也看作一种语言,只不过是很低级的语言;而编译器就可以将C语言这种高级语言S转化为机器语言这种低级语言T,恰好这种低级语言还是可以在机器t上直接运行的。其次,大部分情况下,编译器自己也是一段程序,那么它也可以看做是一段代码。而编译器也会有它的源代码,这个源代码就是一种高级语言的代码,我们这时仍然叫它编译器。随便举个例子,对于gcc.exe,这个程序是一种机器语言A的代码,它可以直接在a机器上运行。而它的功能则是,将C语言的代码转化为机器语言A的代码(一个C语言的编译器),使得本身无法直接运行的C语言程序变得可以在a机器上运行。而在得到gcc.exe之前,我们一定也有一段代码,它是用来生成gcc.exe这个程序的,这段代码可能是一个高级语言的代码,比如汇编语言。那这时候,这段汇编语言的代码即使不能直接在电脑上运行,我们依然说这是一个C语言的编译器。
有了这两点,我们就可以总结出一个编译器的特征:1. 本身是一段代码,假设是A语言的代码,A可以是机器语言,也可以不是;2. 可以接收一段代码,假设是S语言的代码,S一般来说是高级语言,但理论上也可以不是;3. 可以将接收到的S语言代码在内部转换后输出一段代码,假设是T语言的代码,T有可能是机器语言,但也有可能是一种高级语言。这样,一个编译器就可以被表示为A(S –> T),表示编译器本身的代码是A语言,可以接受一个S语言代码作为输入,同时产生相同功能的T语言代码作为输出。之后我们的编译器都会这么表示,请务必牢记。
以上几段的关键点是:编译器是什么?从本质上看是一段代码,而从功能上看可以将一段代码等价地转化为另一段代码。所以,我们用A(S –> T)表示一个编译器,这个编译器本身是由A语言写成的,它的功能是将S语言代码等价地转换为T语言代码。当A语言是机器语言的时候,这个编译器就是可以使用的。

1233356-21af01ad89fa2fcf.jpg

有了之前的知识基础,终于可以开始讲自展了。
不知道读了之前的内容,你有没有这样一个疑问:既然编译器本身是一段代码,那么如果想编译一个编译器,就需要更早的编译器来进行编译操作。而编译这个更早的编译器还需要更更早的编译器——长此以往,第一个编译器是怎么产生的呢?难道是直接用机器语言书写的?如果能这么想,那么你就猜对了。第一个编译器一定是用机器语言写出的。实际上,很久以前的程序员还在纸带上打孔编程呢,他们也照样乐在其中。机器语言只是很难书写,并不是不能书写。但是,毕竟用这么难书写的语言写一个C++编译器,谁都不会愿意去干。所以,第一个编译器的功能一定是很简单的。而人们会用这很简单的编译器的语言,去写一个稍微复杂一些的编译器;然后再用这个新的编译器,去写一个更复杂的编译器;最终得到一个很复杂的编译器,比如C++编译器——好了,如果能够理解这个过程,那么你实际上几乎就理解了自展。
所谓自展,实际上就是用一个功能不太完善的编译器支持的语言写代码,然后放到这个编译器上去编译,产生一个比自己更完善的编译器的过程。用一个不太恰当的例子来描述,就是我们已经有了一个C语言的编译器,然后我们用C语言写一个C++的编译器代码,并用C语言编译器编译这个代码生成可以运行的C++编译器(之所以不太恰当,是因为C语言不是C++语言的严格子集)。也就是说,我们有一个编译器A(C –> A),现在写一个编译器C(C++ –> A),将后者放入前者中进行编译,即A(C(C++ –> A) –> A),得到一个可以执行的编译器A(C++ –> A)。
以上几段的关键点是:自展过程,实际上就是用低级语言先实现一个简单的编译器,然后用这个编译器的语言再去编写一个更高级的编译器——这个新编译器是旧编译器的扩展——的过程。
1233356-c6c9f3bd17fc34db.jpg

自展很简单?那下面就是本文的重头戏——交叉编译了。
交叉编译这个概念,没有自展这个概念那么准确。所以先看一个问题:如果你想写一个手机系统上的程序,你会怎么做呢?我们可能会想到,开发一个手机上的编译器,然后把程序放到上面编译成手机可以运行的机器代码。但是这样就有一个很大的问题了,那就是手机的运行速度和电脑相比十分缓慢(虽然现在手机的CPU已经很强悍了,但是手机的内存一向很小,不太足以运行编译器)。实际上,在现实中我们的做法是,在电脑上完成编译过程,然后直接把在手机上可以运行的机器代码拷贝到手机内。这样就不受限于编译时手机的内存和CPU限制了。这样一个过程,就叫做交叉编译。当然交叉编译也有其它的应用,比如有时候我们也需要在一台电脑a上生成另外一台电脑b上同一个语言的编译器,比如在Windows下编译一个Linux的编译器。这个问题比前面的问题还要多一步编译操作,我们不妨在后面的讨论中将这两个问题称作问题1(前者)和问题2(后者)。
为了方便起见,我们把这两个问题用之前的表示方法来书写一下。首先,将电脑看做是机器a,手机看作是机器b。我们现在手里拥有的是某语言S在a机器上的编译器A(S –> A)。对于问题1,我们需要最终生成一个在a机器上编译b机器代码的编译器,即A(S –> B)。而对于问题2,我们则是需要一个在b机器上生成b机器代码的编译器,B(S –> B)。而我们可以做到的是,用高级语言S写一个自身的编译器(可以是a机器的也可以是b机器的,不过这个问题中只有后者会被用到),即S(S –> B)。好,现在我们就有了两个代码,A(S –> A)和S(S –> B),其中只有第一个代码是可以在a机器上运行的。我们要从这两个代码中得到两个新的代码,那就是A(S –> B)和B(S –> B)。
1233356-f552e2bdcda99c0b.jpg

首先先考虑如何得到一个A(S –> B),即一个在a机器上运行的可以将S语言代码编译为b机器代码的编译器。其实写成现在的形式,这一点如何做到已经很简单了。那就是将S(S –> B)放入A(S –> A)中编译,即A(S(S –> B) –> A),这样就可以得到A(S –> B)。这一步因为使用的编译器是A(S –> A),所以只需要在A机器上执行。这时,可以发现第一个问题已经解决了。我们这时就可以利用得到的A(S –> B)在a机器(电脑)上编译b机器(手机)的代码了。
1233356-6dbc073d988fe31f.jpg

然后在考虑如何得到一个B(S –> B)。可以看到,我们之前一步的时候有了一个A(S –> B),而最开始还拥有一个S(S –> B)。这样,我们只需要将S(S –> B)放入A(S –> B)中编译,即A(S(S –> B) –> B),这样就可以得到一个B(S –> B)了。这样,我们用前一步得到的编译器A(S –> B),在a机器(Windows)上运行,又得到了一个新的编译器B(S –> B),它可以在b机器(Linux)下编译b机器(Linux)的代码。
1233356-e7182d20fe3c1398.jpg

可以看到,我们从最开始的编译器A(S –> A)和代码S(S –> B)得到最终代码B(S –> B)的过程中,所有的步骤都是在A机器上运行的,完全没有用到B机器。这就是交叉编译的有趣之处!使用交叉编译,可以在很多情况下省去不少的麻烦。
另外解释一个问题,为什么不直接编写一个A(S –> B)?因为A是机器代码,而之前也提到过了,“我们倾向于尽量多书写高级语言代码”,因为机器语言十分难以理解和书写。所以,我们写了一个高级语言代码S(S –> B),将它放到已有的A(S –> A)上去编译(为什么是已有的?这个问题实际上不需要回答,因为这里假设a机器是一台很常用的机器(比如安装了Windows系统的PC机),所以A(S –> A)是一个很普通的编译器,它的存在性无需证明;A(S –> A)的书写可以利用之前提到的自展完成),最终得到了一个需要的B(S –> B)。
以上几段的关键点是:关键点太多了有木有!!!这个怎么总结呢……如果看不太懂交叉编译的过程的话,可以暂且把a机器当作一个安装了Windows的PC机,把b机器当作一台手机或者一个装了Linux的PC机,然后再带着这个理解重新阅读整个过程。实际上,交叉编译解决的两个问题分别都只需要一步操作,所以交叉编译没有听起来那么复杂!不要自己吓到自己就好了~


http://chatgpt.dhexx.cn/article/NtIGMcVJ.shtml

相关文章

DC-DC电路中自举电容和自举电阻是什么?

文章目录 1. 什么是自举电容2. 自举电容的原理3. 自举电容的额定电压如何选择4. 什么是自举电阻1. 什么是自举电容 DCDC BUCK芯片有一个管脚叫BOOT,有的叫BST,如下是一个DCDC芯片对BOOT管脚的解释,在外部电路设计时,BOOT和SW管脚之间,需要加一个电容,一般是0.1uF,连接到…

自举电路原理分析

原文来自公众号:工程师看海 自举电路字面意思是自己把自己抬起来的电路,是利用自举升压电容的升压电路,是电子电路中常见的电路之一。 我们经常在IC外围器件中看到自举电容,比如下图同步降压转换器(BUCK)电…

自举电路分析

1.什么是自举电路 自举电路也叫升压电路,是利用自举升压二极管,自举升压电容等电子元件,使电容放电电压和电源电压叠加,从而使电压升高,有的电路升高的电压能达到数倍电源电压。 有一个12V的电路,电路中有一…

自举电路学习(一)

1 什么是自举电路? 利用晶体管,电容,电阻等元器件,通过上电时序等控制,使自举电容实现低电位充电,高电位放电,得到所需的高压(针对同一参考平面)。 自举电路由一个自举二极管,一个自…

【STM32】自举模式 和 程序下载(ST-Link和串口示例)

STM32官方网站STM32中文社区 如果遇到不清楚的概念,可以看之前的文章。 一、自举模式二、程序下载2.1 概述2.2 实操2.21 SWD 方式下载(ST-Link)2.22 串口下载 一、自举模式 STM32有一个特殊的功能,就是可以通过不同的方式启动程序…

自举电容

自举电容的核心原理是:电容两端电压不能突变。 为什么需要自举电容? DCDC电路中有两个mos开关管,如下图所示。对于低端的管子Q2,由于其源极接地,所以想要Q2导通,只要在Q2的栅极加一定的电压即可&#xff…

自举电路原理

充电储能 在充电过程中,开关闭合(三极管导通),这时,输入电压流过电感。二极管防止电容对地放电。由于输入是直流电,所以电感上的电流以一定的比率线性增加,这个比率跟电感大小有关。随着电感电流增加,电感…

自举电路、自举电容

自举电路也叫升压电路,是利用自举升压二极管,自举升压电容等电子元件,使电容放电电压和电源电压叠加,从而使电压升高.有的电路升高的电压能达到数倍电源电压。 举个简单的例子:有一个12V的电路&#xff0c…

自举电路工作原理和自举电阻和电容的选取

自举电路的工作原理 如下图自举电路仅仅需要一个15~18V的电源来给逆变器的驱动级提供能量,所有半桥底部IGBT都与这个电源直接相连,半桥上部IGBT的驱动器通过电阻Rboot和二极管VF连接到电源Vb上,每个驱动器都有一个电容Cboot来缓冲电压&#…

自举电路

自举电路 Bootstrap circuit 我们知道Nmos管开启时需要大于阈值电压Vth,但是当MOS做高端驱动时,G极电压一定要高于Vcc电压(D极)Vth,所以需要一种电路将G极电路举高,这个电路就是自举电路。 如下: P与N m…

C语言中的循环语句(while、dowhile、for)

文章目录 goto语句循环语句的类型while语句死循环语句的嵌套 do..while语句for语句 goto语句 如果我们想要实现从0输出到n,如果要写0~100的话那我们就要写100个printf,这样非常麻烦,也 不现实,在汇编中当程序print完了我们可以用…

【汇编 C】循环语句goto、while、dowhile、for

目录 前言 一、什么是循环语句? 二、goto语句实现循环 goto底层汇编 三、while循环语句 while底层汇编 四、dowhile语句 do while底层汇编 五、for循环 for语句的几种写法 for底层汇编 总结 结语 封面 前言 本教程内容可能会涉及到汇编&#xff0…

【C语言】循环语句(do while循环)

简单不先于复杂,而是在复杂之后。 目录 do while循环 do 语句的用法 执行流程 do语句的特点 go to语句 do while循环 do 语句的用法 while循环和for循环都是入口条件循环,即在循环的每次迭代之前检查测试条件,所以有可能根本不执行循环…

while和do while的用法区别

前言 在上一篇文章中,壹哥给大家讲解了循环的概念,并重点给大家讲解了for循环的使用。但在Java中,除了for循环之外,还有while、do-while、foreach等循环形式。今天小千就再用一篇文章,给大家讲解while循环的使用。 本文…

循环语句—do…while循环

目录 3.do……while循环语句 3.1do……while循环语句格式 3.2三种循环的区别 3.3总结三种循环的区别 3.do……while循环语句 3.1do……while循环语句格式 基本格式: do { 循环体语句; } while(条件判断语句); …

While语句与do...while语句

循环结构之while语句: while(表达式) {语句} 注意:1.while循环的特点是先判断条件表达式,后执行循环体语句。 2.“语句”就是循环体。循环体可以是一个简单的语句,可以是复合语句(用花括号括起来的若干语…

C/C++之while(do-while)详细讲解

目录 while循环有两个重要组成部分: while 是一个预测试循环 无限循环 do-while 循环 while循环有两个重要组成部分: 进行 true 值或 false 值判断的表达式;只要表达式为 true 就重复执行的语句或块; 图 1 显示了 while 循环的…

do{...}while(0)的用法

首发于微信公众号:【码农在新加坡】,欢迎关注。 个人博客网站:do{...}while(0)的用法 零.导引 第一次见到 do{...}while(0)是在学习libevent的时候,看到里面有很多类似 #define TT_URI(want) do { \char *ret evhttp_uri_…

if,while选择结构和while,dowhile,for循环的使用

什么是if和while选择结构 if 选择结构是根据句条件判断之后再做处理的一种语法结构, 语法如下: while是一种先判断再执行循环体的结构; 当while循环条件为真时才执行循环体,然后再判断循环条件,如果条件为真&#xf…

while及do while语句

编写程序时,许多问题都要用到循环控制。循环结构也是结构化程序设计的基本结构之一,因此熟练地掌握循环结构是程序设计最基本的要求。While循环、do while循环、for循环语句,3种循环语句在一般情况下可以相互转换 While 及dowhile语句 Whil…