橄榄果 發表於 2024-4-5 23:51:00

nand2tetris_hack汇编语言

<h2 id="计算机">计算机</h2>
<p>我接触的第一台电脑是winXP系统,我拥有的第一台电脑是win7,也就说一开始我理解的计算机就有着好看的界面,灵活的操作性方式,拥有许多软件,可以做很多事情。<br>
我们可曾想过,大部分机器都有其专属用途,比如榨汁机只能用来榨汁、削皮刀只能用来削皮,而计算机,他可以播放视频、浏览网页等等,这是一件多么神奇的事情!那这是如何做到的<br>
开始的理论模型图灵机,至后来真正实现了的计算机基础架构冯洛伊曼体系。详细的先不讨论,我们看一下,这门课程将要实现的计算机基本架构图<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405125428965-1937725712.png" alt="" loading="lazy"><br>
图中省略了总线和控制单元,大体思路是程序和数据放在内存中的两块,ALU通过从内存和寄存器中取值完成运算,输出至外部设备或内存和寄存器中。架构图已经有了,那第一步怎么做</p>
<h3 id="指令集">指令集</h3>
<p>可以发现,此刻我们对组装的计算机并没有一个具象化的概念。他需要实现哪些功能,他的内部数据如何传输等等,我们一无所知。课程老师在这一节,先假定我们已拥有了这样一个hack计算机,我们学习hack-CPU提供的指令集,进行简单程序开发。这样在实现CPU之前,我们大概知道了我们想做的是什么<br>
计算机中,用户输入的指令,将会被编译器翻译为二进制编码(后面需要实现的),继而交给CPU执行。指令需要实现的功能有三</p>
<ol>
<li>指令需要计算机要做什么,比如取数还是运算</li>
<li>指令需要能够控制硬件的执行,比如正常指令都是逐行执行的,有时需要跳转</li>
<li>指令需要告诉硬件如何做,比如去哪里取值,输出放在哪里</li>
</ol>
<p>实现指令集的过程中,存在硬件和软件之间的权衡。这门课里就没有在指令集层次支持乘法,但是我们通过软件算法实现。所以,硬件设计和机器语言往往是同步进行的</p>
<h2 id="hack汇编">hack汇编</h2>
<p>介绍课程hack汇编语法之前,先引入几段大众认知的标准汇编语言<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405212253971-1452215346.png" alt="" loading="lazy"><br>
我的前几篇博客里,简单的介绍了如何编写8086汇编代码。相比于高级语言里复杂的语法,汇编语言更接近编程的实质,只提供基础的算数运算和逻辑运算。写汇编代码其实很受限,脑子里只能有加加减减、选址赋值这些操作,不过课程老师也说了,“simple people are impressed by sophisticated thing, sophisticated people are impressed by simple thing”</p>
<h3 id="hack计算机内存和寄存器架构">hack计算机内存和寄存器架构</h3>
<p>在通用计算机中,代码其实也是数据,只是我们将他放在了代码段内,这里hack计算机为了简便处理,不在一整块内存上划分代码与数据,而是设计了两块内存,分别存放代码和数据<br>
hack计算机有两个寄存器,存放地址的A寄存器和存放数据的D寄存器。我们可以设置A寄存器的值,去内存中取值,然后将值保存在D寄存器中,同时提供了一个变量M,M代表此刻数据区选中的值,可以对他进行赋值和运算<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405213512937-1227520342.png" alt="" loading="lazy"></p>
<h3 id="hack计算机指令集">hack计算机指令集</h3>
<p>有了内存和寄存器存放数据/代码之后,此时需要一套指令来读取/写入数据,并交给CPU运算。hack计算机提供了两种指令集,依据指令集编写而成的代码被加载入ROM中,而后逐行执行;至于指令怎么被加载,ROM又是如何执行的,暂且先不讨论。现在可以理解就是有了这样一个hack计算机,提供了这样的指令集,请为他编写代码。其实这和我们学一门新的语言一样,学语法,写功能,再去了解其本质<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405215950443-263702547.png" alt="" loading="lazy"></p>
<h4 id="a指令集">A指令集</h4>
<p>这个比较简单A-address取址,并自动赋值A寄存器;至于图中提示的如何确认,随后而来的取值是到数据区还是代码区,这是后面实现CPU时考虑的事情<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405214654658-1258199481.png" alt="" loading="lazy"></p>
<h4 id="c指令集">C指令集</h4>
<p>这个略复杂些,分为了以下几种情况</p>
<ol>
<li>A/D/M可以赋值立即数(常数),不过可选值只有0,-1,1三种</li>
<li>A/D/M可以互相赋值,支持相反数</li>
<li>A/D/M支持+-算数运算,也支持&amp;|逻辑运算,特别的支持立即数1,可以理解为第二种情况的扩展</li>
<li>跳转指令,课程选择了在下文介绍,用于控制程序执行流程<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405220155143-1512529313.png" alt="" loading="lazy"><br>
如果现在和你说hack计算机就是由这两种指令集做为基石搭建而成,你肯定不相信。其实两种指令集配合起来使用,可以实现对任何一个内存单元的取值和赋值,看例子会很容易理解了<br>
这里直接将A寄存器当作立即数来使用了<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405221245412-766781213.png" alt="" loading="lazy"><br>
这里是通过M这个变量,方便的实现内存地址赋值操作,并总结了一种基础范式,A指令取值,C指令赋值<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405221435341-1487794561.png" alt="" loading="lazy"><br>
来一个稍难一点的例子,这里可以实际去CPU模拟器中执行,看一看内存和寄存器在执行过程中,值的变化<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405221807064-1389711176.png" alt="" loading="lazy"></li>
</ol>
<h3 id="hack计算机-symbolic-programming">hack计算机 Symbolic programming</h3>
<p>标题我不知道怎么翻译,实际这里是介绍了使用一些人类可读的方式来影响程序执行</p>
<h4 id="分支">分支</h4>
<p>高级语言里的if else以及for循环,需要通过这种方式实现,不知道为什么,习惯了前几节使用选择器来判断,感觉这种jump方式,也不是那么难受了。跳转指令实际上是包含在C指令集内的,下文会再对C指令集综述<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405222945582-1748934782.png" alt="" loading="lazy"><br>
再贴一个稍微复杂一点的,一般是这样配合使用的<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405223211741-1511659165.png" alt="" loading="lazy"></p>
<h4 id="变量">变量</h4>
<p>之前在介绍A指令集的时候,@x 这里x可以用来表示立即数,也可以用来表示实际内存地址,当混在一起的时候,就看不懂这里x是代表什么的了;所以我们需要一种区分,约定,如果这里是表示实际内存地址的,我们可以使用一个小写英文来代替,相当于高级语言里申请了一个变量,内存地址自动从16号开始,前面被内置变量名R0..R15占据了<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405223727038-2022030746.png" alt="" loading="lazy"><br>
这有一个立即数和实际内存地址混合使用的例子,很容易区分这两者<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405223916710-81469833.png" alt="" loading="lazy"></p>
<h4 id="标签">标签</h4>
<p>这个主要是为了配合跳转指令来使用的,毕竟跳转的时候还要数在第几行,修改代码之后都要调整一下受影响的跳转代码,这也太痛苦了<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405224056487-2087728847.png" alt="" loading="lazy"></p>
<h4 id="实战">实战</h4>
<p>这里主要介绍了一种思路,编写汇编代码时,先尝试思考伪代码,再将伪代码翻译为汇编语言。执行时汇编器再翻译为机器指令,可以发现变量和标签这些语法糖都被抹去了<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405224215979-488314550.png" alt="" loading="lazy"><br>
这里提到了一个问题,因为计算机通电之后是不会停止的(这里我不明白CPU占有率是怎么一回事),程序会一直往下执行的。就像C语言里的数组越界,我们需要这个程序执行结束之后,“停”在最后位置<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405224913875-1211917030.png" alt="" loading="lazy"><br>
顺便说一句,上面的代码可以这样优化。我在平时写代码,也会尽量写if 不写else<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405225147113-1227306723.png" alt="" loading="lazy"></p>
<h3 id="hack计算机低级编程">hack计算机低级编程</h3>
<h4 id="迭代">迭代</h4>
<p><img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405225812192-589886445.png" alt="" loading="lazy"></p>
<h4 id="指针">指针</h4>
<p><img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405230020588-1110515040.png" alt="" loading="lazy"></p>
<h4 id="实战-1">实战</h4>
<p>其实学到这里,基本已经把计算机程序里的三种结构理解了,大部分的功能都可以参考于此<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405230608112-1563231924.png" alt="" loading="lazy"></p>
<h3 id="hack计算机指令集-补充">hack计算机指令集-补充</h3>
<p>标题有点奇怪,主要开始的时候如果介绍完整,很难解释,现在学习了这么多例子,已经对hack汇编语言有了完整的认识,再来谈一谈指令集</p>
<h4 id="a指令集的两种用法">A指令集的两种用法</h4>
<p><img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405231000037-1269996455.png" alt="" loading="lazy"></p>
<h4 id="c指令集的完整概述">C指令集的完整概述</h4>
<p>这里贴两张图感受一些,C指令集的组成规则<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405231200268-1962677974.png" alt="" loading="lazy"><br>
稍复杂些的跳转指令,也被容纳在内了,是不是有种统一美<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405231232073-1200390925.png" alt="" loading="lazy"></p>
<h4 id="注意事项">注意事项</h4>
<p>这里的意思是,不要混用jump指令和M赋值,因为二者都依赖A指令<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405231527566-1107501065.png" alt="" loading="lazy"></p>
<h3 id="输入与输出">输入与输出</h3>
<p>这里hack计算机内置了显示器和键盘芯片,并将他们映射到内存的某一块区域;可以通过读取/写入这一块区域内的值,来显示内容或读取当前正在操作的键值<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405231939865-1036139257.png" alt="" loading="lazy"></p>
<h4 id="显示器">显示器</h4>
<p>@SCREEN为内置变量<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405232052339-1202318143.png" alt="" loading="lazy"><br>
可以将一个16位数设置为-1,快速设置一大块像素点为黑色<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405232404494-1088224297.png" alt="" loading="lazy"></p>
<h4 id="键盘">键盘</h4>
<p>键盘简单一点,这里的值来自于被按下键的Unicode编码值,没有键被按下时,0<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405232629206-537529268.png" alt="" loading="lazy"><br>
使用时,也内置了一个变量@KBD<br>
<img src="https://img2024.cnblogs.com/blog/1130102/202404/1130102-20240405232919711-65029657.png" alt="" loading="lazy"></p>
<h2 id="课程作业">课程作业</h2>
<p>这节课程的基础知识到这里就结束了,留下了两个作业,贴一下代码加深理解</p>
<h3 id="实现乘法">实现乘法</h3>
<p>这里实现的很简单,简单的++,而且没有判断R0和R1谁大谁小寻找更少的迭代次数</p>
<pre><code>// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/4/Mult.asm

