首页 > 编程学习 > 《计算机是怎样跑起来的》之 体验一次手工汇编

几个小问题

  • 什么是机器语言?
    机器语言: 由二进制数字构成的程序, CPU 可以直接对其解释、执行。 不仅是汇编语言, 用C语言、Java等编程语言编写的程序, 也都需要先转换成机器语言才能被执行。 机器语言有时叫做“ 原生代码” (Native Code)

  • 通常把标识内存或 I/O 中存储单元的数字称为什么?
    表示内存或 I/O 中存储单元的数字叫作 “ 地址” 。 内存中有多个数据存储单元。 计算机用从 0 开始的编号标识每个存储单元, 这些编号就是地址(Address)。 I/O 寄存器也可以用地址来标识。 哪个寄存器对应哪个地址, 取决于 CPU 和 I/O 之间的布线方式。

  • CPU 中的标志寄存器(Flags Register)有什么作用?
    一旦执行了算数运算、逻辑运算、 比较运算等指令后, 标志寄存器并不会存放运算结果的值, 而是会把运算后的某些状态存储起来,例如运算结果是否为 0、 是否产生了负数、是否有溢出(Overflow)等。

本章的目标是通过编写程序使诸位亲身体验计算机的运行机制。为了达到这个目的,就需要使用一种叫作“汇编语言”的编程语言来编写程序,然后再把编好的程序通过手工作业转换成 CPU 可以直接执行的机器语言。

这样的转换工作叫做“ 手工汇编”(Hand Assemble)。

3.1 从程序员的角度看硬件

为了体验手工汇编, 下面我们就为之前制作的微型计算机编写一个程序, 因为程序的作用是驱动硬件工作, 所以在编写程序之前必须先了解微型计算机的硬件信息,真正㤇了解的硬件信息只有以下 7 种。在这里插入图片描述
【CPU(处理器)信息】

  • CPU 的种类
  • 时钟信号的频率

【内存信息】

  • 地址空间
  • 每个地址中可以存储多少比特的信息

【I/O 信息】

  • I/O 的种类
  • 地址空间
  • 连接着何种周边设备

可以使用哪种机器语言取决于 CPU 的种类。 所谓机器语言就是只是用 0 和 1 两个进制书写的编程语言。 即便是相同的机器语言, 例如 01010011, 只要 CPU的种类不同,对它的解释也就不同, 有的 CPU会把它解释成是执行加法运算, 有的 CPU 把它解释成向 I/O 输出。 不同的 CPU 对机器语言的解读是不同的。

我们之前所使用的 CPU是 Z80 CPU, 所有要使用适用于 Z80 CPU 的机器语言。 顾名思义, 机器语言就是处理器可以直接理解的编程语言。 机器语言有时也叫作原生代码(Native Code)。

所谓时钟信号的频率,就是由时钟发生器发送给 CPU 的电信号的频率, 表示时钟信号频率的单位是 MHZ。微型计算机使用的是 2.5MHz 的时钟信号。 时钟信号是在 0 和 1两个数之间反复变换的电信号, 就像滴答滴答右摆动的钟摆一样,通常把发出一次滴答的时间称作一个时钟周期。

在机器语言当中,指令执行时所需要的时钟周期数取决于指令的类型

程序员不但可以通过累加时钟周期数估算程序执行的时间
还可以仅在特定的时间执行点亮 LED(发光二极管)等操作。

时钟发生器赋予了计算机时间的概念, 就像钟表赋予了我们时间的概念,有了时间的概念, 进程等概念才能建立。

每个地址都标示着一个内存中的数据存储单元,而这些地址所构成的范围就是内存的地址空间, 在我们的微型计算机中,地址空间为 0~ 255(28), 每一个地址中可以存储 8比特的指令和数据。

连接着 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 机器语言和汇编语言

