本文是我在阅读《计算机系统要素——从零开始构建现代计算机》这本书过程中的相关内容整理,英文名是The Elements of Computing Systems, Building a Modern Computer from First Principles,作者是Noam Nisan和Shimon Schocken。
一般来说,这种书看完过阵儿基本就忘得差不多了,所以把一些要点整理出来,以后看到时至少能快速想起一些关键的内容,同时这篇文章也可以给对这本书感兴趣的人参考。
其实对于大多数人来说,可能并没有必要花很多精力深入阅读这样一本书,特别是要熟悉一种另类的计算机体系或者编程语言。本文的另一个目的就是让别人能够快速掌握这本书中的精髓,在思想上获得一些启示。并且对于想要认真学习这本书的人,提供一下相关资源的介绍以及某些问题的解决方式,以免踩一些没有必要的坑。
这本书对应一套相关的课程,有其官方网站,网址是 https://www.nand2tetris.org/
其中单词nand2tetris的含义是从与非门到俄罗斯方块,很好的概括了本书的思路线索。也就是我们如何从零开始构建一个计算机,这个计算机还能运行我们熟悉和理解的程序,并在这个过程中感受计算机原理的神奇,以及在逻辑运算中蕴含的惊人可能性。难以想象,在现代社会各个领域都广泛使用并大显身手的计算机,竟是从如此简单的基础开始的。
关于该课程的官方网站,提供了一些有用的资源,你可以借助其来构建自己的课程规划。在侧边栏Projects页面中,可以找到章节对应的PPT资源等,用来作为课程辅助。在Software页面中可以找到书中所需的程序文件和仿真器等,不过这个原版的程序只有题目而没有答案,带答案的可以到Github去找,后面会介绍。
然后是Cool Stuff页面中,展示了本书相关的有趣项目。比如一些用Jack语言编写的游戏的演示,或者Hack计算机的硬件实现等。
值得一提的是第一个The Nand Game,对应网址 https://nandgame.com/,可以以网页互动的方式来拼装各种逻辑部件,直到拼成完整的计算机并进行编程。不过这个网站的问题是与本书中的计算机结构不太一样,而且软件部分做的比较抽象,所以玩一玩硬件部分的拼装就可以了。
关于本文,大概打算分成三篇,包括硬件,编译,操作系统三个部分,本篇主题是硬件。不过开始会把整个框架梳理一下,因为我也不知道会不会写完第一篇就坑了。下面先看一下本书目录吧。本书有13章,最后一章没什么内容,所以当成12章就可以了。这12章可以分成硬件部分和软件部分,各章节标题如下。
硬件部分
第1章 布尔逻辑
第2章 布尔运算
第3章 时序逻辑
第4章 机器语言
第5章 计算机体系结构
软件部分
第6章 汇编编译器
第7章 虚拟机I:堆栈运算
第8章 虚拟机II:程序控制
第9章 高级语言
第10章 编译器I:语法分析
第11章 编译器II:代码生成
第12章 操作系统
硬件部分讲的就是怎么用基本的逻辑元件来构建一个完整的计算机,并且能通过模拟器执行机器或汇编语言。首先我们介绍一下本书相关的程序和仿真器资源。
本书的课程配套资源可以在官网上下载到,或许需要梯子,也可以去Github搜索或者我直接分享出来,原版特点是带了一些Mac系统的隐藏文件,需要手动搜索删除,或者不去在意。
主要的文件夹分为projects和tools,前者是每个章节对应的程序,不过只有初始代码,而没有答案。因为这本书的目的是让每个人自己思考来完成其中的项目,虽然从实践角度看很好,但其中很多项目其实挺有难度,至少不适合零基础的读者。所以为了本书的阅读体验,还是推荐去Github找下带答案的版本,从而可以研究和参考。
在tools文件夹中提供了本书需要的仿真器,或者说用来运行硬件和代码的工具。也可以在课程的官网Software页面找到英文介绍,下面我先简单介绍下这些工具。
首先bin文件夹是各种工具的程序资源,下面可以看到各个工具的可执行文件,.bat对应Windows系统,.sh对应Linux系统。这些工具是使用Java语言编写的,可以在官网找到源文件,虽然用途不大。buildInChips文件夹包含了内建芯片的程序,也就是各种类型的芯片,供硬件仿真器使用。而buildInVMCode文件夹包含了操作系统的程序,供VM模拟器使用,这些其实基本不用关心。
OS文件夹值得注意一下,里面是操作系统的.vm程序,不过这其实是官方给的答案。原则上你可以根据vm程序中的代码手动翻译出Jack高级语言的代码,其具体内容还是到操作系统部分再进行讲解。
然后我们看一下提供的可执行程序工具,分为有GUI和没有GUI只能命令行运行的。
有GUI的首先是汇编器Assembler.bat,汇编器这个其实不太能用到,因为汇编代码和机器代码是相互对应的,所以你可以直接在CPU模拟器中打开汇编代码并观察机器代码。这个程序的主要作用是将包含汇编程序文本的.asm文件逐行翻译成包含01二进制机器代码的.hack文件,还有比较机器代码的功能。
然后是硬件模拟器HardwareSimulator.bat,这个是在硬件部分进行各种硬件组合和仿真的核心程序。该程序读取硬件描述语言.hdl文件,该文件中定义了逻辑元件管脚的连接方式,基本单元是Nand,不过你可以使用任何已经定义的元件。
在模拟器中可以设置输入管脚状态,并给出输出管脚和中间管脚的值。该模拟器还可以加载测试脚本.tst文件,用来验证编写的硬件组合是否能够得到期望的计算结果。这些元件逻辑的编写以及脚本的编写规则,有答案参考的话并不难理解,可以仔细研究一下。
稍微介绍下工具栏的使用,在其他几个模拟器中也大同小异。首先是打开文件,然后是运行控制,包括单步执行,顺序执行,停止,复位,很容易理解。然后是计算结果值以及手动控制时钟的变化。之后是加载测试脚本和添加断点时的变量值,这个断点其实我没用过。之后的滑杆是调节程序运行的速度。
然后是动画选项Animate,默认Program flow可以演示程序的逐行运行,下一个是Program&data flow可以提示左边数值的变化,演示更繁琐点。最后一个是No animation无动画,主要是用来执行后面的大型程序的。
下面的就简单了,数字格式十进制十六进制二进制。最后一个是视图View,可以选择右边显示的内容,比如脚本,输出,比较,屏幕。
然后是CPU模拟器CPUEmulator.bat,主要是用来运行汇编代码的,有汇编器的功能。左边的表格显示打开的.asm汇编程序,也可以打开.hack二进制文件,并转换显示方式。这里的汇编代码会自动将符号转化为分配的内存地址。之后的表格显示内存中存储的值,CPU在顺序执行程序的过程中也会读取或存入修改内存中的值。
右上部分是用来显示的512x256屏幕,像素对应于内存地址16384(3FFF)-24575(5FFF)。然后是键盘映射0-256,对应内存地址24576(6000)处的值。右下是ALU的示意图,显示正在进行的运算。此外还有寄存器A,D以及程序计数器PC的表示。
CPU模拟器的基本原理就是,通过从程序或者指定内存单元读取数据,存入到A,D寄存器中,由ALU进行二元运算,并将输出结果存到A,D或指定内存单元中。由于程序运行支持分支跳转和循环,因此可以基于ALU的几种简单计算来实现复杂的运算功能。
最后是VM模拟器VMEmulator.bat。当使用汇编语言编写程序时,需要考虑如何使用寄存器和内存空间等,这是一个很繁琐和费解的过程。所以解决方法是构建另一层抽象,用汇编语言来实现堆栈操作,然后使用堆栈命令来编写程序。
这相当于构建了一个虚拟机,或者称之为堆栈机。堆栈命令存在.vm文件中,可以被模拟器执行。堆栈程序的特点是脱离了具体的硬件结构,而在抽象空间中运行,并且可以简便的转换为高级语言Jack语言。
堆栈机的特点是,通过入栈出栈等命令让数值在堆栈中计算,计算结果也保存在堆栈。基于堆栈的层次性可以进行各种复杂并具有优先级的复合运算。另一点是对内存进行了划分,从而在不同的区域存储不同的变量,包括Static静态变量,Local局部变量,Argument函数参数,This直接操作内存,That操作数组,Temp临时变量等。
堆栈机运行起来相当于从某个变量空间读取数据,在堆栈中计算,再将结果存到某个变量空间。通过更详细的定义,可以实现平常编程中常见的变量定义,变量赋值,将参数传递给函数,在函数中进行计算并返回结果等操作。
关于子程序调用,VM模拟器可以显示全局栈以及当前调用的子程序所使用的局部栈。注意VM模拟器除了打开单个文件外,还可以打开文件夹即其中所有.vm文件,比如操作系统文件,找不到的会调用内置的VM代码。
命令行运行的首先是用来编译Jack高级语言的Jack编译器JackCompiler.bat,主要用来将高级语言Jack语言的代码翻译成虚拟机使用的VM程序代码,供VM模拟器使用。这个自带的编译器会给出错误报告,可以指定编译单个.jack文件还是编译文件夹中的所有.jack文件。基本使用方式如图所示。
然后是文本比较器TextComparer.bat,主要用来对比两个文件的不同,会给出不同所在的位置。这个其实开始没发现有什么用,不过后来项目要求编写操作系统所需的Jack程序,没有参照的话其实很难下手。前面说了tools文件夹中的OS文件夹中给出了操作系统的VM程序,因此可以根据VM命令手动翻译出对应的Jack高级语言程序。但这个翻译过程容易出很多小问题,所以将手写的Jack程序编译出对应的VM程序,并与OS文件夹中的标准VM程序相对比,就可以修正错误,从而得到OS程序的Jack代码标准答案了。
注意这些提供的程序都是Java程序,所以要运行系统需要Java运行环境,找到jdk并设置环境变量的方法就不说了。需要提一下的是,常用的jdk版本有8,11,17,21,其中11和17在运行时屏幕像素和内存空间最后的对应会有一点奇怪的偏差,8和21没有这个问题并且21运行起来会比较快。可以自己选择合适的jdk版本并测试一下。
然后说一下Github的相关资源,也就是网友给出的问题答案。如果在Github搜索nand2tetris,会找到一些star比较多的代码,主要的几个如下。
https://github.com/woai3c/nand2tetris
这个是star最多的,看起来是国人做的,应该没什么大的问题,还附带了官方的工具。硬件部分的hdl项目其实不太难,所以比较需要参考的分别是:
1.汇编器,将.asm翻译成.hack代码,
2.VM翻译器,将.vm翻译成.asm代码,
3.Jack编译器,将.jack翻译成.vm代码。
以上都是文本转换,原则上可以用任意语言编写。
4.操作系统文件,
主要是提供Jack语言或VM命令运行所需的,预先编写的有用子程序,或者说就跟标准库差不多。这个需要用Jack语言编写并编译为.vm文件,供VM模拟器使用,如果找不到相关文件模拟器会使用内置操作系统代码。
如果要说问题,就是这个解答的编译器是用js语言编写,需要node环境来执行。可能有人对js语言不熟悉,所以这个资源的README里有给出别的语言编写的解答的链接。比如用JAVA语言编写的如下。
https://github.com/AllenWrong/nand2tetris
这个其实我没仔细看,我主要找了一下操作系统文件的实现,不过里面有个问题,就是Sys.jack文件中初始化程序init()中各个初始化的顺序有问题,使得无法通过VM模拟器的测试程序,正确顺序可以参考官方OS的Sys.vm里的代码。
原则上正确的操作系统.jack文件应该可以被官方编译器JackCompiler编译为对应的.vm文件,并且在VM模拟器中成功运行官方提供的各种测试用程序,也就是主程序Main.jack或Main.vm。这些操作系统文件的名字分别为Array, Keyboard, Math, Memory, Output, Screen, String, Sys。
不过大多数人可能希望能有一个python编写的答案,上面node资源里给的那个不全,看起来Github上最古老和star最多的python解答是这个
https://github.com/havivha/Nand2Tetris
包括python的汇编器,VM翻译器,分析Jack语言的XML生成器,Jack语言编译器。如果说有问题是不支持代码中的中文,可能需要在打开文件时加上使用utf-8编码才行。
但这个资源的操作系统.jack文件有一些难以描述的问题,导致无法正常被编译或被模拟器执行。比如Screen.jack中51行多了个括号,String.jack中27行应为buffer.dispose(),然后就是Sys.jack中的初始化顺序。还有Output.jack这个文件的编码是Windows-1252,如果不转成与其他文件相同的ascii编码,可能会无法被python程序读取。天知道为什么这些.jack文件看起来写的非常好但却包含了一些感觉不应该有的错误。不过python程序的好处是可以方便的在命令行调试,当然node好像也可以只是我不太熟。
所以还是node那个资源的OS代码没问题,不过这些OS代码的实现都有些微妙的区别,当然可以参考官方给的OS的vm文件,来直接翻译得到标准答案,其中的实现又有一些区别了。
再补充几个问题,虽然课程名叫做nand2tetris,但官方并没有给出俄罗斯方块的程序,当然在Github可以找到几个。看其中一个的描述是,俄罗斯方块太大了,汇编代码行数会超过程序存储器寻址上限,以至于不能在CPU模拟器中运行,但通过删减OS文件和专门写了编译器,可以把行数压缩下来。另外还有一些是可以运行在VM模拟器中的,不受行数限制。可以参考下面两个。
https://github.com/leocassarani/Tetris.jack
https://github.com/max-zia/tetris-jack
然后就是如果你用VSCode来查看Jack语言文件的话,是可以找到语法高亮的扩展的,支持hdl和jack的高亮,当然可能聊胜于无。
下面简单介绍一下这本书的基本思路和主要内容。
首先就是基于简单的逻辑元件来构建计算机的本体,书中的计算机叫做Hack。需要的硬件包含组合逻辑电路和时序逻辑电路,构造ALU和存储器,基于ALU来构建CPU,存储器分为程序存储器和数据存储器,最终组合成为具有完整功能的计算机,并可以用模拟器来运行程序。
基本的结构如下图,程序存储于ROM32K中,可以按地址顺序读取,CPU读取当前行的指令instruction,选择存取操作和运算操作等。CPU通过程序计数器pc控制程序存储器中程序的执行和跳转,通过writeM控制是否写数据内存Memory。CPU可以提供地址addressM来读取或写入Memory中的特定单元,比如将输出结果outM存入Memory,或者读取Memory的数据,从inM输入到CPU中。此外还有复位reset功能,可以让程序从头开始。思考一下其中的数据是怎样交互的。
然后我们考虑一下在Hack计算机中使用的程序语言。最基本的就是所谓的机器语言或10二进制.hack文件,其中的10序列包括了一些数据和控制指令。机器语言是实际存储在ROM32K中并可以被硬件直接运行的,你可以根据其中的01直接分析硬件电路的运行。
不过如果不是分析硬件电路,只考虑编程的话,机器语言就过于费解了,所以合适的方式就是将机器语言每行实现的操作用简单的文字符号来表示,这就是汇编语言,存储在.asm文件中。当然汇编语言也是01构成的文本文件,只是被转换显示成我们熟悉的字符代码了,而汇编语言的01并不像机器语言的01那样能够驱动硬件,所以需要依靠程序进行转换或翻译,这就是汇编器。
形象点我们还是考虑字符的转换而不是01片段的转换,这种翻译过程并不用从零开始在Hack计算机里用机器语言来做,我们可以在熟悉的电脑上,使用我们喜欢的各种高级编程语言来实现这种字符转换的文本操作,或者说读取.asm文件并处理生成.hack文件的过程。对于提供的CPU模拟器来说能够自动翻译汇编语言,所以直接读取.asm文件就可以了,不过书中要求自己编写一个汇编器程序,用python来编写是比较方便的。
当然汇编语言也是相当繁琐的,因为需要直接操作硬件和寄存器,所以就像前面所说,可以构建一个堆栈虚拟机体系,其中运行的是VM命令或虚拟机语言,包括可以指定位置的出栈入栈和运算等命令,每个VM命令都可以用繁琐且重复的汇编指令来实现。
这就又需要一种从VM语言到汇编语言的转换,即VM翻译器。自然书中希望你来自行编写这一翻译器,其可以读取.vm文件并处理生成.asm文件。提供的VM模拟器可以直接运行.vm程序,特别是VM语言支持子程序调用和面向对象等机制,并可以多个.vm文件间相互调用。
VM语言作为一种中间层机制,有效的实现了与高级编程语言和汇编语言直接的思路对接,而在Hack计算机中使用的高级语言是Jack语言,其与我们熟悉的高级语言比如C++或Java十分相似,并且支持面向对象编程的基本特性。
Jack语言可以相对容易的与VM语言的堆栈命令相互转换,事实上你可以手动做这种转换。当然,也可以编写一个Jack编译器,来实现读取.jack文件并处理生成.vm文件。在本书中,这一编译器的实现分为了两个项目,一个是解析.jack文件中的词汇符号等,将字符与对应的属性整理成XML数据文件。第二个项目是完整的编译器,可以直接生成VM命令或.vm文件。
作为高级语言,我们希望能够用Jack语言简单的实现创建数组,读取键盘,显示字符等操作,而不是手动操作内存空间。这需要一些预先定义的程序文件来实现,这些程序使用Jack语言编写,类似于标准程序库,或者看做实现了简单的操作系统功能。
这些OS文件包括Sys.系统初始化,Memory内存的分配和释放,Array数组,String字符处理,Keyboard键盘读取,Output字符输出,Math数学运算,Screen屏幕绘图。
当然这是一个简单的操作系统,主要用来管理硬件资源,没有一般操作系统的进程管理,虚拟内存,文件系统等功能。总之书中要求自行编写操作系统的.jack文件,前面说了,可以参考.vm的标准答案。
有了对整个过程的理解,可以参照下图思考书中包含的各个内容以及相互的联系,感受书中描绘的从最简单的与非门到计算机硬件到编程语言和人类的想法的对接。
布尔逻辑
下面我们正式开始硬件部分的介绍。
计算机的最基本组成单元是什么?晶体管,那么晶体管是什么?一个可以控制线路通断的开关。当基极是高电平时,另两极相当于联通的导线,当基极是低电平时,另两极相当于断开的导线。基于开关的相互组合可以构成基本的逻辑门,进一步构成复杂的逻辑电路。这里的晶体管只是实现开关功能的载体,早期的计算机使用继电器或者电子管实现类似的功能,也可以考虑更加机械朋克式的实现手段。
一般来说与非门Nand是实现起来最简单的逻辑门,如上图所示。
逻辑门的功能可以通过逻辑运算相互实现,例如基本的非门Not,与门And,或门Or都可以使用与非门Nand来构建。这里就不详细的介绍基本的逻辑运算了,不过我们可以将逻辑门的功能看做是输入两个二进制数,输出一个二进制数的二元运算。输入01的方式有4种,而这四种结果用二进制表示即16(1111)种可能性,即16种二元运算。当然我们只需要其中便于使用和理解的几种,其他的可以由基本的运算推出,如下图所示。
对于与非门Nand来说,就是与门And的输出连了一个非门Not或反相器,实现的效果与与门相反,与门是两输入均为1输出才是1,与非门是只有两输入均为1时输出才是0。之所以以与非门为基本单元是因为其实现起来较为简单,就像前面的晶体管电路所示,可以用两个串联的开关同时工作来改变电压从而控制另一个开关的通断。
基于与非门,我们可以首先构建一个非门Not,如下所示。可以从上面的真值表得到其输入输出的关系。之后是非门的.hdl文件,其中用到了一个与非门,并设定了与非门每个管脚的连接方式。
有了与非门和非门,我们就可以将其连起来获得一个与门And。或者说,与门可以由两个与非门构成。
对于或门Or,我们可以在与非门的输入端加上反相器,也就是由3个与非门构成。或者说,也可以由非门和与门组合得到。
下面说一个异或Xor,也就是输入相同时输出为0,输入相异时输出为1。我们可以根据前面图中的运算关系得到接线方式,如下所示。在.hdl文件中的是另一种接线方式,可以根据运算得到相同的结果,如下。
除了单独输入输出的逻辑门,有时还需要用到总线Bus作为输入输出。可以看成相互独立的多个相同逻辑门组合而成,比如16位非门Not16,16位与门And16,16位或门Or16,总线中的特定引线由数组指代,下面是其中一个例子。
与总线输入输出不同,也可以构造多输入单输出的8路或门Or8Way,如下所示
基本的逻辑门就说这么多,为了构造Hack计算机,我们还需要一些简单的功能模块,比如多路选择器Mux,和与之功能相反的分路器DMux。
多路选择器可以控制在多个输入中选择其中一支输出,选择位sel是1位,可以控制两条线路输出哪一个,sel为0的话输出a,sel为1的话输出b。虽然看起来这是个有特殊功能的电路,但其实也可以归结为不同的输入导致不同的输出的逻辑运算,因此可以由基本逻辑门实现,并能写出其真值表。
上面的电路的选择位sel只有1位,可以控制两条线路。如果使用两位选择位sel,便可以控制四条线路。这种选择器或分路器可以由前面的简单模块组合而成。
例如下面的是4路16位选择器Mux4Way16,有两个选择位sel,并且输入输出均是16位总线。当然我们可以用16个相同的双路选择器来构成16位总线选择器,只是选择位所控制的从单条线路变成了整个总线。
类似的,下面是4路分路器DMux4Way,由两个选择位控制两层共三个双路分路器,实现了将输入信息输出到指定路线的功能。
此外在书中项目中还包含16位选择器Mux16,由16个选择器Mux构成。以及8路16位选择器Mux8Way16,由两个4路16位选择器Mux4Way16以及一个16位选择器组成。
除了4路分路器DMux4Way,还有一个8路分路器DMux8Way,可以由4+2+1共7个基本的分路器DMux构成,拥有3位选择位sel,可以控制8条输出线路。这些项目工程中有相应的实现方式,这里就不详细展开了。为了减少基本逻辑门的数量,也可以直接由基本逻辑门来组成以上多路的分路器,如下所示。
布尔运算
除了基本的逻辑电路之外,我们制造计算机的一个基本目的就是用来计算,或者说代替人力来计算,而我们熟悉的运算无非就是加减乘除,其中最基本的便是加法Add。第一台电子管计算机ENIAC便是每秒可以计算5000次加法,我们平时使用的计算器,也是一种简单的计算机。
计算机的加法就是二进制的加法,从单个位的加法开始考虑,便依然是一种输入输出01的逻辑运算。我们需要两个输入端来获取01值,一个输出端来得到输入值加法的和sum,不过为了给多位加法做准备,我们还需要输出一个进位carry。值得注意的是,一位加法运算的结果正好与异或运算相同,也就是说我们可以用异或门来进行加法,而另外处理进位。电路结构和真值表如下所示。这种最简单的加法运算单元叫做半加器HalfAdder。
不过如果我们想要继续做多位加法,便需要一个有3个输入的加法器,这种加法器可以由两个半加器组成,叫做全加器FullAdder。
只能做一位加法对我们来说肯定是不够的,当然也不是要做无限位的加法,只要能满足我们的计算需求就可以了。于是我们可以构建16位加法器Add16,由16个前面所说的加法器依次组合而成。
第一个是半加器,后面15个是全加器。对于16位加法器,我们可以做2^16=65536内的加法,只要最终的和小于65536。16位加法器的输入是两个16位总线,输出和是16位总线,总线的每一位计算都由对应的加法器实现,如图所示。
关于加法器的计算范围,一般我们认为只需要做正数的加法,但这是不足够的。一方面是我们也经常需要做减法,而减法会导致负数的出现,特别是我们可以将减法定义为加上相应的负数。为了满足以上几点,我们可以只使用16位中的15位表示数字,而第16位表示正负号。15位数字范围是2^15=32768,即0-111 1111(32767)。
不过负数的表示并不是相应的正数直接加个符号位,一般我们使用补码,其特点是可以实现对应的正负数相加后结果为0,实际是使二进制数的列表直接和正负数的列表相对应,加法和减法反映的只是在这个列表中的移动。
例如32767为0111 1111,按位求反得到1000 0000,如果相加可以得到1111 1111,只要再加个1便得到0,于是我们可以定义-32767为1000 0001,第15位为符号位,1表示负数。如果我们想要得到对应的正数,可以将15位000 0001减1并按位求反,便得到111 1111也就是32767了。
简单起见我们考虑4位补码,可以表示16个数字,而数字部分3位可以表示0-7范围的正负数,如下图所示。在相应范围内可以计算任意两个数的加减法,并确认对应数字的运算关系。
例如,其中的转换关系可以说十分巧妙。
有了正负数的加法定义之后,我们可以做一个判断数字正负的元件,只需要确定第15位的值便可以了,这就是16位负号判断IsNeg16,输出1时输入数字为负,输出0时为正。这个简单的电路后面会用到,可以注意其.hdl的写法。
还有一个后面会用到的电路,16位递增器Inc16,其使用16位加法器构成,输出值始终是输入值+1,因此可以用来让寄存器中的数逐次递增。计数器一般是用来输出行号地址从而到程序存储器中加载对应行的程序,并让程序随着行号增加顺序运行。
有了16位加法器之后,便满足我们的运算需求了吗?我们往往需要计算机能完成多种类型的运算,并且我们可以控制进行哪种运算,以及对运算结果进行处理等更多的功能。借助多种逻辑电路的组合,我们可以将我们所需的各种功能整合在一个计算模块之中,这就是算术逻辑单元ALU (The Arithmetic Logic Unit),其能执行多种类型的运算,虽然大多数比加法要简单,但对于计算机的很多基本操作来说是必不可少的。
首先我们看一下ALU的引线定义,ALU可以对两个16位二进制数输入进行运算,并输出一个16位计算值。ALU的上方有6个控制位,前面4个分别是 zx 令x输入为0,nx 对x输入求反,zy 令y输入为0,ny 对y输入求反。这4个主要是用来控制输入值的,1为生效0为不生效,通过组合可以得到x与y之间丰富的运算方式。
然后第5个控制位 f 决定运算类型,如果f=0执行16位与运算,如果f=1执行16位加法运算。最后一个控制位 no 是对输出值进行处理,no=1时对输出值取反。
下面有两个是用来判断输出值性质的,其中 zr 是表示输出值为0,而 ng 表示输出值小于0,通过两者的逻辑组合我们还可以得到大于0的判断。这两个输出位主要是用来确定程序跳转的条件的,要实现这一点还需要更多的外围电路,放到后面再进行讨论。
虽然ALU看起来已经十分功能化了,但仍然可以看做是输入值决定输出值的组合逻辑电路,因此也可以写出其真值表。对于ALU我们主要关心其能进行怎样的运算,也就是上面6个控制位的01组合分别能让ALU执行怎样的运算,可以参考下面的表格。
前三行 f=1 使用的是16位加法。第1行为0+0=0,第2行相当于ffff+ffff=ffffe,最后反转了一下得到1。第3行是ffff+0=ffff,因为使用的是补码,所以便是-1,或者说-1对应的是每一位都是1即1111 1111 1111 1111,如果+1可以得到0。
之后四行 f=0 使用的是16位与运算。第4第5行相当于 x&ffff=x 和 ffff&y=y。第6第7行相同但 no=1 对输出结果取了反从而可以得到 !x 和 !y。
之后 f=1 继续使用16位加法。如果我们要得到 -x 即 x 对应的负数,考虑前面补码的定义相加为 0,可以对其求反再 +1,即,同样也可以考虑相反的过程即 -1 再求反即
,也就是表中表示的运算
,y 的情况类似。
对于 x+1 的情况,相当于,与求相反数的方法类似,即
,考虑到同样的方法,可以得到
,y+1的情况类似。
对于 x-1 的情况就是普通的加法运算了,即。
对于 y-1 的情况为。
然后是标准的加法运算 x+y,没有什么问题。
关于 x-y,先明确与补码有关的,然后考虑表中的运算,
可以进行如下转换
然后是 y-x 的过程
虽然这个过程看着很合理,但因为我也不太熟悉逻辑运算,可能会有些意想不到的问题。
最后两行 f=0 使用16位与运算,标准的 x&y 没有什么问题,然后可以根据逻辑运算的关系得到或运算即。由此可以看出,求反运算在很多基本运算中都起到了意想不到的作用,这可能便是二进制运算的一种独特的地方。
然后要做的就是如何基于基本的逻辑原件组装出能满足以上运算要求的ALU了。ALU的核心可以看做是16位加法器和16位与门,而对输入进行的处理可以使用多路选择器实现,对输出进行的处理也可以使用多路选择器,加法和与运算也可以用多路选择器。然后就是还要对输出结果进行判断,确定是否等于0或者为负数,可以用前面提到的简单电路实现。上面便是ALU的具体构成方式,大致的结构是类似的,小的地方可以做一些修改。下面是.hdl文件。
时序逻辑
虽然ALU的功能很强大,但是其只能即时的处理输入的数据并输出结果。对于一台能够完成复杂任务的计算机,我们需要有能够存储大量数据的元件,并且可以被自动读取加入到计算中,同时计算的结果也可以存储到指定的存储单元中,供需要的时候调用。
同时拥有运算器和存储器并能相互配合才算是完整的计算机,这样的计算机可以读取并按照事先设计好的程序运行,经过反复的交互计算并最终得到所需的结果。这使得计算机看起来就像能自动运转并完成某种任务的智能生物一样。
有趣的是,能够实现存储功能的元件,也可以由基本的逻辑门组成。在本书中所使用的基本存储器件为 D 触发器DFF (Data Flip Flop),其他复杂的存储器可以基于D触发器来构成。
D触发器就像逻辑门中的与非门一样,当然D触发器也是由与非门构成的,不过在讨论存储器的时候,可以将DFF看成基本单元,在.hdl文件中可以直接使用。和与非门不同的是,DFF可以被时钟信号驱动,并在硬件模拟器中随着时钟信号变化刷新其中的数据。
对于DFF而言,D是输入,Q是输出。书中的DFF在时钟信号下降沿变化时会启动更新存储的数据,或者说让输出等于输入。DFF本身可以保持输出为存储的数据,或者说可以稳定的保持为0或1的状态,只有输入改变并且时钟信号从1到0时,才会改变存储的状态。
看起来我们可以使用DFF存储1位的数据,不过为了便于使用,我们可以构造一个1位寄存器Bit,特点是我们可以控制是否要输入刷新的数据,这需要一个控制是否写操作的管脚加载load。1位寄存器Bit的结构可以如下。
当 load=0 时,输入in的改变不影响输出out的值或者存储的值,而 load=1 时,存储的值变为输入in的值实现数据的更新。注意其中数据的更新还是会在DFF的时钟信号的下降沿时发生,但在后面我们可以只考虑1位寄存器Bit的功能作用。
由于我们进行运算和输出的都是16位二进制数据,因此我们也就需要能存储16位数据的寄存器,这需要16个1位寄存器Bit简单的组装在一起。16位寄存器Register的结构如下所示,相当于16个独立的1位寄存器Bit共用同一个载入load管脚。
寄存器一般只能存储一组16位二进制数据,如果我们需要存储更加多的数据,便需要大量的16位寄存器。当有了大量寄存器之后,便需要解决如何找到指定的寄存器并进行读写的问题,一般来说我们可以基于二进制的地址来选择读写哪一个寄存器,地址的位数代表了可以关联多少个寄存器。
当我们将地址和大量寄存器组合起来,便得到随机访问存储器RAM (Random Access Memory),也就是一般所说的内存。例如,我们可以将8个16位寄存器Register组合起来进行寻址,需要3位二进制地址。要实现读写指定地址的寄存器,需要使用8路分路器来控制每一个寄存器的load管脚实现指定写入,并使用8路16位选择器来控制输出哪一个寄存器的输出值实现指定读取。显然3位地址同时用来控制分路器和选择器。
8个16位寄存器组成存储器RAM8,具有3位地址。为了增加容量,可以用类似的方式用8个存储器RAM8组成存储器RAM64,具有6位地址,地址的低3位是本来的存储器RAM8的地址,而高3位用来控制8路选择器和分路器,选择使用8个存储器RAM8中的哪一个。进一步扩展8倍可以得到存储器RAM512和RAM4K,地址分别增加了3位和6位。对于存储器RAM4K,具有12位地址,可以随机读取2^12=4096个寄存器。不过最后我们使用4路选择器和分路器得到存储器RAM16K,具有14位地址和16384个寄存器。在Hack计算机中,我们使用存储器RAM16K来作为数据存储器。
最后我们谈一下程序计数器PC (Program Counter),将16位寄存器Register与递增器Inc16相互组合可以得到一个随着时钟信号变化的计数器,寄存器中存储的数字依次增加。
程序计数器中存储的数字作为访问程序存储器的地址,也就是可以按行依次加载程序存储器中的一行程序,实现程序的顺序执行。为了进一步控制程序的运行,还需要给程序计数器加入更多的功能,比如可以重置为0对应程序的重新执行,比如可以存入特定的数值从而可以让程序跳到指定的行数,进而实现分支或循环语句。
程序计数器的运行可以被程序控制,而程序计数器又控制着程序的运行,这是一种精妙的计算结构。程序计数器的构成如下图所示,这个是参考python那个答案的,细节也是可以有些不同的。
关于程序计数器以及时钟信号触发的方式,可以参考下图。当reset=1时,计数归零out=0,inc=0时不计数,inc=1时开始计数out值依次增长。当load=1时可以从in输入指定的数值,从而输出out变为指定的数值后继续计数。每次计数器存储的数值会在时钟信号clock的下降沿更新,也就是clock从1到0的时候。
在硬件模拟器中,我们可以使用工具栏中的时钟按钮,或者在测试脚本中通过tick-tock来控制时钟的运行,在工具栏下面的Time框中可以显示当前的时钟周期。比如在tick的时候显示1+,在tock的时候显示1,然后数据开始刷新,tick-tock对应上升沿和下降沿。
计算机体系结构与机器语言
有了运算器和存储器,我们就可以开始构建完整的Hack计算机了。不过ALU并不能直接拿来使用,还需要增加一些寄存器,程序计数器以及控制电路来制造中央处理器CPU (Central Processing Unit)。
对于CPU,我们希望其能接受程序中的指令,并且能够根据程序中的指令实现相应的运算。同时CPU包含程序计数器,可以根据指令控制读取程序存储器中的哪一行指令。
为了能够处理数据,CPU需要能够从内存输入数据以及输出数据到内存,为了读取内存,CPU还要能提供内存的地址,并能控制是否写入内存。为了理解这些功能可以将CPU放到完整的Hack计算机结构中来考察,如下图所示。
左边是指令存储器ROM32K,作为只读存储器可以接受指定的地址address,并且输出地址指向的寄存器的指令数据instruction,指令的地址由CPU的程序计数器输出pc来控制,CPU还具有重置reset功能,可以重置程序计数器为0使程序重新开始。存储器中的指令是01二进制数据,包含控制指令和需要输入CPU的数据。指令存储器大小是32K,对应15位地址长度,地址范围是0-32767(7FFF)。
右边是数据存储器Memory,可以接受CPU输出的数据outM,可以输出数据out被CPU读取inM,CPU通过地址addressM指定进行数据读写的单元,同时CPU还可以通过输出writeM控制Memory的读写,writeM=0时可以读取指定地址的Memory,writeM=1时可以写入Memory的指定地址。
数据存储器Memory并不只能用来存取数据,还可以进行I/O输入输出映射。如下所示。数据存储器只有16K大小,使用14位地址,更高的地址空间被用来映射输入输出。
数据存储器RAM的范围是0-16383(3FFF),对应16K容量以及14位地址空间。Hack计算机提供了512*256=131072个像素的屏幕显示,每个像素对应存储器中的01数值,0时为白色1时为黑色。由于一个寄存器包含16位二进制数,所以为了表示相应的像素需要8192个存储单元,相应的地址空间为16384(4000)-24575(5FFF)。
屏幕与存储器地址的对应是从左上角开始从左到右从上到下,屏幕共256行,每行512个像素,每个寄存器16位,因此每行有32个寄存器,例如第一行的地址是从16384到16415。这些基本的数据对应关系在后面编写使用屏幕的程序中是需要考虑到的。
除了屏幕输出,Hack计算机还需要键盘输入实现各种控制或交互,对于键盘上的按键只需要一个寄存器就可以了,这个寄存器在存储器地址24576(6000)位置。
关于内存Memory的连接方式可以参考下图,其中屏幕和键盘被看成了一种存储器,连接方式同前面连接多个存储器的方式类似,就是用分路器和多路选择器来选择加载哪个芯片和选择哪个芯片的输出,结构图比较简单就不列出了。
最后我们开始考察Hack计算机最核心和最复杂的硬件电路,那就是处理器CPU是如何由ALU,程序计数器,寄存器和各种逻辑控制电路构成的。要理解CPU的工作原理和基本结构组成,可以参考下图。
在CPU中除了算术逻辑单元ALU,还包括两个寄存器,分别是地址寄存器A Register,以及数据寄存器D Register,另外还有程序计数器PC。在其中通过16位多路选择器Mux16来控制数据通路的流向,而不同的控制位c可以用来控制各种选择器的输出以及寄存器是否进行写入等等。
下面是其中数据通路的简化形式。一般的运算流程可以考虑从指令输入地址,将地址存入A寄存器,基于A寄存器中的地址读取内存中的数据,这时内存中的数据是直接进入ALU的,所以可以指定ALU的输出,沿着返回的路线将数据存入D寄存器,或者进一步返回存入A寄存器。
当然我们也可以直接从指令输入数据,先存到A寄存器,再经过ALU存到D寄存器,然后从指令输入第二个数据到A寄存器。A寄存器有两种用途,存储地址或存储数据,D存储器用来临时存储数据,只不过要存入D存储器需要从ALU绕一个圈子。
有了D寄存器和A寄存器中的数据,便可以指定ALU的运算类型实现二元运算了,输出结果同样可以返回存入D寄存器或A寄存器,也可以输出到内存的指定单元,内存的地址通过A寄存器中的数值来确定。事实上我们可以用ALU计算地址或计算数据,根据计算的结果既可以控制读取的指令地址,也可以随意读写指定的内存单元。
然后我们考虑如何构建CPU的控制部分,基本上控制部分可以分为三种。一种是对数据通路的控制,可以基于控制位对各种选择器和寄存器进行控制。一种是对运算方式的控制,也是依靠控制位直接接入ALU的6个运算控制管脚。最后一种是对程序跳转的控制,这主要依靠ALU输出的结果zr和ng进行跳转判断,并选择如何控制程序计数器PC,比如加载指定的指令存储器的地址,从而实现分支跳转,循环运行或者执行子路径。
对于CPU的组装,可以考虑自行完成,考虑其中精妙的布局。不过也像前面所说,可能有的人觉得没必要花那么多时间在一种另类的计算机结构上,只想快速看到结果并了解其基本思路。那么CPU的详细结构见下图。
要了解CPU的运行原理,首先要了解指令是如何实现对CPU的控制的。指令按顺序存储在指令存储器ROM32K中,每条指令都是16位二进制数,其中不同位的二进制数可以输入到CPU中的各种需要控制的部分。对于Hack计算机来说,有两种类型的指令,一个是A-指令,一个是C-指令。
A-指令特点是最高位ins[15]为0,或者说A-指令就是提供一个数字,这个数字只使用15位。A-指令的数字用来向CPU输入数值或者地址,当ins[15]=0时,加载A寄存器,A-指令的数字进入到A寄存器中。用符号表示这个过程可以写作A=value,在Hack的汇编语言中记做@value。对于Hack计算机的编程来说,在使用A-指令后,有必要使用C-指令来处理输入到A中的数,比如作为地址,比如存到内存,比如存到D寄存器等等。
C-指令的特点是对高位ins[15]为1,ins[14]和ins[13]不使用,剩下的位都是控制位。这些控制位可以分为三种,计算方式comp,结果输出目的地dest,跳转条件jump。
第一个ins[12]用来指定ALU的y的输入来源,控制一个选择器,ins[12]=0时,输入A寄存器中的数据,ins[12]=1时,输入从内存Memory读取的数据inM。
接下来的ins[11]到ins[6]用来指定计算方式,直接控制ALU,其中具体的计算种类前面已经说过了,这里就不多说了,很容易理解。
用来指定ALU输出结果目的地的是ins[5],ins[4]和ins[3],或者说目的地依次是ADM。如图所示,ins[5]控制A寄存器前的选择器,指定输入指令还是ALU的输出。Ins[4]控制D寄存器的写入,而ins[3]控制内存M的写入writeM。输出到A和D很容易理解,要输出到内存M往往要事先指定地址,可以考虑将ALU计算结果暂存到D,然后直接使用A中的地址,或者从ALU输出计算后的地址到A,最后将暂存在D的数据通过ALU存到内存M中。虽然看起来这个过程很繁琐,但毕竟计算机的优点就是计算执行得快。
对于跳转条件ins[2],ins[1]和ins[0],对应于ALU的输出结果小于0等于0大于0,这三个条件可以任意叠加,比如小于等于0,大于等于0,不等于0,三个同时设置便是无条件跳转。这些条件的逻辑控制关系来自于跳转部分的电路,可以仔细研究一下,总之进行跳转的结果就是设置程序计数器PC的load,使程序计数器可以输入指定的跳转地址。地址的设置方式同前面类似,可以直接输入到A寄存器,或者从ALU输出到A寄存器。程序计数器在不加载地址的时候会顺序控制执行,而复位reset可以将计数器值归零。
A-指令和C-指令都是16位二进制数,可以看做是Hack计算机所使用的机器语言。相应的机器语言程序存在.hack文件中。知道了前面所说的指令中包含的意义,事实上我们就可以用机器语言编程了。
不过用机器语言编程还是太抽象了,所以一般可以根据其中01片段的种类,替换为更容易理解的助记符,或者说有意义的英文符号,这就是汇编语言。汇编语言与机器语言是直接对应的,所以下面我们以汇编语言为主,看几个简单的例子,来理解如何为Hack计算机编程。
上面的例子是做一个简单的加法运算,依次解释其作用是,将2存入A,将A中的数通过ALU输出到D,将3存入A,计算D+A的结果并通过ALU输出到D,将0存入A作为地址,将D中的结果通过ALU存入内存M中地址0的单元中。
可以看到A-指令的形式是如@2的形式,而C-指令的形式通常是一个赋值语句如D=A,等号前面表示目的地,等号后面表示运算结果。对比D做目的地和M做目的地时二进制代码相应位的变化。关于运算方式,D=A和D=D+A是两种类型的运算,需要指定ALU的控制管脚,在CPU这里,我们可以参考一张类似的计算表,如下所示。
如果只关心助记符的种类可以参考下图
计算方式和目的地的指令还是比较容易理解的,跳转指令的使用需要配合跳转地址,跳转地址在汇编语言中用标签label来定义,这个稍微复杂一些,其中的转换实际上是由汇编器自动处理的。为了理解其与机器语言的对应,下面还是看一个简单的程序。
这个程序是找出两个数的最大值,两个数分别存在内存的M[0]和M[1]位置,先将第一个数M[0]暂存到D,然后读取第二个数M[1],做减法D=D-M并将结果存到D中,通过判断D的正负来决定是跳转还是顺序执行。如果M[0]比较大会跳到第10行,注意是机器语言一边的行号,然后将M[0]存到D,也就是最大值。如果M[1]比较大会顺序执行,并通过D=M[1]将最大值存到D。然后或者是前者的顺序执行,或者是后者跳到第12行,实现了将最大值存入内存中M[2]=D。程序执行完到达最后的无限循环,也就是反复跳到第14行。
这就是条件跳转和执行分支语句的基本过程。其中汇编语言和机器语言的对应没有什么问题,只要查表就可以得到助记符和01片段的对应关系。但是在汇编语言中,跳转地址是依靠标签label定义的,这个标签并不会被翻译为机器语言,也不算在行号中,而是将标签后一行指令的行号,写入对应的A-指令中,而这一A-指令之后,便是跳转条件。
比如我们看第一个跳转标签(OUTPUT_FIRST),之后指令在机器语言中的行号是10,所以这个标签就可以看做是数字10,而这个标签在A-指令中使用@OUTPUT_FIRST,相当于@10。这时10被存入A中作为输入到程序计数器中的跳转地址。然后是跳转条件D; JGT,意思是让ALU输出D的值,根据其与0的大小关系判断是否跳转,JGT表示的是大于0时跳转,如果D大于0,跳转逻辑电路就会启动程序计数器的加载load管脚,从而将A中的跳转地址导入计数器,进而让指令存储器输出指定行的指令,并继续顺序执行。
最后再看一个使用循环语句做乘法的例子。该程序的作用是求M[0]和M[1]的乘积,并将结果存到M[2]中。使用的方法是反复将M[1]相加,循环M[0]次得到结果。循环次数使用一个变量i,通过使M[16]的值从0增加到M[0],从而控制循环次数。例如,如果我们想计算3*4,便使M[0]=3,M[1]=4,变量M[16]变化为0,1,2,3,在M[16]=3时,做D=M[16]-M[0]会实现跳转到(END)从而结束循环,也就是循环了3次,执行的运算为4+4+4=12,或者M[2]=M[1]+M[2]循环3次,从而M[2]=12也就是乘积的结果。
这个循环结构是在开头验证循环条件,如果不符合条件则跳到18行(END),之后进行循环的加法运算,循环结构的结尾是跳回到第4行(LOOP),重新验证条件。
这种结构类似于while语句,大概是c=0 i=0 while(i<=a) { c=b+c; i=i+1; } // c=a*b的过程。
在Hack的汇编语言中可以自定义变量符号,就像下面程序中的i。汇编器会自动给i分配内存地址,默认的是从M[16]开始按顺序分配,多个变量依次对应多个内存地址。这里@i相当于@16,当然也可以手动给变量分配内存地址。自定义变量和跳转标签都是汇编器实现的功能,毕竟一般也不会手动将汇编代码翻译成机器代码,可以使用自带的汇编器Assembler.bat,如何编写汇编器不在本篇讨论范围内,大约会在下一篇讨论吧。
可以使用CPU模拟器单步逐行运行汇编代码,并观察如何实现跳转和循环的。
书中的工程中还有几个汇编程序,一个是Sum.asm,通过循环计算从1加到100的结果,结果一般都知道是5050。然后一个是Rect.asm,通过循环写入内存空间,在屏幕左上角显示一个高度指定的矩形。另一个是Fill.asm,通过识别键盘按下,循环刷新屏幕所有像素,显示全黑或全白,这个循环运行次数过多,所以要将Animate设为No animation,不过如果改一下程序,还是有可能单步看到最初的刷新过程的。或者可以将运行速度调到Slow。此外还有一个Pong.asm程序,是弹球游戏,行数非常多,是从高级语言代码转换过来的,大概只能验证一下CPU模拟器的正常运行,注意也要调为No animation。
更多的游戏程序,虽然不知道在哪里可以找到。
前面说过的可以用VM模拟器运行的俄罗斯方块
硬件部分的介绍就到这里,单凭这篇文章估计很多地方还是不清不楚的,可以找到原书进行参考学习,虽然原书其实也挺抽象的。这时就有必要研究带答案的工程了,这些工程可以到Github找到,前面介绍过了,或者我也可以考虑分享个度盘的链接。
本篇是第一部分硬件篇,按计划可能还会有第二部分编译篇和第三部分操作系统篇,不过这种东西也算心血来潮,万一后面坑了也是很正常的,不过我感觉后两篇做起来应该比这一篇容易,毕竟不用搞太多图。
这里说一些后面内容大概的介绍吧,编译部分就是基于那几个python编写的编译器把书中后面的内容顺一遍,如果愿意研究python程序应该也不用什么详细说明了。操作系统部分是基于Jack语言的,参考是我手动从OS的vm代码翻译过来的标准答案,顺便比较和几个其他答案的区别,操作系统就是涉及一些基本功能和算法的Jack语言实现。
关于本篇文章的目的,就做一下总结吧。从零开始以基本要素之间的关系按照不同的层次构成最终的复杂结构体系,是大多数逻辑清晰可循的科学技术知识体系的基本认知构成方式。对于计算机科学来说,逻辑构成和抽象转换的力量体现的更为简洁和精辟。
不过现在大多数对计算机的理解都是从编写意义不明的Hello World程序开始的,但是要理解计算机体系的精辟,还是有必要深入计算机的基本原理,理解其是如何构成和运作的。对计算机科学的核心思想的掌握源自于理解计算机体系的整体构成方式,而不是只了解些软件的使用和短小程序的编写。现在由于计算机体系的过度庞大和复杂,使得大多数人很难像计算机发展早期那样,对一些基本底层原理有着清晰的认识。
这本书的目的就是如此,用最简单的方式理解计算机体系,虽然这本书也是几十年前的书了,但由于其专注于原理,使得现在看起来也不算过时,并且包含着精辟的思想。但是对这样一本另类的边缘书籍,要理解其的好处也是很麻烦的,所以本文的目的就是将其中包含的思想,用尽可能简单形象的方式展现出来,例如本篇的思想完全可以按照硬件的拼图,在极短的时间内就可以掌握大致面貌。
我大概期望采用一种核心思想的方式来理解计算机科学的知识体系,然后就发现了这本现成的杰作,所以可以完全以该书为基础来实现。对计算机结构的核心思想的掌握也有助于理解其他领域的各种问题,比如我们前面谈到了程序计数器与指令存储器的控制和跳转,如果我们把脑中的思维也看成顺序执行的程序,思维中包含的内容毫无疑问会控制思维本身的运行方式,这时只要有目的的改变思维中的内容,也就能调控思维本身的运行。
常见的计算机入门往往是一种编程语言把语法点讲一遍,换一种编程语言还是把类似的语法点讲一遍,就像毫无思考的编写字典一样,但是如果专注于程序中的核心思想,便有可能在编程书籍中加入更多的思维方式的实现内容,或者具体的应用构建方式,这样学习编程便不是重复语法点,而可以更专注于运用语言和使用计算机的方式。对于该书后面的软件部分或编译部分来说,也是一种深入理解编程语言的原理的方式,比如如何利用内存的空间和借助精妙的计算方法实现复杂的程序功能等。
感想之类的就随便说这么多,本文就到这里,感谢阅读到这里的你。
2024年1月27日