// Multiplies R0 and R1 and stores the result in R2.
// (R0, R1, R2 refer to RAM, RAM, and RAM, respectively.)
// The algorithm is based on repetitive addition.

@R2
M=0

@R0
D=M
@END
D;JEQ
@R1
D=M
@END
D;JEQ

@i
M=0
(LOOP)
    @i
    D=M
    @R1
    D=D-M
    @END
    D;JEQ

    @R0
    D=M
    @R2
    M=M+D
    @i
    M=M+1
    @LOOP
    0;JMP

(END)
    @END
    0;JMP
</code></pre>
<h3 id="白屏黑屏">白屏/黑屏</h3>
<p>这个程序里就能看出hack计算机的局限性了,因为只有两个寄存器,想把黑屏和白屏代码封装起来,就很有难度了。最后是黑屏写了一套代码,白屏写了一套代码,二者就在大循环里跑,也没有设置子循环,让程序完成渲染后处于监听键盘的状态。不过,至多就是按键的时间长一点就好了..</p>
<pre><code>// Runs an infinite loop that listens to the keyboard input.
// When a key is pressed (any key), the program blackens the screen,
// i.e. writes "black" in every pixel. When no key is pressed,
// the screen should be cleared.

(MAIN)
    @KBD
    D=M
    @BLACK
    D;JNE

    @WHITE
    0;JMP

    @MAIN
    0;JMP