这段程序之前已经介绍过了, 功能是把由指拨开关输入的数据输入CPU, 然后CPU 再把输入数据原封不动地输出到 LED。 也就是说,通过拨动指拨开关控制 LED 的亮或灭。

在这里插入图片描述在这里插入图片描述

这段由 8个比特二进制构成的机器语言程序总共 23 个字节, 若把这些字节一一依次写入内存中,所占据的内存空间就是 00000000~ 00010110 。 一旦重置了 CPU, CPU就会从 0 号地址开始顺序执行这段程序。

在机器语言程序中,虽然到处都是 0 和 1的组合, 但是每个组合都是有特定含义的指令或数据, 可是对人来说, 如果只看 0 和 1的话,很难判断出各个组合都表示什么。

我们需要面向人类的语言

于是有人发明出了一种编程方法, 根据表示指令功能的英语单词起一个相似的昵称, 并将这个昵称赋予给 0 和 1 的组合。 助记符的诞生, 使用助记符的编程语言叫做 “ 汇编语言” 。

无论使用机器序言和还是汇编语言,所实现的功能都是一样的。

区别只在于程序是用于数字表示还是用助记符表示。

以上的机器语言可以转换成如下所示的汇编语言, 汇编语言的语法十分简单,以至于语法只有一个, 即把“标签” “操作码(指令)”和“操作数(指令的对象)”并排写在一行上,仅此而已。在这里插入图片描述

  • 标签的作用是为该行代码对应的内存地址起一个名字。 用汇编语言编程时可以在任何需要标签的地方“ 贴上” 名称任意的标签。 在上图中, 使用了名称为 “ LOOP:” 的标签。
  • 操作码就是表示“ 做什么” 的指令。 因为用助记符表示的指令是英语单词的缩写, 比如 LD 是 Load(加载)的缩写,所以较容易理解, 汇编语言中提供了多少种助记符, CPU 就有多少种功能。

Z80 CPU 的指令全部加起来有 70 条左右。 这里先把主要的指令列在表中, 在浏览的过程中请注意这些指令的分类, 按功能这些指令可以分成运算、 与内存的输入输出和与 I/O 的输入输出三类,这是因为计算机能做的事业只有输入、运算、输出这三种了。

  • 操作数表示的是指令执行的对象。 CPU 的寄存器、内存地址、I/O地址或者直接给出的数字都可以作为操作数

如果某条指令需要多个操作数, 那么它们之间要用逗号分割。 操作数的个数取决于指令的种类。 也有不需要操作数的指令,比如用于停止 CPU 运转的 HALT 指令。

将汇编语言语法总结一下:

操作(指令)发生的场所 - 操作作用于 -某个对象标签                           操作码       操作数

进制的转换

  • 机器语言: 二进制
  • 汇编语言; 十进制和十六进制。

这里是一些Z80 CPU 的主要指令:
在这里插入图片描述
在这里插入图片描述

3.3 Z80 CPU 的寄存器结构

我们之前讲过 CPU负责解释、执行程序, 从内存或 I/O 输入数据,在内部进行运算,再把运算结果输出到内存或 I/O , 内存中存放着程序, 程序是指令和数据的集合。 I/O 中临时存放着用于与周边设备进行输入输出的数据。

既然数据的运算是在 CPU中进行的, 那么在 CPU 中应该有存储数据的地方, 这种存储数据的地方叫做 “寄存器” ,虽然和 I/O 的寄存器都叫一个名字, 但是有着不同的地方, CPU的寄存器不仅能存储数据, 还具备对数据进行运算的能力。

CPU带有什么样的寄存器取决于 CPU 的种类

如下所示: ,A、B、C、D等字母是寄存器的名字。在汇编语言当中,可以将寄存器的名字指定为操作数。
在这里插入图片描述

IX、IY、SP、PC 这 4 个寄存器的大小是 16 比特,其余寄存器的大小都是 8 比特。寄存器的用途取决于它的类型。有的指令只能将特定的寄存器指定为操作数。

