计算机是怎样跑起来的-第3章 体验一次手工汇编

计算机是怎样跑起来的-第3章 体验一次手工汇编
本章的目标是通过编写程序使诸位亲身体验计算
机的运行机制。为了达到这个目的,就需要使用一种
叫作“汇编语言”的编程语言来编写程序,然后再把编好的程序通过手工作业转换成 CPU 可以直接执行的机器语言。
这样的转换工作叫作“手工汇编”(Hand  Assemble)。也许会有人觉得听起来就好像挺麻烦的,事实上也的确如此,但是还是希望所有和计算机相关的技术人员都能亲身体验一下用汇编语言编程和手工汇编。
这次体验应该能加深诸位对计算机的理解,使诸位犹如拨云见日,找到长期困惑着自己的问题的答案,不仅能因“我能看懂程序了”而获得成就感,更能因发现“计算机原来很简单啊”而信心倍增。虽然本章的主题稍有些复杂,但是笔者会放慢讲解的步伐,还请诸位努力跟上。
3.1 从程序员的角度看硬件
为了体验手工汇编,下面我们就为在第 2 章制作的微型计算机编写一个程序吧。因为程序的作用是驱动硬件工作,所以在编写程序之前必须要先了解微型计算机的硬件信息。然而真正需要了解的硬件信息只有以下 7 种(如图 3.1 所示),所以没有必要在编程时还总是盯着详细的电路图看。
在这里插入图片描述
在这里插入图片描述
机器语言就是只用 0 和 1 两个二进制数书写的编程语言。即便是相同的机器语言,例如 01010011,只要 CPU 的种类不同,对它的解释也就不同。有的 CPU 会把它解释成是执行加法运算,有的 CPU 会把它解释成是向 I/O 输出。这就好比同样是 man 这个词,有的人会理解成“慢”,有的人会理解成“男人”。
由于微型计算机上的 CPU 是 Z80 CPU,所以就要使用适用于 Z80 CPU 的机器语言。顾名思义,机器语言就是处理器可以直接理解(与生俱来就能理解)的编程语言。机器语言有时也叫作原生代码(Native Code)。
所谓时钟信号的频率,就是由时钟发生器发送给 CPU 的电信号的频率。表示时钟信号频率的单位是 MHz(兆赫兹 = 100 万回/秒)。微型计算机使用的是 2.5MHz 的时钟信号。时钟信号是在 0 和 1 两个数之间反复变换的电信号,就像滴答滴答左右摆动的钟摆一样。通常把发出一次滴答的时间称作一个时钟周期。
在机器语言当中,指令执行时所需要的时钟周期数取决于指令的类型。程序员不但可以通过累加时钟周期数估算程序执行的时间,还可以仅在特定的时间执行点亮 LED(发光二极管)等操作。
每个地址都标示着一个内存中的数据存储单元,而这些地址所构成的范围就是内存的地址空间。在我们的微型计算机中,地址空间为0~255,每一个地址中可以存储 8 比特(1 字节)的指令或数据。
连接着的 I/O 的种类,就是指连接着微型计算机和周边设备的 I/O的种类。在微型计算机中,只安装了一个 I/O,即上面带有 4 个 8 比特寄存器的 Z80 PIO。只要用 CPU 控制 I/O 的寄存器,就可以设定 I/O 的功能,与周边设备进行数据的输入输出。
所谓 I/O 的地址空间,是指用于指定 I/O 寄存器的地址范围。在Z80 PIO 上,地址空间为 0~3,每一个地址对应一个寄存器。
在内存中,每个地址的功能都一样,既可用于存储指令又可用于存储数据。而 I/O 则不同,地址编号不同(即寄存器的类型不同),功能也就不同。在微型计算机中,是这样分配 Z80 PIO 上的寄存器的:端口 A 数据寄存器对应 0 号地址,端口 B 数据寄存器对应 1 号地址,端口 A 控制寄存器对应 2 号地址,端口 B 控制寄存器对应 3 号地址。端口 A 数据寄存器和端口 B 数据寄存器存储的是与周边设备进行输入输出时所需的数据。其中,端口 A 连接用于输入数据的指拨开关,端口B 连接用于输出数据的 LED。而端口 A 控制寄存器和端口 B 控制寄存器则存储的是用于设定 Z80 PIO 功能的参数。
3.2 机器语言和汇编语言
请看代码清单 3.1 中列出的机器语言程序,这段程序在第 2 章中已经介绍过了,功能是把由指拨开关输入的数据输入 CPU,然后 CPU 再把这些数据原封不动地输出到 LED。也就是说,可以通过拨动指拨开关控制 LED 的亮或灭。
代码清单 3.1 点亮 LED 的机器语言程序
在这里插入图片描述
在这里插入图片描述
这段由 8 比特二进制数构成的机器语言程序总共 23 个字节。若把这些字节一个接一个地依次写入内存中,所占据的内存空间就是00000000~00010110。一旦重置了 CPU,CPU 就会从 0 号地址开始顺序执行这段程序。
在机器语言程序中,虽然到处都是 0 和 1 的组合,但是每个组合都是有特定含义的指令或数据。可是对人来说,如果只看 0 和 1 的话,恐怕很难判断各个组合都表示什么。
于是就有人发明出了一种编程方法,根据表示指令功能的英语单词起一个相似的昵称,并将这个昵称赋予给 0 和 1 的组合。这种类似英语单词的昵称叫作“助记符”,使用助记符的编程语言叫作“汇编语言”。无论是使用机器语言还是汇编语言,所实现的功能都是一样的,区别只在于程序是用数字表示,还是用助记符表示。也就是说,如果理解了汇编语言,也就理解了机器语言,更进一步也就理解了计算机的原始的工作方式。
代码清单 3.1 中的机器语言可以转换成如代码清单 3.2 所示的汇编语言。汇编语言的语法十分简单,以至于语法只有一个,即把“标签”“操作码(指令)”和“操作数(指令的对象)”并排写在一行上,仅此而已。
在这里插入图片描述
标签的作用是为该行代码对应的内存地址起一个名字。编程时如果总要考虑“这一行的内存地址是什么来着?”就会很不方便,所以在汇编语言中用标签来代替地址。用汇编语言编程时可以在任何需要标签的地方“贴上”名称任意的标签。在代码清单 3.2 所示的程序中,使用了名称为“LOOP”的标签。
操作码就是表示“做什么”的指令。因为用助记符表示的指令是英语单词的缩写,比如 LD 是 Load(加载)的缩写,所以多多少少能猜出其中的含义。汇编语言中提供了多少种助记符,CPU 就有多少种功能。Z80 CPU 的指令全部加起来有 70 条左右。这里先把主要的指令列在表 3.1 中,请诸位粗略地浏览一下。在浏览的过程中请注意这些指令的分类,按功能这些指令可以分成运算、与内存的输入输出和与 I/O 的输入输出三类。这是因为计算机能做的事也只有输入、运算、输出这三种了。
操作数表示的是指令执行的对象。CPU 的寄存器、内存地址、I/O地址或者直接给出的数字都可以作为操作数。如果某条指令需要多个操作数,那么它们之间就要用逗号分割。操作数的个数取决于指令的种类。也有不需要操作数的指令,比如用于停止 CPU 运转的 HALT指令。
汇编语言的语法和英语祈使句的语法很像。若对比英语的祈使句Give me money 和汇编语言的语句,就可以看出在英语的祈使句中,一开头放置了一个表示“做什么”的动词,这个动词就相当于汇编语言中的操作码。在动词后面放置了一个表示“动作作用到什么上”的宾语,这个宾语就相当于汇编语言中的操作数。因为程序的作用是向 CPU 发出指令,而且编程语言又是由说英语的人发明的,所以编程语言与英语祈使句类似也就不足为奇了。
构成机器语言的是二进制数,而在汇编语言中,则使用十进制数和十六进制数记录数据。若仅仅写出 123 这样的数字,表示的就是十进制数;而像 123H 这样在数字末尾加上了一个 H(H 表示 Hexadecimal,即十六进制数),表示的就是十六进制数。在代码清单 3.2 所示的程序
中,使用的都是十进制数。
在表 3.1 中有这样几条指令希望诸位注意。在第 2 章中介绍过, Z80 CPU 的 MREQ 引脚和 IORQ 引脚实现了一种能区分输入输出对象的机制,可以区分出使用着相同内存地址的内存和 I/O。在汇编语言中,读写内存的指令不同于读写 I/O 的指令。一旦执行了读写内存的指令,比如 LD 指令,MREQ 引脚上的值就会变为 0,于是内存被选为输入输出的对象;而一旦执行了读写 I/O 的指令,比如 IN 或 OUT 指令, IORQ 引脚上的值就会变为 0,于是 I/O(这里用的是 Z80 PIO)被选为输入输出的对象。
在这里插入图片描述
3.3 Z80CPU 的寄存器结构
这里先稍微复习一下第 2 章的内容。计算机的硬件有三个基本要素,CPU、内存和 I/O。CPU 负责解释、执行程序,从内存或 I/O 输入数据,在内部进行运算,再把运算结果输出到内存或 I/O。内存中存放着程序,程序是指令和数据的集合。I/O 中临时存放着用于与周边设备进行输入输出的数据。
复习就到这里,下面就来扩充所学到的知识吧。既然数据的运算是在 CPU 中进行的,那么在 CPU 内部就应该有存储数据的地方。这种存储数据的地方叫作“寄存器”。虽然也叫寄存器,但是与 I/O 的寄存器不同,CPU 的寄存器不仅能存储数据,还具备对数据进行运算的能力。CPU 带有什么样的寄存器取决于 CPU 的种类。Z80 CPU 所带有的寄存器如图 3.2 所示 A。A、B、C、D 等字母是寄存器的名字。在汇编语言当中,可以将寄存器的名字指定为操作数。
在这里插入图片描述
IX、IY、SP、PC 这 4 个寄存器的大小是 16 比特,其余寄存器的大小都是 8 比特。寄存器的用途取决于它的类型。有的指令只能将特定的寄存器指定为操作数。
举例来说,A 寄存器也叫作“累加器”,是运算的核心。所以连接到它上面的导线也一定会比其他寄存器的多。F 寄存器也叫作“标志寄存器”,用于存储运算结果的状态,比如是否发生了进位,数字大小的比较结果等。PC 寄存器也叫作“程序指针”,存储着指向 CPU 接下来要执行的指令的地址。PC 寄存器的值会随着滴答滴答的时钟信号自动更新,可以说程序就是依靠不断变化的 PC 寄存器的值运行起来的。SP寄存器也叫作“栈顶指针”,用于在内存中创建出一块称为“栈”的临时数据存储区域。
既然诸位已经熟悉了寄存器的功能,下面笔者就开始介绍代码清单 3.2 的内容。这段程序大体上可以分为两部分——“设定 Z80 PIO” 和“与 Z80 PIO 进行输入输出”。Z80 PIO 带有两个端口(端口 A 和端口 B),用于与周边设备输入输出数据。首先必须为每个端口设定输入输出模式。这里端口 A 用于接收由指拨开关输入的数据,为了实现这
个功能,需要如下的代码。
在这里插入图片描述
这里的 207 和 255 是连续向 Z80 PIO 的端口 A 控制寄存器(对应该 I/O 的地址编号为 2)写入的两个数据。虽然使用 OUT 指令可以向I/O 写入数据,但是不能直接把 207、255 这样的数字作为 OUT 指令的操作数。操作数必须是已存储在 CPU 寄存器中的数字,这是汇编语言的规定。
于是,先通过指令“LD A, 207”把数字 207 读入到寄存器 A 中,再通过指令“OUT (2), A”把寄存器 A 中的数据写入到 I/O 地址所对应的寄存器中。像“(2)”这样用括号括起来的数字,表示的是地址编号。端口 A 控制寄存器的 I/O 地址是 2 号。
一旦把 207 写入到端口 A 控制寄存器,Z80 PIO 就明白了:“哦,想要设定端口 A 的输入输出模式啊。”而通过接下来写入的 255,Z80 PIO 就又知道:“哦,要把端口 A 设定为输入模式啊。”
同样地,通过下面的程序可以将端口 B 设定为输出模式。
LD A, 207
OUT (3), A
LD A, 0
OUT (3), A
先把 207 写入到端口 B 控制寄存器(对应的 I/O 地址为 3 号),然后写入 0。这个 0 表示要把端口 B 设定为输出模式。应该使用什么样的数字设定端口,在 Z80 PIO 的资料上都有说明。用 207、255、0 这样的数字来表示功能设定参数,这也是为了适应计算机的处理方式。
完成了 Z80 PIO 的设定后,就进入了一段死循环处理,用于把由指拨开关输入的数据输出到 LED。为了实现这个功能,需要如下的代码。
LOOP: IN A, (0)
OUT (1), A JP LOOP
“IN A, (0)”的作用是把数据由端口 A 数据寄存器(连接在指拨开关上,对应的 I/O 地址为 0 号)输入到 CPU 的寄存器 A。“OUT (1), A”的作用是把寄存器 A 的值输出到端口 B 数据寄存器上(连接在 LED上,对应的 I/O 地址为 1 号)。
“JP LOOP”的作用是使程序的流程跳转到 LOOP(笔者随意起的一个标签名)标签所标识的指令上。JP 是 Jump 的缩写。“IN A, (0)”所在行的开头有一个标签“LOOP:”,代表着这一行的内存地址。正如刚才所讲的那样,在用汇编语言编程时,如果老想着“这一行对应的内存地址是什么来着?”就会很不方便,所以就要用“LOOP:”这样的标签代替内存地址。当把标签作为 JP 指令的操作数时,标签名的结尾不需要冒号“:”,但是在设定标签时,标签名的结尾则需要加上一个冒号,这一点请诸位注意。
3.4 追踪程序的运行过程
用汇编语言编写的程序是不能直接运行的,必须先转换成机器语言。机器语言是唯一一种 CPU 能直接理解的编程语言。从汇编语言到机器语言的转换方法将在稍后介绍,这里先来看一下代码清单 3.3,里面列出了事先转换出来的机器语言,以及对应的汇编语言。1 条汇编语言的指令所对应的机器语言由多个字节构成。而且,同样是汇编语言中的 1 条指令,有的指令对应着 1 个字节的机器语言,有的指令则对应着多个字节的机器语言。转换而成的机器语言有多少个字节取决于汇编语言指令的种类以及操作数的个数。代码清单 3.3 中第一个内存地址是 00000000(0 号地址),下一个地址是 00000010(2 号地址),中间跳过了 1 个地址,这说明如果从 0 号地址开始存储一条 2 字节的机器语言,那么下一条机器语言就从 2 号地址开始存储。
下面就一边看着代码清单 3.3,一边跟随着 CPU 解释、执行机器语言程序吧。在这里,我们假设机器语言的程序是像代码清单 3.3 那样被存储在内存中的。
一旦重置了 CPU,00000000 就会被自动存储到 PC 寄存器中,这意味着接下来 CPU 将要从 00000000 号地址读出程序。首先 CPU 会从00000000 号地址读出指令 00111110,判断出这是一条由 2 个字节构成的指令,于是接下来会从下一个地址(即 00000001,1 号地址,代码清单 3.3 中并没有标记出该地址本身)读出数据 11001111,把这两个数据汇集到一起解释、执行。执行的指令是把数值 207 写入到寄存器 A,用汇编语言表示的话就是“LD A, 207”。这时,由于刚刚从内存读出了一条 2 字节的指令(占用 2 个内存地址),所以 PC 寄存器的值要增加2,并接着从 00000010 号地址读出指令,解释并执行。
接下来的流程与此类似,通过反复进行“读取指令”“解释、执行指令”“更新 PC 寄存器的值”这 3 个操作,程序就能运行起来了。一旦执行完最后一行的 JP LOOP 所对应的机器语言,PC 寄存器的值就会被设为标签 LOOP 对应的地址 00010000,这样就可以循环执行同样的操作。请诸位重点观察 PC 寄存器是如何控制程序流程的。
代码清单 3.3 汇编语言与机器语言的对应关系
在这里插入图片描述
3.5 尝试手工汇编
在 CPU 的资料中,明确写有所有可以使用的助记符,以及助记符转换成机器语言后的数值。只要查看这些资料,就可以把用汇编语言编写的程序手工转换成机器语言的程序,这样的工作称为“手工汇编”。进行手工汇编时,要一行一行地把用汇编语言编写的程序转换成机器语言。下面就实际动手试一试吧。表 3.2 列出了汇编语言中必要指令的助记符、助记符所对应的机器语言,以及执行这些机器语言所需的时钟周期数。
表 3.2 从助记符到机器语言的转换方法
在这里插入图片描述
下面就从汇编语言的第 1 行开始转换。第一行的“LD A, 207”匹配“LD A, num”这个模式,所以可以先转换成“00111110 num”。然后将十进制数的 207 转换成 8 比特的二进制数,用这个二进制数替换num。使用 Windows 自带的计算器程序就可以很方便地把十进制数转换成二进制数。从 Windows 的开始菜单中选择“运行”,输入 calc 后点击“确定”按钮,就可以启动计算器程序。
接下来,从计算器的“查看”菜单中选择“科学型”,这样就得到了一个可以用十进制数或二进制数表示数字的计算器了。首先选中“十进制”单选框,然后输入 207,接下来选中“二进制”单选框,这样207 就变成了二进制数的 11001111(如图 3.3 所示)。至此,“LD A, 207”就转换成了机器语言 00111110 11001111。由于这条指令存储在内存最开始的部分(00000000 号地址),所以要把这条指令和内存地址像下面这样并排写下来
在这里插入图片描述
第 2 条指令“OUT (2), A”匹配“OUT (num), A”这个模式,所以可以先转换成“11010011 num”。然后把 num 的部分替换成 00000010,即用 8 比特的二进制数表示的十进制数 2,最终就得到了机器语言 “11010011 00000010”。因为内存中已经存储了 2 字节的机器语言,所以这条机器语言要从 00000010 号地址(用十进制表示的话就是 2 号地址)开始记录。
在这里插入图片描述
这之后由于 LD 指令和 OUT 指令又以相同的模式出现了 3 次,所以可以用相同的步骤转换成机器语言。请诸位注意,机器语言中每条语句的字节数是多少,内存地址就相应地增加多少。
在这里插入图片描述
接下来是“IN A, (0)”匹配“IN A, (num)”这个模式,所以可以先转换成“11011011 num”。然后把 num 替换成 00000000,即用 8 比特的二进制数表示的十进制数 0,最终就得到了机器语言“11011011 00000000”。对于接下来的“OUT (1), A”,也可以按照同样的方法转换。
在这里插入图片描述
最后一句的 JP LOOP 匹配模式“JP num”,所以可以先转换成
“11000011 num”。请注意这里要用 16 比特的二进制数替代作为内存地址的 num。在微型计算机中是以 8 比特为单位指定内存地址的,但在Z80 CPU 中用于设定内存地址的引脚却有 16 个,所以在机器语言中也要用 16 比特的二进制数设定内存地址。JP 指令跳转的目的地为00010000,即“LOOP:”标签所标示的语句“LD A, 0”对应的内存地址。把这个地址扩充为 16 比特就是“00000000 00010000”。要扩充到16 位,只需要把高 8 位全部设为 0 就可以了。
还有一点希望诸位注意,在将一个 2 字节的数据存储到内存时,存储顺序是低 8 位在前、高 8 位在后(也就是逆序存储)。这样的存储顺序叫作“小端序”(Little Endian),与此相反,将数据由高位到低位顺序地存储到内存的存储顺序则叫作“大端序”(Big Endian)。根据CPU 种类的不同,有的 CPU 使用大端序,有的 CPU 使用小端序。Z80 CPU 使用的是小端序,因此 JP LOOP 对应的机器语言为“11000011 00010000 00000000”。
在这里插入图片描述
手工汇编至此就结束了。自己写的汇编语言程序,又通过自己的双手转换成了机器语言,我们应该为此感到骄傲。
3.6 尝试估算程序的执行时间
在本章的最后,介绍一下如何通过时钟周期数估算程序的执行时间。请先向前翻到表 3.2,找出执行每条汇编语言指令所需的时钟周期数。然后把代码清单 3.2 中所用到的每条指令的时钟周期数累加起来。于是可以算出到 LOOP 标签为止的 8 条指令共需要 7+11+7+11+7+11+7+11 = 72 个时钟周期;LOOP 标签之后的 3 条指令共需要 11+11+10 = 32 个时钟周期。因为微型计算机采用的是 2.5MHz 的晶振,也就是 1 秒可以产生 250 万个时钟周期,所以每个时钟周期是 1 秒 ÷250万 = 0.0000004 秒 = 0.4 微秒。72 个时钟周期就是 72×0.4 = 28.8 微秒;32 个时钟周期就是 12.8 微秒。这段程序是用 LED 的亮或灭来表示指拨开关的开关状态,所以 LOOP 标签之后所执行的操作“输入、输出、跳转”每 1 秒可以反复执行 1 秒 ÷12.8 微秒 / 次 = 78125 次之多,可见计算机的计算速度有多么惊人。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值