(BLACK)
    @i
    M=0
    @j
    M=0
    @head
    M=0


    (BLACK-LOOP1)
      @i
      D=M
      @255
      D=D-A
      @MAIN
      D;JGT
   
      @j
      M=0
      (BLACK-LOOP2)
            @j
            D=M
            @31
            D=D-A
            @BLACK-LOOP1-END
            D;JGT

            @head
            D=M
            @j
            D=D+M
            @SCREEN
            A=A+D
            M=-1

            @j
            M=M+1
            @BLACK-LOOP2
            0;JMP

      (BLACK-LOOP1-END)
      @32
      D=A
      @head
      M=M+D
      @i
      M=M+1
      @BLACK-LOOP1
      0;JMP

(WHITE)
    ...
    M=0
    ...

(END)
    @END
    0;JMP
</code></pre>
<h2 id="总结">总结</h2>
<p>这一节贴了好多图,不过想想还是很有成就感的,我在学会一门语言之后,还可以把他表述出来。这节看懂了课程上的例子之后,难度就不大了,最后的白屏/黑屏作业也只是复杂,而不是困难。期待下节课的CPU实现!!</p><br><br>
来源:https://www.cnblogs.com/snowsteps/p/18115678
頁: [1]
查看完整版本: nand2tetris_hack汇编语言