举例来说, A寄存器也叫做 ‘’累加器 ” , 是运算的核心。 所以连接到它上面的导线也一定会比其他寄存器多。

F 寄存器也叫作“标志寄存器”,用于存储运算结果的状态,比如是否发生了进位,数字大小的比较结果等。

PC 寄存器也叫作“程序指针”,存储着指向 CPU 接下来要执行的指令的地址。PC 寄存器的值会随着滴答滴答的时钟信号自动更新,可以说程序就是依靠不断变化的 PC 寄存器的值运行起来的。

SP寄存器 也叫作“栈顶指针”,用于在内存中创建出一块称为“栈”的临时数据存储区域。

寄存器有了一定的了解后。再来看一下之前代码的内容。

在这里插入图片描述

这段程序大体上可以分为两部分

  • 设定 Z80 PIO
  • 与 Z80 PIO 进行输入输出

Z80 带有两个端口(端口 A 和端口 B),用于与周边设备输入输出数据。 首先必须为每个端口设定输入输出模式。
在这里插入图片描述
在这里插入图片描述

  • 首先必须为每个端口设定输入输出模式

这里端口 A用于接收由指拨开关输入的数据, 为了实现这个功能, 需要如下代码。

LD A, 207  -- 把数值 207 写入到寄存器 A 中
OUT (2), A -- 把寄存器 A 的值写入到地址 2 (I/O所对应的 A控制寄存器)中
LD A, 255 -- 把数值 255 写入到寄存器 A中
OUT (2), A  -- 把寄存器 A的值写入到 地址 2中

这里的 207 和 255 是连续向 Z80 PIO 的端口 A控制寄存器(对应地址编号为 2)写入的两个数据, 虽然使用 OUT 指令可以向 I/O 写入数据,但是不能直接把 207、 255 这样的数字作为 OUT 指令的操作数, 操作数必须是已存储在 CPU 寄存器中的数字,这是汇编语言的规定

一旦把 207 写入到端口 A控制寄存器, Z80 PIO 就明白了(想要设定端口 A的输入输出模式)。通过接下来写入的 255, Z80 PIO 就又知道(要把端口 A设定为输入模式)。

端口 B设定为输出模式

LD A, 207
OUT (3), A
LD A, 0  --与 255对应,将端口 B 设定为输出模式
OUT (3), A

完成了 Z80 PIO 的设定后, 就进入了一段死循环处理, 用于把由直播开关输入的数据输出到 LED。

LOOP: IN A, (0)  --不断从地址 0 (端口 A数据寄存器)中读取数据,输入到 CPU的寄存器 A中OUT (1), A -- 把寄存器 A的值写入到地址 1 (B数据寄存器上)JP LOOP  --跳转到 LOOP 继续执行指令

补充:
地址 0 —— 端口 A数据寄存器
地址 1 —— 端口 B数据寄存器
地址 2 —— 端口 A控制寄存器
地址 3 —— 端口 B控制寄存器

3.4 追踪程序的运行过程

** 用汇编语言编写的程序是不能直接运行的,必须先转换成机器语言。

CPU只能理解机器语言, 无论是什么语言都必须最后转化为机器语言。

看下图所示,里面列出了实现转换出来的机器语言,以及对应的汇编语言。

1条汇编语言的指令所对应的机器语言由多个字节构成。
有的指令对应着 1 个字节的机器语言,有的指令则对应着多个字节的机器语言。 
汇编语言中的 1 条指令能转换多少条机器语言取决于指令的种类以及操作数的个数。

在这里插入图片描述

。代码清单 3.3 中第一个内存地址是 00000000(0 号地址),下一个地址是 00000010(2 号地址),中间隔了 2 个地址,这说明如果从 0 号地址开始存储一条 2 字节的机器语言,那么下一条机器语言就从 2 号地址开始存储。

在这里,我们假设机器语言的程序是向上图代码一样被存储到内存中。

  • 一旦重置了CPU, 00000000就会自动存储到 PC 寄存器中, 这意味着接下里 CPU 将要从 00000000号地址读出程序。

  • 首先 CPU 会从 0000000号地址 读出指令 00111110, 判断这是一个 2个字节构成的指令,紧接着寻找指令的对象(数据)

  • 接下来会从下一个地址读出数据 11001111,把这两个数据汇集到一起解释、执行。 执行的指令是把数值 207 写入到寄存器 A, 用汇编语言表示的话就是“LD A, 207”

由于刚刚从内存读出了一条 2 字节的指令(占用 2 个内存地址),所以 PC 寄存器的值要增加
2,并接着从 00000010 号地址读出指令,解释并执行。

接下来的流程与此类似,通过反复进行“读取指令”“解释、执行指令”“更新 PC 寄存器的值”这 3 个操作,程序就能运行起来了。一旦执行完最后一行的 JP LOOP 所对应的机器语言,PC 寄存器的值就会被设为标签 LOOP 对应的地址 00010000,这样就可以循环执行同样的操作。请诸位重点观察 PC 寄存器是如何控制程序流程的。

3.5 尝试手工汇编

在 CPU 的资料中,明确写有所有可以使用的助记符,以及助记符转换成机器语言后的数值。只要查看这些资料,就可以把用汇编语言编写的程序手工转换成机器语言的程序,这样的工作称为“手工汇编”。进行手工汇编时,要一行一行地把用汇编语言编写的程序转换成机器语言。

下面就实际动手试一试吧。表 3.2 列出了汇编语言中必要指令的助记符、助记符所对应的机器语言,以及执行这些机器语言所需的时钟周期数。在这里插入图片描述
下面就从汇编语言的第 1 行开始转换。第一行的“LD A, 207”匹配“LD A, num”这个模式,所以可以先转换成“00111110 num”。然后将十进制数的 207 转换成 8 比特的二进制数,用这个二进制数替换 num。使用 Windows 自带的计算器程序就可以很方便地把十进制数转换成二进制数。从 Windows 的开始菜单中选择“运行”,输入 calc 后点击“确定”按钮,就可以启动计算器程序。

可以理解为助记符 LD A ,num分为两部分组成,一部分表示为操作行为,比如往寄存器中输入数值(num):这里转化为 00111110 ,另一部分是操作对象(数值 num),然后将助记符中的 num(十进制数) 转化为机器语言的 num(二进制数)。

在计算器中, 207 的二进制为 11001111 ,至此, “LD A 207” 就转换成了机器语言 00111110 11001111 。 由于这条指令存储在内存最开始的部分(00000000号地址),因为它是第一条指令。所以要把这条指令和内存地址想像下面这样并排写下来。

在这里插入图片描述
第二部分一样,最终就得到了机器语言 “ 11010011 00000010” ,因为之前内存中已经存储了 2 自己解的机器语言, 所以这条机器语言从 00000010 号地址开始记录。
在这里插入图片描述
在这里, 内存中存储如下:在这里插入图片描述

一点重要知识

到最后一句的 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 使用小端序。Z80CPU 使用的是小端序,因此 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.5 MHz 的晶振, 也就是 1 s 可以产生 250 万个时钟周期。

所以每个时钟周期是 1秒 ÷ 250 万 = 0.0000004 秒 = 0.4 微秒。72 个时钟周期就是 72×0.4 = 28.8 微秒;32 个时钟周期就是 12.8 微秒。

这段程序是用 LED 的亮或灭来表示指拨开关的开关状态,所以 LOOP 标签之后所执行的操作“输入、输出、跳转”每 1 秒可以反复执行 1 秒 ÷12.8 微秒 / 次 = 78125 次之多,可见计算机的计算速度有多么惊人。


本文链接:https://www.ngui.cc/zz/1544911.html
Copyright © 2010-2022 ngui.cc 版权所有 |关于我们| 联系方式| 豫B2-20100000