Python-和-ARM-计算机架构实践指南-全-

Python 和 ARM 计算机架构实践指南(全)

原文:zh.annas-archive.org/md5/88a0660893ea657c4b774fbdd9fcdb49

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

计算机科学的根本线索是计算机架构。这个主题曾经被称为计算机硬件,它关注的是物理计算机本身;也就是说,中央处理单元CPU)、内存、总线和外设。计算机硬件与计算机软件相对立,后者适用于计算机执行的程序、应用程序和操作系统。

大多数用户对计算机硬件和架构的关注程度不亚于司机对车辆化油器操作的关心。然而,了解计算机架构在很多方面都是有用的,从如何高效地操作计算机到最大化其安全性。一个很好的类比是飞行员。他们学习如何驾驶飞机,而了解其发动机的工作原理被认为在处理异常情况、延长发动机寿命和最小化燃料消耗方面至关重要。

计算机架构是一个庞大的主题,大致分为三个相互关联的领域:add P,A,B,即将 A 加到 B 上,并将和放入 P。本书通过演示如何编写一个模拟计算机的程序来解释计算机架构。

计算机科学中处理计算机如何实现其架构动作的部分被称为计算机组织,这部分内容在很大程度上超出了本文的范围。计算机组织关注的是计算机的门电路和电路。

一本书无法全面地涵盖计算机的所有方面。在这里,我感兴趣的是探讨一个主题:指令集架构(ISA)。我将介绍计算机的指令集并解释其功能。我还会讨论不同类型的指令集;例如,大多数手机中使用的 ARM 处理器与 PC 和笔记本电脑核心的英特尔和 AMD 处理器非常不同。本书的第二部分,我们将专注于特定的计算机,并研究一个现实世界的架构,即 ARM 处理器。

这本书与众不同。有关于计算机架构的书,有关于 Python 的书,还有关于树莓派计算机的书。在这里,我们结合了这三个主题。然而,我并不是肤浅地处理这些主题,让读者对每个主题都只有浅薄和不满意的知识。

我的目的是介绍计算机架构及其指令集。也就是说,我将解释计算机在其原生指令(称为汇编语言)级别是如何工作的。我描述了指令的功能以及它是如何被读取、解释(即解码)和执行的。我还会讨论计算机实现的操作类型。

那么,Python 在这个体系中如何定位呢?Python 是一种流行的通用高级编程语言,可以在 PC、Apple Mac 和树莓派上免费使用。此外,Python 可能是最容易学习的计算机语言之一,而且功能非常强大。

人们通过实践来学习。我决定包含足够的 Python 代码,让读者能够构建一个简单的计算机模拟器,该模拟器可以读取机器级计算机指令并执行它。因为我将展示这个 Python 模拟器的工作原理,学生可以构建符合他们自己规格的计算机。他们可以实验指令集、寻址模式、指令格式等等。他们甚至可以根据自己的规格构建不同类型的计算机,例如,使用复杂指令集计算机CISC)或精简指令集计算机RISC)架构。CISC 和 RISC 提供了两种不同的计算机设计哲学。本质上,RISC 计算机具有固定长度的指令,只允许寄存器加载和存储内存操作,而 CISC 计算机可以具有可变长度的指令,并允许直接在内存上执行数据操作。实际上,RISC 和 CISC 之间的区别更为复杂。第一代微处理器都遵循 CISC 哲学。

读者可以构建计算机,因为他们可以用 Python 编写程序来执行特定计算机体系结构的目标语言,并且他们可以自己设计那种目标语言。

最受欢迎的计算机体系结构之一是 ARM 处理器,它被无数移动应用甚至苹果笔记本电脑所采用。这不仅是一个经济上占主导地位的处理器系列,而且由于其有趣和创新的架构以及相对平缓的学习曲线,它在教育领域也非常受欢迎。更好的是,这是低成本 Raspberry Pi 计算机所使用的处理器。您可以使用随计算机提供的软件工具在 Raspberry Pi 上运行 ARM 代码。您还可以使用免费软件在 Raspberry Pi 上运行 Python 程序。因此,Raspberry Pi 为学生提供了一个优秀的低成本机器,让他们能够以无需进一步投资硬件或软件的方式研究核心硬件主题。

这本书面向的对象

多年来,我一直在教授计算机体系结构,并使用模拟器来教授汇编语言。这种方法展示了指令做什么,但没有展示它们是如何做或如何设计、解码和执行的。我决定为课堂使用创建一个简单的指令模拟器。这本书就是从这个项目演变而来的。

我的目标受众可以分为四个主要群体,如下所示:

  • 正在修读计算机体系结构课程并希望通过通过模拟自己的 CPU 来增强他们对计算机体系结构的体验的学生。这种方法将加深他们对计算机体系结构的了解,并增强他们对计算机设计师所面临的权衡的理解。

  • 非计算机专业人士、普通人和那些想了解计算机工作原理的爱好者。通过使用 Python 作为设计语言并提供 Python 入门课程,我试图使本书对那些编程经验很少或没有经验的人变得易于理解。

  • 树莓派用户。树莓派对计算机科学教育产生了巨大的影响。本书简要介绍了树莓派,并展示了如何使用 ARM 的本地语言编写汇编语言程序。此外,树莓派还提供了一个环境(Python 及其工具),使读者能够理解和模拟计算机。

  • 想要学习 Python 的读者。尽管这不是一门正式的 Python 课程,但它提供了一个以目标为导向的 Python 入门介绍;也就是说,它将 Python 应用于实际示例。这种方法避免了传统课程的范围广泛,使得读者能够以相对较浅的学习曲线构建一个实际的应用程序。

  • 我并没有假设初学者读者对计算机一无所知。本书假设读者对二进制算术和数制以及布尔变量的基本概念有非常基础的了解。

本书涵盖的内容

  • 第一章**,从有限状态机到计算机,通过用于模拟简单控制系统的有限状态机引入了数字计算机的概念。从那里,我们引入了算法和程序的概念。一旦我们知道我们想让计算机做什么,我们就可以考虑我们需要实现计算机的内容。

  • 第二章**,Python 的高效入门,提供了在 Python 中实现计算机所需的基本背景知识。

  • 第三章**,计算机中的数据流,展示了在程序执行过程中信息在计算机周围流动的方式。当我们用软件模拟程序时,我们必须实现的就是这种数据流。

  • 第四章**,构建解释器——第一步,开始了通往模拟器的旅程。现在我们已经了解了计算机的概念和一点 Python,我们可以进一步描述计算机模拟器背后的基本思想。在本章中,我们还探讨了计算机指令的本质。

  • 第五章**,更多 Python 知识,扩展了我们对 Python 的了解,并介绍了诸如 Python 字典等关键主题,这些工具极大地简化了计算机模拟器的设计。

  • 第六章**,TC1 汇编器和模拟器设计,是本书的核心。在这里,我们讨论了模拟器的组件,然后提供了一个可以模拟假设教学计算机 TC1 的程序。

  • 第七章**,扩展 TC1,为模拟器添加了更多功能,例如数据检查和创建新指令。

  • 第八章**,其他架构的模拟器,探讨了不同类型的计算机架构并描述了替代的模拟器。

  • 第九章**,树莓派简介,改变了方向。在这里,我们探讨了流行的树莓派及其核心的 ARM 处理器。特别是,我们学习了如何将程序输入 ARM 汇编语言并在调试模式下运行它。

  • 第十章**,深入探讨 ARM,更详细地探讨了 ARM 的指令集,并为编写汇编语言程序提供了基础。

  • 第十一章**,ARM 寻址模式,更详细地探讨了 ARM 的寻址模式并解释了一些其特殊功能。

  • 第十二章**,子程序和堆栈,实际上是前一章的扩展,因为我们探讨了 ARM 如何使用其寻址模式来实现堆栈操作,这在汇编语言编程中非常重要。

为了充分利用本书

本书分为两部分。第一部分使用 Python 开发计算机模拟器,第二部分简要介绍了树莓派,并利用它作为教授 ARM 汇编语言编程的工具。

我使用装有 Windows 的 PC 来开发 Python 程序。读者可以使用基于 Windows 的系统、苹果 Mac 或任何基于 Linux 的计算机来开发 Python。所有必要的软件都是免费可用的。

当然,您可以使用树莓派本身来开发 Python。

为了编写 ARM 汇编语言程序并调试它们,您需要一个树莓派。这是一个单板计算机,需要电源、键盘、鼠标和显示器。我使用了树莓派 3 型号 A+和树莓派 4 型号 B 版本。

开发 Python 程序所需的软件可以从www.python.org免费获取。树莓派单板计算机不附带操作系统。您必须购买已安装操作系统的 SD 卡或自行下载。详细信息请参阅 https://www.raspberrypi.com/documentation/computers/getting-started.html。

如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的单词不是普通英语单词,而是属于程序中的单词。

break指令会退出while循环(即,执行将继续到循环的末尾之外 - 它是一种短路机制)。

为了吸引您对代码中功能的注意,我们有时会使用粗体字体或阴影来突出显示功能。考虑以下示例:

后面的文本使用非等宽字体,并且是计算机忽略的注释:


for i in range (0,6):         # Repeat six times 

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送给我们,邮箱地址为 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,我们将非常感激您提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供材料的链接。

如果您想成为一名作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《使用 Python 和 ARM 的实用计算机架构》,我们很乐意听到您的想法!请点击此处直接转到本书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。

优惠远不止这些,您还可以获得独家折扣、时事通讯和每日邮箱中的优质免费内容。

按照以下简单步骤获取优惠:

  1. 扫描下面的二维码或访问以下链接

下载此书的免费 PDF 副本

https://packt.link/free-ebook/9781837636679

  1. 提交您的购买证明

  2. 就这么简单!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱

第一部分:使用 Python 模拟计算机

在这部分,你将介绍两个主题:数字计算机和编程语言 Python。本书的目的是通过在软件中构建计算机来解释计算机是如何工作的。因为我们假设读者没有 Python 知识,所以我们将提供 Python 的介绍。然而,我们只涉及那些与构建计算机模拟器相关的主题。计算机的结构和组织以及 Python 编程的主题是交织在一起的。一旦我们介绍了 TC1(教学计算机 1)模拟器,最后两章将首先探讨增强模拟器功能的方法,然后查看替代架构的模拟器。

本节包括以下章节:

  • 第一章**,从有限状态机到计算机

  • 第二章**,Python 快速入门

  • 第三章**,计算机中的数据流

  • 第四章**,构建解释器 – 第一步

  • 第五章**,更多 Python 入门

  • 第六章**,TC1 汇编器和模拟器设计

  • 第七章**,扩展 TC1

  • 第八章**,其他架构的模拟器

第一章:从有限状态机到计算机

在本章中,你将发现计算机的基本本质。我们的目标是解释是什么让计算机成为计算机。这些概念很重要,因为在你理解计算机的工作原理之前,你必须欣赏其顺序性的影响。

一旦我们介绍了数字系统的概念,下一章将演示计算机如何通过从内存中获取指令并执行它们来运行。之后,我们将介绍 Python,并展示如何编写程序来模拟计算机并观察其操作。这本书完全是关于通过实践学习的;通过用软件构建计算机,你将学习它是如何运行的以及如何扩展和修改它。

本书剩余部分将探讨一台真实的计算机,树莓派(Raspberry Pi),并展示如何为其编写程序并观察它们的执行。在这个过程中,我们将从模拟假设的计算机转向学习真实的计算机。

计算机是一个确定性符号处理机器。但这究竟意味着什么?确定性告诉我们,当计算机在相同条件下操作时(即,程序和输入),它总是以相同的方式行为。如果你用计算机来评估√2,无论你执行操作多少次,你都会得到相同的结果。并非所有系统都是确定性的——如果你抛硬币,正面和反面的序列是不可预测的。

当我们说计算机是一个符号处理机器时,我们的意思是它接受符号并对它们进行操作,以提供新的符号作为输出。这些符号可以是任何可以用数字形式表示的东西:字母、单词、数字、图像、声音和视频。考虑一台正在下棋的计算机。程序接收到的输入符号对应于玩家的移动。程序根据一组规则对输入符号进行操作,并产生一个输出符号——计算机的移动。

尽管我们刚刚提供了一个计算机的理论定义,但重要的是要认识到编程涉及将现实世界中的信息转换为计算机可以操作的符号——编写一组规则(即,程序),告诉计算机如何操作这些符号,然后将输出符号转换为对人类有意义的格式。计算机处理的符号对计算机本身没有固有的意义——一定的比特模式(即,符号)可能代表一个数字、一个名字、游戏中的移动等等。计算机处理这些比特以产生一个新的比特模式(即,输出符号),这种模式只对程序员或用户有意义。

我们将提出一个简单的问题,并解决它。我们的解决方案将引导我们了解算法和计算机的概念,并介绍诸如离散数字操作、内存和存储、变量以及条件操作等关键概念。通过这样做,我们可以确定计算机需要执行哪些操作来解决一个问题。在此之后,我们可以问,“我们如何自动化这个过程?也就是说,我们如何构建一台计算机?”

这是一句陈词滥调,但一旦你理解了一个问题,你就已经找到了解决问题的方法。当你第一次遇到需要算法解决的问题时,你必须考虑你想要做什么,而不是你打算如何去做。解决问题的最糟糕的方法是在完全探索问题之前就开始编写算法(甚至实际的计算机代码)。假设有人要求你为汽车设计一个巡航控制系统。从原则上讲,这是一个非常简单的问题,解决方案同样简单:


IF cruise control on THEN keep speed constant
                     ELSE read the position of the gas pedal

这难道不是再简单不过了吗?好吧,如果你已经选择了巡航控制,而有人在你面前突然驶出,会发生什么?你可以刹车,但这个算法会试图在刹车的同时通过全油门来保持速度不变。或者,你可能会建议,刹车应该解除巡航控制机制。但是,巡航控制应该永久解除,还是汽车在刹车动作停止后应该加速到之前的速度?你必须考虑问题的所有方面。

即使你设计了一个正确的算法,你也必须考虑错误或虚假数据对你的系统产生的影响。对计算机最普遍的批评之一是,如果你向它们提供错误的数据,它们会产生无意义的结果。这个观点可以用“垃圾输入,垃圾输出”(GIGO)这个表达来概括。一个构造良好的算法应该能够检测并过滤掉输入数据流中的任何垃圾。

在本章中,我们将介绍以下主题:

  • 有限状态机

  • 算法解决问题

技术要求

你可以在 GitHub 上找到本章使用的程序,链接为github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter01

有限状态机

你需要了解的基本概念寥寥无几,才能理解数字计算机的功能及其工作原理。其中最重要的概念之一是离散,它既是计算机操作的核心,也是计算机程序的核心。

计算机操作的是离散数据元素——也就是说,其值是从一个固定且有限的值范围内选择的元素。我们在日常生活中使用离散值——例如,属于集合{A...Z}的罗马字母表中的字母。一个字母永远不会位于两个可能值之间。星期几也是如此;你可以有七天中的一天,但你不能有一个稍微是星期一或者稍微比星期三大一点的日子。在计算机的情况下,基本数据元素是比特,它只能有 0 或 1 的值,并且所有数据结构都是由 1 和 0 的字符串表示的。

除了离散的数据值,我们还可以有时间上的离散点。想象一下时间沿着一个方向移动,从一个离散点移动到另一个离散点。时间上的两个离散点之间不存在任何东西。这有点像从 12:15:59 到 12:16:00 跳转的数字时钟。中间没有任何东西。

现在,想象一下状态空间,这是一个宏伟的术语,用于描述系统可能处于的所有状态(例如,飞机可能处于爬升、下降或水平飞行状态)。状态有点像时间,不同之处在于你可以在状态空间中的离散点之间向前或向后移动。如果可能的状态数量有限,那么一个模拟状态之间转换的设备被称为有限状态机FSM)。电梯就是一个有限状态机:它有状态(楼层位置、门开或关等)和输入(电梯呼叫按钮、楼层选择按钮以及开门和关门按钮)。

在我们认真研究有限状态机之前,让我们从一个简单的例子开始,看看如何使用有限状态机来描述一个真实系统。考虑昨天的电视,这是一个有两个状态的设备:开启和关闭。它总是处于这两种状态之一,并且可以在这些状态之间移动。它永远不会处于既不是开启也不是关闭的状态。现代电视通常有三个状态——开启、待机和关闭——其中待机状态提供了一个快速开启机制(也就是说,电子设备的一部分处于活跃的开启状态,但显示和声音系统是关闭的)。待机状态通常被称为睡眠状态或空闲状态。我们可以使用图表来模拟离散状态。每个状态都由一个标记的圆圈表示,如图 1.1所示:

图 1.1 – 表示电视的三个状态

图 1.1 – 表示电视的三个状态

图 1.1显示了三个状态,但它没有告诉我们我们需要知道的最重要信息:我们如何在不同状态之间移动。我们可以通过在状态之间画线并标注触发状态变化的触发事件来实现这一点。图 1.2就是这样做的。请注意,我们将首先构建一个不正确的系统来展示一些关于有限状态机的概念:

图 1.2 – 表示电视状态及其转换

图 1.2 – 用转换表示电视的状态

图 1.2 中,我们通过触发事件的名称来标记每个转换;在每种情况下,都是按下遥控器上的按钮。要从关闭状态切换到开启状态,您必须首先按下待机按钮,然后按下开启按钮。要在开启和待机状态之间切换,您必须按下开启按钮或待机按钮。

我们忘记了一些事情——如果您已经处于某个状态并按下相同的按钮怎么办?例如,假设电视处于开启状态,您按下开启按钮。此外,初始状态是什么?图 1.3 纠正了这些遗漏。

图 1.3 有两个创新。有一个指向关闭状态的箭头标记为 开启电源。这条线表示您第一次将系统接入电源时系统进入的状态。图 1.3 的第二个创新是每个状态都有一个回到自身的循环;例如,如果您处于开启状态并按下开启按钮,您将保持在那个状态:

图 1.3 – 初始化后的电视控制

图 1.3 – 初始化后的电视控制

图 1.3 中所示的状态图既有逻辑错误也有人体工程学错误。如果您处于关闭状态并按下开启按钮会发生什么?如果您处于关闭状态,在这个系统中按下开启按钮是不正确的,因为您必须先进入待机状态。图 1.4 通过处理错误输入来纠正这个错误:

图 1.4 – 具有错误按钮纠正的电视控制

图 1.4 – 具有错误按钮纠正的电视控制

图 1.4 现在提供了从任何状态的正确操作,并包括按下不会改变状态按钮的效果。但是,我们仍然存在人体工程学错误——也就是说,这是一个正确的设计,但它的行为方式许多人会认为不好。待机状态是一种方便,可以加快操作速度。然而,用户不需要了解这种状态——它应该对用户不可见。

图 1.5 展示了控制器的最终版本。我们消除了待机按钮,但没有消除待机状态。当用户按下开启按钮时,系统直接进入开启状态。然而,当用户在开启状态下按下关闭按钮时,系统进入待机状态。从待机状态,按下开启按钮会导致开机状态,而按下关闭按钮会导致关机状态。请注意,同一动作(按下关闭按钮)可能根据当前状态产生不同的效果:

图 1.5 – 具有隐藏待机状态的电视控制

图 1.5 – 具有隐藏待机状态的电视控制

我们之所以详细讨论这个例子,是因为 FSM 的概念是所有数字系统的核心。所有数字系统,除了最简单的,都会根据当前输入和过去状态从一种状态转移到另一种状态。在数字计算机中,状态变化的触发器是系统时钟。现代计算机以 4 GHz 的时钟速度运行,每 0.25 x 10^-9 秒或每 0.25 纳秒改变一次状态。以 300,000 km/s(186,000 mph)的速度传播的光在时钟周期内大约移动 7.5 厘米或 3 英寸。

交通信号灯示例

让我们来看一个 FSM 的第二个例子。FSM 的一个经典例子是交叉路口的交通信号灯。考虑一个南北或东西方向交通流动的交叉口。交通可能一次只能在一个方向上流动。假设这是一个带有时钟的系统,并且每分钟允许状态变化:

当前灯光状态 南北方向交通 东西方向交通 下一个时钟周期要采取的行动 灯光的下一个状态
南北方向 无交通,无变化 南北方向
南北方向 一个或多个 东西方向,强制变化 东西方向
南北方向 一个或多个 南北方向,无变化 南北方向
南北方向 一个或多个 一个或多个 东西方向,强制变化 东西方向
东西方向 无交通,无变化 东西方向
东西方向 一个或多个 东西方向,无变化 东西方向
东西方向 一个或多个 南北方向,强制变化 南北方向
东西方向 一个或多个 一个或多个 南北方向,强制变化 南北方向

表 1.1 – 交通信号灯序列表

假设交通目前是南北方向流动。在下一个时钟周期,它可能继续保持南北方向流动,或者信号灯可能改变以允许东西方向交通。同样,如果交通是东西方向流动,在下一个时钟周期,它可能继续保持东西方向流动,或者信号灯可能改变以允许南北方向交通。

我们可以使用表 1.1来描述这个系统。我们提供了灯光的当前状态(交通流动方向),指示是否在南北或东西方向检测到任何交通,下一个时钟周期要采取的行动,以及下一个状态。交通规则很简单:除非有其他方向的待处理交通,否则信号灯保持当前状态。

我们现在可以将这个表格转换为图 1.6中所示的 FSM 图。请注意,我们将东西方向状态设置为开机状态;这是一个任意的选择:

图 1.6 – 表 1.1 的有限状态机

图 1.6 – 表 1.1 的有限状态机

我们学到了什么?最重要的观点是,在任何时刻,系统都处于特定的状态,并且根据定义的一组条件进行状态转换(或转换回当前状态)。有限状态机作为教学工具和设计工具都有几个优点:

  • 它使用一个简单直观的图来描述具有离散状态的系统

  • 状态转换图(如果正确)提供了一种完整且无歧义的方式来描述一个系统

  • 有限状态机也是一个抽象机,因为它模拟了一个真实系统,但我们不必担心有限状态机在真实硬件或软件中的实现

  • 任何有限状态机都可以在硬件或软件中实现;也就是说,如果你能在纸上定义一个状态图,你就可以在专用硬件中构建电路,或者编写在通用计算机上运行的程序

我包括了对有限状态机的简要介绍,因为它们可以被认为是数字计算机的先驱。有限状态机被设计用来执行特定任务;这是内置到其硬件中的。计算机具有有限状态机的某些特征,但你可以在状态之间编程转换。

以算法方式解决简单问题

现在我们已经介绍了有限状态机(FSM),我们将描述一个问题,解决问题,然后构建我们的计算机。一个袋子包含红色和白色标记的混合物。假设我们一次从袋子中取出一个标记,直到我们取出三个连续的红色标记。我们想要构建一个算法,告诉我们何时停止从袋子中取出标记,当检测到三个连续的红色标记时。

在创建算法之前,我们将为此问题提供一个有限状态机:

图 1.7 – 三标记检测器的有限状态机

图 1.7 – 三标记检测器的有限状态机

如您所见,这里有四种状态。我们从状态 S0 开始。每次接收到一个标记时,如果它是红色的,我们就移动到下一个状态,如果是白色的,就回到初始状态。一旦我们达到状态 S3,过程就结束了。现在,我们将以算法的方式执行相同的操作。

如果白色标记用符号 W 表示,红色标记用 R 表示,一个可能的标记序列可能是 RRWRWWWWRWWRRR(序列由三个 R 终止)。一个告诉我们何时停止取出标记的算法可以写成以下形式:

  • 第 1 行:从袋子中获取一个标记

  • 第 2 行:如果标记是白色的,则回到第 1 行

  • 第 3 行:从袋子中获取一个标记

  • 第 4 行:如果标记是白色的,则回到第 1 行

  • 第 5 行:从袋子中获取一个标记

  • 第 6 行:如果标记是白色的,则回到第 1 行

  • 第 7 行:成功 – 已从袋子中取出三个连续的红色标记

如您所见,该算法用普通英语表达。它是从上到下,逐行阅读的,并且每行指定的操作在处理下一行之前执行。在这个算法中,每一行都有一个独特的名称(即行 1、行 2 等)。给行标记使我们能够引用它;例如,当算法声明我们必须返回行 1 时,这意味着下一步要执行的操作由行 1 指定,并且我们从行 1 开始继续执行操作。这个算法并不完全令人满意 – 我们还没有检查袋子中是否只包含红色和白色标记,并且我们还没有处理在找到我们正在寻找的序列之前用完标记的情况。目前,我们并不关心这些细节。

这个问题没有唯一的解决方案 – 更多的时候,可以构建许多算法来解决给定的问题。让我们推导出另一个算法来检测三个连续红色标记的序列:

行 1:将迄今为止找到的连续红色标记的总数设置为 0

行 2:从袋子中取一个标记

行 3:如果标记是白色的,则返回行 1

行 4:将迄今为止找到的连续红色标记的数量加 1

行 5:如果连续红色标记的数量少于 3,则返回行 2

行 6:成功 – 已从袋子中取出 3 个连续的红色标记

这个算法更灵活,因为它可以很容易地通过更改算法中行 5 的 3 的值来检测任意数量的连续红色标记。

图 1.8 以流程图的形式直观地表示了这个算法,展示了执行算法时发生的操作序列。带箭头的线条表示操作的执行顺序。方框表示操作本身,菱形代表条件操作。菱形中的表达式评估为“是”或“否”,并且控制流向一个方向或另一个方向。一般来说,流程图非常适合表示简单的算法,但它们被认为不适合复杂的算法。复杂算法的流程图看起来像一碗意大利面 – 但没有意大利面固有的清晰性和组织性。

图 1.8 – 用流程图表示的算法

图 1.8 – 用流程图表示的算法

构建算法

下一步是提供一个 算法,它清楚地告诉我们如何明确无歧义地解决这个问题。当我们遍历数字序列时,我们需要跟踪正在发生的事情,正如 表 1.2 所展示的:

序列中的位置 0 1 2 3 4 5 6 7 8 9 10 11 12 13
新标记 R R W R W W W W R W W R R R
是红色吗? Y Y N Y N N N N Y N N Y Y Y
红色数量 1 2 0 1 0 0 0 0 1 0 0 1 2 3

表 1.2 – 随机选择的标记序列

REPEAT...UNTIL 是粗体的。当我们介绍 Python 时,我们将详细研究这些操作:


1.    Set numRed to 0
2.    Set maxRed to 3
3.    REPEAT
4.        Get a token
5.        IF its color is red
6.        THEN numRed = numRed + 1
7.        ELSE numRed = 0
8.    UNTIL numRed = maxRed

这段伪代码采用了许多高级计算机语言中常见的两种结构:第 3 到 8 行的 REPEAT...UNTIL 结构,以及第 5 到 7 行的 IF...THEN...ELSE 结构。REPEAT...UNTIL 允许你执行一次或多次动作,而 IF...THEN...ELSE 允许你在两种可能的行动之间进行选择。

IF...THEN...ELSE 结构是数字计算机操作的核心,你将在本书中多次遇到它。

下一步是介绍 Python,这样我们就可以编写一个程序来实现这个算法。然后,我们可以开始研究计算机。以下代码显示了一个 Python 程序及其执行时的输出。我们还没有介绍 Python。这个程序的目的在于展示它与前面的算法多么接近,以及 Python 的简单性:


Sequence =['W','R','R','W','R','W','W','W','W','R','W','W','R','R','R']
numRed = 0
maxRed = 3
count  = 0
while numRed != maxRed:
    token = sequence[count]
    if token == 'R':
        numRed = numRed + 1
    else: numRed = 0
    print('count', count,'token',token,'numRed',numRed)
    count = count + 1
print('Three reds found starting at location', count - 3)

while numRed != maxRed: 这一行表示 只要 (while*) numRed 的值不等于 maxRed,就执行缩进的代码块。Python 的 != 操作符表示 不等于

这是程序执行时的输出。它正确地识别了三个连续的红色,并指出了三个红色序列中第一个红色出现的位置:


count  0 token  W numRed 0
count  1 token  R numRed 1
count  2 token  R numRed 2
count  3 token  W numRed 0
count  4 token  R numRed 1
count  5 token  W numRed 0
count  6 token  W numRed 0
count  7 token  W numRed 0
count  8 token  W numRed 0
count  9 token  R numRed 1
count 10 token  W numRed 0
count 11 token  W numRed 0
count 12 token  R numRed 1
count 13 token  R numRed 2
count 14 token  R numRed 3
Three reds found starting at location 12

摘要

在本章中,我们通过有限状态机(FSM)的概念介绍了计算机。状态机是任何可以在任何时刻存在于几种状态之一的系统的抽象。状态机是在状态及其状态之间转换的术语中定义的。我们引入状态机作为数字系统的一个先导。状态机引入了离散状态和离散时间的概念。状态机在离散的时间点从一个状态移动到另一个状态。这反映了程序的行为,其中动作(状态改变)仅在执行指令时发生。

状态机可以模拟从交通灯到棋类游戏或计算机程序这样简单的系统。我们还介绍了算法的概念——即,用于解决问题的规则集。在本书的后面部分,我们将解释计算机如何实现算法。

第二章 中,我们将简要概述 n。我们选择这种语言是因为它有一个非常平缓的学习曲线,功能非常强大(用几行代码就能做很多事情),在许多大学中都有教授,并且可以在 PC、Mac 和 Raspberry Pi 系统上免费运行。

第二章:高速 Python 简介

本章介绍了 Python,并演示了如何编写 Python 程序来解决我们在 第一章 中描述的问题类型。在本章中,我们不会深入探讨 Python,但我们会足够地介绍 Python,以便您能够模拟和修改计算机以融入您自己的想法。

传统上,计算机程序被比作烹饪菜谱,因为它们是相似的。严格来说,这个陈述只适用于像 Python、Java 和 C 这样的过程式语言。像 Lisp 这样的函数式语言不符合这种严格的顺序范式,并且超出了本文的范围。

菜谱是一系列 操作(即动作或步骤)的序列,按照顺序在菜谱中使用的 原料 上执行。程序也是如此;它是一系列按照顺序执行的操作(指令)的序列。程序中的指令是按顺序、从上到下逐个执行的,排列得就像一页打印文本。也可以重复执行一组或指令块多次,甚至可以跳过或忽略指令块。

菜谱和程序之间的类比惊人地准确。在菜谱中,您可以有 条件 指令,例如,“如果酱太稠,则添加更多水。” 在编程中,您可以有条件指令,例如,“如果 x 为 0,则将 1 加到 y 上。” 同样,在烹饪中,您可以通过表达式如,“搅拌混合物直到变硬。” 来表达重复动作。在计算中,重复可以通过如,“从 z 中减去 1,直到 z = 0。” 这样的结构来表示。

本章我们将涵盖以下主题:

  • 阅读程序

  • Python 入门

  • Python 的数据类型

  • 数学运算符

  • 名称、值和变量

  • 注释

  • 列表 - Python 的一种关键数据结构

  • Python 中的函数

  • 条件操作和决策

  • 从文件中读取数据

  • 将令牌检测算法转换为 Python

  • 计算机内存

  • 寄存器传输语言

技术要求

您可以在 GitHub 上找到本章使用的程序,链接为 github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter02

编写 Python 程序并运行它的要求非常少。Python 是一种开源语言,可以在 PC、Mac 和 Linux 平台上免费使用。您需要设置计算机系统上的 Python 环境的所有信息都可以在主页 www.python.org 上找到。

令人惊讶的是,您不需要任何其他软件就可以在 Python 中构建计算机模拟器。Python 包自带 Python 的 集成学习与开发环境IDLE),它允许您编辑 Python 程序、保存它、运行它和调试它。

除了 IDLE,还有其他替代方案可以让您创建受 Python 平台支持的 Python 源文件。这些替代方案通常更复杂,针对的是专业开发者。在本章中,IDLE 已经足够使用,而且本书中的几乎所有 Python 程序都是使用 IDLE 开发的。

替代 IDE 包括微软的 Visual Studio Code、Thonny 和 Geany。所有这些 IDE 都是免费提供的。Thonny 是为 Raspberry Pi 开发的,我们将在后续章节中使用它:

对于初学者来说,阅读程序并不容易,因为你不知道如何解释你所看到的内容。以下部分将描述我们在本章中将要使用的一些排版和布局约定,以使程序的意义更加清晰,并突出程序的特点。

阅读程序

为了帮助您跟踪程序,我们采用了两种不同的字体——一种变宽字体(其中字母有不同的宽度,如这里的大部分文本)和一种单倍间距字体,如旧式机械打字机上的 Courier 字体 看起来 像这样

使用单倍间距字体表示代码的原因有两个。首先,它告诉读者这个词是计算机代码,而不仅仅是叙述文本的一部分。其次,在计算机程序中,间距对于可读性很重要,单倍间距字体能够整齐地排列相邻行中的字母和数字,形成列。以下是从后续章节中摘取的代码示例,以说明这一点。右侧带有 # 前缀的按比例间距文本表示该文本不是代码,而是一段普通语言的注释:


elif litV[0]   == '%': literal = int(litV[1:],2)     # If first % convert binary to integer
elif litV[0:2] == '0B':literal = int(litV[2:],2)     # If prefix 0B convert binary to integer
elif litV[0:1] == '$': literal = int(litV[1:],16)    # If $, convert hex string to integer
elif litV[0:2] == '0X':literal = int(litV[2:],16)    # If 0x convert hex string to integer

我们偶尔会使用阴影或粗体字体来区分代码片段的不同功能。例如,x = y.split('.') 使用粗体字体来强调 split 函数。

考虑以下示例。灰色文本表示 Python 中必要的保留词和符号,用于指定此结构。粗体黑体中的数字是程序员提供的值。# 后面的文本使用非单倍间距字体,是计算机忽略的注释:

for i in range (0,6):# 重复六次

在这个例子中,我们使用阴影来强调一个功能——例如,rS1 = int(inst[2][1]) 将你的注意力引向第一个参数 [2]

Python 入门

在我们更详细地了解 Python 之前,我们将简要介绍如何开始使用 Python,并演示一个简短的程序。尽管我们还没有介绍语言的基础,Python 程序却非常容易理解。

Python 是一种类似于 C 或 Java 的计算机语言,就像每一种计算机语言一样,它都有其优点和缺点。我们选择使用 Python,因为它免费且通用,有关它的信息量很大,最重要的是,它的学习曲线非常平缓。实际上,Python 已经成为计算机科学教育中最受欢迎的语言之一。

Python 是由荷兰计算机科学家 Guido van Rossum 构思的,稳定的 Python 2.0 版本于 2000 年发布。今天,Python 由 Python 软件基金会推广,这是一个志愿者的组织,其目标是将其作为公共服务来发展语言。

文本中的高级语言程序是在 PC 上用 Python 编写的。要安装 Python,您需要访问其主页www.python.org并遵循说明。由于 Python 不断增长,因此有多个版本。两个主要分支是 Python 2 和 Python 3;我将使用后者。Python 不断更新,并引入了新版本。我最初使用 Python 3.6,在撰写本文时,我们已达到 Python 3.11。然而,新的增量版本并没有带来根本性的变化,而且许多新特性在本书的 Python 版本中并未使用。

加载 Python 包为您提供了 Python 文档库、Python 解释器和名为 IDLE 的 Python 编辑器。所有这些都是免费提供的。IDLE 允许您进行一个循环,编辑程序、运行它并进行调试,直到您对结果满意。我还使用了免费提供的 Thonny IDE,我发现它甚至更容易使用。使用 IDE 可以让您开发程序、运行它,然后修改它,而无需在单独的编辑器、编译器和运行时环境之间切换。

在本书写作的后期,Graeme Harker 向我介绍了Visual Studio CodeVS Code)。这是一个由微软开发的流行的集成开发系统,支持包括 Python 在内的多种语言,并在包括 Windows、Linux 和 macOS 在内的多个平台上运行。VS Code 确实是一个非常强大的 IDE,并包括超越 IDLE 的功能。

考虑到图 2**.1中的 IDE(在这种情况下,IDLE)的例子。假设我们想要创建一个四功能计算器,它可以执行简单的运算,如 23 x 46 或 58 - 32。我选择这个例子,因为它实际上是一个非常简单的计算机模拟器。图 2**.1是在使用file函数加载 Python 程序后所截取的屏幕截图。您也可以直接从键盘输入 Python 程序。

如果我们点击+-/*,它将终止执行并打印一条消息。

与大多数高性能 IDE 系统一样,IDLE 使用颜色来帮助你阅读程序。Python 允许你在程序中添加注释,因为代码并不总是容易理解。任何跟随 # 符号的文本都会被忽略。在这种情况下,代码是可理解的,这些注释是不必要的。

在运行程序之前,你必须保存它。从 IDLE 保存程序会自动追加 .py 扩展名,因此名为 calc1 的文件将被保存为 calc1.py

图 2.1 —— IDLE 中 Python 程序的截图

图 2.1 —— IDLE 中 Python 程序的截图

图 2.1 展示了 Python 编程的布局,包括至关重要的 缩进,这是 Python 的一个关键特性,它指示哪些操作属于特定的命令。

下面是这个程序的简要描述。我们将在稍后更详细地介绍这些内容。while 关键字引导一组缩进的指令,这些指令会一直重复执行,直到某个条件不再为真。在这种情况下,只要 go 变量是 1,指令就会执行。

if 语句定义了一个或多个在 if 为真时执行的运算。在这种情况下,如果输入是 'E',则 go 变量被设置为 0,并且 while 后面的缩进操作不再执行。

break 指令会跳出 while 循环(即,执行会继续到循环的末尾——它是一种短路机制)。

最后,int(x) 函数将键盘字符转换为整数——例如,int('27') 将两个键,27,转换为整数 27。我们将在稍后更详细地讨论这些内容。

要运行程序,你选择 F5 键。以下演示了运行此程序的效果。粗体的文本是我使用键盘输入的:


Hello. Input operations in the form 23 * 4\. Type E to end.
Type first number 12
Type operator + or - or / or * +
Type second number 7
Result =  19
Type first number 15
Type operator + or - or / or * -
Type second number 8
Result =  7
Type first number 55
Type operator + or - or / or * /
Type second number 5
Result =  11
Type first number 2
Type operator + or - or / or * E
Program ended

这个简短演示的目的是展示如何输入和执行 Python 程序。实际上,没有程序员会编写前面的代码。这是低效的。它没有处理错误(如果你输入 @ 而不是 *,或者 $ 而不是 4 会发生什么?)。更糟糕的是,为了停止程序,你必须在输入 E 结束程序之前输入一个虚拟数字。我将这个任务留给你,将这个程序转换为更用户友好的版本。

我们已经演示了一个简单的 Python 程序。下一步是介绍 Python 操作的数据——也就是说,我们展示 Python 如何处理数字和文本。

Python 的数据类型

一个食谱使用的是属于不同组分的 原料(水果、蔬菜、坚果、奶酪、香料等)。计算机程序使用的是称为 类型 的数据。Python 使用的某些主要数据类型如下:

  • 整数:这使用的是整数,例如 0、1、2 等等。整数还包括负数 -1、-2、-3 等等。正整数也被称为自然数。

  • 浮点数:这些是带有小数点的数字(例如,23.5 和-0.87)。这些也被称为实数。令人惊讶的是,我们在这篇文本中不会使用实数。

  • 字符:字符是计算机键盘上的一个键——例如,QZa$@。计算机语言通常通过将字符放在引号中来表示它——例如,'R'。实际上,Python 没有显式的字符类型。

  • 'This''my program'。Python 没有字符类型,因为它将字符视为长度为 1 的字符串。Python 允许你单引号或双引号互换使用——例如,x = 'this'x = "this"在 Python 中是相同的。这种机制允许你输入x = ''The boy’s books''(即,你可以使用撇号而不会被当作引号处理)。在计算机科学中,术语字符串类似于英语中的单词。字符串是任何字符序列——例如,'time''!££??'是合法的 Python 字符串。回想一下,长度为 1 的字符串是一个单个字符——例如,'Q'。你可以有一个长度为零的字符串——也就是说,''。这里,你有两个引号之间没有任何内容。这表示一个空字符串。

  • TrueFalse。布尔类型用于布尔逻辑表达式。它也用于比较——例如,英语表达式,“x是否大于y?”有两个可能的结果——True 或 False。如果你在 Python 中输入print(5 == 5),它将打印 True,因为==意味着“是否相同?”,结果是 True。

在程序中使用的每个数据元素都会被赋予一个名称,这样我们就可以访问它。程序员为变量选择名称,一个好的程序员会选择有意义的名称来表示变量的含义。没有人会称一个变量为qZz3yT。名称必须以字母开头,然后可以跟任何字母和数字的序列——例如,alanalan123a123lan是合法的 Python 名称,而2AlanAl@n则不是合法的。然而,名称中可以使用下划线——例如,time_one。按照惯例,Python 使用小写字母作为变量和函数名称的第一个字符。大写字母开头保留用于类名称(在本文本中我们将不会使用)。这种限制是一种编程约定——也就是说,如果你给变量起一个以大写字母开头的名称,不会发生错误。

现在我们已经介绍了数据元素,接下来我们将描述一些可以应用于数值数据的操作。这些操作本质上是我们高中时遇到的算术运算符,除了除法,它有三种变体。

数学运算符

计算机可以执行四种标准的算术运算——加法、减法、乘法和除法。例如,我们可以写出以下内容:


X1 = a + b    Addition
X2 = a – b    Subtraction
X3 = a * b    Multiplication
X4 = a / b    Division

乘号的符号是(星号),而不是传统的x。使用字母x来表示乘法会导致字母x和乘法运算符x之间的混淆。

除法比乘法更复杂,因为表达 x ÷ y 的结果有三种方式。例如,15/5 等于 3,结果是一个整数。17/5 可以有两种表达方式——作为一个分数值(即浮点数)3.4,或者作为 3 余 2。在 Python 中,如果结果不是整数,除法运算符会提供一个浮点数结果(例如,100/8 = 12.5)。

除了除法运算符 / 之外,Python 还有两个其他除法运算符——//%

Python 的 // 是一个地板整数除法,通过向下取整生成一个整数结果。例如,操作 x = 17//5 得到的结果是 3,小数部分(余数)被舍弃。注意,x = 19//5 得到的结果是 3,因为它向下取整,尽管 4 是最接近的整数。如果你执行 x = -19//5,那么结果将是 -4,因为它向下取整到最接近的整数。

% 符号是模数运算符,提供除法后的余数。例如,x = 17%5 得到的结果是 2。假设,周一有人说他将在 425 天后拜访你。那将是星期几?425%7 是 5,这表示星期六。我们可以使用 % 来测试一个数字是奇数还是偶数。如果你写 y = x%2,那么如果数字是偶数,y0,如果是奇数,y1

名称、值和变量

计算机在它们的内存中存储数据。每个数据元素都与两个值相关联——它在哪,以及它是什么。在计算机术语中,“在哪”对应于数据在内存中的位置(地址),而“它是什么”对应于该数据的值。这并不是什么火箭科学,这与日常生活相符——例如,我可能有一个账号编号为 111023024,里面有 890 美元。在这里,111023024 是“在哪”,而 890 美元是“它是什么”。

现在,假设我写 111023024 + 1。我究竟是什么意思?我是想将 1 加到账号编号上得到 111023025(这是一个不同的账号),还是想将 1 美元加到编号为 111023024 的这个账号中,得到 891 美元?在日常生活中,这是如此明显,以至于我们甚至没有考虑过。在计算机中,我们必须更加小心,并清楚地思考我们在做什么。

数据元素被称为变量,因为它们的值可以改变。变量在内存中有一个地址(位置)。程序员不必担心数据的实际位置;操作系统会自动为他们处理。他们只需要为变量想一个名字。例如,让我们看看以下:


totalPopulation = 8024

当你写下这段代码时,你定义了一个名为totalPopulation的新变量,并告诉计算机在该位置存储数字8024。你不必担心完成此操作所涉及的所有动作;那是操作系统和编译器的工作。

让我们总结一下。变量是一个名称,它指向存储在内存中的某个值。假设你写下以下内容:


totalPopulation = totalPopulation + 24

这里,计算机读取分配给名称totalPopulation的内存位置中的当前值。然后,它将 24 加到这个值上,最后将结果存储在totalPopulation中。

在下一节中,我们将更详细地探讨用户注释和程序布局。

注释

由于计算机语言可能对人类读者来说既简短又令人困惑,它们允许你添加计算机忽略的注释(即,它们不是程序的一部分)。在 Python 中,同一行中跟随#符号的任何文本都会被忽略。在以下示例中,我们将该文本放在不同的字体中以强调它不是程序的一部分。首先,考虑以下 Python 表达式:


hours = 12           # Set the value of the variable hours to 12

这段代码创建了一个名为hours的新数据元素,并给它赋了整数值12图 2**.2展示了这一行的结构。

图 2.2 – 带注释的语句结构

图 2.2 – 带注释的语句结构

假设你随后写下以下内容:

allTime = hours + 3    # Add 3 to the hours and assign a result to a new variable, allTime

然后,计算机将读取表达式右侧名称(即hours)的值,并用 12 替换其值。然后,它会加 3 得到 12 + 3 = 15,并将这个新值存储在名为allTime的内存位置中。#后面的文本被计算机忽略,仅用于帮助人类理解程序。

许多计算机语言使用=符号的方式与我们高中所学的方式不同。在学校代数中,=表示“等于,”,所以x = y + 2 意味着x和(y + 2)的值是相同的。如果y的值是 12,那么x的值就是 14。

在编程中,语句x = y + 2表示计算y + 2 的值并将其转移x。如果一个程序员写下x = x + 2,这意味着要将 2 加到x上。如果这是一个数学方程式,我们可以写成xx = xx + 2,这简化为无意义的表达式 0 = 2。在 Python 中,=符号并不表示等于!

在 Python 中,=符号表示“赋值”。一个更好的符号是,这样x y + 2 就能清楚地表达我们所做的操作。一些语言,如 Pascal,使用符号对:=来表示赋值。遗憾的是,回箭头不在键盘上,我们只能坚持使用=。请注意,在 Python 中,一个语句是一个命令,如print()或一个赋值,如x = 4.。一个表达式是由变量或值和操作组合而成,返回一个结果。

正如我们所见,Python 有一个特殊的符号表示“等于”,这个符号是‘’。永远不要混淆=和。很容易写成if x = 5而不是if x == 5,然后 wonder 为什么你的程序给出了错误的结果。

考虑以下简单的 Python 程序示例,其中我们计算圆的面积。名称radiuspiarea都是由程序员选择的。我们也可以使用其他名称,但这些对读者来说更直观:


radius = 2.5                # Define the radius as 2.5
pi = 3.142                  # Define the value of pi
area = pi*radius*radius     # Calculate the area of a circle
print(area)                 # The print function prints the value of the area on the display

回想一下,Python 的名称由字母和(可选的)数字以及下划线组成,但名称中的第一个字符必须是字母而不是数字。在 Python 中,大写和小写字母被视为不同——也就是说,tesT22test22是不同的名称。选择有意义的名称是一种良好的实践,因为这使得阅读程序更加容易。考虑以下示例:


wagesFred = hoursWorkedFred*12 + hourOvertimeFred*18

前面的表达式对人类读者来说可能有些冗长,但很清晰。这是一个骆驼命名法的例子,这是一种用于没有空格连接的单词的流行术语,使用大写字母来表示链中的每个单词,例如asWeAreDoingNow。记住,Python 的变量和标签不以大写字母开头(这是一个约定而不是要求)。

数据元素分为两类——常量和变量。一个常量在首次命名时被赋予一个值,并且该值不能被更改——例如,如果你在 C 语言等语言中将x = 2定义为常量,则x的值是固定的(例如,表达式x = 3将是非法的)。一个变量是可以被修改的元素。Python 不支持常量。

让我们计算前n个整数的和,s = 1 + 2 + 3 + … + n。代数告诉我们,n个整数的和是n(n + 1)/2。我们可以将其转换为以下 Python 程序:


n = int(input("How many integers are you adding? "))
s = n * (n + 1)/2
print("Sum of ",n, " integers is",s)    # We used double quotes " instead of single

这三个 Python 程序行请求一个整数,执行计算,并打印结果。假设我们不知道前n个整数的和的公式。我们可以通过逐个相加来硬算。完成这个任务的 Python 代码在列表 2.3中给出。

我们还没有详细涵盖这个程序的所有方面。这里给出是为了展示 Python 的基本简单性。然而,请注意,Python 允许你同时从键盘请求输入并提供提示。

图 2.3 - 一个用于计算前 n 个整数的 Python 程序

图 2.3 - 一个用于计算前 n 个整数的 Python 程序

我们接下来要介绍 Python 的关键元素之一——一个功能强大且灵活的特性,这使得 Python 成为一门流行的语言,尤其是对于初学者。我们将描述列表。

列表——Python 的关键数据结构

现在我们介绍 Python 的列表,它将一系列元素组合在一起。我们展示了如何访问列表的元素。由于列表是一个关键特性,我们将多次回到它,每次都介绍新的特性。

数据结构是有序数据元素的集合。术语有序表示数据元素的位置很重要——例如,在日常生活中,一周是有序的,因为天数的顺序是固定的。表格队列列表堆叠文件数组都体现了数据结构的概念。在英语中,一袋书意味着一个无序的物品集合。Python 确实有一个专门用于无序项的特殊数据结构,称为集合。在本章中,我们不会使用集合。

就像任何其他对象一样,程序员会给列表赋予一个名称。以下 Python 列表给出了一个学生连续八次考试成绩的结果,以百分比表示:


myTest = [63, 67, 70, 90, 71, 83, 70, 76]

我们已经给列表起了一个有意义的名称,myTest。在 Python 中,列表用方括号括起来,项目之间用逗号分隔。列表可以增长(向其中添加项目)和缩小(从中删除项目)。你可以有一个空列表——例如,abc = []

列表的全部意义在于能够访问单个项目并对其进行操作——例如,我们可能想要找到最高分或最低分,或者在myTest列表中找到平均值。因此,我们需要指定一个项目的位置。数学家使用下标来完成这项任务——例如,他们可能会写myTest5。不幸的是,计算机科学是在机械信使打字机脚本黑暗时代出现的,当时没有不同的字体、颜色、斜体样式和下标。因此,我们必须使用所有键盘上都有符号来表示列表中的位置。

Python 通过括号来表示列表(或其他数据结构)中一个项目的位置——例如,myTest中的第 3 个元素表示为myTest[3]。列表的第一个元素位置是 0,因为计算机从 0 开始计数。myTest中的第一个值是myTest[0],即 63。同样,最后一个结果是myTest[7],即 76。我们共有 8 个结果,编号从 0 到 7。

Python 比某些语言更灵活。在 Python 中,列表是一个“通用的”数据结构。如果你想在列表中放置不同类型的项,你可以这样做。其他计算机语言要求列表中的项必须是相同的(例如,所有整数、所有浮点数或所有字符串)。以下示例展示了 Python 中一些合法的列表。正如你所见,你甚至可以有一个列表的列表:


roomTemps = [15,17,14,23,22,19]                       # A list of six integers
words     = ['this', 'that', 'then']                  # A list of three strings
x47       = [x21,timeSlot,oldValue]                   # A list of three variables
opCodes   = [['add',3,4],['jmp',12,1],['stop',0,0]]   # A list of three lists
mixedUp   = [15,time,'test',x[4]]                     # A list of mixed elements
inputs    = []                                        # An empty list

注意,我们在左边列中对等号进行了对齐。这不是 Python 的特性,也不是必需的。我们这样做是为了使代码更容易阅读,并使调试更容易。

让我们回到购物清单的例子,并将这个列表命名为veg1。我们可以用以下方式设置这个列表:


veg1 = ['potatoes', 'onions', 'tomatoes']         # A list of three strings

这个列表中的各个项目都在引号中。为什么?因为它们是字符串——即文本。如果没有引号,项目将引用之前定义的变量,如下所示:


opClass = 4                      # Define opClass
addOp   = ['add', opClass]       # A list with an instruction name and its class

切片列表和字符串

在这里,我们将演示如何从一个字符串或列表中提取数据元素(即切片)。字符串的各个字符可以访问——例如,如果 y = 'absde',那么 y[0] = 'a',y[1]= 'b',y[2]= 's',以此类推。

Python 允许您使用 -1 索引 选择字符串或列表中的最后一个元素——例如,如果您写 x = y[-1],则 x = 'e'。同样,y[-2]'d'(即倒数第二个)。

您可以使用 list[start:end] 符号选择字符串或列表的切片。由 [start:end] 指定的切片选择从 startend - 1 的元素,即,您指定的结束是所需最后一个元素之后的元素。如果 q = 'abcdefg',那么 r = q[2:5] 切片将 r 赋值为 'cde'

表达式 z = q[3:] 指的是从 3 到列表末尾的所有元素,并将 'defg' 赋值给 z。这是一个非常实用的特性,因为它允许我们从文本字符串,例如 s='R14' 中提取寄存器的数值,使用 t=int(s[1:])。表达式 s[1:] 返回从第二个字符到末尾的子字符串——即,"R14" 变成 "14"。这个值是一个字符串类型。int('14') 操作将字符串类型转换为整数类型并返回 14。

len 函数返回字符串的长度。如果 y='absde',则 p=len(y) 返回 5,因为字符串中有 5 个字符。

考虑一个汇编语言指令列表 – ops=['ADD', 'MUL', 'BNE', 'LDRL']。如果我们写 len(ops),我们得到结果 4,因为 ops 列表中有 4 个字符串。假设我们写 x=ops[3][2]ops[3] 的值是 'LDRL',而 LDRL[2] 的值是 'R'。换句话说,我们可以从字符串列表中提取一个或多个字符。x[a][b] 的表示法意味着取列表 x 中的项目 a,然后取该元素的项 b

我们已经使用了 print()len() 这样的函数。现在我们将更详细地讨论函数。稍后,我们将向您展示如何定义自己的函数以及如何使用 Python 的内置函数。

Python 中的函数

在继续之前,我们需要介绍高级语言中函数的概念。在高中数学中,我们遇到像 sqrt(x) 这样的函数,它是 square root 的缩写,并返回 x 的平方根——例如,sqrt(9) = 3。计算机语言借鉴了同样的概念。

Python 提供了语言内置的函数。你调用一个函数来执行特定的操作(这在现实世界中有点像分包)——例如,len()操作字符串和列表。如果你用列表x调用len(x),它将返回该列表中的项目数量。考虑以下情况:


toBuy = len(veg1)    # Determine the length of list veg1 (number of items in it)

这将我们称为veg1的列表中的项目数量取出来,并将该值复制到toBuy变量中。执行此操作后,toBuy的值将是整数 3,因为veg1中有 3 个项目。

假设你写下以下内容:


q = 'abcdefgh'
print(len(q))

然后,打印的数字是 8,因为字符串q中有八个字符。

假设我们还有另一个食物项目的列表:


fruit1 = ['apples', 'oranges', 'grapes', 'bananas']

让我们把两个列表中的项目数量加起来。我们可以用以下方法做到:


totalShopping = len(veg1) + len(fruit1)

这个表达式计算两个列表的长度(分别得到整数34),将它们相加,并将值7赋给totalShopping变量。你可以通过使用print()函数在屏幕上打印这个值:


print('Number of things to buy ', totalShopping)

这个表达式由 Python 单词print组成,它在屏幕上显示某些内容。print使用的参数被括号包围,并用逗号分隔。用引号括起来的参数是一个字符串,并且按原样打印。记住,Python 允许你使用单引号或双引号。变量参数按其值打印。在这种情况下,totalShopping参数的值是7。这个表达式将显示以下内容:

Things to buy 7

考虑以下表达式(我们已用阴影标出要打印的文本)。将要打印的变量值用粗体表示以增强理解:

print('Total items to buy', totalShopping, 'These are: ', fruit1)

这会打印什么?输出将是以下内容:


Total items to buy 7 These are: ['apples', 'oranges', 'grapes', 'bananas']

这就是你预期的结果吗?你可能期望看到一个按苹果、橙子、葡萄顺序排列的列表。但你所要求的是fruit1 Python 列表,这正是你得到的结果(这就是为什么有括号,列表中每个项目周围都有引号)。如果我们想以apples, oranges, grapes, bananas的形式打印列表,我们首先必须将字符串列表转换成一个单独的字符串项(我们稍后会讨论)。然而,对于你们中间的急躁者,这里是如何使用join()函数完成的:


fruit1 = ['apples', 'oranges', 'grapes', 'bananas']
print('Fruit1 as list = ', fruit1)
fruit1 = (' ').join(fruit1)                # This function joins the individual strings
print('Fruit1 as string = ', fruit1)

这段代码的输出如下:


Fruit1 as list =  ['apples', 'oranges', 'grapes', 'bananas']
Fruit1 as string =  apples oranges grapes bananas

接下来,我们将讨论使计算机成为计算机的非常项目——根据测试结果选择两个或更多可能行动路线的条件操作。

条件操作和决策

现在是时候讨论大问题——条件操作。让我们考虑以下食谱:

  1. 拿两个鸡蛋。

  2. 分离蛋白。

  3. 加入糖。

  4. 打发至发硬。

这些动作是按顺序执行的。前三个动作是简单的操作。第四个动作与前三个非常不同,正是这个动作赋予了计算机其力量。这是一个根据测试结果执行两种动作之一的操作。在这种情况下,鸡蛋被搅拌,然后测试。如果它们不够硬,搅拌会继续,然后重新测试,依此类推。

计算机之所以强大,是因为它具有做出决策的能力。如果没有 决策能力,计算机就无法对其环境做出反应;它只能反复执行相同的计算。人类智能的基础也是决策。如果你下棋,你会根据当前的棋盘位置做出决策,选择下一步棋。你所做的选择是给你从当前情况下赢得最佳机会的那一步。

计算机也是如此。在任何程序点,计算机都可以被赋予两种不同的行动方案。根据测试的结果,计算机选择其中一种行动方案,然后执行相应的操作。

典型的条件操作是 if 语句。在 Python 中,这可以表示为以下:


z = 9                        # Set z to 9
if x > 5: z = 20           # If x is greater than 5, then make the value of z equal to 20.

我们将条件加粗,动作被阴影覆盖。如果条件为真,则执行动作;如果条件不为真,则忽略动作。例如,如果 x 是 6,则 z 的值将是 20。如果 x 是 4,则 z 的值将保持为 9。

计算机执行简单的测试,其结果为两个布尔值之一,TrueFalse。通常,测试会问,“这个变量是否等于零?

在 Python 中,if 语句具有以下形式:


if condition: action         # The colon is mandatory, and indicates the end of the condition

Python 中只有两个保留元素,即 if 和冒号。术语 condition 是任何返回 TrueFalse 值的 Python 表达式,而 action 是计算机在条件为 True 时将执行的一组指令。一些程序员将动作放在新的一行上。在这种情况下,动作必须缩进,如下所示 图 2**.4

图 2.4 – 一个用于计算前 n 个整数的 Python 程序

图 2.4 – 一个用于计算前 n 个整数的 Python 程序

虽然 Python 允许你使用任何缩进,但良好的实践建议使用四个空格进行缩进。

条件是 x > 5,动作是 z = 20。条件是一个布尔逻辑表达式,它产生两种结果之一——如果 x 大于 5 则为 True,如果 x 不大于 5 则为 False。

在 Python 中使用条件表达式

一个用于控制房间温度的 Python 程序可能看起来像这样:


cold = 15                        # Define the temperature at which we turn on the heater
hot = 25                         # The temperature at which we turn on the cooler
nowTemp = getTemp()              # getTemp() is a function that reads the thermometer
if nowTemp < cold: heat(on)      # If too cold, then turn on the heater
if nowTemp > hot:  cool(on)      # If too hot, then turn on the cooler
if nowTemp > cold: heat(off)     # If not cold, then turn off the heater
if nowTemp < hot:  cool(off)     # If not hot, then turn off the cooler

常见的布尔条件如下:


x == y
x != y
x >  y
x <  y

这四个条件分别是等于、不等于、大于和小于。记住,表达式x == y读作“x 等于 y 吗?”考虑以下示例(使用 Python IDLE 解释器):


>>> x = 3
>>> y = 4
>>> x == y
False
>>> x + 1 == y
True

if语句的示例

变量x在 0 到 9 之间变化。假设我们想如果x小于 5,则y为 0;如果x大于 4 且小于 8,则y为 1;如果x大于 7,则y为 2。我们可以这样表示:


if x < 5: y = 0
if x > 4 and x < 8: y = 1    # A compound test. Is x greater than 4 AND x is less than 8
if x > 7: y = 2

第二个if语句通过使用and运算符同时测试两个条件。只有当两个条件x > 4x < 8都为真时,整个表达式才为真。

我们还可以使用 Python 的or运算符,这意味着如果任何条件为真。你可以编写如下复杂条件:

if (x > 4 and x < 8) or (z == 5): y = 1

正如传统的算术一样,必须使用括号来确保操作按适当的顺序执行。

我们可以通过生成x的连续值并打印相应的y值来测试此代码,正如图 2.5中所示。第一条语句执行缩进的块,当x为 0 到 9(比给定的范围少一个)时:

图 2.5 – 迭代演示

图 2.5 – 迭代演示

此代码的输出如下:


x =  0 y =  0
x =  1 y =  0
x =  2 y =  0
x =  3 y =  0
x =  4 y =  0
x =  5 y =  1
x =  6 y =  1
x =  7 y =  1
x =  8 y =  2
x =  9 y =  2

Python 的 if … else

前面的代码是正确的,但效率不高。如果第一个if的结果为真,其他if语句就不可能为真,但它们仍然被测试。我们需要的是一个测试,如果它是真的,我们就执行相应的操作。如果不是真的,我们就执行不同的操作。在英语中,我们有一个词来表示这个——它被称为“else”:

考虑以下内容:


if   x < 5:          # Is x less than 5?
     y = 0           # If x is less than 5, then y = 0
else:
     y = 3           # otherwise, y is 3

Python 有一个elif(else if)语句,允许进行多个测试。我们可以使用elif在第一个if的结果为假时执行另一个if。考虑使用elif的前面代码:


if   x < 5:                # Is x less than 5?
     y = 0                 # If x is less than 5, then y = 0
elif x > 4 and x < 8:      # If x is not less than 5, test whether it's between 5 and 7
     y = 1                 # If x is between 5 and 7, then set y to 1
elif x > 7:                # If both previous tests fail, then test whether x is 8 or more
     y = 2
print('x and y ', x, y)

在这种情况下,计算机一旦其中一个条件测试为真,就会退出这个结构。它与使用if语句的前一个示例完全一样,但更高效,因为它一旦满足条件就终止。也就是说,一旦任何测试结果为真,就执行其关联的操作,然后控制传递到if … elif序列之后的下一个语句(在这种情况下,print)。

编程的伟大之处在于,做同样的事情有很多种方法。假设我们知道一个变量x始终在 0 到 10 的范围内,并且我们想知道每个x对应的y值(使用前面的算法)。我们可以通过使用条件语句和编程来像以前一样计算y的值。

我们可以通过创建一个提供任何xy值的查找表来实现算法,而不使用if语句。让我们称这个表为lookupY,并为其加载从 0 到 10 的x输入的y值。表 2.1给出了每个可能的xy值。

输入 x 输出 y
0 0
1 0
2 0
3 0
4 0
5 1
6 1
7 1
8 2
9 2
10 2

表 2.1 – 使用查找表执行布尔运算

我们可以在 Python 中用两行代码来编写这个 lookup 操作:


lookup  = [0,0,0,0,0,1,1,1,2,2,2]      # Create a lookup table
y = lookup[x]                          # Read the value of y from the lookup table

当我们模拟计算机时,我们需要加载要模拟的程序。下一节将描述我们如何从内存中读取源文件(文本格式)到模拟器中。

从文件中读取数据

在测试汇编器或模拟器时,你需要输入测试数据(源程序)。这可以通过三种方式完成。

  • 在程序中包含数据 - 例如,myProg = ['add r1,r2', 'inc r1', '``beq 7']

  • 从键盘获取文本 - 例如,myProg = input('Type program ')

  • 在内存中创建一个文本文件并从你的程序中读取它

前两种技术非常适合测试非常短的程序,但对于长源程序来说则不太适用。第三种技术意味着你使用你喜欢的文本编辑器编写源代码,将其保存为 .txt 文件,然后在汇编器中读取该文件。

在开发工作中,我经常在我的汇编器中放置测试程序的名称,以避免输入名称(因为我大部分时间都在处理相同的源文件)。假设我的文件名为 c.txt


myFile = 'E:/simPython/c.txt'          # This is my source text file on my computer
with open(myFile,'r') as sFile:        # Open the file for reading (indicated by 'r')
    newFile = sFile.readlines()        # Read the code and copy to newFile.

在前面的代码中,myFile 变量提供了源文件名称作为字符串。然后,with open 操作打开并读取 myFile。此操作在使用后也会关闭文件。前面的代码以读取模式打开 c.txt 并创建一个新的文件 sFile

open 函数可以用以下三种方式使用:


open('thisFile')                       # Open a file
open('thatFile', 'r')                  # Open a file for reading
open('thatFile', 'w')                  # Open a file for writing

在使用文件后关闭文件是一个好习惯,使用 filename.close()。因为 with open 操作在操作结束时自动关闭文件,所以不需要调用 close() 函数。

readlines() 函数对 sFile 进行操作以创建一个包含我的源代码行列表的文件,newFile。以下代码片段演示了从磁盘读取文件并清理文本文件中的行尾序列。这是因为文本文件是以每行末尾的 '\n' 序列存储的,而这个序列必须被移除,因为它不是源程序的一部分。我们将在稍后回到字符串处理:


#                                               # Test reading a file
with open("E:\simPython.txt",'r') as example:   # Open the file for reading
    theText = example.readlines()               # Read the file example
print('The source file ',theText)               # Display the file
for i in range(0,len(theText)):                 # This loop scans the file
    theText[i] = theText[i].rstrip()            # rstrip() removes end-of-file markers
print('The source file ',theText)

以下是从该代码输出的内容。你可以看到 ‘*\n’* 序列已经被移除:


%Run testRead.py
The source file  ['# test file\n', 'NOP\n', ' LDRL 5\n', ' NOP\n', 'STOP']
The source file  ['# test file', 'NOP', ' LDRL 5', ' NOP', 'STOP']

我们已经提供了足够的信息来演示一个简单的 Python 程序,该程序实现了我们之前描述的简单序列检测器。

将令牌检测算法翻译成 Python

我们可以轻松地将我们的伪代码转换为检测读/写令牌流中的红色令牌序列,如下所示。在这个 Python 片段中,你被邀请首先输入要检测的红色令牌的数量,然后输入 rw 以指示每个令牌的颜色。一旦检测到适当的令牌数量,程序就会终止:


# Simple algorithm to detect consecutive tokens
maxRed = int(input("How many red tokens are you looking for? "))
go = 1
numRed = 0
while go == 1:
    y = input("Which token is it? Red or white? ")
    if y == 'w': numRed = 0
    else:        numRed = numRed + 1
    if numRed == maxRed: go = 0
print(maxRed, "Reds found")

这个程序对我输入的序列输出的结果是以下内容:


How many red tokens are you looking for? 3
Which token is it? Red or white? r
Which token is it? Red or white? r
Which token is it? Red or white? w
Which token is it? Red or white? r
Which token is it? Red or white? w
Which token is it? Red or white? w
Which token is it? Red or white? w
Which token is it? Red or white? r
Which token is it? Red or white? r
Which token is it? Red or white? r
3 Reds found

计算机有三个基本元素——一个执行所有算术和逻辑运算的算术逻辑单元ALU),一个存储程序和数据的内存,以及一个从内存读取指令并执行它们的控制单元。在下一节中,我们将介绍一些内存的基本概念。

计算机内存

现在,我们将介绍内存的概念,即存储程序和数据的机制。实际或物理内存以 DRAM、闪存和硬盘驱动器实现。这部分内存是计算机硬件的一部分。本书不涉及物理内存。相反,我们将讨论抽象内存以及它是如何由 Python 建模的。这是程序员的内存视图。

所有数据都存储在物理内存中,程序员设计的所有数据结构都必须映射到物理内存上。映射过程是操作系统的任务,本书不涉及将抽象内存地址转换为实际内存地址的转换。

考虑以下 Python 列表:


friends = ['John', 'Jenny', 'Rumpelstiltskin']

这三个字符串在friends列表中分别有[0][1][2]地址。操作系统将这些元素映射到物理内存存储位置。这些字符串各自需要不同数量的物理内存位置,因为它们的长度不同。幸运的是,计算机用户不必担心这些。这是操作系统的任务。

现在,我们将简要地看一下内存的概念,因为您必须理解内存的本质,才能理解计算机是如何工作的,以及如何编写汇编语言程序。

首先,考虑我们自己的记忆。人类的记忆是一种奇怪且不精确的东西。一个事件会触发我们回忆或检索一个我们称之为记忆的数据元素。这个事件可能是一个某人问你的问题,或者可能是一件提醒你过去发生的事件。通常,我们只部分地记住信息,甚至可能记错。

人类的记忆似乎是通过将事件与存储的信息进行匹配来访问的。也就是说,我们的记忆是关联的,因为我们把一个记忆与另一个记忆联系起来。计算机的内存以非常不同的方式运行,最好将其视为一个列表的存储项。您需要知道列表上(其地址)项的位置才能访问它。我们很快就会遇到 Python 内存,它是一种关联的,被称为字典的数据结构。您不是用地址访问字典,而是用与所需数据元素相关联的来访问。

图 2.6 展示了查找字符串中红色标记数量的程序是如何存储在一个假设的内存中的。我必须强调,这个程序是概念性的,而不是实际的,因为实际的计算机指令使用比这些更原始的机器级指令。这个被称为内存映射的图显示了内存中信息的位置。它是一个快照,因为它代表了内存在一个特定时刻的状态。内存映射还包括程序使用的变量和一串数字。存储程序计算机将指令、变量和常量存储在相同的内存中。

图 2.6 证明了内存中的每个位置都包含一个指令或一个数据元素。第一列中的数字 0 到 23 是地址,表示数据元素和指令在内存中的位置(地址从 0 开始而不是 1,因为 0 是一个有效的标识符)。

程序位于 0 到 8 的位置,变量位于 9、10 和 11 的位置,数据(标记)位于 12 到 23 的位置。您可以将计算机的内存视为一个项目表,每个项目的位置就是其地址——例如,内存位置 1 包含指令Set numRed to 0,位置 10 包含numRed元素的值,位置 11 包含当前数据元素的值(RW)。从位置 12 开始使用粗体字体表示它们包含我们操作的标记序列的值。

图 2.6 – 内存映射

图 2.6 – 内存映射

图 2.6 是为了教学目的对计算机内存的抽象视图。实际的内存是一个位置数组,每个位置大小相同(通常是 16、32 或 64 位)。单个指令可能占用一个、两个或多个连续的位置,具体取决于数据的实际大小。

尽管我们不会详细描述物理内存,但一些评论将有助于区分它与抽象内存。图 2.7展示了实际计算机内存(例如,DRAM)的组织结构。处理器通过地址总线向内存提供一个地址,并通过一个控制信号选择是进行读取还是写入周期。在读取周期中,内存将数据放置到数据总线上供 CPU 读取。在写入周期中,CPU 的数据被存储在内存中。信息通过端口进入或离开内存(或计算机系统的任何其他功能部分)。一台真正的计算机具有不同技术实现的内存系统层次结构——例如,DRAM 用于主存储器,SSD 用于较慢的二级存储,高速缓存用于频繁使用的数据和指令。在这本书中,我们只处理用 Python 实现的抽象内存,而不是真实的物理内存。操作系统的任务是整合程序员使用的抽象内存地址与内存中数据的真实物理地址。

计算机的主存储器由高速随机存取存储器RAM)组成,可以对其进行写入或读取。它通常由动态半导体存储器 DRAM(如今是 DDR4 和 DDR5 技术)组成,典型 PC 的 RAM 大小通常从平板电脑的 4GB 到高性能系统的 64GB 不等。RAM 是易失性的,这意味着当你关闭它时(与保留数据的闪存相比),其数据会丢失。

DRAM 缩写中的动态一词描述了其存储机制(作为电容器上的电荷,它会泄漏并需要定期写回)。从用户的角度来看,DRAM 提供了非常便宜、大量存储。从设计者的角度来看,DRAM 由于其操作特性而带来了特定的设计问题。DRAM 的替代品是静态 RAMSRAM),它比 DRAM 更容易使用且速度更快,但价格昂贵得多,这阻止了它在 PC 上的使用,除了缓存内存等特殊应用。缓存内存是一种特殊的高速内存,包含频繁使用的数据。

二级(物理)存储器是硬盘驱动器HDD)或固态驱动器SSD)。这两种存储器都存储大量数据,比主存储器的 DRAM 慢得多。计算机不能直接访问二级存储器。操作系统将数据,一次一页,从 SSD/HDD 传输到计算机的主存储器。这对用户来说是不可见的。

描述抽象内存的下一步是引入一种称为 RTL 的符号,它常用于描述或定义内存事务。

图 2.7 – 内存系统

图 2.7 – 内存系统

寄存器传输语言

用文字描述计算机操作通常很繁琐。在这里,我们将介绍寄存器传输语言(RTL),它使得定义计算机操作变得容易。RTL 既不是汇编语言也不是编程语言。它是一种表示法。

区分内存位置和其内容是很重要的。寄存器传输语言使用方括号[ ]来表示内存位置的内容。表达式[15] = maxRed被解释为内存位置 15 的内容包含 maxRed 的值。如果我们想给内存命名,我们可以写,例如,dram[15]而不是仅仅[15]

←符号表示数据传输。例如,表达式[15] ← [15] + 1被解释为内存位置 15 的内容增加 1,并将结果放回内存位置 15。考虑以下 RTL 表达式:


a.    [20] = 5
b.    [20] ← 6
c.    [20] ← [6]
d.    [12] ← [3] + 4
e.    [19] ← [7] + [8]
f.    [4] ← [[2]]

表达式(a)说明内存位置 20 的内容等于数字 5。表达式(b)说明数字 6 被放入(复制或加载到)内存位置 20。表达式(c)表示内存位置 6 的内容被复制到内存位置 20。

表达式(d)表示将 4 加到位置 3 的内容上,并将和放入位置 12。表达式(e)表示位置 7 和 8 的内容之和被放入位置 19。

表达式(f)表示使用位置 2 的内容来访问内存以读取一个值,即一个地址。那个第二个地址的内容被放入位置 4。这个表达式是最有趣的,因为它引入了指针的概念。内存位置 2 的值[2]是一个指针,它指向另一个内存位置。如果我们执行[2] ← [2] + 1,指针现在指向内存中的下一个位置。当我们讨论间接寻址(也称为基于指针的寻址或索引寻址)时,我们将回到这一点。

“←”RTL 符号等同于某些高级语言中使用的传统赋值符号“=”。RTL 不是计算机语言;它是一种用于定义计算机操作的表示法。

嵌套的方括号,如[[4]],表示地址由内存位置 4 的内容给出的内存位置的内容。这被称为间接寻址。图 2.8 演示了位置 4 指向位置 12,其中包含所需数据的价值——即 9。

图 2.8 – 基于指针的寻址

图 2.8 – 基于指针的寻址

RTL 应用的示例

让我们看看 RTL 如何被应用的示例。图 2.9展示了一个小型的抽象内存,有 12 个位置。

地址 数据
0 6
1 2
2 3
3 4
4 5
5 2
6 8
7 1
8 5
9 2
10 1
11 5

图 2.9 – 抽象内存映射的示例

我们希望评估表达式 X = 3 + [4] + [1+[3]] + [[10]] + [[9]*3]。这个表达式可以通过求和其组成部分来评估,如下所示:

  • 序列中的第一个元素是字面量 3

  • 表达式 [4] 表示内存位置 4 的内容——即,5。

  • 表达式 [1+[3]] 表示 [1 + 4] = [5] = 2

  • 表达式 [[10]] 表示 [1],即 2

  • 表达式 [[9]3] 表示 [23] = [6] = 8

最终的值是 3 + 5 + 2 + 2 + 8 = 20。

下一步是在 Python 中模拟抽象内存。

在 Python 中模拟内存

现在我们演示如何在 Python 程序中模拟计算机内存。为了模拟计算机的主内存(即时访问存储器,通常称为 DRAM),我们只需创建一个 Python 列表。典型的 PC 有超过 4G 的内存位置(2 的 22 次方)。在这里,我们将创建易于模拟的小型内存。如果我们称这个内存为mem,我们可以创建一个包含八个位置的内存,并用以下方式将其初始化为零:


mem = [0,0,0,0,0,0,0,0]         # Create 8-location memory. All locations set to zero

如果你有更大的内存,这种初始化机制就不方便了。如果是这样,你可以使用 Python 提供的一种功能,允许你创建一个列表并用相同的元素填充它。让我们考虑以下内容:


mem = [0]*128                   # Create memory with 128 locations, all set to 0

为了演示对内存的简单操作,我们将两个数字加载到模拟内存中,检索它们,将它们相加,并使用 RTL 符号将结果存储在内存中:


mem[3] ← 4                      # Load location 3 with 4\. Note this is RTL not Python
mem[5] ← 9                      # Load location 5 with 9
sum    ← mem[3] + mem[5]        # Add contents of locations 3 and  5, and put result in sum
mem[6] ← sum                    # Store sum in location 6

我们可以将这个表达式翻译成 Python,并打印位置 6 的内容,如下所示:


mem = [0]*8                     # Create memory with 8 locations, all set to 0\. This is Python
mem[3] = 4                      # Load location 3 with 4
mem[5] = 9                      # Load location 5 with 9
sum    = mem[3] + mem[5]        # Add locations 3 and 5 and assign result to sum
mem[6] = sum                    # Store sum in location 6
print('mem[6] =', mem[6])       # Print contents of location 6
print('Memory =', mem)          # Print all memory locations

如您所见,Python 与 RTL 符号非常接近。现在,让我们使用内存中的数据作为指针。回想一下,指针是一个指向内存中另一个位置的值。图 2.10显示了一个包含五个整数存储的八个位置的内存映射。

图 2.10 – 加法操作的内存映射

图 2.10 – 加法操作的内存映射

我们将执行以下操作,首先在 RTL 中,然后在 Python 中:


mem[mem[0]]← mem[mem[1]] + mem[mem[2]]

或者,如果我们使用简化后的 RTL,其中“mem”是已知的,我们可以这样写:


[[0]]← [[1]] + [[2]]

这个操作将位置 1 的内容指向的内存位置的值与位置 2 的内容指向的内存位置的值相加,并将结果放入位置 0 指向的内存位置中。我们可以用 Python 表示如下:


mem = [7,5,4,0,3,9,0,0]           # Preset the memory. Unspecified locations are set to 0
pointer0 = mem[0]                 # Get pointer 0 to result
pointer1 = mem[1]                 # Get pointer 1 to source 1
pointer2 = mem[2]                 # Get pointer 2 to source 2
source1  = mem[pointer1]          # Get source data 1
source2  = mem[pointer2]          # Get source data 2
result   = source1 + source2      # Do the addition
mem[pointer0] = result            # Store result at the location pointed at by pointer 0
print('Memory =', mem)            # Print all memory locations

这个程序将打印内存的内容,Memory = [7, 5, 4, 0, 3, 9, 0, 12]。如您所见,内存位置 7 已从其初始值 0 变为值 12——即,位置 5 和 4 的内容之和。

摘要

本书的一个主要主题是编写一个程序来模拟计算机,以便你可以在自己设计的计算机上运行程序。为此,有必要用一种合适的高级语言编写模拟器。我们选择 Python 是因为它的可用性、简单性和强大功能。

本章通过介绍你需要设计计算机模拟器的数据结构和流程控制功能,简要概述了 Python。我们已经详细介绍了 Python 的基础知识,以便你能够跟随不使用 Python 任何更神秘特性的简单程序。

Python 的两个重要且非常基本的功能是字符串(由于模拟涉及文本处理,因此很重要)和列表。列表简单地说就是由逗号分隔并由方括号包围的元素序列。Python 列表的特殊之处在于,元素可以是任何数据元素,并且它们很容易访问——例如,列表x的第 10 个元素简单地表示为x[10]。同样,字符串x = 'a test'的第 5 个字符表示为[5],其值为'i'。像所有计算机语言一样,Python 从 0 开始编号元素。

我们还探讨了函数,这是一段可以从程序中的任何位置调用来执行某些操作的代码。你不需要函数。然而,如果你经常做同样的事情,调用一段代码来完成任务可以使程序更容易阅读和调试。

我们演示了一个非常简单的 Python 程序来模拟内存,并迈出了通往模拟器道路的第一步。

然而,Python 的强大之处(简单性)也是其弱点。更复杂的语言通过提供严格的规则来确保正确性——例如,强类型要求你必须声明每个变量的类型,然后相应地使用它。Python 中一个非常常见的陷阱是错误的缩进。循环或if结构中的代码缩进是为了显示它属于保护它的任何结构。如果你在编辑程序时出错缩进,程序可能会崩溃或表现出与预期完全不同的行为。

第三章中,我们探讨了中央处理单元CPU)的基本结构,并演示了如何从内存中读取指令、解码和执行。

第三章:计算机中的数据流

在本章中,我们将学习计算机如何执行指令。在我们能够在 Python 中模拟计算机的行为之前,我们必须理解计算机的内部运作方式。我们还将介绍计算机指令(它可以被命令执行的最小操作)的概念,并展示计算机指令的外观。

什么是计算机?它是如何工作的?它能做什么?我们将通过演示如何使用 Python 设计计算机,以及如何在模拟计算机上运行程序来回答这些问题。在这里,我们只对计算机在机器级别上的行为感兴趣——也就是说,它执行的操作类型。我们不关心计算机的内部设计或计算机是如何电子实现的(即用于构建计算机的电路)。

本章我们将涵盖以下主题:

  • 指令集架构(ISA)

  • 冯·诺伊曼架构

  • 汇编级程序

  • 机器级指令

技术要求

你可以在 GitHub 上找到本章使用的程序,链接为 github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter03

指令架构级别(ISA)

在本节中,我们将展示如何以不同的方式描述计算机,并解释本书是从计算机的指令集和能力角度来探讨计算机的。

“计算机”这个词对不同的人意味着不同的事情。对一个工程师来说,计算机是一组执行特定功能的电路集合;对一个飞行员来说,它是一架能够将飞机从一个机场飞到另一个机场并在浓雾中降落的机器。

表 3.1 展示了计算机语言和结构的层次结构。在最顶层,你有计算机实际使用的应用程序。在这个级别,应用程序可能是一个用户选择的程序(例如飞行模拟器),或者它可能是在嵌入式系统上运行的程序(例如自动柜员机)。与该应用程序的通信是通用的,并且独立于运行应用程序的实际计算机。

级别 实现 通用性
1. 应用 Word、Excel、Photoshop 和飞行模拟器 通用
2. 高级语言 Python、Java、C++ 通用
3. 汇编语言 ADD r1,r2, and r3 计算机家族
4. 二进制(机器代码) 00111010111100001010101110001010 二进制 计算机家族
5. 电路(微处理器) 逻辑门、加法器、计数器和内存 特定家族成员
6. 硅 物理硅芯片 特定芯片

表 3.1 – 计算机语言和系统的层次结构

在应用层之下,是用于构建应用程序的高级语言。这种语言可能是 Python、Java、C++等等。高级语言被设计成允许程序员构建能在不同类型的计算机上运行的应用程序。例如,用 Python 编写的程序可以在任何有 Python 解释器或编译器的机器上运行。在高级语言引入之前,你必须为每台特定的计算机设计应用程序。

目前,大多数计算机无法直接执行高级语言。每台计算机都有自己的本地语言,这种语言被计算机家族(例如,英特尔酷睿、摩托罗拉 68K、ARMv4 和 MIPS)所理解。这些语言与计算机的结构及其硬件有关,这由计算机的 ISA 表达。这个层级在表 3.1中由两层表示,即汇编语言和机器码。

高级语言之下的层级是汇编语言级别,这是计算机的二进制机器码的人类表示形式。人们无法记住或轻易操作由 1 和 0 组成的字符串。汇编语言是机器码的文本版本。例如,汇编语言操作ADD A,B,C表示将B加到C上,并将结果放入A(即,A = B + C),在机器码中可能表示为00110101011100111100001010101010`。

机器码层是计算机实际执行的二进制代码。在 PC 世界中,机器码程序具有.exe文件扩展名,因为它们可以被计算机执行。所有计算机都执行二进制代码,尽管这个层对于每种类型的计算机都是不同的——例如,英特尔酷睿、ARM 和 MIPS 是三种计算机家族,每个都有自己的机器码。

虽然汇编语言层是机器码层的表示,但这两个层级之间存在差异。汇编语言层包括帮助程序员编写程序的设施,例如定义变量名和将独立编写的模块链接到单个机器码程序的能力。

在机器码层之下是电子电路,通常被称为微处理器,或者简称为芯片。这是英特尔等公司制造的硬件,它从内存中读取程序并执行它们。一般来说,这个层级不能编程或修改,就像你不能改变你汽车引擎的气缸数量一样。

今天,一些数字系统确实有可以电子修改的电路——也就是说,通过改变通过称为现场可编程门阵列(FPGA)的电路的信号路由,可以重新配置计算机的电路。FPGA 包含大量门和专用电路块,可以通过编程相互连接。FPGA 可以被编程以执行专用应用,例如在医疗或航空航天系统中的信号处理。

在电子电路层面,同一组电路可能有不同的版本。例如,微处理器可以使用 7 纳米或 14 纳米的器件技术来实现(这些数字表示芯片上组件的基本尺寸,尺寸越小越好)。这两个电路在各个方面可能都是操作上相同的,但一个版本可能比另一个版本更快、更便宜、更可靠或功耗更低。

本书是关于汇编语言和机器代码层的,表 3.1中的层允许我们编写由计算机执行的程序。到本书结束时,你将能够设计自己的机器代码、自己的汇编语言以及自己的计算机。

在 20 世纪 40 年代和 50 年代,所有的编程都是在汇编语言(甚至机器代码)中完成的。现在不是这样了。编写汇编语言程序是繁琐且极具挑战性的。计算机科学家创建了 C++、Java、Python 和 Fortran 等高级语言。这些语言是为了允许程序员用接近英语的语言编写程序而开发的,这种语言比汇编语言能表达更强大的思想。例如,在 Python 中,你可以使用print("Hello World.")指令在屏幕上打印文本"Hello World"。如果你想在汇编语言中做到这一点,你将不得不编写大约 100 个单独的机器级指令。此外,Python 版本将在所有计算机上运行,但机器级版本必须为每种特定的计算机类型编写。

高级语言的秘密在于编译器。你用高级语言编写程序,然后将其编译成你想要运行的特定计算机的机器代码。你可能会遇到解释器这个术语,它执行的功能与编译器相同。编译器将整个高级语言程序翻译成机器代码,而解释器则是逐行进行翻译,并在解释的同时执行每一行。

在今天(除学术界外),用汇编语言编写代码并不流行。然而,它有一个优点,即(原则上)你可以编写比编译代码运行更快的优化代码。

本书的一个主要主题是实践学习。我们将解释什么是计算机,介绍它执行的指令,然后展示如何用高级语言构建(即模拟)它。我们将要构建的程序称为TC1(教学计算机 1)。这个程序将执行一个假设计算机的汇编语言。你将能够用汇编语言编写程序,TC1 程序将读取指令并像在真实的 TC1 计算机上运行一样执行它们。

当你运行程序时,你可以逐条执行指令并观察它们的输出结果 v – 也就是说,你可以在程序运行时读取寄存器和内存中数据的值。这台计算机的目的不是执行有用的计算功能,而是展示指令的外观以及它们的执行方式。

这台计算机演示了指令的执行方式和如何使用汇编语言。此外,你可以修改计算机的指令集以创建你自己的专用指令。你可以删除指令、添加新指令、扩展指令,甚至改变指令的格式。TC1 被设计用来使学习计算机体系结构的学生能够理解指令格式、指令解码和指令复杂性。它也有助于理解寻址模式(即数据在内存中的定位方式),例如基于指针的寻址。我们将在稍后更详细地讨论这些主题。

TC1 有几个在传统计算机指令集中不存在的有用功能。例如,你可以直接从键盘将数据输入到计算机中,你还可以将随机数加载到内存中。这允许你为测试目的创建数据。

首先,我们需要介绍原型计算机,即所谓的冯·诺伊曼机,它在 20 世纪 40 年代和 50 年代被创造出来,并成为大多数现代计算机的模板。实际上,已经相当偏离了纯冯·诺伊曼架构,但我们仍然倾向于使用这个术语来区分其他类别的计算机(例如,模拟计算机、神经网络和量子计算机)。

ISAs – 部件命名

在我们介绍计算机的结构之前,我们需要介绍一些你必须了解的术语:

  • 01。你可以将位设置为0,设置为1,或者切换它(翻转它)。

  • 8163264。一般来说,字是计算机操作的基本数据单位。在 64 位计算机上执行ADD a,b,c这样的指令会将字b的 64 位与字c的 64 位相加,并将 64 位的结果放入字a中。变量abc指的是内存或寄存器中的存储位置。

  • 指令:机器级指令是程序员可以指定的最基本操作,它定义了计算机执行的单个动作。在硅片上可能存在更原始的操作级别,但程序员无法直接访问这些。指令主要分为三类——数据移动,将数据从一个地方复制到另一个地方,算术和逻辑操作,处理数据,以及指令序列命令,确定指令执行的顺序(这是实现形式为 如果这个,那么 做那个 的决策所必需的)。

  • 立即访问内存:这通常被许多程序员称为内存或 RAM 或 DRAM。这是程序执行期间程序和数据的存储位置。今天,术语 随机访问内存 意味着相同的东西。然而,严格来说,术语 随机 指的是随机选择的一个内存元素访问时间对每个元素都是相同的(与磁带不同,磁带的访问时间取决于数据在磁带上的位置)。

  • 在芯片上,864 个寄存器,指定一个特定的寄存器只需要 36 位,而不是通常用来访问内存位置的 3264 位。大多数计算机操作都是作用于寄存器的内容,而不是主内存。寄存器的命名没有通用的约定。在本章中,我们将一般使用以下命名方式——例如,INC r3 表示将寄存器 r3 的内容增加 1

  • 32 位或 64 位长。然而,8 位和 16 位计算机在嵌入式控制应用中经常被使用(例如,玩具、电视、洗衣机和汽车)。

  • ADD a,b,5 其中 5 是一个字面量,那么它的意思就是,将整数 5 加到 b 上。一些计算机使用 # 来表示字面量——例如,ADD a,b,#12 的意思是,将数字 12 加到内存位置 b 的内容上。

  • A 移动到 B——简单至极。在计算机科学中,术语 move 表示原本在 A 的东西最终到了 B,也就是说,它现在既在 A 也在 B。换句话说,程序员使用 move 来表示 copy。如果你将数据从寄存器移动到内存,数据仍然保留在寄存器中,并复制到内存。我们将在下一节介绍原型计算机。

下一步是介绍冯·诺依曼计算机的概念,它可以被认为是大多数现代计算机的祖先。数学家冯·诺依曼是 1945 年 EDVAC 第一草案报告 的作者之一,该报告描述了数字计算机的结构。

冯·诺依曼架构

原型计算机通常被称为存储程序冯·诺伊曼机。它有一个存储在内存中的程序,按指令顺序逐条执行。此外,程序存储在与计算机操作的数据相同的内存中。这种结构以计算先驱之一约翰·冯·诺伊曼的名字命名。一旦你理解了冯·诺伊曼机,你就理解了所有计算机。

图 3.1展示了包含三个基本元素的简化冯·诺伊曼机:

  • 一个存储程序和程序使用的任何数据的内存

  • 一组寄存器,每个寄存器存储一个数据字(在图 3.1中有一个寄存器,r0

  • 一个算术逻辑单元(ALU),执行所有数据处理

图 3.1 – 冯·诺伊曼架构

图 3.1 – 冯·诺伊曼架构

内存包含要执行的指令。数据和指令都以二进制形式存储,尽管我们经常以汇编语言形式展示操作,以便于阅读。每个指令都是从内存中读取、解码和解释(即执行)。图 3.1的主要简化是没有执行条件操作(即if…then)的手段。我们将在稍后解决这个问题。

图 3.1看起来很复杂。其实并不复杂。我们将一步一步地解释其操作。一旦我们了解了计算机在原理上的工作方式,我们就可以看看它如何在软件中实现。我们描述了一个非常简单的、所谓的一又二分之一地址机的操作,其指令有两个操作数——一个在内存中,一个在寄存器中。指令的形式为ADD B,A,它将A加到B上,并将结果放入BAB必须在一个寄存器中。两个操作数都可以在寄存器中。一又二分之一地址机的术语是对事实的评论,即内存地址是 16 到 32 位,选择数百万个内存位置中的一个,而寄存器地址通常是 2 到 6 位,只选择少数几个寄存器中的一个。

我们不会一次性引入计算机,而是逐步构建 CPU。这种方法有助于展示指令是如何被执行的,因为计算机的发展大体上遵循了指令执行过程中发生的事件顺序。现实中的计算机不会从开始到结束执行一个指令。今天的计算机重叠指令的执行。一旦一条指令从内存中取出,下一条指令就会在上一条指令完成执行之前被取出。这种机制称为流水线,是现代计算机组织的一个重要方面。流水线类似于汽车生产线,计算机指令在各个阶段被执行,以便同时执行多个指令。我们将从用于定位将要执行的下一个指令的地址路径开始。在这本书中,我们不会涉及流水线,因为它是一个实现因素,而不是指令集设计。

地址路径

地址是一个表示内存中数据项位置的数字。图 3**.2 仅显示了读取内存中指令所需的地址路径。

图 3.2 – CPU 的地址路径

图 3.2 – CPU 的地址路径

地址路径是 CPU 和内存之间移动地址的数据高速公路。地址告诉内存我们想要从哪里读取数据,或者我们想要存储在哪里。例如,指令 ADD r0,234` 表示操作从内存位置 234 读取内容,将它们加到寄存器 r0 的内容上,然后将结果放入 r0图 3**.2 省略了执行指令所需的数据路径,以避免混乱。

计算机中有三种类型的信息流——地址、数据和控制。数据包括存储在内存和寄存器中的指令、常量和变量。控制路径包括触发事件、提供时钟以及确定数据流和地址在计算机中流动的信号。

读取指令

在 CPU 执行指令之前,必须将指令从计算机的内存中取出。我们开始描述程序是如何被执行的,从 CPU 的程序计数器(也称为指令指针位置计数器)开始。程序计数器这个名称是不准确的。程序计数器并不计算程序或其他任何东西,而是包含内存中将要执行的下一个指令的地址。

程序计数器指向将要执行的下一个指令。例如,如果 [PC] = 1234(即,PC 包含数字 1234),则将要执行的下一个指令将在内存位置 1234 中找到。

获取指令的过程始于程序计数器的内容被移动到内存地址寄存器(即 [MAR][PC])。一旦程序计数器的内容已转移到内存地址寄存器,程序计数器的内容会增加并重新移动回程序计数器,如下所示:

[PC][PC] + 1.

PC 增量为 1 因为下一个指令位于一个位置之后。现实中的计算机通常是按字节寻址的——也就是说,字节按顺序编号 0, 1, 2, 3 … 现代计算机有 32 位或 64 位数据字——即 4 字节或 8 字节字。因此,现实中的计算机在每条指令之后将 PC 增加 4 或 8。

在此操作之后,程序计数器指向 下一个 指令,而当前指令正在执行。

内存地址寄存器(MAR) 存储了在写周期中写入数据到内存中的位置地址,或在读周期中从该地址读取数据。

当执行 内存读周期 时,由 MAR 指定的内存位置的内容从内存中读取并传输到 内存缓冲寄存器(MBR)。我们可以用 RTL 术语表示这个读操作如下:

[MBR] ← [[MAR]]   @ A read operation (example of indirect addressing)

我们将 [[MAR]] 表达式解释为 由 MAR 内容指定的内存的内容。内存缓冲寄存器是读取周期中从内存接收数据的临时存储位置,或写入周期中要传输到内存的数据的临时存储位置。一些文本将 MBR 称为 内存数据寄存器(MDR)。在执行指令的这个阶段,MBR 包含要执行的指令的位模式。

接下来,指令从 MBR 移动到 指令寄存器(IR),在那里它被分为两个字段。一个 字段 是一个字的一部分,其中位被分组成一个逻辑实体——例如,一个人的名字可以分为两个字段,即名字和姓氏。IR 中的一个字段包含 操作码(opcode),它告诉 CPU 要执行什么操作。另一个字段,称为 操作数字段,包含指令要使用的数据的地址。操作数字段还可以提供操作码在立即或直接寻址时使用的常数——也就是说,当操作数是一个实际(即文字)值而不是地址时。就我们当前的目的而言,寄存器地址被认为是指令的一部分。稍后,我们将介绍具有多个寄存器的计算机。现实中的计算机将指令分为超过两个字段——例如,可能有两个或三个寄存器选择字段。

控制单元(CU)从指令寄存器中获取操作码,以及一系列时钟脉冲,并生成控制 CPU 所有部分的信号。单个时钟脉冲之间的时间通常在 0.3 ns 到 100 ns(即 3 x 10^-10 到 10^-7 s)的范围内,对应于 3.3 GHz 到 10 MHz 的频率。CU 负责将程序计数器的内容移动到 MAR,执行读周期,并将 MBR 的内容移动到 IR。

指令在两阶段的fetch-execute cycle中执行。在fetch phase阶段,指令从内存中读取并由控制单元解码。fetch phase 阶段之后是execute phase阶段,在这个阶段,控制单元生成执行指令所需的所有信号。以下 RTL 符号描述了 fetch phase 中发生的操作序列。FETCH是一个标签,用于指示操作序列中的特定行。符号 IRopcode 表示指令寄存器的操作码字段。我们使用#来表示 Python 中的注释,并在汇编语言中使用@以符合 ARM 的约定。一些汇编器使用分号来表示注释字段:


FETCH [MAR] ← [PC]        @ Copy contents of the PC to the MAR
      [PC]  ← [PC] + 1    @ Increment the contents of the PC to point to next instruction
      [MBR] ← [[MAR]]     @ Read the instruction from memory
      [IR]  ← [MBR]       @ Move the instruction to the instruction register for processing
      CU    ← [IRopcode]  @ Transmit the opcode to the control unit

以下是如何将 fetch cycle 编码为 Python 中的函数的示例,以及测试它的所需代码。我们定义了一个 12 位指令,其中包含 4 位操作码和 8 位地址。内存有 16 个位置,我们加载前两个位置以测试程序。Python 表达式 p >> q 表示将二进制值 p 右移 q 位,&执行逻辑AND操作。我们将在稍后详细讨论这个问题。例如,011000001010 >> 8变为0110。这提取了操作码。同样,0b011011111010 & 0b111111111111 = 0b000000001010`以提取地址:


                                # Testing the fetch cycle
mem = [0] * 16                  # Set up 16 locations in memory
pc = 0                          # Initialize pc to 0
mem[0] = 0b011000001010         # Dummy first instruction (opcode in bold) 0b indicates binary value
mem[1] = 0b100011111111         # Dummy second instruction
def fetch(memory):              # Fetch cycle implemented using a function
    global pc                   # Make pc global because we change it
    mar = pc                    # Copy pc to mar
    pc = pc + 1                 # Increment the pc ready for next instruction
    mbr = memory[mar]           # Read instruction from memory
    ir = mbr                    # Copy instruction to instruction register
    cu = ir >> 8                # Shift instruction 8 places right to get the operation code
    address = ir & 0xFF         # Mask opcode to 8-bit address
    return(cu, address)         # Return instruction and address
opCode,address = fetch(mem)     # Do a fetch cycle
print('pc =', pc - 1, 'opcode =', opCode, ' Operand =', address)
opCode,address = fetch(mem)     # Do a fetch cycle
print('pc =', pc - 1, 'opcode =', opCode, ' Operand =', address)

在前面的代码中,数值0b011000001010通过0b前缀以二进制形式表示。同样,0xFF表示法表示十六进制形式的数字——即十进制中的255或二进制形式中的11111111

通过两次调用opCode,address = fetch(mem)来测试函数。Python 允许我们在一行中接收两个返回的参数,即操作码和地址。注意 Python 代码如何紧密地遵循 RTL。在实际应用中,你不会编写这样的代码。我们不需要 MAR 和 MBR 寄存器。我包括它们是为了帮助模拟硬件。我们可以简单地编写以下代码:


    ir  = mem[pc]               # Read current instruction into ir
    pc = pc + 1                 # Increment program counter ready for next cycle
    cu = ir >> 8                # Extract the opcode
    address = ir & 0xFF         # Extract the operand address

CPU 的数据路径

在解决了 fetch phase 之后,让我们看看还需要什么来执行指令。图 3**.3将数据路径添加到图 3**.2的 CPU 中,以及从指令寄存器的地址字段到内存地址寄存器的地址路径。其他添加包括数据寄存器r0和一个执行实际计算的 ALU。它执行的运算通常是算术运算(加、减、乘、除)和逻辑运算(ANDOREOR以及左移或右移)。

图 3.3 – CPU 的地址和数据路径

图 3.3 – CPU 的地址和数据路径

数据寄存器r0在计算过程中持有临时结果。你需要一个数据寄存器(即累加器),因为像ADD这样的二进制操作需要两个操作数,一个由指令指定,另一个是数据寄存器的内容。ADD,P将内存位置P的内容加到通用寄存器r0的内容上,并将和存入数据寄存器,破坏了原始的一个操作数。*图 3.3**.3*的安排有一个我们称之为r0的通用数据寄存器。一个真实的处理器,如 ARM,有 16 个寄存器,从r0r15`(尽管并非所有这些都是通用数据寄存器)。

典型的数据移动指令

所有计算机都有数据移动指令,用于将数据从一个地方传输(即复制)到另一个地方。这些是最简单的指令,因为它们不涉及数据处理。数据移动指令因计算机而异。在这里,我们将提供一些典型的例子,以帮助您理解本章中的示例。请注意,我们将在此文本中使用不同的约定。例如,我们有时会在字面量前加#(例如,ADD ,#6),有时会在指令后加后缀L(例如,ADDL r1,6)。这是因为计算机中有几个标准/约定在使用中,并且它们因计算机而异。以下是一些通用的代码示例。注意加载字面量的重复。一些处理器使用move,而另一些使用load

助记符 示例 名称 RTL 注释
MOV MOV  r1,r4 移动寄存器 [r1] ← [r4] 将寄存器 r4 复制到寄存器 r1
MOVL MOVL r1,5 移动字面量 [r1] ← 5 将整数 5 复制到寄存器 r1
LDR LDR  r3,12 加载寄存器 [r3] ← [12] 将内存位置 12 的内容加载到 r3
LDRL LDRL r0,13 加载字面量 [r0] ← 13 将整数 13 加载到寄存器 r0
STR STR  r4,8 存储寄存器 [8] ← [r4] 将 r4 的内容存储在内存位置 8

表 3.2 – 典型的数据移动指令

数据处理指令

让我们看看一个典型的数据处理操作。我们可以用一个ADD r0,X指令用 RTL 表达式表示:


[r0] ← [r0] + [X]       @ Add the contents of the memory
                         location X to register r0

ALU(算术逻辑单元)是 CPU 的“工作马”,因为它执行所有计算。算术和逻辑运算应用于数据寄存器的内容以及数据寄存器或 MBR 的内容。ALU 的输出被反馈到数据寄存器或 MBR。

算术和逻辑操作之间的基本区别是,当对A字的位 ai 和B字的位 bi 进行操作时,逻辑操作不会产生进位。表 3.2提供了典型算术和逻辑操作的例子。

操作 类别 典型助记符
加法 算术 ADD (a = b + c)
减法 算术 SUB (a = b - c)
取反 算术 NEG (a = -b)
乘法 算术 MUL (a = b * c)
除法 算术 DIV (a = b / c)
除以 2 算术 ASR (a = b / 2)
乘以 2 算术 ASL (a = b * 2)
AND 逻辑 AND (a = b & c)
OR 逻辑 OR (a = b ∨ c)
NOT 逻辑 NOT (a = !b)
EOR 逻辑 EOR (a = b ⊕ c)
左移 逻辑 LSL (将所有位左移 a = b << 1)
右移 逻辑 LSR (将所有位右移 a = b >> 1)

表 3.3 – 典型的算术和逻辑运算

逻辑移位将操作数视为一个位串,这些位向左或向右移动。算术移位将数字视为一个有符号的 2 的补码值,并在右移时传播符号位(即符号位被复制和重复)。这些操作中的大多数都由 68K、Intel Core 和 ARM 等计算机实现。

再次看看数据流

让我们再次看看计算机中的数据流,以巩固基本概念。在 图 3.4 中,我们有一个支持涉及三个寄存器(如 ARM 计算机的特点)的操作的计算机。在这里,我们有三个寄存器,r1r2r3。标记为 加法器 的块是 ALU 的一部分,用于将两个数字相加以产生和。指令 LDR r2,X 将内存地址 X 的内容加载到寄存器 r2。指令 STR r1,Z 将寄存器 r1 的内容存储在内存地址 Z

指令 ADD r1,r2,r3 读取寄存器 r2r3 的内容,将它们相加,并将结果存入寄存器 r1。由于不清楚哪个寄存器是目标寄存器(即结果),我们使用粗体字来突出目标操作数,这通常是左边的操作数。

图 3.4 展示了计算机的几个基本组件。这本质上与我们在演示取指/执行周期时使用的更详细的结构相同。在这里,我们关注的是数据流向内存以及从内存中流出。

图 3.4 – 冯·诺依曼机的细节

图 3.4 – 冯·诺依曼机的细节

以下是我们感兴趣的部分:

  • 产生定时脉冲的时钟。所有操作都在时钟触发时进行。

  • 一个解释器或控制单元,它接受一条指令,以及一系列时钟脉冲,并将其转换为执行所需操作所需的动作。在 图 3.4 中,解释器将寄存器 r2r3 的内容路由到加法器,使加法器将两个值相加,然后将结果从加法器路由到目标寄存器。

  • 存储器中的程序。数据从存储器加载到寄存器 r2r2。然后,r2r3 相加,结果存入 r3。最后,将 r3 的内容移动到存储器中。

现在我们已经介绍了计算机的基本结构和一些指令,下一步是查看一个执行特定功能的完整程序。

汇编级程序

在进一步开发我们的计算机之后,在本节中,我们将展示如何执行一个简单的程序。假设这台计算机不提供三地址指令(即,你不能指定三个寄存器和/或内存地址的操作),我们想要实现高级语言操作 Z = X + Y。在这里,+ 符号表示算术加法。执行此操作的汇编语言程序如下所示。记住,XYZ 是指代内存中变量位置的符号名称。从逻辑上讲,存储操作应该写成 STR Z,r2,目标操作数在左边,就像其他指令一样。按照惯例,它被写成 STR r2,Z,源寄存器在左边。这是编程历史的一个怪癖:

LDR,X``r2从内存位置X` 获取内容

ADD,Y``Y将数据寄存器r2的内容加到Y`

STR,r2,Zr2 存入内存位置 Z

八位计算机有一个单地址机,仅为了执行加法这样简单的操作,就需要一个相当繁琐的操作序列。如果我们有一个三地址格式的计算机,我们可以写出以下内容:

ADD,X,Y` 将 X 的内容加到 Y 的内容上,并将结果存入 Z

三地址机相对于单地址机来说可能更快,因为它们可以在一个指令中完成其他机器需要三个操作才能完成的事情。不幸的是,这是技术发展的一个因素,片上寄存器比 DRAM 快,计算机设计者试图尽可能地将数据保持在片上寄存器中。

事实是更复杂的。与访问寄存器相比,访问内存要慢。这是硬件的一个特性。因此,将数据保持在寄存器中更有效率。

通过检查例如 ADD r2,Y 的执行,可以最好地了解 CPU 的工作方式。在以下代码块中,我们描述了 ADD r2,Y 指令的取指和执行阶段所执行的操作:


FETCH [MAR] ← [PC]        Move the contents of the PC to the MAR
      [PC]  ← [PC] + 1    Increment the contents of the PC
      [MBR] ← [[MAR]]      Read the current instruction from the memory
      [IR]  ← [MBR]       Move the contents of the MBR to the IR
      CU    ← [IRopcode]      Move the opcode from the IR to the CU
ADD   [MAR] ← [IRaddress]             Move the operand address to the MAR
[MBR] ← [[MAR]]Read the data from memory
      ALU← [MBR], ALU ← [r2] Perform the addition
      [r2]  ← ALUMove the output of ALU to the data register

同一行上的操作是同时执行的。

在取指阶段,操作码通过 CU ← [IRopcode] 传送到控制单元,并用于生成放置 ALU 在加法模式所需的所有内部信号。当 ALU 被编程为加法时,它将两个输入端的数据相加,在输出端产生一个和。

形式为 [PC][MAR][r2][r2] + [MBR] 的操作通常被称为 微指令。每个汇编级指令(例如,MOV, ADD)都作为一系列微指令执行。微指令是计算机设计者的领域。在 20 世纪 70 年代,一些机器是用户可微编程的——也就是说,你可以定义自己的指令集。

我们可以通过扩展取指阶段代码来测试执行阶段。以下 Python 代码提供了三条指令——将字面量加载到寄存器中,将内存内容加到寄存器中,以及停止。我们还使 Python 代码更加紧凑——例如,你可以在函数的返回语句中放置表达式。在这个例子中,我们返回两个值:ir >> 8ir & 0xFF。操作x >> yx的二进制值右移y位;例如,0b0011010110 >> 2给出0b0000110101。代码的阴影部分是我们执行的机器级程序:


# Implement fetch cycle and execute cycle: include three test instructions
mem = [0] * 12                     # Set up 12-location memory
pc = 0                             # Initialize pc to 0
mem[0] = 0b000100001100            # First instruction load r0 with 12
mem[1] = 0b001000000111            # Second instruction add mem[7] to r0
mem[2] = 0b111100000000            # Third instruction is stop
mem[7] = 8                         # Initial data inlocation 7 is 8
def fetch(memory):                 # Function for fetch phase
    global pc                      # Make pc a global variable
    ir = memory[pc]                # Read instruction and move to IR
    pc = pc + 1                    # Increment program counter for next cycle
    return(ir >> 8, ir & 0xFF)     # Returns opCode and operand
run = 1                            # run = 1 to continue
while run == 1:                    # REPEAT: The program execution loop
    opCode, address = fetch(mem)   # Call fetch to perform fetch phase
    if   opCode == 0b1111: run = 0 # Execute phase for  stop (set run to 0 on stop)
    elif opCode == 0b0001:         # Execute phase for load number
         r0 = address              # Load r0 with contents of address field
    elif opCode == 0b0010:         # Execute phase for add
         mar = address             # Copy address in opCode to MAR
         mbr = mem[mar]            # Read the number to be dded
         r0 = mbr + r0             # Do the addition
    print('pc = ',pc - 1, 'opCode =', opCode, 'Register r0 =',r0)
                                   # We print pc – 1 because the pc is incremented

此代码的输出如下:


pc =  0 opCode = 1  Register r0 = 12
pc =  1 opCode = 2  Register r0 = 20
pc =  2 opCode = 15 Register r0 = 20

注意,Python 术语elifelse if的缩写。前面的情况说明,“操作码是否为停止。如果不是,操作码是否为加载。如果不是,操作码是否为加法。”这允许进行一系列测试。我们将在稍后更详细地讨论elif

执行条件指令

到目前为止,我们已查看了一种能够以纯顺序模式执行程序的 CPU 结构——也就是说,计算机只能按严格顺序逐个执行指令流。我们在上一章中介绍了条件行为,现在我们将扩展 CPU,使其能够执行诸如BEQ Target(在零标志设置为Target时的分支)之类的指令,这些指令能够执行非顺序指令。

图 3.1中的计算机缺乏做出选择或重复一组指令的机制。为此,CPU 必须能够执行条件分支跳转图 3.5的框图显示了 CPU 实现条件分支所需的新地址和数据路径。

**图 3.5**——CPU 中的信息路径和条件指令

图 3.5——CPU 中的信息路径和条件指令

图 3.5中,我们向我们的计算机添加了三项内容。这些内容被突出显示:

  • 条件码 寄存器 (CCR)

  • CCR 和控制单元之间的路径

  • 指令寄存器的地址字段和程序计数器之间的路径。

条件码寄存器处理器状态寄存器记录了每个指令执行后的 ALU 状态,并更新进位、负数、零和溢出标志位。条件分支指令会查询 CCR 的标志。CU 随后要么按顺序执行下一条指令,要么跳转到另一条指令。让我们看看条件分支的细节。以下是对 CCR 位功能的提醒:

  • C = 进位:  当 n 位操作产生 n+1 位结果时发生进位(例如,当你的汽车里程表从 999...9 滚动到 000...0 时)。在 8 位计算机术语中,这是当 11111111 + 1 = 0000000 carry 1。

  • Z = 零:   如果最后操作生成了零结果,则设置此位。

  • N = 负数:如果最后的结果在 2 的补码算术中生成了负数,则设置此位——也就是说,如果字的最重要位为 1(例如,当数字被视为 2 的补码值时,00101010 是正数,10101010 是负数)。

  • V = 溢出:如果最后操作导致算术溢出,则设置此位,在 2 的补码算术中,如果结果超出了其允许的范围,则发生溢出。在此文本中,为了简单起见,我们通常不实现 V 标志。

条件代码寄存器连接到控制单元,使得指令可以查询它。例如,一些指令测试操作是否产生了正结果,进位位是否被设置,或者是否发生了算术溢出。我们需要一个机制,如果测试结果为true则执行一项操作,如果测试结果为false则执行另一项操作。

包含在图 3**.5中的最终修改是在指令寄存器的操作数字段(即目标地址)和程序计数器之间添加了一条路径。正是这个特性使得计算机能够对其对 CCR 的查询结果做出响应。

条件分支指令,例如带进位设置分支(BCS),测试 CCR 的进位位,如果测试的位是清除的,则按正常方式从内存中获取下一条指令。如果测试的位是设置的,则从指令寄存器中目标地址的位置获取下一条指令。在先前的描述中,我们说如果 CCR 的位被设置则进行分支;同样,如果位被清除,也可以进行分支(分支也可以基于多个 CCR 位的组合状态)。

分支操作可以用以下形式在寄存器传输语言中表达:

IF condition THEN action

在 RTL 中表达的传统机器级条件操作如下:

  1. 清除进位分支(如果 CCR 中的进位位为 0,则跳转到目标地址)

BCC target: IF [C] = 0 THEN [PC] [IRaddress]

  1. 等于分支(如果 CCR 中的 Z 位为 1,则跳转到目标地址)

BEQ target: IF [Z] = 1 THEN [PC] [IRaddress]

这两个动作都有一个ELSE条件,默认为[PC][PC] + 1

以下是一个汇编语言中条件分支的例子:


     SUB  r0,x    @ Subtract the contents of memory location x from register r0
     BEQ  Last    @ If the result was zero, then branch to Last; otherwise, continue
     .            @ Execute here if branch not taken
     .
Last              @ Target address of branch (if taken)

扩展计算机体系结构的最后一步是引入数据路径,允许将字面量加载到寄存器中——也就是说,将一个数字加载到寄存器中,这个数字在指令中而不是从内存中。z 位可能会令人困惑。如果结果为零,则 z 位设置为 1,如果结果不为零,则设置为 0。

处理字面量操作数

ADD r0,abc`` 这样的计算机指令指的是 CPU 内存中的某个操作数。有时,我们想要使用像 ADD r0,#12 这样的指令,其中源操作数提供了指令操作码部分所引用数据的 实际值——在这种情况下,12。尽管当这个指令以助记符形式编写时,符号 # 出现在操作数部分,但汇编器使用不同的操作码代码。

ADD r0,#literalADD r0,address

指令 ADD r0,#12RTL 中定义为 [r0] ← [r0] + 12.

注意,我们使用两种字面量的约定。一种是 ADD r0,#12,另一种是 ADDL r0,12。这符合典型的指令集。

图 3.6 显示,在 IR 的操作数字段和数据寄存器以及 ALU 之间需要额外的数据路径来处理字面量操作数。图 3.6 包含三个通用寄存器,r0r1r2。原则上,我们无法添加任何数量的寄存器。然而,内部寄存器的数量受指令中指定寄存器所需位数的限制。正如你所看到的,三个数据总线 ABC 用于在寄存器和 ALU 之间传输数据。

图 3.6 的结构可以实现比我们迄今为止使用的简单直接(绝对)寻址更复杂的寻址模式。考虑 MOV r1,[r0],它复制了地址在 r0 中的内存位置的值。在这里,r0 是实际数据的指针。这个指令可以通过以下微操作序列来实现:


MOV   [MAR] ← [r0]        Move the source operand address to the MAR
      [MBR] ← [[MAR]]     Read the actual operand from memory
      [MAR] ← [MBR]       Copy the address back to the MAR
      [r1]  ← [[MAR]]     Copy the data from memory to r1

这个序列已经被简化,因为,正如你从 图 3.6 中看到的,寄存器 r0 和 MBR 之间没有直接路径。你将不得不将 r0 的内容放到总线 A 上,通过 ALU 将总线 A 的内容传递到总线 C,然后将总线 C 复制到 MAR。

图 3.6 – 修改 CPU 以处理字面量操作数

图 3.6 – 修改 CPU 以处理字面量操作数

让我们扩展我们的 Python 代码,包括字面量操作和条件操作。以下 Python 代码实现了一个加载寄存器的字面量指令、加/减操作、零条件分支和停止。在这里,我们使用 LDRL 来表示字面量,而不是在字面量前加 #。要执行的程序如下:

地址 助记符 指令 二进制代码 备注
0 LDRL r0,9 将字面量9加载到r0 000100001001 [r0]9
1 SUB r0,7 r0中减去内存[7] 001100000111 [r0][r0] – mem[7]
2 BEQ 6 如果为零则跳转到 6 010000000110 如果z = 1 [PC] ← 6
3 STOP top execution 111100000000
4
5
6 STOP top execution 111100000000

表 3.4 – 图注

为了实现加载寄存器,我们只需将指令中的字面量移动到寄存器中。减法测试结果并设置零状态位z,如果结果是0则设置为1,如果不是则设置为0。条件分支测试 z 位,如果z = 1则将指令中的字面量加载到 pc 中。


                             # Simple program to test a branch instruction
mem = [0] * 12               # Set up a 12-location memory
pc = 0                       # Initialize program counter to 0
mem[0] = 0b000100001001      # First instruction loads r0 with 9 (i.e., 1001)
mem[1] = 0b001100000111      # Second instruction subtracts mem[7] from r0
mem[2] = 0b010000000110      # Third instruction is BEQ 6 (branch on zero to 6)
mem[3] = 0b111100000000      # Fourth instruction is stop
mem[6] = 0b111100000000      # Seventh instruction is stop
mem[7] = 9                   # Initial data in location 7 is 9
                             # Fetch returns opcode and address
def fetch(memory):           # This function, fetch, gets the instruction from memory
    global pc                # Declare pc as global because we modify it in the function
    ir = memory[pc]          # Read the instruction from memory
    pc = pc + 1              # Now point to the next instruction
    return(ir>>8, ir&0xFF)   # Return the opcode and address
z = 0                                             # Clear z bit initially
run = 1                                           # run = 1 to continue
while run == 1:                                   # Main loop REPEAT until stop found
    pcOld = pc                                    # Save current pc for display
    opCode, address = fetch(mem)                  # Perform fetch to get opcode
    if   opCode == 0b1111: run = 0                # Test for stop
    elif opCode == 0b0001: r0 = address           # Test for load literal
    elif opCode == 0b0010: r0 = r0 + mem[address] # Test for add
    elif opCode == 0b0011:                        # Test for subtract
        r0 = r0 - mem[address]                    # Do subtraction
        if r0 == 0: z = 1                         # Update z flag on subtract
        else:       z = 0
    elif opCode == 0b0100:                        # Test for branch on zero
        if z == 1: pc = address                   # If BEQ, load PC on zero flag
    print('pc = ',pcOld,'opCode =',opCode,'\tRegister r0 =',r0,'z = ',z)
                                                  # The '\t' performs a tab operation

此 Python 代码的输出如下:


pc =  0 opCode = 1  Register r0 = 9 z =  0
pc =  1 opCode = 3  Register r0 = 0 z =  1
pc =  2 opCode = 4  Register r0 = 0 z =  1        z = 1 so branch taken
pc =  6 opCode = 15 Register r0 = 0 z =  1

我们将字面量9加载到r0中,从内存位置7(包含9)的内容中减去,然后如果结果是0则跳转到位置6。这就是发生的事情。

描述了计算机的结构后,下一步是看看计算机执行的指令。

机器级指令

描述了计算机的工作原理后,我们现在更深入地看看计算机。我们感兴趣的是一条指令做了什么以及它需要什么资源(即数据位置或常量)。低级计算机操作(即机器代码或汇编语言)在内存或寄存器中操作二进制数据。尽管几十年来计算机的速度提高了数百万倍,但低级指令的本质几乎没有改变。

20 世纪 70 年代和 80 年代的第一代微处理器(例如 8080、6800、Z80 和 6502)使用了 8 位指令,这些指令必须连接起来以创建更实用的指令——例如,8 位微处理器通过连接两个连续的 8 位指令提供 16 位指令。

第二代微处理器,如英特尔(Intel)的 8086 和摩托罗拉(Motorola)的 68000,具有 16 位指令。这些指令也被连接起来,以创建足够长的指令来执行所有必要的操作。实际上,68000 实际上将五个连续的 16 位字连接起来,创建了一个巨大的 80 位指令。现代高性能微处理器(例如 ARM、MIPS 和 RISC-V)具有 32 位或 64 位指令,提供完整的指令集,无需连接连续的指令。当时将指令连接起来以增加操作码数量的 CISC 方法是一个很好的想法。然而,它通过使并行执行指令变得困难来降低性能,因为计算机不知道指令之间的边界在哪里,直到它们被解码。

在本书的后面部分,我们将简要地看看多长度指令集的概念。

指令类型和格式

现在,我们将描述计算机执行的基本操作类型。首先,一个惊喜。计算机需要多少种不同的指令?我的意思是,它需要多少种来解决问题,这些问题今天、明天以及未来任何时刻都可以通过计算机解决?惊人的答案是只有一个。是的,你可以通过一个单一指令的排列来解决任何问题。

根据维基百科,SBNZ a,b,c,d 指令(“如果非零则减去并分支”)从地址 a 的内容减去地址 b 的内容,将结果存储在地址 c,然后,如果结果不是 0,则将控制权转移到地址 d(如果结果是零,则执行顺序中的下一个指令)。用 RTL 表达,SBNZ 指令如下:


[c] ← [b] – [a]
if [c] != 0: [pc] ← d
else: [pc] ← [pc] + 1

所有的计算都可以使用这个单一指令来完成。实际上,这样的计算机将是不可能的低效和不切实际的。然而,它暗示了一个庞大而复杂的指令集并不是构建计算机的必要条件。今天的大多数计算机都有相对有限的指令数量。然而,一些计算机设计师现在为特定应用(例如,图形、信号处理和人工智能)创建优化的专用指令集增强。

从第一台计算机到今天拥有超过 1000 亿个晶体管的芯片,计算机的指令集包括以下三个操作类别。表 3.3 给出了指令组的名称、Python 中的操作示例以及典型的汇编语言指令。

指令组 典型 Python 代码 汇编语言
算术和逻辑 c = (a + b) * c ADD ,r2,r3
数据移动 x = y MOV ,r2
条件 if x == 4: y = 7 BEQ next

表 3.5 – 指令类别

我们将要设计的 TC1 有 32 位指令,可以提供高达 2³² = 4,294,967,296 个独特的指令。实际上,指令提供内存地址、数值常数(即,字面量)和寄存器编号,这意味着你可以定义的独特指令的数量要小得多。

TC1 有 32 位指令,但只有 16 位数据字。这种配置使得设计和理解计算机更容易,你可以用单个 32 位指令加载一个 16 位数据字。具有 32 位指令和数据的计算机必须使用复杂的加载方法来加载 32 位数据字,正如我们将在介绍 ARM 时看到的那样。

看起来可能有些奇怪,我允许二进制数以0b1101%1101的形式指定,也允许十六进制数以0x1AC$1AC的格式表示。我这样做有两个原因。第一个原因是我在摩托罗拉的世界长大,那里使用%$前缀,但现在我生活在 C 的世界,那里使用0b0x前缀。习惯使我更自然地使用%$。第二个原因是我想向你展示你可以选择自己的格式和约定。

TC1 的指令集是为了简单而不是计算上的优雅而设计的。从涉及的概念来看,指令集是现实的,但在实现上并不现实。就我们的目的而言,我们已将所有指令以相同的格式给出。在真实的计算机中,通常有几种指令类别,每种都有自己的格式。通过使用单一格式,我们可以简化指令解码和执行。

许多高性能微处理器使用 32 位指令集,这对于演示和教学目的来说非常理想。通常,计算机使用与指令相同大小的数据元素。TC1 使用 32 位指令但 16 位数据元素,因为学生比 32 位值更容易阅读和操作 16 位值(将 TC1 计算机修改为使用 32 位数据将是一个非常简单的任务)。

为了为 TC1 编写机器级程序,你必须手动将每个指令编码到一个 32 位的二进制序列中。这很容易做到,但非常繁琐。我们还设计了一个简单的汇编器,允许你以汇编语言的形式编写指令。TC1 汇编器将形式为ADD r7,r2,r3的指令翻译成一个二进制字符串,例如000000111101001100000000000000000

通常,汇编器是模拟器之外的一个独立代码片段。你为汇编器提供一个源文件(文本格式),汇编器创建一个二进制代码文件,模拟器执行该文件。TC1 汇编器是模拟器的一部分,所以你不必担心为模拟器创建二进制文件。

我们使 TC1 汇编器尽可能简单,以降低复杂性并保持最终的 Python 程序相对简短。编写一个全面的汇编器需要更多的高级语言代码。这个汇编器不对源程序进行错误检查(即,当你输入错误时不会检测到错误)。它支持使用符号值表示变量和地址——也就是说,你可以写BEQ loop而不是BEQ 7,其中符号名loop标记行号7

TC1 汇编器允许你以十进制、二进制或十六进制格式输入数字——例如,你可以写LDRL r0,255LDRL r0,0xFFLDRL r0,%11111111。操作LDRL的意思是,“将一个字面值(即实际值)加载到寄存器中。”在每种情况下,指令都将255的二进制值放入寄存器r0中。

所有计算机都基于内存或芯片上几个寄存器中的数据运行。通常,计算机有232个芯片寄存器。TC1 计算机有八个寄存器,r0r7

计算机指令有许多不同的格式,这取决于计算机的架构。以下是最基本的两种格式:

  • CISC 风格操作允许一般指令访问内存(例如,ADD r3, 1200 表示,将内存位置 1,200 的内容加到寄存器 3)

  • RISC 风格:所有数据处理操作都在寄存器之间(例如,ADD r1, r2, r3),唯一的内存访问是从内存中加载寄存器并存储寄存器到内存

我们在第一台计算机中使用的典型汇编语言指令格式如下:

格式 助记符 操作
双地址 MOV r0, r1 将寄存器r1的内容复制到寄存器r0
三地址 ADD r0, r1, r2 将寄存器r1的内容加到r2上,结果存入r0
文字 ADDL r0, r1, 24 将文字24加到r1上,并将结果存入r0
分支 BEQ 5 如果 z 位设置,则跳转到地址5的指令
间接加载寄存器 LDRI r0, [r1,10] 将地址r1 + 10的内存内容加载到r0

没有一种通用的汇编语言格式,不同的汇编器之间(即使是同一台机器)的约定也有所不同。例如,一种汇编语言可能使用MOV r1, r2的格式来将r2加载到r1,而另一种汇编语言可能使用它来将r1加载到r2——也就是说,目标可以在左边或右边。我把操作数的目标放在左边,这似乎是更常见的约定。我还将目标以粗体字显示,以作为提醒。

下面是一个示例程序,它将前 10 个整数相加,这是在一种假设的汇编语言中的代码片段。这不是编写此代码片段最有效的方法;它只是作为一个演示。我们在这里设计的 TC1 汇编器版本接受大写或小写字母,并且可以使用空格或逗号作为分隔符——例如,你可以愉快地写出以下内容:

lOOp aDdL r1 R2,r3 or

Loop ADDL R1, r2, R3.`

考虑以下 TC1 汇编语言示例。请注意,我使用@来表示注释字段,因为这是 ARM 汇编语言的标准,我们将在后面介绍。我们将继续在 Python 中使用#符号作为注释。


        LDRL r0,0          @ Load register r0 with 0 (the sum)
        LDRL r1,0          @ Load register r1 with 0 (the counter)
Loop    ADDL r1,r1,1       @ Increment the counter in r1
        ADD  r0,r0,r1      @ Add the count to the sum in r0
        CMPL r1,10         @ Compare the count with 10
        BNE  Loop          @ Branch back to Loop until all numbers added

CISC 和 RISC

在这本书中,我们反复使用 RISC 和 CISC 这两个术语。这两个术语对于理解现代计算机至关重要。它们描述了两种不同的计算机实现方法。20 世纪 80 年代初,CISC 与 RISC 之战见证了两种不同架构在计算市场中的竞争。术语复杂指令集计算机(CISC)与术语模拟手表类似。当数字手表被发明时,带有移动指针的手表突然变成了模拟手表,以区别于数字手表。同样,术语复杂指令集计算机直到它被创造出来以与新的精简指令集****计算机(RISC)对比之前并不存在。

从计算机被发明的那一刻起,它们就一直在增长。随着技术的进步,新特性只是被附加到现有的计算机上。有人曾经说过,如果飞机的发展像计算机一样,每架巨型喷气式飞机的核心都会有一个 1903 年的莱特兄弟飞机。这种方法并不经济,因为技术发生了如此巨大的变化,计算机的设计需要重新考虑。特别是,内存容量呈指数增长,每位的成本急剧下降。同样,8 位和 16 位字长被 32 位和 64 位指令集所取代。用老方法做事并不高效。

第一代和第二代微处理器是基于累加器的。它们在处理器的累加器和内存位置上执行操作。由于只有少数累加器,指令被称为一个半地址,因为它们有一个内存地址和一个累加器地址(由于只有少数累加器,它们被戏称为有“半个地址”)。要执行C = A + B(其中ABC是内存地址),你必须编写以下代码:


LDA A            @ Load accumulator with A
ADD B            @ Add B to the accumulator
STA C            @ Store the accumulator in C

通过累加器传递所有数据会形成瓶颈。由于计算机速度增长速度快于内存速度,程序员希望尽可能多地将数据保持在芯片上。

RISC 解决方案采用了寄存器到寄存器架构。在内存上允许的唯一操作是在寄存器和内存之间传输数据。RISC 处理器没有一或两个累加器,而是有 16 或 32 个寄存器。在 RISC 处理器上的前述代码通常可以表示为以下内容:


LDR r0,[r1]      @ Load r0 with data pointed at by r1
LDR r2,[r3]      @ Load r2 with data pointed at by r3
ADD r4,r0,r2     @ Add r0 and r2, result in r4
STR r4,[r5]      @ Store r4 in memory pointed at by r5

所有数据移动都是在内存和寄存器之间进行的,数据处理操作仅适用于寄存器。指令有三个操作数。

RISC 计算机引入了其他增强功能,例如重叠指令执行(称为流水线)。20 世纪 80 年代许多人预期英特尔的 CISC 计算机会消失。但它们并没有。英特尔巧妙地将 RISC 特性融入其 CISC 处理器中。AMD 设计了一种 RISC 架构,它将英特尔的 CISC 指令转换成一系列 RISC 命令,然后再执行。

总结来说,CISC 处理器的指令集在内存中的操作数和寄存器中的操作数之间执行操作。所有 RISC 数据处理操作都在寄存器中的操作数之间进行。RISC 处理器允许的唯一内存操作是从内存中加载寄存器和将寄存器存储在内存中

表示字面量的两种方式

由于汇编器在竞争激烈的行业中迅速发展,汇编器在表示指令方面有所不同。每个制造商都为其自己的微处理器设计了一个汇编器。一些采用了从左到右的惯例,其中目标操作数在右边,而另一些则采用了从右到左的惯例,其中目标操作数也在右边。因此,一个制造商的mov a,b意味着a ← b,而另一个的则意味着b ← a。同样,助记符也没有标准化——例如,MOVE, MOVLDA都定义了一个复制操作。

由于汇编器只是机器代码的人类可读版本,我们如何表示指令实际上并不重要。真正执行的是二进制代码,而不论我们在文本形式中如何表示它。然而,从教学和学习的角度来看,这些惯例的变化是一种麻烦。考虑指令中字面值的表示。

一些汇编器通过使用特殊的指令来表示字面量,例如,对于三个寄存器的加法,使用ADDr1,r2,r3,而对于字面操作数,使用ADDL r1,r2,24。其他汇编器在两种情况下都使用相同的助记符,但通过一个符号来前缀字面量以表示这是一个字面量操作,例如,ADD r1,r2,#25。有些汇编器使用#来表示字面量,而有些则使用%`。

在本文中,我们在一些模拟器的设计中使用了ADDL惯例,但当我们介绍 ARM 处理器时,我们将使用#惯例,因为 ARM 汇编器就是这样使用的。回顾起来,如果我现在再写这本书,我可能会倾向于只使用一种表示,即#符号。然而,通过使用ADDADDL,我能够简化 Python 代码,因为寄存器和字面操作数之间的决策点是在检查助记符时做出的,而不是在检查字面量时。

总结

在本章的关键部分,我们介绍了冯·诺伊曼计算机及其fetch-execute周期,其中指令从内存中读取,解码,并在两个阶段操作中执行。正是这些动作,我们将在后面的章节中学习来模拟,以便在软件中构建计算机。我们已经研究了指令执行时的信息流。我们在这里介绍的计算机模型是传统模型,它没有考虑到当前在流水线中执行多个指令的技术。

我们还研究了指令格式,并描述了它包含几个字段——例如,定义操作的指令码以及操作所需的数据(例如,地址、字面量和寄存器编号)。你最终将能够设计自己的指令(从而定义计算机的指令集架构)并创建一个能够执行这些指令的计算机。

在描述冯·诺伊曼计算机的操作时,我们引入了足够的 Python 代码来展示我们的方向,并暗示了如何进行模拟。

在下一章中,我们将开始更仔细地研究解释器的概念,它读取机器级指令并执行其预期操作。

第四章:编译解释器 – 第一步

在本章中,我们将通过构建一个只能执行单个指令的非常原始的模拟器来迈出构建计算机模拟器的第一步。一旦我们迈出这一步,我们就可以通过逐步增强这个模拟器来继续前进。

本章我们将涵盖的关键主题如下:

  • 使用一条指令设计最小计算机

  • 设计一个可以解码和执行多个指令的简单模拟器

  • 被称为 TC1 的通用计算机指令集

  • 在 Python 中处理位(布尔运算)

  • 将二进制形式的指令解码为其组成部分

  • 解码后执行指令

  • 计算机中的算术运算

  • 在 Python 中设计函数

  • 计算机指令集中的分支和流程控制指令

这些主题共同涵盖了三个领域。一些主题扩展了我们对 Python 的了解,帮助我们构建模拟器。一些主题介绍了典型数字计算机的指令集,我们称之为 TC1(TC1 简单意味着教学计算机 1)。一些主题涵盖了 TC1 在 Python 中的实际设计。

本章将介绍计算机模拟器并查看一些基本构建块。实际的模拟器将在第六章中介绍。

技术要求

您可以在 GitHub 上找到本章使用的程序,链接为github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter04

一个超原始的单指令计算机

我们的第一个单指令解释器演示了指令解码和执行,这是所有模拟器的关键。这台计算机有一个九个位置的内存,mem[0]mem[8],排列成一个整数列表。内存的内容预设为mem = [4,6,1,2,7,8,4,4,5]。内存位置为 0 到 8,在列表中从左到右读取;例如,内存位置 0 包含值为 4,位置 1 包含值为 6,位置 8 包含值为 5。

计算机有一个包含八个寄存器的数组,r[0]r[7]。这些在 Python 中通过以下方式指定:


 r = [0,0,0,0,0,0,0,0]                  #. Define a list of 8 registers and set them all to 0.

我们将要执行的单一指令是add r[4],mem[3],mem[7]。这条指令将内存位置 3 的内容与内存位置 7 的内容相加,并将和放入寄存器 4。我们选择从单指令计算机开始,因为它可以用几行 Python 代码表达,同时它执行了许多真实计算机模拟器所需的操作。

我们将这种内存到内存的操作定义为演示。它不是任何真实计算机语言的一部分。有趣的是,它比大多数真实计算机指令更复杂,因为它使用内存到内存操作而不是寄存器到寄存器操作。

我们将编写必要的 Python 代码来以文本形式读取这个指令并执行它定义的操作。下面代码中的两条阴影行将这个指令分割成一个可以处理的标记列表。标记是指令中的一个元素(就像英语中的句子可以被分割成我们称之为单词的标记一样)。这里的标记是 'add''r[4]''mem[3]''mem[7]'

这个指令读取 mem[3] 的内容,它是 2;读取 mem[7] 的内容,它是 4;将它们相加得到 2 + 4 = 6;然后将值 6 存储在寄存器 4 中。执行此指令后,寄存器 r[4] 的值应该是 6:


mem = [4,6,1,2,7,8,4,4,5]            # Create a 9-location memory. Fill with some data
r =   [0,0,0,0,0,0,0,0]                 # Create a set of 8 registers, all initialized to 0
inst   = 'add r[4],mem[3],mem[7]'       # inst is our solitary instruction, stored as a string
inst1  = inst.replace(' ',',')          # Step 1: Replace any space with a comma
inst2  = inst1.split(',')               # Step 2: Split instruction into tokens at each comma
token0 = inst2[0]                       # Step 3: Get token0 via the 'add' instruction
token1 = inst2[1]                       # Step 4: Get token1, register 'r[4]'
token2 = inst2[2]                       # Step 5: Get token2, 'mem[3]'
token3 = inst2[3]                       # Step 6: Get token3, 'mem[7]'
value1 = int(token1[2])                 # Step 7: Get the register number as an integer
value2 = int(token2[4])                 # Step 8: Get the first memory number as an integer
value3 = int(token3[4])                 # Step 9: Get the second memory number as an integer
if token0 == ‹add›:                     # Step 10: Test for an 'add' instruction
  r[value1] = mem[value2] + mem[value3]# Step 11: If ADD, then add the contents of the memory
print('Registers: ',r)

inst1 = inst.replace(' ',',') 操作将指令中的空格替换为逗号,得到 'add r[4],mem[3],mem[7]'。现在这是一个由逗号分隔的标记字符串。

下一步是创建一个标记列表,以便我们可以访问指令的各个组成部分。inst2 = inst1.split(',') 的效果是创建一个字符串列表:


inst2 = ['add', 'r[4]', 'mem[3]', 'mem[7]']

split() 方法接受一个字符串,并使用指定的分隔符创建一个字符串列表。如果 y = x.split('!'),则 y 的值是一个字符串列表,分隔符是 !。下面展示了 split() 的一个使用示例:


>>> x = 'asssfg! !   !,!!rr'
>>> x
'asssfg! !   !,!!rr'
>>> y = x.split('!')
>>> y
['asssfg', ' ', '   ', ',', '', 'rr']

token2 = inst2[2] 这一行给出 token2 = 'mem[3]';即第四个标记。

value2 = int(token2[4]) 这一行给出 value2 = 3,因为第二个切片是 'mem[3]' 字符串中的 3。注意我们使用 int() 函数将字符 4、3 和 7 转换为整数值。当从字符串到数值操作时,你必须记住在字符和整数字符类型之间进行转换。

如果我们执行这个程序,我们会得到以下输出:


Registers:  [0, 0, 0, 0, 6, 0, 0, 0]    Output from the program. The correct value is in r4

现在我们已经介绍了模拟器的基本组件,下一步是构建一个可以处理更多指令的计算机,尽管这只是典型操作的微小子集。然而,这个子集包括了所有真实计算机操作类别。

在 Python 中构建一个简单的计算机解释器

我们可以将模拟器的概念进一步发展,并使用我们刚刚开发的概念执行包含多个指令的程序。连续的指令通过从程序内存中逐个读取它们来执行,并使用程序计数器来跟踪我们在程序中的位置。

请注意,当我们提到一个 程序 时可能存在的一个混淆来源。我们正在用高级语言 Python 编写程序来模拟计算机。那个模拟计算机运行的是汇编语言编写的程序。因此,程序这个术语可以指两个不同的实体。应该从上下文中清楚我们指的是哪一个。

注意,伪代码不是一种计算机语言,而是一种用几乎纯英语表达计算机算法的方法。因此,一段伪代码可以代表高级语言(如 Python)或汇编语言。

在以下示例中,汇编语言指令的源程序以 Python 列表的形式表达,其中每个指令都是一个字符串:


prog=['LDRL r0 0','LDRL r1 0','ADDL r1 r1 1','ADD r0 r0 r1','CMPL r1 10', \
     'BNE 2','STOP']

这些指令的效果如下:


LDRL r0 0           Load r0 with literal 0
ADDL r1 r1 1        Add 1 to r1 and put the result in r1
ADD  r0 r0 r1       Add r1 to r0 and put the result in r0
CMPL r1 10          Compare the contents of r1 with literal 10
BNE  2              Branch to instruction 2 if the last result is not 0
STOP                Stop

为了简化 Python 代码,我们使用了空格作为分隔符——例如,LDRL r0,0 被写成 LDRL r0 0

真实计算机将汇编语言指令存储为 32 位或 64 位二进制数字的字符串。我们将直接从文本字符串中执行汇编语言指令,以避免将文本转换为二进制,然后再将二进制作为指令进行解释。在这里,我们有一个目标:展示程序是如何执行的。

在运行上述汇编级代码的计算机上,尽管添加额外指令非常容易,但只有少数指令。在整个文本中,术语 opcode 或操作码表示汇编语言指令(如 ADDBNE)的二元代码(或文本版本)。模拟器程序的伪代码结构如下:


prog=['LDRL r0 0','LDRL r1 0','ADDL r1 r1 1','ADD r0 r0 r1', \
      'CMPL r1 10','BNE 2','STOP']
Define and initialize variables (PC, registers, memory)
while run == True:
   read instruction from prog
   point to next instruction (increment program counter)
   split instruction into fields (opcode plus operands)
   if   first field = op-code1 get operands and execute
   elif first field = op-code2 get operands and execute
   elif first field = op-code3 . . .
   . . .
   else declare an error if no instruction matches.

这个汇编语言程序,prog(在模拟器代码中作为列表提供),使用条件分支 BNE 2,如果前一个操作结果不是 0,则跳回指令 2。在下一段 Python 程序中的汇编语言版本使用符号名 Loop 来指示分支的目标,但实际上代码使用的是字面量 2。我们将在稍后查看如何处理像 Loop 这样的符号名。

原始模拟器的 Python 代码

下面的代码是此模拟器的 Python 代码。从第 0 行到第 6 行的初始注释显示了汇编语言程序:


#                                 @ Test fetch/execute cycle
#0       LDRL r0 0                @ Load register r0 with 0 (the sum)
#1       LDRL r1 0                @ Load register r1 with 0 (the counter)
#2 Loop  ADDL r1 r1 1             @ REPEAT Increment counter in r1\. Loop address = 2
#3       ADD  r0 r0 r1            @ Add the count to the sum in r0
#4       CMPL r1 10               @ Compare the count with 10
#5       BNE  Loop                @ Branch back to Loop until all numbers added (BNE 2)
#6       STOP                     @ Terminate execution
prog=['LDRL r0 0','LDRL r1 0','ADDL r1 r1 1','ADD r0 r0 r1','CMPL r1 10', \
      'BNE 2','STOP']
r = [0] * 8                       # Initialize r[0], r[1], ... r[7] and initialize to 0
z = 0                             # Z is zero flag: if a compare result is 0, z = 1
run = True                        # Run flag True to execute
pc = 0                            # Pc is program counter, initially 0
while run == True:                # The fetch/execute loop
    inst = prog[pc]               # Read next instruction from memory
    oldPC = pc                    # Save the old value of the the pc (program counter)
    pc = pc + 1                   # Point to the next instruction
    inst = inst.split(' ')        # Split divides the instruction into tokens (separate fields)
    if inst[0] == 'ADD':          # Test for ADD rd,rS1,rS2 instruction
        rd  = int(inst[1][1])     # Get dest, source 1 and source 2
        rS1 = int(inst[2][1])
        rS2 = int(inst[3][1])
        r[rd] = r[rS1] + r[rS2]   # Add reg 1 and 2 sum in destination register
    elif inst[0] == 'ADDL':       # Test for ADD literal instruction, ADDL
        rd  = int(inst[1][1])     # If found, get destination register
        rS1 = int(inst[2][1])     # Now get source 1 register
        literal =  int(inst[3])   # Now get the literal
        r[rd] = r[rS1] + literal  # Add reg 1 and literal
    elif inst[0] == 'BNE':        # Test for branch on not zero
        if z == 0:                # If z is 0 (last register not zero)
           pc = int(inst[1])      # Get branch destination from operation
    elif inst[0] == 'CMPL':       # Test register for equality with a literal
        z = 0                     # Set z flag to 0 (assume not equal)
        rVal = r[int(inst[1][1])] # Register value
        intVal = int(inst[2])     # Literal value
        if rVal == intVal: z = 1  # If reg value =s literal, z=1
    elif inst[0] == 'LDRL':       # Test for load literal into register operation
        rd = int(inst[1][1])      # Get destination register
        data = int(inst[2])       # Test literal value
        r[rd] = data              # Store literal in destination register
    elif inst[0] == 'STOP':       # Test for STOP instruction
        run = False               # If STOP found, then set run flag to False
        print('End of program reached')
    else:                         # If we end up here, not a valid instruction
        run = False               # So set run flag to False and stop
        print('Error: illegal instruction ',inst)
    print('PC = ',oldPC,'r0 = ',r[0],'r1 = ',r[1],'z = ',z)  # Print results
                                  # Repeat loop until Run = False

代码的有趣部分是从指令中提取操作数。考虑 ADDL r1 r2 3 指令,这意味着将一个字面量加到源寄存器上,并将和放入目标寄存器。目标寄存器是 r1,源寄存器是 r2,字面量是 3

Python 的 inst = inst.split(' ') 操作使用空格作为分隔符将字符串转换为子字符串列表。因此,inst 的新值是以下列表:


inst =  ['ADDL', 'r1', 'r2', '3'] # An instruction converted into a list of substrings

我们现在可以检查这个列表的四个字段;例如,inst[0] = 'ADDL' 给我们实际的指令助记符。

假设我们想要获取源寄存器的内容,r2。源寄存器在列表的第三个位置,['ADDL', 'r1', 'r2', '3'];即 inst[2]。让我们写下 rS1 = inst[2]rS1 的值是 'r2'

我们希望将寄存器号(即 2)作为整数,因此我们必须获取 r2 的第二个字符并将其转换为整数。我们可以用以下方式做到这一点:


rS1 = int(rS1[1])              # get second character of the rS1 string and convert it into an integer

我们可以将这两个表达式合并为一个,如下所示。


rS1 = int(inst[2][1])        # inst[2][1], which gets character 1 of substring 2.

我们创建的小型计算机只执行了五种不同的指令,但它包括了真实计算机中的许多重要组件。这台计算机直接从汇编语言形式执行指令,而不是从二进制代码执行。下一步是在我们能够构建一个更真实的机器之前,更仔细地看看指令集。

构建了模拟器之后,下一步是看看计算机可以执行哪种类型的指令。

在下一节中,我们将为 TC1 计算机开发一套指令集。除了提供一个指令集设计的实际例子外,我们还将展示指令是如何被分成多个字段,并且每个字段都提供了关于当前指令的一些信息。

TC1 指令集

在本节中,我们将介绍我们演示计算机的关键组件:其指令集。这个计算机,TC1,具有许多真实计算机的功能,易于理解和修改。我们将首先介绍 TC1 指令集编码。

为了简化,我们可以使用单独的程序和数据内存。这种与传统冯·诺伊曼模型的偏离使我们能够拥有 32 位的程序内存和 16 位的数据内存。此外,我们不必担心不小心将数据放在程序区域的中间。

典型的指令有几个字段;例如,操作码、寄存器和字面量(即常数)。然而,每个字段中的位数总和必须等于指令的总长度。

现代计算机通常为每类指令采用不同的格式。这优化了操作码到位的分配;例如,一个分支指令可能有一个 4 位的操作码和一个 28 位的字面量字段,而一个数据处理指令可能有一个 17 位的操作码和三个 5 位的寄存器选择字段。

为了简化起见,TC1 计算机有一个单一、固定的格式。所有指令的字段数量相同,并且每个指令的字段大小相同。如图图 4**.1所示,一条指令由操作类别加上操作码、三个寄存器字段和一个字面量字段组成。图 4**.1显示了操作码字段为 7 位,其中包含 2 位的操作码类别和 5 位的实际操作码。

这种指令格式的结构效率低下,因为如果一个指令不访问寄存器,那么三个寄存器选择字段就浪费了。在一个如 ADD rd,rS1,rS2 的三寄存器指令中,rd 寄存器是目的寄存器,rS1 是源寄存器 1,rS2 是源寄存器 2:

图 4.1 – TC1 指令格式(RISC 风格)

图 4.1 – TC1 指令格式(RISC 风格)

我们将 16 位分配给字面量字段,以便我们可以用一条指令将常数加载到内存中。这留下了 32 - 16 = 16 位可以分配给所有其他字段。

TC1 具有三寄存器格式,这是 ARM 和 MIPS 等加载和存储计算机的典型特征。如果我们有八个寄存器,则需要 3 x 3 = 9 位来指定所有三个寄存器。在为立即数分配 16 位和为寄存器选择分配 9 位之后,我们剩下 32 - (16 + 9) = 7 位来指定多达 128 种不同的可能指令(2⁷ = 128)。

操作码字段本身分为四个类别或类别,占用两位,为每个类别的指令留下 7 - 2 = 5 位。表 4.1定义了指令的类别(类别):

班级 注释
0 0 特殊操作 执行如STOP或读取键盘等功能的操作
0 1 数据传输 将数据从一个地方复制到另一个地方的操作
1 0 数据处理 算术和逻辑数据处理操作
1 1 流控制 控制指令序列的操作,如BEQ

表 4.1 – TC1 指令类别

表 4.2展示了 TC1 指令集。第一列(01 00001)将内存位置的值加载到寄存器中。最左边的两位分开表示指令组:

二进制代码 操作 助记符 指令格式****rrr = Rd, aaa = rS1, bbb = rS2 代码格式
00 00000 停止操作 STOP 00 00000 000 000 000 0 0 0 0 0
00 00001 无操作 NOP 00 00001 000 000 000 0 0 0 0 0
00 00 010 从键盘获取字符 GET r0 00 00010 rrr 000 000 0 1 0 0 0
00 00011 获取随机字符 RND r0 00 00011 rrr 000 000 L 1 0 0 1
00 00100 在寄存器中交换字节 SWAP r0 00 00100 rrr 000 000 0 1 0 0 0
00 01000 在寄存器中打印十六进制值 PRT r0 00 01000 rrr 000 000 0 1 0 0 0
00 11111 终止程序 END 00 11111 000 000 000 0 0 0 0 0
01 00000 从寄存器加载寄存器 MOV r0,r1 01 00000 rrr aaa 000 0 1 1 0 0
01 00001 从内存加载寄存器 LDRM r0,L 01 00001 rrr 000 000 L 1 0 0 1
01 00010 用立即数加载寄存器 LDRL r0,L 01 00010 rrr 000 000 L 1 0 0 1
01 00011 间接加载寄存器 LDRI r0,[r1,L] 01 00011 rrr aaa 000 L 1 1 0 1
01 00100 将寄存器存储到内存 STRM r0,L 01 00100 rrr 000 000 L 1 0 0 1
01 00101 间接存储寄存器 STRI r0,[r1,L] 01 00101 rrr aaa 000 L 1 1 0 1
10 00000 寄存器加寄存器 ADD r0,r1,r2 10 00000 rrr aaa bbb 0 1 1 1 0
10 00001 将寄存器加到立即数 ADDL r0,r1,L 10 00001 rrr aaa 000 L 1 1 0 1
10 00010 从寄存器减去寄存器 SUB r0,r1,r2 10 00010 rrr aaa bbb 0 1 1 1 0
10 00011 从寄存器减去立即数 SUBL r0,r1,L 10 00011 rrr aaa 000 L 1 1 0 1
10 00100 寄存器乘寄存器 MUL r0,r1,r2 10 00100 rrr aaa bbb 0 1 1 1 0
10 00101 立即数乘以寄存器 MULL r0,r1,L 10 00101 rrr aaa 000 L 1 1 0 1
10 00110 寄存器除以寄存器 DIV r0,r1,r2 10 00110 rrr aaa bbb 0 1 1 1 0
10 00111 寄存器除以立即数 DIVL r0,r1,L 10 00111 rrr aaa 000 L 1 1 0 1
10 01000 寄存器模寄存器 MOD r0,r1,r2 10 01000 rrr aaa bbb 0 1 1 1 0
10 01001 寄存器模立即数 MOD r0,r1,L 10 01001 rrr aaa 000 L 1 1 0 1
10 01010 与寄存器到寄存器 AND r0,r1,r2 10 01000 rrr aaa bbb 0 1 1 1 0
10 01011 与寄存器到立即数 ANDL r0,r1,L 10 01001 rrr aaa 000 L 1 1 0 1
10 01100 或寄存器到寄存器 OR r0,r1,r2 10 01010 rrr aaa bbb 0 1 1 1 0
10 01101 或寄存器到立即数 ORL r0,r1,L 10 01011 rrr aaa 000 L 1 1 0 1
10 01110 异或寄存器到寄存器 EOR r0,r1,r2 10 01010 rrr aaa bbb 0 1 1 1 0
10 01111 异或寄存器到立即数 EORL r0,r1,L 10 01011 rrr aaa 000 L 1 1 0 1
10 10000 非寄存器 NOT r0 10 10000 rrr 000 000 0 1 0 0 0
10 10010 增加寄存器 INC r0 10 10010 rrr 000 000 0 1 0 0 0
10 10011 减少寄存器 DEC r0 10 10011 rrr 000 000 0 1 0 0 0
10 10100 寄存器比较寄存器 CMP r0,r1 10 10100 rrr aaa 000 0 1 1 0 0
10 10101 比较寄存器与立即数 CMPL r0,L 10 10101 rrr 000 000 L 1 0 0 1
10 10110 带进位加 ADC r0,r1,r2 10 10110 rrr aaa bbb 0 1 1 1 0
10 10111 带借位减 SBC r0,r1,r2 10 10111 rrr aaa bbb 0 1 1 1 0
10 11000 逻辑左移 LSL r0,L 10 10000 rrr 000 000 0 1 0 0 1
10 11001 逻辑左移立即数 LSLL r0,L 10 10000 rrr 000 000 L 1 0 0 1
10 11010 逻辑右移 LSR r0,L 10 10001 rrr 000 000 0 1 0 0 1
10 11011 逻辑右移立即数 LSRL r0,L 10 10001 rrr 000 000 L 1 0 0 1
10 11100 左旋转 ROL r0,L 10 10010 rrr 000 000 0 1 0 0 1
10 11101 左旋转立即数 ROLL r0,L 10 10010 rrr 000 000 L 1 0 0 1
10 11110 右旋转 ROR r0,L 10 10010 rrr 000 000 0 1 0 0 1
10 11111 右旋转立即数 RORL r0,L 10 10010 rrr 000 000 L 1 0 0 1
11 00000 无条件分支 BRA L 11 00000 000 000 000 L 0 0 0 1
11 00001 零条件分支 BEQ L 11 00001 000 000 000 L 0 0 0 1
11 00010 非零条件分支 BNE L 11 00010 000 000 000 L 0 0 0 1
11 00011 负条件分支 BMI L 11 00011 000 000 000 L 0 0 0 1
11 00100 跳转到子程序 BSR L 11 00100 000 000 000 L 0 0 0 1
11 00101 从子程序返回 RTS 11 00101 000 000 000 0 0 0 0 0
11 00110 减少且非零分支 DBNE r0,L 11 00110 rrr 000 000 L 1 0 0 1
11 00111 减少且零分支 DBEQ r0,L 11 00111 rrr 000 000 L 1 0 0 1
11 01000 将寄存器推入堆栈 PUSH r0 11 01000 rrr 000 000 0 1 0 0 0
11 01001 从堆栈中拉取寄存器 PULL r0 11 01001 rrr 000 000 0 1 0 0 0

表 4.2 – TC1 指令编码(4 个代码格式位不是操作码的一部分)

最右侧的列称为1001,告诉汇编器该指令需要目标寄存器和 16 位字面值。代码0000告诉我们该指令不包含任何操作数。

我们选择这些指令来展示一系列操作。许多这些指令是 RISC 计算机(如 ARM)的典型代表(即我们不包含对内存中数据的操作)。注意,稍后我们将介绍一个内存到寄存器指令集的模拟器,它允许执行如ADD r1,12这样的操作,其中12是一个内存位置。

表 4.2 中的一些指令在真实计算机上不存在;例如,从键盘读取字符。我们添加这些指令是为了简化调试和实验。同样,TC1 可以生成随机数,这是大多数计算机指令集中没有的特性。然而,它对于生成随机测试数据很有用。一些指令,如子程序调用和返回,实现了更复杂的操作,我们将在后面遇到。

标有L的列表示字面值(即 16 位常数)。

第四列(rrraaabbb)显示了三个寄存器选择字段的位置。最右侧的字段(0L)表示字面值的 16 位。如果所有位都是 0,我们显示0;如果指令需要 16 位字面值,则显示L。如果一个寄存器字段不是必需的,相应的字段将用零填充(尽管这些位是什么值并不重要,因为指令不会使用这些位)。

记住,最右侧列中的四个位不是操作码或指令本身的一部分。这些位显示了当前指令所需的字段:目标寄存器(rrr)、源 1 寄存器(aaa)、源 2 寄存器(bb)和字面值L。例如,代码1100表示该指令有一个目标寄存器和单个源寄存器。

解释一些指令

表 4.2 中的一些指令是真实处理器的典型代表。包括了一些指令以提供有用的功能,例如生成随机数以供测试使用。本节描述了表 4.2 中的一些指令。

STOP终止指令处理。大多数现代计算机没有显式的STOP操作。

NOP除了推进程序计数器外不做任何事情。它是一个有用的占位符操作,可以作为代码中的标记、未来代码的占位符或测试的辅助。

GET从键盘读取数据,并提供了一种简单的方法将键盘输入传送到寄存器。这在测试程序时很有用,但它不是一个正常的计算机指令。

RND 生成随机数。它不在计算机指令集中,但在测试代码时提供了一种生成内部数据的优秀手段。

SWAP 交换高字节和低字节 – 例如,0x1234 变为 0x3412

加载和存储指令在内存和寄存器之间移动数据。这类指令成员之间的区别在于方向(存储是从计算机到内存,而加载是从内存到计算机),大小(某些计算机允许字节、16 位或 32 位传输),以及寻址模式(使用字面值、绝对地址或寄存器地址)。

MOV 将一个寄存器的内容复制到另一个寄存器 – 例如,MOV r3,r1r1 的内容复制到 r3MOV 实质上是一个加载寄存器与寄存器(LDRR)指令。

LDRL 将寄存器加载为字面值 – 例如,LDRL r3,20 将寄存器 r3 加载为 20

LDRM 将寄存器的内容加载到由字面值地址指定的内存位置。LDRM r1,42r1 加载为内存位置 42 的内容。这个指令在现代 RISC 处理器上不存在。大多数现代处理器不允许你访问绝对内存地址,

LDRI 是加载寄存器索引(或加载寄存器间接)指令,将寄存器的内容加载到由寄存器内容加字面值地址指定的内存位置。LDRM r2,[r3,10]r2 加载为地址由 r3 内容加 10 给定的内存位置的内容。这个指令是标准的 RISC 加载操作。

STRM 是存储寄存器内存指令,将寄存器存储在由字面值地址指定的位置。STRM r2,15 将寄存器 r2 存储在内存位置 15。RISC 处理器不实现这个指令。

STRI 是存储寄存器索引指令,将寄存器存储在由寄存器加字面值指定的位置。STRI r2,[r6,8]r2 存储在地址为 r68 的内存位置。

STRSTRMSTRI 指令是异常的,因为它们将源操作数和目标操作数写入的顺序与 LDR(以及所有其他处理器操作)相反;也就是说,如果你写 LDR r0,PQR,你应该写 STR PQR,r0 来表示反向数据流。但是,按照惯例和实践,我们并不这样做。

DBNE 我们添加了递减且非零分支指令是为了好玩,因为这让我想起了我的老摩托罗拉时代。68000 是一个强大的微处理器(在当时)具有 32 位架构。它有一个递减和分支指令,用于循环的末尾。在循环的每次迭代中,指定的寄存器都会递减,如果计数器不是-1,则返回到标签。DBNE r0,L 递减 r0,如果计数器不为零,则跳转到编号为 L 的行。

寄存器间接寻址

有两条指令需要特别提及,因为它们不是关于数据操作,而是关于在内存中访问数据。这些是 LDRISTRI,它们都使用寄存器间接寻址。在 表 4.2 中,0b0100011 操作码对应于 LDRI(即,加载寄存器 间接)。这条指令并不给出操作数的实际地址;它告诉你地址在哪里可以找到。寄存器间接寻址模式也称为基于指针或索引寻址。内存中的操作数地址由寄存器的内容加上偏移量给出;即,r[rD]=mem[r[rS1]+lit]。如果我们使用粗体和阴影,可以使它的解释更容易一些:


r[rD] = mem[r[rS1]+lit]

图 4**.2 展示了 LDRI r1,[r0,3] TC1 指令的效果,其中指针寄存器 r0 包含 100,操作数在内存地址 100 + 3 = 103 处访问:

图 4.2 – 寄存器间接寻址

图 4.2 – 寄存器间接寻址

这种寻址模式允许你在程序运行时修改地址(因为你可以更改指针的内容,r0)。基于指针的寻址使得能够逐个元素地遍历项目列表,只需递增指针即可。以下是一个例子:


LDRI  r1,[r2,0]    @ Get the element pointed at by r2\. Here the offset is 0
INC   r2           @ Increment r2 to point to the next element

我们访问由 r2 指向的元素(在这种情况下,偏移量是 0)。下一行将 r2 递增以指向下一个元素。如果这个序列在循环中执行,数据将被逐个元素访问。

计算机实现算术和布尔(逻辑)操作。在下一节中,我们将简要介绍 Python 如何在汇编语言级别模拟逻辑操作。

Python 中的位处理

在本节中,我们将探讨 Python 如何处理所有计算机数据的基本组成部分:位。因为模拟计算机在位级别上操作,我们必须在构建能够执行 ANDOR 等逻辑操作的模拟器之前,查看 Python 中位是如何被操作的。

计算机可以作为一个整体操作整个字,或者操作字中的单个位。因此,我们必须能够执行位操作和字操作来模拟计算机。

Python 允许你输入数据并将其显示为位字符串的二进制表示。你可以使用布尔运算符操作字符串的各个位,并且可以左右移位。Python 中有你需要的一切工具。我们现在将探讨 Python,它允许你操作整数的各个位。

因为模拟器将在二进制数字上操作,我们必须利用几个 Python 功能;例如,二进制值通过在其前面加上 0b 来表示。如果你想使用 15 的二进制等效值,你必须写成 0b1111 并像十进制数字一样使用它。以下两个操作具有相同的效果:


x = y + 0b111      # Addition using a binary integer
x = y + 15         # Addition using a decimal integer

两个重要的二进制运算符是 >>(右移)和 <<(左移)。移位表达式写作 p >> qp << q,其中 p 是要操作的数,q 是移位的位数。考虑以下示例:


x = 0b1110110
x = x << 2

这将 x 左移两位,得到新值 0b111011000。所有位都向左移动了两位,并在右手端输入 0 来填充新空出的位置。x 的移位版本现在有九位,而不是七位。然而,当我们模拟计算机时,我们必须确保无论我们做什么,字中的位数都保持不变。如果一个寄存器有 16 位,那么你对它进行的任何操作都必须产生 16 位。

我们可以对两个字执行布尔(位运算)操作;例如,C = A & B 通过计算 ci = ai · bi 对字中的每个位(即,对于 i = 0 到 7)进行操作来将字 A 和 B 连接在一起。考虑以下示例:


x = 0b11101101 & 0b00001111
AND 运算的真值表
x
0
0
1
1

这将 x 设置为 0b000001101,因为位与 0 进行 AND 运算会得到 0 结果,而位 x 与 1 进行 AND 运算会得到 x,因为 1 & 0 = 0 和 1 & 1 = 1,正如真值表所示。

在 Python 中,十六进制数以 0x 开头;例如,0x2F1A。我们在 Python 程序中使用十六进制数,因为它们比二进制数短。以下是一个示例:


0x1234 = 0b0001001000110100
0xA0C2 = 0b1010000011000010
0xFFFF = 0b1111111111111111

考虑以下 Python 代码片段:


x = 0b0011100101010011   # A 16-bit binary string we are going to process
y = (x >> 8) & 0xF       # Shift x right 8 places and mask it to 4 bits
print('y is ',bin(y))    # Print the result in binary form using bin()

此代码的输出是 y is 0b1001

Python 使用超过 16 位来表示数字。为了在 TC1 中模拟二进制算术,我们必须将 Python 的数字限制在 16 位。我们必须使用 Python 提供的较长的字,并在必要时将它们截断到 16 或 32 位。考虑以下示例:


x = 0xF009       # Set up 16-bit number       1111000000001001
y = x << 2       # Shift left twice to get      111100000000100100 = 0x3C024 (18 bits)
y = y & 0xFFFF   # Constrain to 16 bits to get     1100000000100100 = 0xC024

将 16 位字左移两位使其成为 18 位字。与二进制值 0b1111111111111111 进行 AND 运算强制其变为 16 位有效位。

Python 的运算符优先级

现在,我们必须重新学习我们在高中算术中学到的某些知识:运算符优先级。当你在表达式中使用多个运算符时,你必须注意运算符优先级;也就是说,哪些操作先于其他操作执行。例如,4 * 5 + 3 等于 23 还是 32?所有计算机语言都有运算符优先级的层次结构。Python 的运算符优先级部分列表如下,优先级最高的排在前面。我们经常使用括号来使表达式的含义更清晰,即使这不是必需的——例如,(xxx << 4) & (yyy << 2)


()                          Parentheses               Highest precedence
~                           Negation
*,/, %                      Multiplication, division, modulus
+,-                         Addition, subtraction
<<, >>                      Bitwise shift left, right
&                           Logical (bitwise) AND
^                           Logical (bitwise) XOR
|                           Logical (bitwise) OR
<, <+, >, >+, <>, !=, ==    Boolean comparisons         Lowest precedence

在下一节中,我们将探讨如何解码一条指令,该指令是一串 1 和 0,以及执行适当的操作。

接下来,我们将构建模拟器迈出重大一步——即解码机器级二进制指令以提取它将要模拟的操作信息。

解码和执行指令

在本节中,我们将探讨一些指令编码和解码的例子。考虑ADD r1,r2,r3操作(其中寄存器rDrS1rS2的代码被阴影覆盖),它在 RTL 中的定义如下:

[r1] [r2] + [``r3]

表 4.2 显示了ADD r1,r2,r3的编码是10 00000 001 010 011 0000000000000000。定义要使用的寄存器的 4 位格式代码是1110,因为我们使用了三个寄存器而没有文字字段(记住,这个 4 位格式代码不是操作码的一部分,而是由汇编器用来解释指令的)。

以下 Python 代码展示了我们如何解码指令以提取五个变量:操作码(binOp);三个寄存器,rDrS1rS2;以及一个文字。解码是通过将指令的位右移以将所需字段移动到最低有效位,然后与掩码进行与操作以移除任何其他字段来执行的。考虑xxxxxxx xxx 010 xxx xxxxxxxxxxxxxxxx。如果我们将其右移 19 位,我们得到0000000000000000000xxxxxxxxxx010。零已经被移位,我们感兴趣的三个位现在在最低有效位中右对齐。如果我们用0b111与它进行与操作以只选择三个最低有效位,我们得到0000000000000000000000000000010——也就是说,所需的寄存器值现在右对齐,我们可以使用它:


binOp = binCode >> 25             # Extract the 7-bit opcode binOp by shifting 25 bits right
rD    = binCode >> 22 & 0b111     # Extract the destination register as rD. 0b111 is a mask
rS1   = binCode >> 19 & 0b111     # Extract source register 1 as rS1
rS2   = binCode >> 16 & 0b111     # Extract the source register 2 as rS2
lit   = binCode & 0xFFFF          # Extract the low-order 16 bits (the literal) as lit
op0 = r[rD]                       # Read contents of destination register operand 0 as op0
op1 = r[rS1]                      # Read contents of source 1 register as op1
op2 = r[rS2]                      # Read contents of source 2 register as op2

Python 中的右移操作符是>>,位逻辑与操作符是&。掩码以位串的形式表示(而不是十进制数),因为与二进制 111 进行与操作比与十进制 7 进行与操作更清晰。在 Python 中,二进制值前面有一个 0b,所以 7 表示为0b111。文字与 16 个 1 进行与操作,表示为0xFFFF。我们使用二进制表示短字段,使用十六进制值表示长字段。这只是一种个人偏好。

binCode >> 22 & 0b111表达式将binCode的位右移 22 位,然后将结果与000…000111进行位与操作。由于操作符优先级,先进行移位操作。否则,我们会写成(binCode >> 22) & 0b111。我们经常使用括号,即使不是严格必要的,也要强调操作符优先级。

注意,我们提取了所有字段,即使它们可能不是每个指令所必需的。同样,我们读取了三个寄存器的所有内容。这种方法简化了 Python 代码,但以效率为代价。

考虑提取目标寄存器字段,rrr。假设指令是ADD r1,r2,r3,操作码是10000000010100110000000000000000`。我们已经用粗体字和阴影表示了交替字段,目标字段被阴影覆盖以使其更容易阅读。执行 22 位右移将目标寄存器移动到最低有效位,并留下

00000000000000000000001000000001   (after shifting 22 places right).

现在,如果我们用 111(所有左边的位都是零)进行与操作,我们得到以下结果:


00000000000000000000001000000001      (after shifting 22 places right)
00000000000000000000000000000111      (the three-bit mask)
00000000000000000000000000000001      (The result; the 1 in bit position 9 has been removed)

我们现在已将第一个寄存器字段隔离出来以获得 001,这对应于寄存器 r1。解码指令的程序的最后三行如下:


op0 = r[rD]    # Operand 0 is the contents of the destination register
op1 = r[rS1]   # Operand 1 is the contents of source register 1
op2 = r[rS2]   # Operand 2 is the contents of source register 2

这些指令使用寄存器地址(rDrS1rS2)来访问寄存器;即指令指定了要使用的寄存器。例如,如果 op0 = r[5],寄存器 r5 是操作数零,目标寄存器。如果指令没有指定寄存器,则未使用字段被设置为零。

一旦指令被解码,它就可以被执行。幸运的是,执行大多数指令非常简单。考虑以下示例:


if   opCode == 0b0100010: r[rD] = lit               # Load register with literal
elif opCode == 0b0100001: r[rD] = mem[lit]          # Load register from memory
elif opCode == 0b0100011: r[rD] = mem[op1 + lit]    # Load register indirect
elif opCode == 0b0100100: mem[lit] = r[rD]          # Store register in memory

这段代码片段展示了从加载和存储(即内存访问)组执行四个指令的过程。Python 的 if … elif 结构依次测试每个操作码。第一行将 7 位操作码与二进制值 0100010 进行比较,这对应于 LDRL(使用立即数加载寄存器)指令。顺便提一下,在 TC1 的最终版本中,我们通过将操作与实际助记符进行比较来使代码更容易阅读;例如,'LDRL' 比其代码 0b100010 更容易阅读。

如果匹配成功,冒号后面的代码将被执行,(r[rD] = lit)。如果不匹配,下一行使用 elif 命令将操作码与 010001 进行比较。如果匹配,则执行冒号后面的代码。这样,操作码将与所有可能的值进行比较,直到找到匹配项。

假设指令的操作码是 0100010,并且执行了 r[rD] = lit 行。这个 Python 操作将指令中提供的 16 位立即数的值转移到指令中指定的目标寄存器。在 RTL 术语中,它执行 r[rD] ← lit 并被程序员用来将整数加载到寄存器中。假设指令代码的二进制模式如下:


01 00010 110 000 000 0000000011000001,

在这里,16 位立即数被加载到寄存器 r6 中以实现 LDRL r6,193

算术和逻辑操作

算术和逻辑操作组在一个程序中执行所有的工作(数据处理)。它们包括算术操作(加法、减法和乘法),以及逻辑操作(ANDOREORNOT)。这些是三操作数指令,除了 NOT;例如,AND r3,r0,r5r0r5 的位之间执行位运算 AND,并将结果放入 r3

以下 Python 代码片段展示了 TC1 如何解释这一组中的四个指令。有两个指令对:加法和减法。每一对包括一个寄存器到寄存器的操作和一个寄存器到立即数的操作;例如,ADD r1,r2,r3 将寄存器 r2r3 的内容相加,而 ADDL r1,r2,L 将 16 位立即数的内容加到寄存器 r2 的内容上。这里我们不进行算术运算。相反,我们调用一个名为 alu(f,a,b) 的函数来执行所需操作。下一节将展示如何创建自己的函数:


elif opCode==0b1000000: reg[dest]=alu(1,reg[src1],reg[src2]) # Add register to register
elif opCode==0b1000001: reg[dest]=alu(1,reg[src1],literal)   # Add literal to register
elif opCode==0b1000010: reg[dest]=alu(2,reg[src1],reg[src2]) # Subtract register from register
elif opCode==0b1000011: reg[dest]=alu(2,reg[src1],literal)   # Subtract literal from register

如果要编写执行每个运算所需的完整 Python 代码,每条指令可能需要几行代码。例如,加法运算 reg[dest] = reg[src1] + reg[src2] 看起来很简单,但算术运算中还有更多内容。

为了解释原因,我们需要讨论条件标志位的作用。回想一下,计算机在决定是否按顺序执行下一条指令,或者是否跳转到代码中的不同位置(即 if…then…else 结构)时做出决策。是否采取 if 分支或 else 分支的决定取决于算术或逻辑运算的结果(这包括比较运算)。

算术或逻辑运算的结果会被测试并用于设置或清除条件代码标志。这些标志通常是负数标志、零标志、进位标志和溢出标志,分别表示为 nzcv。例如,如果运算表达式是 x = p – q,且结果为 x = 0,那么零位(零位)会被设置为 1。同样,如果运算结果无法用 16 位表示,进位位 c 会被设置。由于大多数计算机使用二进制补码算术,如果结果在二进制补码中表示为负数(即其最高位为 1),则负数标志 n 会被设置。v 位表示算术溢出,如果结果超出范围,则该位会被设置。这意味着两个正数相加的结果为负数,或者两个负数相加的结果为正数。

表 4.3 展示了如何将 4 位分配给数字 0 到 15(无符号)或 -8 到 7(有符号二进制补码)。程序员需要选择约定。然而,两种约定下的算术运算都是相同的。

考虑 0110 + 1100,结果是 10010(或 4 位中的 0010)。如果我们将这些数字解释为无符号数,计算结果为 6 + 12 = 2,这是不正确的,因为结果 18 无法用 4 位表示。如果我们将这些数字解释为有符号值,计算为 6 + -4 = 2,结果是正确的:

8 4 2 1 无符号值 有符号值
0 0 0 0 0 0
0 0 0 1 1 1
0 0 1 0 2 2
0 0 1 1 3 3
0 1 0 0 4 4
0 1 0 1 5 5
0 1 1 0 6 6
0 1 1 1 7 7
8 4 2 1
1 0 0 0 8 -8
1 0 0 1 9 -7
1 0 1 0 10 -6
1 0 1 1 11 -5
1 1 0 0 12 -4
1 1 0 1 13 -3
1 1 1 0 14 -2
1 1 1 1 15 -1

表 4.3——4 位数值的有符号和无符号表示

二进制补码算术——注意事项

在二进制算术中表示负数有几种方式。其中最古老的一种称为符号和数值,其中如果数值为正,则前面加 0;如果为负,则加 1。这种方法今天在二进制算术中不再使用(除了浮点数之外)。

几乎所有现代计算机都使用二进制补码表示有符号整数。在二进制补码算术中,如果 N 是 n 位中的正数,则-N 的值由 2^n – 1 给出。N 的二进制补码可以通过反转位并加 1 来轻松计算。例如,如果 N = 000110在六位中,-6 的值由111010表示。

二进制补码算术的优势在于加法和减法以相同的方式进行。如果你将一个二进制补码值加到另一个数上,结果会被正确计算。例如,如果我们把 9(即001001)加到-6 的前一个值上,我们得到001001 + 111010 = 1000011,这是六位中的+3。进位(粗体字体)被舍弃。

零、进位和符号位

进位位和符号位用于确定结果是否在范围内或超出范围。在有符号算术中,如果 S = A+B 的二进制补码加法的结果中,A 和 B 的符号位都是 0 而 S 的符号位是 1,或者 A 和 B 的符号位都是 1 而 S 的符号位是 0,则结果超出范围。考虑使用四位进行加法和减法。使用二进制补码算术,最左边的位是符号位。在每种情况下,我们都对有符号数进行了二进制补码加法。结果给出了标志位的状态:


 +0101                               +5 + -5 = 0
 +1011
 10000 Z = 1, N = 0, C = 1, V = 0   
                            The result is zero and the Z-bit is set. The C bit is 1, and N, V bits are clear
 +1101                               -3 + -3 = -6
 +1101
 11010 Z = 0, N = 1, C = 1, V = 0    The result is negative, N = 1, and a carry is generated, C = 1
 +1101                               -3 + +5 = +2
 +0101
 10010 Z = 0, N = 0, C = 1, V = 0    The result is positive and the carry bit is set
 +0101                               +5 + 6 = -5
 +0110
  1011 Z = 0, N = 1, C = 0, V = 1
                                 Two positive numbers are added and the result is negative. V is set
  1010                               -6 + -4 = +7
 +1100
 10111 Z = 0, N = 0, C = 1, V = 1
                                 Two negative numbers are added and the result is positive. V is set

我们如何知道一个数是有符号的还是无符号的?我们不知道!没有区别。如果你使用有符号数,你必须将结果解释为有符号值。如果你使用无符号数,你必须将结果解释为无符号值。例如,在四位中,1110 既可以表示-2 也可以表示+14,这取决于你如何看待它。

下一个主题将讨论我们如何处理重复操作组。如果程序中将要多次使用相同的操作序列,将它们组合成一个组并在需要时调用该组是有意义的。在 Python 中,这个动作组被称为函数。

Python 中的函数

我们现在将描述 Python 的函数。我们已经使用了语言的一部分函数,如len()。在本节中,我们将做以下几件事:

  • 解释为什么函数是有用的

  • 提供一个实现 ALU 功能的示例

  • 解释变量如何对函数是私有的或函数间共享的(作用域)

  • 描述如何将参数传递给函数

  • 描述函数如何返回结果

编写处理由模拟器实现的每个算术或逻辑操作的 Python 代码将会很繁琐,因为大量的代码会被单个指令重复。相反,我们可以创建一个 Python 函数(即子程序或过程),它执行算术/逻辑操作以及适当的标志位设置。

考虑一个名为 alu(f,p,q) 的 Python 函数,它返回一个整数,该整数是 fpq 参数的函数。要执行的操作以整数 f 的形式传递给过程。该函数还更新 z 和 n 标志位。这是我们将使用的实际函数的简化版本,它将提供两个操作,加法和减法,并只更新两个标志位:


def alu(f,p,q):                  # Define the alu function with the f, p, and q input parameters
    global z,n                   # Declare flags as global
    if f == 1: r = p + q         # If f (the operation) is 1, do an addition
    if f == 2: r = p – q         # If f is 2, do a subtraction
    z, n = 0, 0                  # Clear zero and negative flags
    if r & 0xFFFF == 0: z = 1    # If the result is 0 in 16 bits, then z = 1
    if r & 0x8000 != 0: n = 1    # If result is negative (msb = 1), then n = 1
    return (0xFFFF & r)          # Ensure result is restricted to 16 bits

函数通过 def、其名称和任何参数后跟冒号来引入。函数体是缩进的。第一个参数 f 选择我们希望执行的操作(f = 1 表示加法,2 表示减法)。接下来的两个输入参数 pq 是函数使用的数据值。函数的最后一行将结果返回到函数的调用点。这个函数可以通过例如 opVal = alu(2,s1,s2) 来调用。在这种情况下,结果 opVal 将是 s1 – s2 的值。

我们还更新了两个标志位,zn。最初,zn 都通过 z, n = 0, 0 被设置为零。(Python 允许在同一行进行多个赋值;例如,a,b,c = 1,5,0abc 分别设置为 1、5 和 0。)

你可以通过参数将数据传递给函数,并通过 return() 接收结果。然而,你可以在函数中声明变量为 global,这意味着它们可以像调用程序的一部分一样被访问和修改。

return() 不是强制的,因为有些函数不返回值。一个函数中可以有多个 return() 语句,因为你可以从函数的多个点返回。返回可以传递多个值,因为 Python 允许一行中的多个赋值;例如,参见以下:


this, that  = myFunction(f,g)  # Assign f to this and g to that

通过比较结果 r 与 0 来测试零结果可以很容易地完成。测试负结果则更困难。在二进制补码算术中,如果一个有符号值的最高位是 1,则该值是负数。我们使用 16 位算术,因此这对应于位 15。我们可以通过将结果 r 与二进制值 1000000000000000 进行 AND 操作来提取位 15,即通过编写 r&0x8000(字面量以十六进制形式表示为 0x8000,这比二进制版本要短得多)。

要调用函数,我们可以编写以下代码:


 if opCode == 0b1000000: r[rD] = alu(1,r[rS1],r[rS2])

我们测试操作码 1000000,如果它对应于ADD,则调用alu函数。该函数使用f = 1参数进行加法;要加的数字是两个源寄存器的内容。结果被加载到r[rD]寄存器中。在 TC1 的当前版本中,我们使用操作码查找助记符,然后如果mnemonic == 'ADD'则应用测试。这种方法更容易阅读,并且在跟踪输出时可以使用助记符。

我们已经将zn变量设置为全局变量(即,它们可以被函数更改并从外部访问)。如果我们没有将它们设置为全局变量,我们就必须使它们作为返回参数。一般来说,将变量作为参数传递而不是使它们成为全局变量是一种更好的做法。

函数和作用域

变量与作用域可见性相关联。如果你编写一个没有函数的程序,变量可以在程序中的任何地方访问。如果你使用函数,生活就变得相当复杂。

如果你将变量声明在主体中,该变量将在函数中可见(即,你可以使用它)。然而,你无法在函数中更改它并在函数外部访问新值。函数内部发生的事情就留在函数内部。如果你希望从函数外部访问它,你必须将其声明为全局。如果你编写global temp3,那么temp3变量在函数内外是同一个变量。

如果你在一个函数中创建了一个变量,该变量不能在函数外部访问,因为它对函数是私有的(一个局部变量)。你可以在另一个函数或程序体中创建具有相同名称的变量,但这个函数中的变量对函数外具有相同名称的变量没有影响。

如果你在一个函数中创建了一个你希望在外部可见的变量,你必须在函数中将其声明为全局变量。考虑以下代码:


def fun_1():                      # A dummy function
    p = 3                         # p is local to fun_1 and set to 3
    global q                      # q is global and visible everywhere
    print('In fun_1 p =',p)       # Print p in fun_1
    print('In fun_1 q =',q)       # Print q in fun_1
    q = q + 1                     # q is changed in this function
    p = p + 1                     # p is changed in this function
    r = 5                         # set local variable r to 5
    return()                      # You don't need a return
p = 5                             # p is defined in the body
q = 10                            # q is defined in the body
print('In body: p =',p, 'q =',q ) # Print current values of p and q
fun_1()                           # Call fun_1 and see what happens to p and q
print('In body after fun_1 q =',q, 'after fun_1 p = ',p)

运行此代码后,我们得到以下结果。正如你所见,函数因为它是全局的而改变了q,而p没有改变,因为它在函数中是一个局部变量:


In body: p = 5 q = 10
In fun_1 p = 3
In fun_1 q = 10
In body after fun_1 q = 11 after fun_1 p =  5

这里是规则的总结:

  • 如果你不在函数中更改它,你可以在任何地方使用它

  • 如果你在一个函数中声明它,它就属于你,并且对函数是私有的

  • 如果你希望在函数中更改它并使用它在外部,那么在函数中将其声明为全局变量

这意味着像zn这样的条件码标志变量可以在函数中访问。如果你希望在函数中更改它们,你必须使用以下命令将它们声明为全局变量:


global z,n

如果一个函数返回一个值,它以return(whatever)结束。如果你从函数中不返回值,则不需要return语句。然而,我们通常包括一个返回空值的return(),然后我们明确标记函数的结束。

虽然 Python 支持全局变量的使用,但一些程序员避免使用它们,因为全局变量使得在函数中意外更改变量并没注意到它变得过于容易,导致在调试有缺陷的程序时头疼。他们认为所有变量都应该作为参数传递给函数。

在下一节中,我们将描述使计算机成为计算机的真正原因——它根据操作的结果采取两种不同行动的能力。

分支和流程控制

所有微处理器都有流程控制指令;例如,无条件分支 BRA XYZ 表示在地址 XYZ 执行下一个指令。一个典型的条件指令是 BNE XYZ,它实现如果且仅当 z 标志未设置时,跳转到位置 XYZ 的指令。BNE 的助记符意味着“不等时分支”,因为 z 位是通过使用减法比较两个值来评估的。考虑以下使用 TC1 汇编语言的示例:


     LDRL r2,0        @ Load r2 (the sum) with 0
     LDRL r0,5        @ Load r0 (the loop counter) with 5
     LDRL r1,1        @ Load r1 (the register added to the sum) with 1
Loop ADD  r2,r2,r1    @ Repeat: add r1 to r2
     INC  r1          @ Increment r1 so that we add 1, 2, 3 … as we go round the loop
     DEC  r0          @ Decrement the loop counter in r0
     BNE  Loop        @ Until loop counter = 0
     STOP             @ Program completed

使用文数字段加载寄存器(LDRL)三次,将 r2 加载为 0,r0 加载为 5,r1 加载为 1。在标记为 Loop 的行中,我们将 r2 加到 r1 上,并将结果放入 r2。在其第一次执行时,r2 变为 0 + 1 = 1。

下两条指令增加 r1 的值并减少 r0 的值,使得 r1 变为 2 而 r0 变为 4。当我们减少 r0 的值时,如果结果是零,则设置 z 标志。由于 r2 初始包含 5,然后减量后变为 4,所以结果不是零,z 标志没有被设置。

当执行 BNE 时,会测试 z 标志。如果它不是零,则会从标记为 Loop 的行进行分支,并再次执行相同的四条指令。当第二次遇到 BNE 时,z 位仍然是零,因为这次减量是从 4 到 3。最终,r0 包含 1 并减量到 0。然后设置 z 位。当 BNE 下次执行时,不会执行跳转到循环,而是执行序列中的下一个指令 STOP。循环重复 5 次。这个程序计算 1 + 2 + 3 + 4 + 5 = 15,这是 r2 中的结果。

分支地址可能有点复杂。你可以提供一个绝对地址;例如,BRA 24 跳转到内存地址 24 的指令。大多数计算机使用相对地址,它是相对于当前指令的。例如,在这种情况下,我们会从当前位置(即 BNE 指令)回跳三个指令。所以,你可能认为相对地址是 -3。这是一个负地址,因为它是从当前地址往回的。由于程序计数器已经增加以指向下一个指令,所以回跳是 -4。因此,BNE 循环的文数字段将是 -4。

以下代码片段演示了测试四个分支指令及其实现。在这个例子中,我们使用相对寻址来表示分支;也就是说,目标地址是相对于程序计数器指定的。第一个分支BRA是无条件分支,计算下一个pc的值。其余的都是条件分支,只有当满足所需条件时,pc才会改变。请注意,最后一个操作是BMI,表示基于负数的分支,尽管有些人称之为基于负数的分支(意思相同):


if   opCode == 0b1100000:            pc = 0xFFFF & (pc + literal) # Branch always
elif opCode == 0b1100001 and z == 1: pc = 0xFFFF & (pc + literal) # Branch on zero
elif opCode == 0b1100010 and z == 0: pc = 0xFFFF & (pc + literal) # Branch on not zero
elif opCode == 0b1100011 and n == 1: pc = 0xFFFF & (pc + literal) # Branch on minus

The pc是通过pc + literal来增加的。然而,由于向后分支以二进制补码形式表示,因此需要将结果与 0xFFFF 进行AND操作,以确保它为 16 位并生成正确的值。这是因为我们正在使用表示超过 16 位数字的计算机语言 Python 来模拟 16 位算术。这仅仅是计算机算术的一个不幸后果,当你模拟二进制算术时必须注意。

在下一章中,我们将回到 Python,并扩展我们处理数据结构和使用 Python 函数的能力。

摘要

我们从设计计算机模拟器开始本章。然而,我们还没有创建最终产品。相反,我们研究了涉及的一些问题,例如指令集的性质和操作码的结构。

我们研究了指令的解码和执行方式。我们还趁机扩展了我们对 Python 的了解,并介绍了 Python 的位操作功能,这些功能使我们能够实现操作位级的机器指令。

我们还介绍了一个重要的 Python 组件,称为函数,它允许程序员创建一个自包含的代码单元,可以从程序中的多个点调用以执行特定任务。函数对现代编程至关重要,因为它们通过将复杂操作序列捆绑成一个单元来促进优雅的设计,你可以调用该单元来执行任务。

第五章中,我们将回到 Python,并更深入地探讨一些主题——特别是列表。列表可能是 Python 最有趣和最重要的特性。

第五章:更多 Python 知识

我们已经介绍了 Python,并且一直在使用它。在本章中,我们扩展了 Python 的知识,并扩展了一些我们遇到的概念,还介绍了新特性。特别是,我们检查了数据结构,从处理元素列表或字符字符串的方式开始。本章为第六章**铺平了道路,其中我们完成了计算机模拟器的设计。但在那之前,我们提供了一些关于我们在讨论 Python 特性时使用的术语的说明。

我们将讨论以下主题:

  • 语句和表达式

  • 更多字符串特性

  • 列表推导式

  • 字符串处理

  • 重复和循环

  • 字典

  • 函数

  • 列表中的列表

  • 导入

  • Python 中的缩进

技术要求

你可以在 GitHub 上找到本章使用的程序github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter05

本章不需要比前几章更多的资源。所需的一切仅是一个带有 Python IDE 的计算机。所有章节都是如此,直到我们达到第九章,该章节涉及树莓派。

语句和表达式

一个表达式是值和运算符的组合,可以评估为提供结果;例如(p+q)*7 - r。布尔表达式是值和逻辑运算符的组合,产生TrueFalse的值;例如,p > q

一个语句是 Python 操作,必须由解释器评估;也就是说,它是一个动作。典型的 Python 语句涉及iffor…动作。这两个术语常用于正式定义中;例如,if语句的定义如下:

if <expr>:

<``statement>

在语言描述中使用尖括号表示将被其实际值替换的内容;例如,一个有效的if语句如下:

if x > y:

p = p + q

在这种情况下,表达式x > y,而语句p = p + q

更多字符串特性

现在我们将扩展我们操作字符串的能力。字符串是 Python 最重要的数据结构之一,也是我们在这本书中编写的所有程序的核心。Python 的字符串处理功能使其成为最强大、最易用的文本处理语言之一。字符串由引号表示,可以是单引号或双引号;例如,x = "Two"y = 'One'是 Python 字符串。Python 使用两种字符串终止符的能力意味着我们可以创建像“Alan 的书”这样的字符串(即,将撇号用作正常的语法元素)。

在执行 Python 程序的过程中,你可以从键盘读取一个字符串,也可以以下面的方式提供提示。

x = input('请输入' 一些内容)

执行此命令将在屏幕上显示"请输入一些内容",等待您的输入,然后将您输入的字符串分配给变量x。这种机制在模拟程序时非常有用,因为您可以在程序运行时提供输入。请注意,输入字符串必须通过按Enter键来终止。

字符串处理

您可以使用replace方法在字符串中更改(替换)字符。例如,假设我们希望将字符串price中所有'$'的实例替换为'£'


price =  'eggs $2, cheese $4'
price = price.replace('$', '£')      # Replace $ by £ in the string price

如果我们打印price,现在我们得到'eggs £2, cheese £4'

其他字符串方法包括upper()(将文本转换为大写)、lstrip()(删除前导字符)和rstrip()(删除尾随字符)。设x ='###this Is A test???'。考虑以下序列:


x = x.lstrip('#')      # Remove left-hand leading '#' characters to get x = 'this Is A test???'
x = x.rstrip('?')      # Remove right-hand trailing '?' characters to get x = 'this Is A test'
x = x.lower()          # Convert to lower-case to get x = 'this is a test'

这个序列产生x = 'this is is a test'

字符串是不可变的。一旦定义了字符串,就无法更改它们。在先前的代码中,看起来我们通过删除前导和尾随字符以及将大写转换为小写来修改了 x。不!在每种情况下,我们都创建了一个与旧字符串具有相同名称的字符串(即,x)。

如果您输入y = 'Allen'并尝试通过将'e'改为'a'来编辑它以读取'Allan',您将得到一个错误,因为您尝试更改一个不可变的字符串。然而,您可以合法地写出y = y[0:3] + 'a' + y[4]来创建一个具有值'Allan'的新字符串y

加号符号+在算术中执行加法,在字符串处理中执行连接;例如,x = 4 + 5给出9,而x = '4' + '5'给出'45'。这个动作被称为操作符重载,表示函数的扩展,例如,当函数应用于不同的对象时。

TC1 汇编器使用以下字符串方法来删除指令前的空格,使用户能够使用大写或小写,并允许使用空格或逗号作为分隔符。例如,此代码片段允许您以相同的意义编写r0,[r1]R0,R1。下面的代码显示了 TC1 如何将输入行(即汇编指令)简化以便稍后转换为二进制:


    line = line.replace(',',' ')     # Allow use of a space or comma to separate operands
    line = line.replace('[','')      # Allow LDRI r0,[r2] or LDRI r0,r2 First remove '['
    line = line.replace(']','')      # Replace ']' by null string.
    line = line.upper()              # Let's force lower- to upper-case
    line = line.lstrip(' ')          # Remove leading spaces
    line = line.rstrip('\n')         # Remove end of line chars. End-of-line is \n

假设一条指令以字符串形式输入为 ' ADD r0,R1,r3'。这包含前导空格、大小写文本和逗号,并且通过上述操作序列转换为 'ADD R0 R1 R3'。下一步是将字符串转换为汇编器分析的单独标记。我们可以使用split()方法来完成此操作。它将字符串转换为由括号中的字符分隔的字符串列表。请注意,默认参数是空格。如果s = 'ADD R0 R1 R2',则s.split()s.split(' ')的结果如下:

s = ['ADD', 'R1', 'R2', 'R3']      # 四个标记的列表,每个标记都是一个字符串

现在我们有一个包含四个单独字符串的列表;也就是说,一个命令后跟三个参数。我们可以使用索引符号访问这些字符串:


T1 = s[0]                       # This is the mnemonic 'ADD'
T2 = s[1]                       # This is the destination register 'R1'
T3 = s[2]                       # This is the first source register 'R2'
T4 = s[3]                       # This is the second source register 'R3'

我们现在可以对标记进行操作:


if T1 == 'STOP': then run = 0   # If the instruction is stop then halt processing
firstReg = int(T2[1:])          # Get the register number as an integer

第二个语句使用 T2[1:] 将字符串 'R2' 转换为新的字符串 '2',通过删除第一个字符。切片符号 [1:] 被解释为“第一个字符之后的所有字符”。这使我们能够处理像 R2 或 R23\ 这样的单或双位数值。由于只有 8 个寄存器,我们本来可以写 [1:2]。使用 [1:] 允许在 TC1 的未来版本中扩展到 16 个寄存器,而无需更改代码。

我们必须使用整数函数 int 将寄存器号从字符串转换为整数值。在学习 Python 时,一个常见的错误是忘记将字符串转换为整数:


regNum = input('Please enter register number >>>')
contents = reg[regNum]

这段代码会生成错误。因为 regNum 是作为一个字符串创建的,它包含了你输入的值。然而,在第二行,程序期望一个整数作为列表索引。你必须写 reg[int(regNum)] 来将数字字符串转换为整数值。

示例 – 文本输入

这里有一个使用 Python 和简单 .txt 文件进行文本输入的简单示例。源文件如下:


@ This is text
NOP
 NOP
NOP
NOP
END

此文件是用 Python 处理的。我使用它在我的电脑上的地址。这是读取到变量 sFile 中的内容:


myFile = 'E:\\ArchitectureWithPython\\testText.txt'
with open(myFile,'r') as sFile:
    sFile = sFile.readlines()           # Open the source program
print (sFile)

这段代码的输出如下:


['@ This is text\n', 'NOP\n', ' NOP\n', ' \n', 'NOP\n', '\n', 'NOP\n', 'END']

现在我们有一个与源文本每一行对应的 Python 字符串列表。请注意,每一行都以换行符(即,\n)结束,并且文本中的任何空格都被包含在内。例如,有一个完全空的行和一个带有空格的空行。在处理输入时,我们必须考虑到这些。

在增强了我们操作文本字符串的能力之后,在下一节中,我们将探讨 Python 最有趣的功能之一:使用列表推导式在单行代码中执行对字符串或列表的一系列操作。

列表推导式

现在我们介绍 Python 的一个非常强大的功能,即列表推导式。它的强大之处不在于它能做什么,而在于它的简洁性。列表推导式允许你在单行中处理列表。我们来看看列表推导式,因为它们在处理文本时非常有用;例如,你可以使用列表推导式将一行文本中的所有双空格替换为单空格,或者将所有小写字符转换为大写字符。

列表推导式可以应用于任何可迭代对象。可迭代对象是一种你可以遍历的结构,例如列表、字符串或元组。列表推导式的最简单形式如下:

x = [i for i in y]

在这里,xy是字符串(或列表)。粗体的文本代表 Python 保留字和标点符号。变量i是一个用户选择的变量,用于遍历列表。我们可以用任何名字代替i;这根本不重要。考虑以下示例:

lettersList = [i for i in 'Tuesday']

正在处理的可迭代对象是字符串'Tuesday',它逐个字符复制到lettersList中。这返回lettersList作为字符列表['T','u','e','s','d','a','y']。我们已经将一个字符串转换成了字符列表。

我们可以创建一个更复杂的列表推导式版本,如下所示:


x = [expression for i in y if condition]

其中expression是一个 Python 表达式,condition是一个布尔条件。我们遍历可迭代对象y,查看每个元素,并根据条件选择它,根据表达式处理它,然后将它放入一个新的列表中。这在一行中做了很多工作。考虑以下内容:


y = 'this$is$a$test'
x = [i for i in y if i != '$']

条件是如果 y 中的项目 i 不等于'$'。这个列表推导式表示,“将字符串 y 中的字符逐个复制到字符串 x 中,只要字符不是'$'字符。”结果是这个:


x =['t', 'h', 'i', 's', 'i', 's', 'a', 't', 'e', 's', 't'].

原始字符串已被替换为单字符字符串的列表,但去除了每个'$'

让我们看看列表推导式的三个示例:

  1. 第一个示例演示了如何从输入中移除空行(即'')。我们可以通过以下方式复制所有不等于空或空字符串" "的行:

    sFile = [i for i in sFile if i != '']   # Remove blank lines
    

我们将新列表重命名为与旧列表相同的名称。我们没有理由不能这样做,这可以节省为每个列表推导式发明新名字的需要。

  1. 第二个示例是这样的:

    sFile = [i.upper() for i in sFile]      # Convert to upper-case
    

我们将函数.upper()应用于每个元素i。这个表达式将小写字符转换为其大写等效字符;也就是说,它将所有字符串转换为大写。最后的列表推导式是这样的:


sFile = [i.split() for i in sFile if i != '']

表达式i.split()将源字符串在每个空格处分割成单独的标记(字符串)。这意味着我们可以将行作为标记序列进行处理。条件if i != ''用于通过不复制它们来删除空字符串。

  1. 第三个示例移除了空行。

我们创建了一个包含空行的三个指令列表,用' '表示。当我们执行这个列表推导式时,我们将每个字符串转换成子列表,并移除空行:


sFile = ['ADD R1 R2 R3', 'BEQ LOOP', '', 'LDRL R2 4','']
sFile = [i.split() for i in sFile if i != '']
print(sFile)

此代码的输出如下:


[['ADD', 'R1', 'R2', 'R3'], ['BEQ', 'LOOP'], ['LDRL', 'R2', '4']]

元组

现在我们为了完整性引入了元组,尽管在这篇文本中我们很少使用它。列表是由方括号包围的元素序列;例如,P = [1,4,8,9]。元组是由圆括号分隔的元素序列;例如,Q = (1,4,8,9)

元组和列表之间的区别很小;它们都是包含元素序列的数据结构。然而,元组是不可变的,不能被修改,与列表不同。元组是一个只读列表,用于存储不更改的数据。尽管这里不相关,但元组在实现和性能上比列表有优势;也就是说,如果你有一个固定的列表,最好使用元组。

之后,我们将使用元组在字典结构中,如下所示:


opCodes = {'add':(2,34), 'inc':(4,37)}

在这种情况下,加粗的值是每个两个组件的元组。我们本来可以用列表,但元组表示一个不能改变的结构。如果你要用列表代替元组,你会这样写:


opCodes = {'add':[2,34], 'inc':[4,37]}

重复和循环

现在我们扩展我们对 Python 重复机制的认知,这是所有高级过程式语言共有的特性。在本节中,我们学习如何做以下事情:

  • 重复执行操作多次

  • 每次执行操作时使用不同的数据和参数

  • 遍历列表中的元素

  • 使用 enumerate 函数

假设我们想要测试一个列表是否包含特定的项目。我们可以创建一个 for 循环来完成这个任务:


fruit1 = ['apples', 'oranges', 'grapes', 'bananas', 'peaches']
size = len(fruit1)               # Get length of the list
inList = False                   # inList is False until we find item
for i in range (0,size):         # Repeat for each item in list
    if fruit1[i] == 'grapes':
        inList = True            # Set flag to True if we find the item

此代码首先将 inList 设置为 False 以指示元素 'grapes' 未被找到。for 循环遍历列表中的所有元素,测试每个元素是否是我们正在寻找的项目。如果找到了,inList 被设置为 True。这段代码是有效的,但它并不好。如果列表中有百万个元素,而 'grapes' 是第一个,代码仍然会遍历剩余的 999,999 个元素。这是效率极低的。

在以下示例中,我们比较连续的元素与一个值,并在找到它时停止。在每次循环中,如果未找到项目,我们继续。如果我们找到我们想要的项目,我们跳出循环而不是通过测试每个单独的项目来继续到痛苦的结尾。当我们进行比较时,如果当前项目不等于 'grapes',我们将 inList 设置为 False,然后循环继续到下一个元素。

如果我们找到 'grapes'if 语句的 else 部分将 inList 设置为 True,然后使用 break 语句退出循环,避免进一步的无效循环。在 forwhile 循环中的 break 告诉 Python 立即退出循环并继续执行循环后的下一个指令:


listSize = len(fruit1)
for i in range (0,listSize):
    if fruit1[i] != 'grapes': inList = False   # Is the item here?"
    else:                                      # If it is, drop out of the loop
        inList = True                          # Set flag on finding it
        break                                  # Jump out of the loop

变量 inList 只是一个我们可以在程序中稍后使用的标志;例如,我们可以这样写:


if inList == False: print('Yes, we have no grapes')
if inList == True:  print('Grapes --- we got lots')

另一种方法是使用列表操作符 in。如果我们有一个列表,我们可以通过以下结构来检查一个项目是否是该列表的成员:


if    'grapes' in fruit1:
      inList = True
else: inList = False

第一行如果 'grapes' 在列表 fruit1 中返回 True,否则返回 Falsein 结构在测试一个项目是否属于作为列表排列的其他项目组时非常有用;例如,如果所有员工都在列表 staff 中,那么

如果 Smith 是有效的员工姓名,则将 worksHere 设置为 True,否则为 False


if 'Smith' in staff:  worksHere = True
else:                 worksHere = False

之后我们将使用in运算符来测试一个指令是否是集合的成员,就像这样:


arithOps = ['ADD','SUB','MUL','DIV']       # Define list of arithmetic operations
.
.
if 'mnemonic' in arithOps:                 # Test whether instruction is arithmetic
    .
    .
else:

重复和可迭代对象

for循环的另一种版本如下遍历列表的元素:


for i in anyList:                          # Operate on each element of the list one-by-one
    <body of loop>

粗体的单词是 Python 的保留字;其他单词是用户定义的变量。在这里,i不是像在之前使用range()的例子中那样的序列计数整数。它是列表中每个元素(或可迭代对象)按顺序取的值。考虑以下使用颜色列表的例子:


car = ['red', 'white', 'green' ,'silver', 'teal']
for color in car: print(color)             # Color is a loop variable; we could have used i.

这段代码遍历列表car的每个元素,并按如下方式打印其值。


red
white
green
silver
teal

考虑使用计算机操作列表的一个例子。在这种情况下,我们取了一个元组的列表,每个元组对应四个操作码。这个元组由一个操作码字符串、一个指令类型和所需的操作数数量组成。这只是一个演示。我们本来可以用列表,但选择元组来强调元组值不会改变:


opCodes = [('NOP','misc',0),('BEQ','flow',1),('LDR','move',2), \
          ('ADD', 'arith',3)]
for instruction in opCodes:                # Step through the op-codes
    print(instruction)                     # Print the current op-code
    op     = instruction[0]                # Extract the three tuple members
    group  = instruction[1]
    params = instruction[2]
    print(op, group, params)               # Print the three tuple values
    if op == 'BEQ': print('beq found')     # Demo! Print BEQ when we find it

这段代码的输出如下:


('NOP', 'misc', 0)
NOP misc 0
('BEQ', 'flow', 1)
BEQ flow 1
beq found
('LDR', 'move', 2)
LDR move 2
('ADD', 'arith', 3)
ADD arith 3

我们现在已经证明,在 Python 中你可以迭代任何类型的列表。

Python 的一个特别有趣的特征是使用双重索引进行循环。你可以通过数字索引或对象名称遍历列表。当你想按对象名称遍历但还想知道它在列表中的位置时,这很有用。Python 函数enumerate允许这种形式的循环。演示enumerate的作用比解释它更容易。考虑之前关于列表car的例子:


car = [ 'red', 'white', 'green', 'silver', 'teal']
for color in enumerate(car):
    print (color)

这段代码的输出如下:


(0, 'red')
(1, 'white')
(2, 'green')
(3, 'silver')
(4, 'teal')

迭代器color已经变成了一个包含元素索引和列表中相应值的元组序列。记住,元组就像列表一样,除了它的元素是不可变的,不能被更改。这里是一个我会使用像color这样的迭代器名称而不是i的情况,因为它更明确/描述性,并且与整数的混淆更少。

另一种枚举形式使用两个索引,一个是显式的整数计数,另一个是元素计数。在以下例子中,count是显式的整数索引,而color是枚举索引。因此,count步进012等,而color步进redwhitegreen…:


for count, color in enumerate(car):
    print ('count = ', count, 'Color =', color)

这会产生以下输出:


count =  0 Color = red
count =  1 Color = white
count =  2 Color = green
count =  3 Color = silver
count =  4 Color = teal

列表的列表

在这里,我们扩展了 Python 最重要的数据结构列表的使用。首先,我们演示了列表可以包含列表本身。Python 允许你用任何类型的项构建列表;例如,x = [1,2,'test',v,True]定义了一个包含两个整数、一个字符串、一个变量和一个布尔常量的列表。由于你可以在列表中使用任何合法的元素,你可以创建一个列表的列表。考虑以下例子:


fruit = [['apple',2.35,150], ['orange',4.10,200], ['banana',3.65,70]]

这是一个包含三个项目的列表,每个项目本身也是一个列表(阴影部分);例如,这个列表中的第一个项目是列表['apple',2.35,150]。每个子列表由一个命名水果的字符串、水果的价格和当前库存水平组成。

假设我们想知道橙子的价格;我们可以写点像这样的事情:


for i in range (0,len(fruit)):     # Step through the list of fruit.  len(fruit) is 3
    if fruit[i][0] == 'orange':    # If the first element in the current item is 'orange',
        price = fruit[i][1]        # then get the second element in that item
        break                      # If we do find 'orange' we can break out of the loop

我们使用for循环遍历水果列表。然后,当我们找到我们想要的项(它是一个列表)时,我们读取那个列表的第二个项。正如你所看到的,我们使用了两个下标,首先是[i],然后是[1]。

考虑以下列表的列表的例子:


testList = [[4,9,[1,6]],[8,7,[0,9]]]

这对眼睛来说并不容易!让我们使用粗体和阴影来强调字符串的组成部分:


testList = [[4,9,[1,6]], [8,7,[0,9]]]  # Each element in the list is itself a list

这是一个包含两个项的列表:[4,9,[1,6]]和[8,74, 9以及列表[1,6]。

如果我写x = testList[1][2][1]x的值会是什么?

它会是9,因为testList[1]是[8,7,[0,9]]testList[1][2]是[0,9],而testList[1][2][1]是9。想象这是一个有分支的树!第一个分支是testList的元素[1]。第二个分支是那个分支的元素[2],第三个(最后的)分支是第二个分支上的元素[1]图 5**.1以图形方式说明了嵌套列表的概念。

图 5.1 - 嵌套列表的说明

图 5.1 - 嵌套列表的说明

考虑第二个例子:


x = ['this', 'that', 'then']

x[2][3]是什么意思?这个表达式返回'n',因为x[2]'then',而那个元素中的第 3 个是'n'

Python 中的缩进

自从我们引入 Python 以来,我们就开始缩进代码。现在我们再次强调 Python 中缩进的使用,因为它对正确的编程至关重要。大多数计算机语言允许你将语句组合在一起,作为一个块,用于特定目的。通常,一组中的指令会作为一个批次,一个接一个地执行。这样的组通常与条件语句和循环相关联。

一些语言通过将指令包围在花括号{}中来表示指令块。考虑以下内容。这不是真正的代码;它只是用于说明程序布局的纯文本:


{some operations}
{main loop
{some other operations}
if x == 1 {Do this batch of operations}
repeat
{do these operations}
}

这里,你有几个操作块,包括嵌套块(即一个块在另一个块内)。块被当作一个单一的操作来执行;也就是说,它们是计算机中的分包等效。尽管这不是编程要求,但通常使用缩进来作为阅读辅助,使代码对人们来说更容易理解,如下所示:


{some operations}
{main loop
     {some operations}
     if x == 1
        {do this batch of operations}
     repeat
        {do these operations}
}

Python 不使用括号来表示连续操作的块。它要求代码块必须缩进(并且每个块成员使用相同的缩进)。如果缩进不正确,可能会导致程序无法编译,或者程序的行为不符合你的预期。缩进错误是新手程序员遇到的第一件事之一。缩进错误很容易忽略。例如,如果你在编辑程序时意外地创建或删除了缩进,你可能会很容易地得到一个难以定位的错误。

下面给出了 Python 中使用块和缩进的简单示例。虽然任何数量的缩进都是合法的,但按照惯例,缩进通常是四个空格。每个缩进级别都有阴影。注意,最后一行的 else 属于第一个 if。如果我们进一步缩进它,它将属于第二个 if


x = 1
y = 2
if z == 4:
    s = 1
    b = 2
    if b == 3:
        g = 1
else: p = 7

字典

在本节中,我们介绍了 Python 的字典机制,这使得编写模拟器变得非常容易。在这里,你将学习如何创建一个将一个事物转换成另一个事物的字典,例如,将指令的名称转换成其二进制代码。在这里,我们学习以下内容:

  • 字典的本质

  • 字典相对于列表的优点

  • 字典键和字典值之间的区别

  • 在字典中插入项目

  • 从字典中提取项目

  • 使用字典解决问题

字典是 Python 最有用的特性之一,这个特性使得设计模拟器变得容易得多。Python 字典是一个令人印象深刻的通过 访问的数据结构,而不是通过数据结构内的位置。你不需要提供像 myList[5] 这样的位置,你可以在字典中查找一个项目,就像你在日常生活中使用字典一样。你向字典提供一个名称(我们称之为 ),然后就会弹出与该名称 相关联 的信息。键是唯一的;同一个键在字典中不能出现多次(就像社会保险号码是唯一的)。

Python 字典的形式是 {key1:value1, key2:value2, key3:value3};例如,{'UK':44, 'USA':1, 'Germany':49, 'France':33} 可以用来查找一个国家的国际电话区号。字典被括号包围,key:value 对之间用冒号分隔。字典中对的顺序不重要,因为项目是通过其 而不是其在字典中的位置来访问的。

键通常是字符串,但这不是必需的。在我们的计算机模拟器中,键通常是计算机语言的助记码。与键相关联的值可以是任何合法的 Python 数据结构。在我们创建的一些模拟器中,我们经常指定值为一个元组,它是一个有序列表。例如,字典条目 'INC':(8,16) 有键 'INC' 和值 (8,16)。使用键 'INC' 搜索字典,返回元组 (8,16)。在这种情况下,值是指令的格式(即,8),以及它的操作码(即,16)。

你可以用列表代替元组作为值,即 'INC':[8,16]。唯一的显著区别是,一旦定义了元组,就不能更改它。

你可以通过编写 if key in dictionary 来检查一个项目是否在字典中,如下所示:


if 'INC' in opCodes:       # This returns True if 'INC' is in opCodes

要获取特定助记符的信息,我们可以使用 get 方法来读取与键关联的值。例如,opData = opCodes.get('INC') 返回 (8,16)

然后,我们可以按照以下方式访问与 'INC' 关联的元组的两个字段:


binaryCode  = opData[0]
formatStyle = opData[1]

如果请求的键不在字典中,get 方法返回 NoneNone 是 Python 的保留字,表示空值。请注意,None 既不是零也不是空字符串,它有自己的类型 None。考虑以下情况:


if opCodes.get(thisInstruction) == None: # Ensure that the instruction is valid
   print("Error. Illegal operation")

以下代码使用我们上面描述的电话前缀目录来演示如何使用 None 来处理错误。请注意,这使用了一个无限循环,并在错误发生时终止。当检测到 None 时,break 强制退出无限循环:


prefixes = {'UK':44, 'USA':1, 'Germany':49, 'France':33}
while True:                              # Infinite loop
    x = input('Country? ')               # Ask for the country
    y = prefixes.get(x)                  # Look up the prefix
    if y == None:                        # If None print error message
        print('Prefix not found')
        break                            # And exit the loop
    else: print('Prefix = ',y)
print('Program terminated')

Python 的字典使得实现标签和变量的符号名称变得非常容易。只需创建一个包含 name: value 对的字典,并使用名称来获取与标签关联的值;例如,你可能想将 Hastings 与值 1066 关联。典型的汇编器使用 directives 来表达这一点:


Hastings EQU 1066      @ Equate the Name "Hastings" to the value 1066

在你的程序稍后,你可能会写 LDRL r0,Hastings以将r0装载为1066。假设你有一个包含名称和值的表 namSub`,它被设置为一个字典:


namSub = {'Hastings':1066, 'Agincourt':1415, 'Trafalgar':1805}

如果我们想获取与 Hastings 关联的值,我们可以写下以下内容:


x = namSub.get('Hastings')

名称将被转换为它的值。

在接下来的内容中,我们编写了几个 Python 代码片段,以展示如何使用字典。这些示例演示了如何设置字典、向其中添加信息以及访问它。当汇编语言程序运行时,一些字典在执行之前就已经设置好了,例如,合法指令列表。一些目录,如汇编语言程序中出现的符号表,将在程序运行时构建。

示例中的第一个目录将寄存器名称转换为它的寄存器号;例如,寄存器名称 x 可以通过 y = regs.get(x) 转换为其寄存器号 y。当然,你不需要使用字典。我们可以简单地写 y = int(x[1:]) 来通过字符串处理将字符串 'r6' 转换为整数 6。然而,字典方法更为优雅且易于理解。此外,它还更灵活:


regs = {'r0':0, 'r1':1, 'r2':2, 'r3':3, 'r4':4}   # Register name-to-number translation
symTab = {'start':0,'time':24,'stackP':'sp','next':0xF2} 
                                            # Symbol table converts symbolic name to value
x0 = 'add r1,r2,r4'                # An example of an instruction in text form
x1 = x0.split(' ')                 # Split instruction into op-code and predicate
x2 = x1[1].split(',')              # Split the predicate into tokens
x3 = x2[0]                         # Get the first token of x2
if x3 in regs:                     # Is this a valid register?
    x4 = regs.get(x3)              # Use get() to read its value
print ('x0 = ',x0, '\nx1 = ',x1, '\nx2 = ',x2, '\nx3 = ',x3, '\nx4 = ',x4)
y0 = 'beq next'                    # Another example: instruction with a label
y1 = y0.split(' ')                 # Split into op-code and predicate on the space
y2 = y1[1]                         # Read the predicate (i.e.,'next')
y3 = symTab.get(y2)                # Get its value from the symbol table (i.e., 0xF2)
print('beq ',y3)                   # Print the instruction with the actual address
z = symTab.get('beq next'.split(' ')[1])  # We've done it all in one line. Not so easy to follow.
print('beq ',z)
print('Symbol table ', symTab)            # Print the symbol table using a print
symTab['nextOne'] = 1234                  # This is how we add a new key and value
print('Symbol table ', symTab)            # Here's the augmented symbol table
opCode = {'add':('Arith',0b0001,3),'ldr':('Move',0b1100,2), \
       'nop':('Miscellaneous',1111,0)}    # New directory. Each key has three values in a tuple
thisInst = 'ldr'                          # Let's look up an instruction
if thisInst in opCode:                    # First test if it's valid and in the dictionary
    if thisInst == 'ldr':                 # If it is:
        instClass = opCode.get('ldr')[0]  # Get first element of the instruction
        binaryVal = opCode.get('ldr')[1]  # Get the second element
        operands  = opCode.get('ldr')[2]  # Get the third element
print('\nFor opCode: ',thisInst, '\nClass = ', instClass, \
      '\nBinary code = ', bin(binaryVal), '\nNumber of operands = ',operands)
print('\nThis is how to print a directory')
                                   # Now print a formatted dictionary (key and value on each line)
for key,value in opCode.items():
    print(key, ':', value)
print()
for i,j in opCode.items():         # Note that key and value can be any two variables
    print(i, ':', j)
theKeys = opCode.keys()            # The function .keys() returns the keys in a dictionary
print('The keys are: ',theKeys)
test = {'a':0,'b':0,'c':0,'d':0}   # A new directory. The values are just integers
test['a'] = test['a'] + 1          # You can change a value! Use the key to locate it
test['d'] = test['d'] + 7
test1 = {'e':0, 'f':0}             # Here's a second dictionary.
test.update(test1)                 # Append it to test using .update()
print('Updated dictionary test is: ',test)   # Not convinced? Here it is then.

以下是在执行上述代码片段后的输出:


x0 =  add r1,r2,r4
x1 =  ['add', 'r1,r2,r4']
x2 =  ['r1', 'r2', 'r3']
x3 =  r1
x4 =  1
beq  242
beq  242
Symbol table  {'start': 0, 'time': 24, 'stackPointer': 'sp', 'next': 242}
Symbol table  {'start': 0, 'time': 24, 'stackPointer': 'sp', 'next': 242,
               'nextOne': 1234}
For opCode:  ldr
Class =  Move
Binary code =  0b1100
Number of operands =  2
This is how to print a directory
add : ('Arith', 1, 3)
ldr : ('Move', 12, 2)
nop : ('Miscellaneous', 1111, 0)
add : ('Arith', 1, 3)
ldr : ('Move', 12, 2)
nop : ('Miscellaneous', 1111, 0)
The keys are:  dict_keys(['add', 'ldr', 'nop'])
Updated dictionary test is:  {'a': 1, 'b': 0, 'c': 0, 'd': 7, 'e': 0, 'f': 0}

让我们通过另一个示例更详细地看看字典。使用 Python 的字典使得实现标签和变量的符号名称变得容易。我们只需创建一个包含 name: value 对的字典,并使用名称来获取其关联的值。假设我们已经读取了一条指令,比如说 'ADD r4,r2,r3',并将其标记化如下:

predicate = ['r4','r2','r3']  # The list of parameters for the op-code

我们可以通过切片的艰难方式获取寄存器的整数值:


rD = int([predicate[0]][1:])

让我们简化表达式,以便更容易解释。假设我们写下以下内容:


rD = predicate[0]

rD 的值是字符串 'r4'。我们需要做的是从 'r4' 中隔离 '4',然后将字符 '4' 转换为整数 4

我们可以写 rD = rD[1:] 来返回字符串中除了初始 'r' 之外的所有字符。最后一步是将它转换为整数,我们可以用 rD = int(rD) 来完成。

[1:] 表示第一个字符 r 之后的所有字符,如果寄存器是 'r4',则返回 '4'。我们本来可以写 [1:2] 而不是 [1:]。然而,通过使用 [1:],我们可以在不更改程序的情况下增加寄存器的数量超过 9。将这三个步骤结合起来,我们得到以下内容:


rD = int([predicate[0]][1:])

让我们使用字典执行相同的操作。假设我们已为寄存器设置了一个目录:


regs = {'r0':0, 'r1':1, 'r2':2, 'r3':3, 'r4':4}   # Register names and values

在处理寄存器名称之前,我们可以使用以下方法测试有效的寄存器符号名称:


if predicate[0] in regs:
      <deal with valid name>
else: <deal with error>

提取寄存器的实际整数编号很简单:


rD = regs.get(predicate[0])

最后,请注意,您可以通过两种方式访问字典。考虑以下内容:


regs = {'r0':0, 'r1':1, 'r2':2, 'r3':3, 'r4':4}
aaa  = regs.get('r3')
bbb  = regs['r3']
print('Test aaa = ',aaa, 'bbb =',bbb)

这给我们以下内容:


Test aaa =  3 bbb = 3

get 的优点是,如果找不到键,则返回 None,而另一种方法会创建一个运行时错误,称为 KeyError

函数回顾

本节将更详细地探讨函数,并演示如何使用global语句使参数在函数外部可访问。

参数可以通过函数调用中的括号传递给函数,并通过 return() 语句检索结果。回想一下,在函数中创建的变量是函数本地的,除非它们被声明为全局变量;也就是说,如果您在函数中写 x = 5,您已经创建了一个值为 5 的局部变量 x。如果函数外部有一个 x,它是一个不同的 x。在函数内部可以访问在函数外部声明的值,前提是它没有被在函数内部声明为局部变量。

函数体从初始的 def 语句开始缩进。函数不需要显式终止,因为 缩进 会处理这一点。考虑以下内容:


def adder(P,Q):                    # Adder function
   R = P + Q
   return (R)                      # Return the sum R
def subtractor(P,Q):               # Subtractor function
   global R                        # Make R global
   R = P – Q                       # No need to return a value
A, B = 7, 2                        # Note Python's multiple assignment
C = adder(A,B)                     # Do addition
subtractor(A,B)                    # Do subtraction (just call the function)
print('Sum =', C, 'Diff = ',R)

如果我们运行此程序,我们会得到以下内容:


Sum = 9 Diff =  5

如您所见,我们可以在返回语句中作为参数返回一个值,或者我们可以将其设置为全局变量。当我们使用全局变量时,我们不需要将参数传递到或从函数中。

导入

本节展示了如何访问不属于 Python 语言本身的操作。这些函数对于编写模拟器并非至关重要,但它们确实提供了一些非常有用的功能。我们包括这个简短的部分来演示如何访问这些功能。

Python 的一个优点是它包含几个函数库,您可以通过它们来方便地设计程序,例如图形。我们不需要很多外部函数来完成我们的工作。在这里,我们将演示两个:randomsys

当模拟计算机时,你通常需要测试数据。手动输入它很耗时。幸运的是,Python 有一个生成随机数的函数库。为了使用库,你首先必须导入它。考虑以下内容:


import random                   # Get the library (usually at the start of the program)
.
.
X = random.randint(0,256)       # Generate a random integer in the range 0 to 255

函数调用通常的形式是library.action。在这种情况下,库是random,操作是randomint(a,b)。参数ab给出了随机整数的范围。

另一个有用的库是sys,它提供了操作系统功能,如exit(),该功能终止 Python 程序并返回其调用级别。例如,请参阅以下内容:


import sys                      # Get the system library
.
.
if run == 0: sys.exit()         # If run is 0 then go home (exit the Python program)

我们现在已经涵盖了足够的 Python 主题,可以开始设计一个真正的计算机模拟器了。

摘要

在本章中,我们扩展了我们对 Python 的了解,并介绍或扩展了一些展示其强大和多功能性的特性。例如,我们研究了列表和字符串,这两个对我们来说最重要的数据结构。我们还扩展了循环和其他重复结构的使用。

我们介绍了字典,这是一个令人愉快优雅的数据结构,使我们能够通过描述它的键来定位信息,而不是它在目录中的位置。例如,如果我们想将指令名称转换为二进制操作码,我们可以创建一个字典,并只需查找任何助记符的适当代码。这个特性确实简化了解释器和汇编器的开发。

我们还研究了 Python 的一个不太常见的特性:列表推导。这些需要一点时间来适应,但它们可以使通过清理输入(例如,通过删除空格或修改标点符号和语法)来处理汇编语言指令的文本变得更加容易。

第六章中,我们将关于计算机操作的知识整合起来,并为一个名为 TC1 的假设计算机设计了一个汇编器和模拟器。

第六章:TC1 汇编器和模拟器设计

在本章中,我们将结合之前章节学到的知识,构建一个计算机模拟器。本章我们将涵盖的关键主题如下:

  • 分析指令

  • 处理汇编指令

  • 构建二进制指令

  • pre-TC1(实际模拟器的前传)

  • TC1 模拟器程序

  • 一个 TC1 汇编语言程序

  • TC1 后记

到本章结束时,您应该了解模拟器是如何设计的,并且能够创建一个模拟器。接下来的两个章节将专注于扩展模拟器并提供更多功能,例如输入错误检测。

技术要求

您可以在 GitHub 上找到本章使用的程序:github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter06

为了构建一个基于 Python 的模拟器,您需要之前章节中使用的相同工具;也就是说,您需要一个编辑器来创建 Python 程序,以及一个 Python 解释器。这些工具包含在我们之前在第一章中介绍的免费 Python 软件包中。

分析指令

在本节中,我们将探讨如何将表示汇编语言指令的文本字符串转换为可以被模拟器执行的二进制代码。

有趣的是,汇编器可能比实际的模拟器更复杂。实际上,在本章中,我们对模拟器本身的篇幅相对较少。我们实际上不需要汇编器,因为将汇编级操作手动翻译成二进制代码很容易;这只是填写 32 位指令格式字段的问题。例如,将常量值 42 加载到寄存器 R7 中可以写成LDRL R7,42。这个指令有 7 位操作码,01 01010,目标寄存器是r7(代码111),两个源寄存器未使用,它们的字段都可以设置为000。常量是42,或者作为 16 位二进制值0000000000101010。二进制编码的指令如下:

00010101110000000000000000101010

手动翻译代码很容易,但并不有趣。我们将创建一个汇编器来自动化翻译过程,并允许您使用符号名称而不是实际的常量(常数)。考虑以下汇编语言代码的示例。这是使用数值(阴影标注)而不是符号名称编写的。这并不是一个特定的汇编语言;它旨在说明基本概念:


        LDRL R0,60              @ Load R0 with the time factor, 60
        .
        CMP  R3,R5              @ Compare R3 and R5
        BEQ  1                  @ If equal, jump to next-but-one instruction
        ADD  R2,R2,4
        SUB  R7,R1,R2

在以下示例中,常量值已被替换为符号名称。这些名称被阴影标注:


Minutes EQU  60                 @ Set up a constant
Test    EQU  4
.       LDRL R0,Minutes         @ Load R0 with the time factor
.       CMP  R3,R5
        BEQ  Next
        ADD  R2,R2,Test
Next    SUB  R7,R1,R2

Python 的字典结构确实使得处理符号名称变得非常容易。前面的示例展示了处理包含汇编语言的文本文件的过程。这个文件被称为sFile,它只是一个包含汇编语言指令的.txt文件。

处理输入

我们现在将探讨如何处理原始输入——即包含汇编语言源代码的文本文件。原则上,有一个源文件,其中汇编语言指令都完美格式化和排列,那将是很好的。

在现实中,一个程序可能不是理想地格式化的;例如,可能有空白行或需要忽略的程序注释。

我们设计这种汇编语言,以便在编写 TC1 程序时具有相当大的灵活性。实际上,它允许一种大多数真实汇编器都没有实现的自由格式。我们采取这种方法的几个原因包括。首先,它展示了如何执行文本处理,这是汇编器设计的基本部分。其次,自由格式意味着你不必记住是否使用大写或小写名称和标签。

一些语言是大小写敏感的,而另一些则不是。我们设计的汇编语言是不区分大小写的;也就是说,你可以写 ADD r0,r1,r2 或者 ADD R0,R1,R2。因此,我们可以以下列所有形式编写加载寄存器立即指令来执行加载 寄存器索引操作:

LDRI R2,[R1],10 或者

LDRI R2,r1,10 或者

LDRI R2,[R1,10] 或者 ,

LDRI r2,r1,10

这种记法自由度之所以可能,是因为[]括号实际上并不必要来识别指令;它们在程序中使用,因为程序员将[r0]与间接寻址联系起来。换句话说,括号是为了程序员而不是计算机而存在的,是多余的。

然而,这种自由度并不一定是可取的,因为它可能导致错误,并使一个人阅读另一个人的程序变得更加困难。所有设计决策都伴随着利弊。

以下 Python 示例包含一个简短的嵌入式汇编语言程序。Python 代码被设计成你可以使用汇编器的一部分汇编语言程序(这只是为了测试和调试目的,因为它避免了每次想要测试一个特性时都需要进入文本编辑器的情况)或者磁盘上的文本形式程序。在这个例子中,我们在我的电脑上找到了测试程序,位于E:\testCode.txt。当演示文本处理代码运行时,它会询问你是从磁盘读取代码还是读取嵌入式代码。输入d读取磁盘,输入任何其他输入读取嵌入式代码。

汇编语言程序的文件名为 testCode = 'E://testCode.txt'。在 Python 程序中,我们使用双反斜杠代替传统的文件命名约定。

文本处理程序移除空白行,将文本转换为大写(允许您写r0R0),并允许您使用逗号或空格作为分隔符(您可以写ADD R0,R1,R2ADD r0 r1 r2)。我们还移除了指令前后多余的空格。最终结果是标记化列表;也就是说,ADD r0,r1,r2被转换为['ADD','R0','R1','R2']列表。现在,汇编器可以查找指令,然后提取它所需的信息(寄存器编号和字面量)。

在以下程序中,我们每次处理一行时都会使用一个新的变量,以便帮助您跟踪变量。以下是一个示例:


sFile2 = [i.upper() for i in sFile1]          # Convert in to uppercase
sFile3 = [i.split('@')[0] for i in sFile2]    # Remove comments

我们为了清晰起见使用了不同的变量名。通常,你会这样写:


sFile = [i.upper() for i in sFile]            #
sFile = [i.split('@')[0] for i in sFile]      #

我们使用文件理解来移除代码中的注释:


sFile3 = [i.split('@')[0] for i in sFile2]    # Remove comments

这是一个相当巧妙的技巧,需要解释。它将sFile2中的每一行复制到sFile3。然而,对于每一行的复制值是i.split('@')[0],其中i是当前行。split('@')方法使用'@'作为分隔符将列表分割成字符串。如果原始字符串中没有'@',则字符串将被复制。如果有'@',它将被复制为两个字符串;例如,ADD R1,R2,R3 @ Sum the totals将被复制到sFile3作为'ADD R1,R2,R3','@ Sum the totals'。然而,由于[0]索引,只复制列表的第一个元素;也就是说,只复制'ADD R1,R2,R3',并移除了注释。

文本输入处理块如下所示:


testCode = 'E://testCode.txt'
altCode  = ['nop', 'NOP 5', 'add R1,R2','', 'LDR r1,[r2]', \
            'ldr r1,[R2]','\n', 'BEQ test @www','\n']
x = input('For disk enter d, else any character ')
if x == 'd':
    with open(testCode, 'r') as source0:
         source = source0.readlines()
    source = [i.replace('\n','') for i in source]
else:    source = altCode
print('Source code to test is',source)
sFile0 = []
for i in range(0,len(source)):                # Process the source file in list sFile
    t1 =  source[i].replace(',',' ')          # Replace comma with space
    t2 =  t1.replace('[',' ')                 # Remove [ brackets
    t3 =  t2.replace(']',' ')                 # Remove ] brackets
    t4 =  t3.replace('  ',' ')                # Remove any double spaces
    sFile0.append(t4)                         # Add result to source file
sFile1= [i for i in sFile0 if i[-1:]!='\n']   # Remove end-of-lines
sFile2= [i.upper() for i in sFile1]           # All uppercase
sFile3= [i.split('@')[0] for i in sFile2]     # Remove comments with @
sFile4= [i.rstrip(' ') for i in sFile3 ]      # Remove trailing spaces
sFile5= [i.lstrip(' ') for i in sFile4 ]      # Remove leading spaces
sFile6=[i for i in sFile5 if i != '']         # Remove blank lines
print ('Post-processed output',  sFile6)

以下是用此代码的两个示例。在第一种情况下,用户输入是d,表示磁盘程序,在第二种情况下,输入是x,表示使用嵌入式源程序。在每种情况下,都会打印课程和输出值以演示字符串处理操作。

情况 1 – 磁盘输入


For disk enter d, else any character d
Source code to test is ['diskCode', 'b', 'add r1,r2,[r3]', '', 'ADD r3 @test', ' ', 'r2,,r3', ' ', 'gg']
Post-processed output ['DISKCODE', 'B', 'ADD R1 R2 R3', 'ADD R3', 'R2 R3', 'GG']

情况 2 – 使用嵌入式测试程序


For disk enter d, else any character x
Source code to test is ['nop', 'NOP 5', 'add R1,R2', '', 'LDR r1,[r2]', 'ldr r1,[R2]', '\n', 'BEQ test @www', '\n']
Post-processed output ['NOP', 'NOP 5', 'ADD R1 R2', 'LDR R1 R2', 'LDR R1 R2', 'BEQ TEST']

上述代码并不代表一个最优的文本处理系统。它被设计用来演示在处理文本之前涉及的基本过程。然而,这些概念将在 TC1 中再次出现。

处理助记符

名字里有什么?我们如何知道一个NOP指令是独立的,而一个ADD指令需要三个寄存器?在本节中,我们将开始讨论汇编语言指令是如何被处理的,以便提取它们的含义(即,将它们转换为二进制形式)。

考虑以下 TC1 汇编语言的片段:


ADD  R1,R3,R7        @ Three operands (three registers)
NOP                  @ No operands
LDRL R4,27           @ Two operands (register and a literal value)

当汇编器读取一行时,它需要知道如何处理操作码及其操作数。那么,它是如何知道如何进行操作的?我们可以使用 Python 的字典功能以非常简单的方式解决这个问题,只需查看表格以查看操作码需要哪些信息。

回想一下,字典是一组或集合的项目,其中每个项目有两个组成部分;例如,一个英德词典包含由一个英语单词及其德语对应词组成的项目。你查找的单词称为 ,它提供 。例如,在英德词典中,项目 'town':'Stadt' 由键 town 和值 Stadt 组成。字典是 查找表 的一个高级名称。

Python 中的字典由其标点符号定义(即,它不需要任何特殊的保留 Python 词汇);它是一种由花括号 {} 包围的列表类型。每个列表项由一个键及其值组成,键和值之间用冒号分隔。连续的项用逗号分隔,就像列表一样。

用于访问字典中适当的价值。在 TC1 中,键是用于查找指令详细信息的 助记符。让我们创建一个名为 codes 的字典,其中包含三个键,这些键是表示有效 TC1 指令的字符串:STOPADDLDRL。这个字典可以写成以下形式:


codes = {'STOP':P, 'ADD':Q, 'LDRL':R}               # P, Q, R are variables

每个键都是一个以冒号结尾的字符串,后面跟着它的值。键不一定是字符串。在这种情况下,它是一个字符串,因为我们正在使用它来查找助记符,这些助记符是文本字符串。第一个 key:value 对是 'STOP':P,其中 'STOP' 是键,P 是它的值。假设我们想知道 ADD 是否是一个合法的指令(即,在字典中)。我们可以通过以下方式测试这个指令(即,键)是否在字典中:


    if 'ADD' in codes:  # Test whether 'ADD' is a valid mnemonic in the dictionary

如果键在字典中,则返回 True,否则返回 False。你可以使用 not in 来测试某个元素是否不在字典中。

Python 允许将任何有效的对象与键关联,例如,一个列表。例如,我们可以写出以下 key:value 对:


 'ADD': [3, 0b1101001, 'Addition', '07/05/2021', timesUsed]

在这里,与键关联的值是一个包含五个元素的列表,它将 ADD 助记符与操作数的数量、其二进制编码、其名称、设计日期以及它在当前程序中被使用的次数(以及能够从字典中读取值,你还可以写入并更新它)相关联。

以下代码设置了一个字典,将助记符与变量(预设为整数 1,2,3,4)绑定:


P,Q,R,N = 1,2,3,4                                   # Set up dummy opcodes
validCd = {'STOP':P, 'ADD':Q, 'LDRL':R, 'NOP':N}    # Dictionary of codes
x = input('Please enter a code  ')                  # Request an opcode
if x not in validCd:                                # Check dictionary for errors
    print('Error! This is not valid')
if x in validCd:                                    # Check for valid opcode
    print('Valid op ', validCd.get(x))              # If found, read its value

在这个例子中,我们使用了 get() 方法来读取与键关联的值。如果键是 x,其值由 validCd.get(x) 给出;即,语法是 dictionaryName.get(key)

汇编语言包含要执行的指令。然而,它还包含称为 汇编指令 的信息,这些信息告诉程序有关环境的一些信息;例如,数据在内存中的位置或如何将符号名称绑定到值。我们现在将探讨汇编指令。

处理汇编指令

在本节中,我们将学习以下内容:

  • 汇编指令的作用

  • 如何创建一个将符号名称与值链接的符号表

  • 如何访问符号表

  • 如何更新符号表

  • 处理标签

我们将演示程序员选择的名称是如何被操作和转换为适当的数值的。

TC1 的第一个版本要求你为所有名称和标签提供实际值。如果你想跳转到一条指令,你必须提供要跳转的行数。允许程序员编写以下内容会更好:

JMP next

在这里,next 是目标行的标签。这比编写以下内容更受欢迎:

JMP 21

类似地,如果文字 60 代表一小时中的分钟,请编写以下内容:

MULL ,R1,MINUTES

这比以下内容更受欢迎:

MULL ,R1,60

我们需要一种方法来 链接 next21MINUTES60

Python 的 字典 结构解决了这个问题。我们只需创建 key:value 对,其中 key 是我们想要定义的标签,value 是它的值。在这个例子中,前面例子中的字典将是 {'NEXT':21, 'MINUTES':60}。注意这个例子使用 整数 作为值。在这本书中,我们也将使用 字符串 作为值,因为我们以文本形式输入数据;例如,'MINUTES':'60'

EQU 汇编指令将一个值与一个符号名称关联。例如,TC1 允许你编写以下内容:

MINUTES EQU 60

使用字典

MINUTES EQU 60 汇编指令有三个令牌:一个标签、一个函数(等价)和一个值。我们从源代码中提取 'MINUTES':60 字典对,并将其插入到名为 symbolTab 的字典中。以下代码演示了该过程。第一行设置符号表。我们用虚拟条目 'START':0 初始化它。我们创建了这个初始条目用于测试目的:


symbolTab = {'START':0}                              # Symbol table for labels
for i in range (0,len(sFile)):                       # Deal with equates
    if len(sFile[i]) > 2 and sFile[i][1] == 'EQU':   # Is token 'EQU'?
        symbolTab[sFile[i][0]] = sFile[i][2]         # If so, update table
sFile = [i for i in sFile if i.count('EQU') == 0]    # Delete EQU from source

for 循环(阴影部分)读取源代码的每一行,sFile,并检查 'EQU' 是否是该行的第二个令牌。len(sFile[i]) > 2 的比较确保该行至少有三个令牌,以确保它是一个有效的等价指令。文本以粗体字体显示。

我们可以通过使用 and 布尔运算符同时执行两个测试,这样测试只有在两个条件都为真时才为真。

我们检查第二个令牌是否为 'EQU',使用 sFile[i][1] == 'EQU'sFile[i][1] 表示法有两个列表索引。第一个,以粗体显示,表示源代码的第 i 行,第二个索引表示该行的令牌 1;也就是说,它是第二个元素。

如果找到 'EQU',我们将第一个令牌 [sFile[i][0]] 作为键添加到符号表中,第三个令牌 sFile[i][2] 作为值。

考虑以下 MINUTES EQU 60 源代码行。

关键是 sFile[i][0] 和它的值是 sFile[i][2],因为 MINUTES 是第 i 行的第一个标记,而 60 是第 i 行的第三个标记。存储的键是 'MINUTES',其值是 60。但请注意,值 60 是以 字符串 形式存在,而不是 整数 形式。为什么?因为汇编指令是一个字符串,而不是一个整数。如果我们想要数值,我们必须使用 int()

这段代码的最后一行如下:


sFile = [i for i in sFile if i.count('EQU') == 0]

这一行使用列表推导来扫描源文件,并删除任何包含 EQU 的行,因为只有指令被加载到程序内存中。包含 EQU 的行是一个指令,而不是指令。这个操作使用计数方法,i.count('EQU') 来计算 EQU 在一行中出现的次数,然后如果计数不是 0,则删除该行。我们在移动(即保留)一行之前测试的条件如下:

if i.count('EQU') == 0:

在这里,i 是当前正在处理的行。将 count 方法应用于当前行,并计算 'EQU' 字符串在该行中出现的次数。只有当计数是 0(即,它不是一个带有 EQU 指令的行)时,该行才会被复制到 sFile

由于检测 EQU 指令、将其放入符号表和从代码中删除非常重要,我们将通过一小段测试代码来演示其操作。以下代码片段在 sFile 中设置了一个包含三个指令的列表以进行测试。请记住,sFile 是一个列表的列表,每个列表是一个由标记组成的指令,每个标记都是一个字符串:


sFile=[['test','EQU','5'],['not','a','thing'],['xxx','EQU','88'], \
       ['ADD','r1','r2','r3']]
print('Source: ', sFile)
symbolTab = {}                                    # Creates empty symbol table
for i in range (0,len(sFile)):                    # Deal with equates e.g., PQR EQU 25
    print('sFile[i]', sFile[i])
    if len(sFile[i]) > 2 and sFile[i][1] == 'EQU':  # Is the second token 'EQU'?
        print('key/val', sFile[i][0], sFile[i][2])  # Display key-value pair
        symbolTab[sFile[i][0]] = sFile[i][2]        # Now update symbol table
sFile = [i for i in sFile if i.count('EQU') == 0]   # Delete equates from source file
print('Symbol table: ', symbolTab)
print('Processed input: ',sFile)

粗体的代码是我们讨论过的代码。其余的代码是由 print 语句组成的,用于观察代码的行为。代码中的关键行如下:

symbolTab[sFile[i][0]] = sFile[i][2]

这通过以下格式的 key:value 对更新符号表:

symbolTab[key] = value

当运行此代码时,它生成以下输出:


Source [['test','EQU','5'],['not','a','thing'],['xxx','EQU','88'], ['ADD','r1','r2','r3']]
sFile[i] ['test', 'EQU', '5']
key/val test 5
sFile[i] ['not', 'a', 'thing']
sFile[i] ['xxx', 'EQU', '88']
key/val xxx 88
sFile[i] ['ADD', 'r1', 'r2', 'r3']
Symbol table {'test': '5', 'xxx': '88'}
Processed input [['not', 'a', 'thing'], ['ADD', 'r1', 'r2', 'r3']]

最后两行给出了符号表和 sFile 的后处理版本。两个等式已经被加载到字典(符号表)中,并且处理后的输出已经去除了这两个等式。

向字典中添加新的 key:value 对的方法有很多。我们本可以应用 update 方法到 symbolTab 并编写以下内容:


symbolTab.update({[sFile[i][0]]:sFile[i][2]})

在汇编器的后续示例中,我们将采用不同的汇编指令约定,并使用 .equ name value 的格式,因为这种约定被 ARM 处理器采用,正如我们将在后面的章节中看到的。表示汇编指令的方法通常不止一种,每种方法都有其自身的优缺点(例如,编码的简便性或与特定标准和约定相匹配)。

标签

处理源文件的下一步是处理标签。以下是一个示例:


      DEC  r1                                   @ Decrement r1
      BEQ  NEXT1                                @ If result in r1 is 0, then jump to line NEXT1
      INC  r2                                   @ If result not 0, increment r2
      .
NEXT1 .

在这个例子中,递减操作从寄存器 r1 的内容中减去 1。如果结果是 0,则设置 Z flag。下一条指令是 零分支到 NEXT1。如果 Z = 1,则跳转到标签为 NEXT1 的行;否则,执行紧随 BEQ 后面的 INC r2 指令。

由 TC1 生成的二进制程序(机器代码)不存储或使用标签。它需要下一个指令的实际地址或其相对地址(即,它需要从当前位置跳转多远)。换句话说,我们需要将 NEXT1 标签转换为程序中的实际地址。

这是字典的工作。我们只需将标签作为键放入字典,然后将相应的地址作为与键关联的值插入。以下三行 Python 代码演示了如何收集标签地址并将它们放入符号表中:


1. for i in range(0,len(sFile)):                  # Add branch addresses to symbol tab
2.     if sFile[i][0] not in codes:               # If first token not an opcode, it's a label
3.        symbolTab.update({sFile[i][0]:str(i)})  # Add pc value, i to sym tab as string
4. print('\nEquate and branch table\n')           # Display symbol table
5. for x,y in symbolTab.items():                  # Step through symbol table
6.     print('{:<8}'.format(x),y)

这三条线,1 到 3,定义了一个 for 循环,遍历 sFile 中的每一行源代码。因为我们已经处理了代码,将每条指令转换成令牌列表,所以每一行以有效的助记符或标签开始。我们只需检查一行上的第一个令牌是否在助记符列表(或字典)中。如果第一个令牌在列表中,则它是一条指令。如果不在列表中,则它是一个标签(我们忽略它是错误的情况)。

我们使用以下方式来检查有效的助记符:


2\. if sFile[i][0] not in codes:

在这里,sFile[i][0] 代表字典中第 i 行的第一个项目(即令牌)。Python 中的 not in 代码返回 True 如果助记符不在名为 codes 的字典中。如果测试返回 True,则我们有一个标签,必须使用以下操作将其放入符号表中:


3\. symbolTab.update({sFile[i][0]:str(i)})                  # i is the pc value

这个表达式表示,“将指定的 key:value 对添加到名为 symbolTable* 的字典中*。”为什么与标签关联的值给定为 i?与标签关联的值是该行的地址(即,程序计数器 pc 在执行该行时的值)。由于我们是逐行遍历源代码,计数器 i 是程序计数器的对应值。

update 方法应用于符号表,其中 sFile[i][0] 作为键,str(i) 作为值。键是 sFile[i][0],即标签(即字符串)。然而,i 不是一个 字符串。这个值是一个 整数i,它是当前行地址。我们通过 str(i) 将整数地址转换为字符串,因为等式以字符串的形式存储在表中(即,这是由我做出的设计决策)。

下两行打印符号表:


4\. print('\nEquate and branch table\n')                    # Display symbol table
5\. for x,y in symbolTab.items(): print('{:<8}'.format(x),y) # Step through symbol table

使用 for 循环打印符号表中的值。我们使用以下方式提取 key:value 对:


5\. for x,y in symbolTab.items():

items() 方法遍历 symbolTab 字典的所有元素,并允许我们打印每个 key:pair 值(即,所有名称/标签及其值)。print 语句使用 {:<8}.format(x) 格式化 x 的值,以显示八个字符,右对齐。

解码指令后,我们接下来必须将其转换为适当的二进制代码。

构建二进制指令

汇编过程的下一步是为每个指令生成适当的二进制模式。在本节中,我们展示了如何将指令的各个组件组合起来,以创建可以由计算机稍后执行的二进制值。

注意,本节中的代码描述了分析指令时涉及的一些指令处理。实际的模拟器在细节上有所不同,尽管原理是相同的。

我们首先必须提取助记符,将其转换为二进制,然后提取寄存器编号(如果适用),最后插入 16 位字面量。此外,因为汇编器是文本形式,我们必须能够处理符号字面量(即,它们是名称而不是数字)、十进制、负数、二进制或十六进制;也就是说,我们必须处理以下形式的指令:


LDRL r0,24                   @ Decimal numeric value
LDRL r0,0xF2C3               @ Hexadecimal numeric value
LDRL r0,$F2C3                @ Hexadecimal numeric value (alternative representation)
LDRL r0,%00110101            @ Binary numeric value
LDRL r0,0b00110101           @ Binary numeric value (alternative representation)
LDRL r0,-234                 @ Negative decimal numeric value
LDRL r0,ALAN2                @ Symbolic value requiring symbol table look-up

汇编器查看源代码的每一行,并提取助记符。指令是一系列标记(例如,'NEXT''ADD''r1''r2''0x12FA',这是五个标记,或者 'STOP',这是一个标记)。情况变得更加复杂,因为助记符可能是第一个标记,或者如果指令有标签,则是第二个标记。在以下示例中,sFile 包含程序作为指令列表,我们正在处理第 i 行,sFile[i]。我们的解决方案如下:

  1. 读取第一个标记,sFile[i][0]。如果此标记在代码列表中,则它是一个指令。如果不在此代码列表中,则它是一个标签,第二个标记 sFile[i][1] 是指令。

  2. 获取指令详细信息。这些信息存储在一个名为 codes 的字典中。如果助记符在字典中,则键返回一个包含两个组件的元组。第一个组件是指令的格式,它定义了所需的操作数序列 rD, rS1, rS2, literal;例如,代码 1001 表示一个具有目标寄存器和字面量的指令。元组的第二个组件是操作码的值。我们使用十进制值(理想情况下,为了可读性,它应该是二进制,但二进制值太长,使得文本难以阅读)。

  3. 从指令中的标记读取寄存器编号;例如,ADD r3,r2,r7 将返回 3,2,7,而 NOP 将返回 0,0,0(如果寄存器字段未使用,则将其设置为 0)。

  4. 读取任何字面量并将其转换为 16 位整数。这是最复杂的操作,因为字面量可能有之前描述的七种不同格式之一。

  5. 本节中的讨论指的是在章节末尾完整呈现的 TC1 程序。在这里,我们展示该程序的部分内容,并解释它们是如何工作的以及汇编过程中的步骤。

提取指令及其参数

以下代码片段展示了扫描源代码并创建二进制值的循环的开始。此代码初始化变量,提取操作码作为助记符,提取任何标签,提取助记符所需的参数,并查找操作码及其格式:


for i in range(0,len(sFile)):                     # Assembly loop reads instruction
    opCode,label,literal,predicate = [],[],[],[]  # Initialize opcode, label, literal, predicate
    rD, rS1, rS2  = 0, 0, 0                       # Clear register-select fields to zeros
    if sFile[i][0] in codes: opCode = sFile[i][0]   # If first token is a valid opcode, get it
    else:                    opCode = sFile[i][1]   # If not, then opcode is second token
    if (sFile[i][0] in codes) and (len(sFile[i]) > 1): # If opcode valid and length > 1
        predicate = sFile[i][1:]
    else:
        if len(sFile[i]) > 2: predicate = sFile[i][2:] \
                                              # Lines with a label longer than 2 tokens
    form = codes.get(opCode)                  # Use mnemonic to read instruction format
    if form[0] & 0b1000 == 0b1000:            # Bit 4 of format selects destination register rD
    if predicate[0] in symbolTab:                 # If first token in symbol tab, it's a label
            rD = int(symbolTab[predicate[0]][1:]) # If it is a label, then get its value

循环中的第 2 和 3 行声明并初始化变量,并提供了默认值。

第 4 行的第一个if…else语句检查源代码第i行的第一个令牌sFile[i][0]。如果该令牌在codes字典中,则sFile[i][0]是操作码。如果不在此字典中,则该令牌必须是标签,第二个令牌是操作码(第 4 和 5 行):


4.    if sFile[i][0] in codes: opcode = sFile[i][0] # If first token is a valid opcode, get it
5.    else:                    opCode = sFile[i][1] # If not, then it's the second token

如果我们遇到标签,我们可以将其转换为其实际地址,该地址在symbolTab中,使用以下方法:


    if sFile[i][0] in symbolTab: label = sFile[i][0] # Get label

第 6、7、8 和 9 行从汇编语言中提取谓词。记住,谓词由助记符之后的令牌组成,包括指令所需的任何寄存器和字面量:


6\. if (sFile[i][0] in codes) and (len(sFile[i])>1): # Get everything after opcode
7.                        predicate = sFile[i][1:]  # Line with opcode
8\. else:
9.    if len(sFile[i])>2: predicate = sFile[i][2:]  # If label and len > 2 tokens

我们必须处理两种情况:第一个令牌是助记符,第二个令牌也是助记符。我们还检查该行是否足够长以包含谓词。如果有谓词,它将通过第 7 和第 9 行被提取:


7.          predicate = sFile[i][1:]     # The predicate is the second and following tokens
9.          predicate = sFile[i][2:]     # The predicate is the third and following tokens

符号[2:]表示从令牌 2 到行尾的所有内容。这是 Python 的一个非常好的特性,因为它不需要你明确地声明行的长度。一旦我们提取了包含寄存器和字面量信息的谓词,我们就可以开始汇编指令。

接下来,我们提取当前行的代码格式以获取从谓词中所需的信息。第 10 行,形式= codes.get(opCode),访问codes字典以查找opCode变量中的助记符。get方法应用于codesform变量接收键值,即(格式,代码)元组,例如(8,10)。form[0]变量是指令格式,form[1]是操作码:


10\. form = codes.get(opCode)                    # Use opcode to read instruction format
11\. if form[0] & 0b1000 == 0b1000:              # Bit 3 of format selects destination reg rD
12.     if predicate[0] in symbolTab:           # Check whether first token is symbol table
13.        rD =int(symbolTab[predicate[0]][1:]) # If it's a label, then get its value

元组的第二个元素 form[1] 提供了 7 位指令码;即 0100010 对于 LDRL。行 1013 展示了如何提取目标寄存器。我们首先使用 AND form[0]0b1000 来测试最高有效位,该位指示是否需要此指令的目标寄存器 rD。如果需要,我们首先测试寄存器是否以 R0 的形式表示,或者是否以名称给出,例如 TIME。我们必须这样做,因为 TC1 允许你使用 EQU 指令重命名寄存器。

你可以使用 dictionary 来检查一个项目是否在字典中。以下是一个例子:

if 'INC' in opCodes:

要获取有关特定助记符的信息,我们可以使用 get 方法读取与键 关联的值,例如,format = opCodes.get('INC')

前面的例子返回 format = (8,82)8 指的是格式代码 0b1000(指定目标寄存器)。82 是该指令的指令码。我们可以使用以下方式访问与 'INC' 关联的值的两个字段:


binaryCode  = format[0]
formatStyle = format[1]

我们首先在行 12 中测试寄存器是否有符号名称。if predicate[0] in symbolTab:,如果它在符号表中,我们在行 13 中读取它的值。

rD = int(symbolTab[predicate[0]][1:])

我们使用键来查询符号表,这个键是谓词的第一个元素,因为在 TC1 汇编语言指令中(例如在 ADD r4,r7,r2 中),目标寄存器总是第一个。寄存器由 predicate[0] 提供。symbolTab[predicate[0]] 表达式查找符号名称并提供其值;例如,考虑 TIME EQU R3INC TIME 汇编语言指令将查找 TIME 并返回 R3。我们现在有了目标操作数,但它是一个字符串,'R3',而不是一个数字。我们只需要 3,并必须使用 int 函数将字符串格式的数字转换为整数值。

让我们简化 Python 表达式,以便更容易解释。假设我们写下以下内容:

destReg = symbolTab[predicate[0]]

destReg 的值是表示目标寄存器的字符串。假设这是 'R3'。我们需要做的是从 'R3' 中隔离 '3',然后将字符 '3' 转换为整数 3。我们可以写 destRegNum = destReg[1:] 来返回字符串中除了初始 'R' 之外的所有字符。最后一步是将它转换为整数,我们可以用 rD = int(destRegNum) 来完成。

记住,[1:] 表示第一个字符 'R' 之后的所有字符。因此,如果寄存器是 'R3',则返回 '3'。我们本来可以写 [1:2] 而不是 [1:],因为数字在 1 到 7 的范围内。然而,通过使用 [1:] 符号,我们可以在不改变程序的情况下,以后增加超过 9 个寄存器的数量。

将这三个步骤结合起来,我们得到rD = int(symbolTab[predicate[0]][1:])

以下 Python 代码显示了整个解码过程:


form = codes.get(opCode)                      # Use opcode to read type of instruction
if form[0] & 0b1000 == 0b1000:                # Bit 4 of format selects destination register rD
    if predicate[0] in symbolTab:             # Check whether first token is sym tab
          rD = int(symbolTab[predicate[0]][1:])  # If it is, then get its value
    else: rD = int(predicate[0][1:])          # If it's not a label, get from the predicate
if form[0] & 0b0100 == 0b0100:                # Bit 3 selects register source register 1, rS1
    if predicate[1] in symbolTab:
          rS1 = int(symbolTab[predicate[1]][1:])
    else: rS1 = int(predicate[1][1:])
if form[0] & 0b0010 == 0b0010:                # Bit 2 of format selects register rS1
    if predicate[2] in symbolTab:
          rS2 = int(symbolTab[predicate[2]][1:])
    else: rS2 = int(predicate[2][1:])
if form[0] & 0b0001 == 0b0001:                # Bit 1 of format indicates a literal
    if predicate[-1] in symbolTab:            # If literal in symbol table, get it
        predicate[-1] = symbolTab[predicate[-1]]
    elif type(predicate[-1]) == 'int':                                # Integer
        literal = str(literal)
    elif predicate[-1][0]    == '%':                                  # Binary
        literal=int(predicate[-1][1:],2) 
    elif predicate[-1][0:2]  == '0B':                                 # Binary 
        literal=int(predicate[-1][2:],2)
    elif predicate[-1][0:1]  == '$':                                  # Hex
        literal=int(predicate[-1][1:],16)
    elif predicate[-1][0:2]  == '0X':                                 # Hex
        literal=int(predicate[-1][2:],16)
    elif predicate[-1].isnumeric():                                   # Decimal
        literal=int(predicate[-1])
    elif predicate[-1][0]    == '-':                                  # Negative
        literal=(-int(predicate[-1][1:]))&0xFFFF
    else:  literal = 0                                                # Default

这段代码块执行相同的操作序列三次,以相同的方式处理rD,然后是rS1(第一个源寄存器),然后是rS2(第二个源寄存器)。这个代码块的最后部分(阴影部分)更复杂,因为我们允许字面量的多种表示。我们使用if…elif结构来测试符号字面量、二进制字面量、十六进制字面量、无符号十进制数值字面量,最后是负十进制数值字面量。

字面量是指令使用的数值常数。然而,在汇编语言中,字面量由一个文本字符串表示;也就是说,如果字面量是12,它就是字符串'12'而不是数值12。它必须通过int()函数转换成数值形式。

我们最初决定允许十进制、二进制或十六进制整数。后来,我们包括了符号名称,因为它们使用 Python 的字典处理起来非常容易。假设我们有一个指令,它已经被分解成一个助记符和一个包含寄存器和字面量或符号名称的谓词,例如,['R1', 'R2', 'myData']。考虑以下代码:


if predicate[-1] in symbolTab:                 # If literal is in symbol table, look up value
   predicate[-1] = symbolTab[predicate[-1]]    # Get its value from the symbol table

这取谓词的最后一个元素(由[-1]索引指示)并查看它是否在符号表中。如果没有,代码将测试其他类型的字面量。如果在符号表中,它将被提取出来,并且myData符号名称将被其实际值替换。

表中的字面量可能是一个整数或字符串。以下代码将其转换为字符串(如果它是字面量的话):


if type(predicate[-1])=='int': literal=str(literal) # Integer to string

if结构使用type()函数,该函数返回对象的类型。在这种情况下,如果对象是整数,它将是'int'str()函数将整数对象转换为字符串对象。

这个操作看起来可能很奇怪,因为我们正在将一个整数(我们想要的)转换成一个字符串(我们不想要的)。这种异常的原因是我们稍后将要测试十六进制、二进制和有符号值,这些值将是字符串,并且将所有字面量都作为字符串来处理可以简化编码。

以下代码演示了如何将三种数字格式转换为整数形式,以便将其打包到最终的 32 位 TC1 机器指令中:


if   predicate[-1][0]   == '%':  literal = int(predicate[-1][1:],2)
elif predicate[-1][0:2  == '0B': literal = int(predicate[-1][2:],2)
elif predicate[-1][0:1] == '$':  literal = int(predicate[-1][1:],16)
elif predicate[-1][0:2] == '0X': literal = int(predicate[-1][2:],16)
elif predicate[-1].isnumeric():  literal = int(predicate[-1])

在 TC1 汇编语言中,二进制数以%0b为前缀,十六进制值以$0x为前缀。常数被测试以确定它是十进制、二进制还是十六进制,然后执行适当的转换。将二进制字符串x转换为整数y使用y = int(x,2)完成。粗体参数是数字基数。在这种情况下,它是二进制格式的2。在十六进制格式中,它是16

让我们看看十六进制转换。我们必须进行两个选择:首先是标记,然后是标记的具体字符。考虑ADDL R1,R2,0XF2A4。谓词是'R1 R2 0XF2A4',它被标记为predicate = ['R1', '``R2', '0XF2A4']

predicate[-1]的值是'0XF2A4'。为了测试十六进制值,我们必须查看前两个字符,看它们是否是'0X'。注意0X不是0x,因为 TC1 将输入转换为大写。我们可以编写以下代码:


lastToken = predicate[-1]              # Get the last token from the predicate
prefix = lastToken[0:2]                # Get the first two characters of this token to test for '0X'

我们可以通过合并两个列表索引后缀[-1][0:2]来节省一行,即predicate[-1][0:2]

代码的第三行elif predicate[-1].isnumeric(): literal=int(predicate[-1])检测十进制字符串并将它们转换为数值形式。由于十进制值没有前缀,我们使用isnumeric方法来测试具有数值的字符串。这一行读作,“如果谓词中的最后一个标记是数值,则将其转换为 整数值。”

最后,我们必须处理负数(例如,-5)。如果一个字面量以-开头,则读取剩余的字符串并将其转换为 16 位二进制补码形式。这是必要的,因为 TC1 计算机以 16 位二进制补码形式表示有符号整数。

生成指令的最终 32 位二进制代码很容易。我们有一个操作码和零到四个字段可以插入。字段最初设置为全零(默认值)。然后,每个字段向左移动到指令中所需的位置,并通过位运算OR将其插入到指令中。相应的代码如下:


s2      = s2      << 16                # Shift source 2 16 places left
s1      = s1      << 19                # Shift source 1 19 places left
destReg = destReg << 22                # Shift destination register 22 places left
op      = op      << 25                # Shift opcode 25 places left
binCode = lit | s2 | s1 | destReg | op # Logical OR the fields

我们可以一行完成所有这些,如下所示:


binCode = lit | (s2 << 16) | (s1 << 19) | (destReg << 22)| (op << 25)

在下一章中,我们将回到 TC1 模拟器并对其进行扩展。我们还将展示如何通过向指令集添加新操作和打印模拟器结果的一些方法来扩展 TC1 模拟器。

在展示完整的 TC1 之前,我们将演示一个简化的版本,它可以执行汇编语言程序,本质上与 TC1 相同。然而,这个版本已被设计为通过省略诸如符号名称或指定常量时使用不同数基的能力等特性来降低总复杂性。在这种情况下,所有字面量都是简单的十进制整数。

休息:TC1 之前

为了提供一个更完整的 CPU 模拟器操作概述,我们将引入一个高度简化的但完整的版本,以便在我们创建更复杂的系统之前,给你一个了解事物如何组合的概念。

在本节中,你将学习如何设计一个模拟器,它不包含与完整设计相关的某些复杂性。

这个版本的 TC1,称为 TC1mini,可以执行汇编语言。然而,我们使用固定的汇编级指令格式(输入区分大小写)和固定的字面量格式(不支持十六进制或二进制数字),并且我们不支持标签和符号名称。这种方法有助于防止细节干扰大局。

模拟器

模拟器支持寄存器到寄存器的操作,例如 ADD r1,r2,r3。它的唯一内存访问是基于指针的,即 LDRI r1,[r2]STRI r1,[r2]。它提供递增和递减指令,INC r1INC r2。有两种比较操作:CMPI r1,5CMP r1,r2(前者比较寄存器与字面量,后者比较两个寄存器)。为了保持简单,唯一的状态标志是 z(零),并且这个标志仅用于比较和减法操作。

提供了三种分支指令(无条件分支、零分支和非零分支)。由于这个模拟器不支持符号名称,分支需要一个字面量来指示目标。分支相对于分支指令的当前位置;例如,BRA 3 表示跳转到三个位置之后的指令,而 BRA -2 表示跳转回两个指令之前。

我没有提供基于文件的程序输入机制(即,将源程序作为文本文件读取)。要执行的汇编语言程序作为名为 sFile 的 Python 字符串列表嵌入。你可以轻松修改它或替换代码以输入文件。

指令集被设置为以下形式的字典:


codes = {'STOP':[0], 'LDRL':[3], 'STRL':[7]}

key:value 对使用助记符作为键,并使用一个包含一个项目的列表作为值,该列表表示指令的类。类从 0(没有操作数的助记符)到 7(具有寄存器和寄存器间接操作数的助记符)。我们没有实现 TC1 的 4 位格式代码,该代码用于确定指令所需的参数,因为该信息在类中是隐含的。此外,我们不会将指令汇编成二进制代码。我们以文本形式读取助记符,并直接执行它。

当读取指令时,它首先被标记化以创建一个包含一到四个标记的列表,例如,['CMPL', 'r3', '5']。当从源读取指令时,确定类并用于从标记中提取所需信息。

一旦知道了助记符、寄存器编号/值和字面量,就使用简单的 if .. elif 结构来选择适当的指令,然后执行它。大多数指令都在 Python 的单行中解释。

在指令读取和执行循环结束时,你会被邀请按下一个键来按顺序执行下一个指令。每条指令之后显示的数据是程序计数器、z 位、指令、寄存器和内存位置。我们只使用四个寄存器和八个内存位置。

我们将这个程序分成几个部分,每个部分之间都有简短的描述。第一部分提供源代码作为内置列表。它定义了指令类并提供了一组操作码及其类。我们为此不使用字典。然而,我们为寄存器及其间接版本提供了字典,以简化指令分析。例如,我们可以在LDRI r1,[r2]指令中查找r1r2


sFile = ['LDRL r2,1','LDRL r0,4','NOP','STRI r0,[r2]','LDRI r3,[r2]',   \
         'INC r3','ADDL r3,r3,2','NOP','DEC r3', 'BNE -2','DEC r3','STOP']
                                            # Source program for testing
# Simple CPU instruction interpreter. Direct instruction interpretation. 30 September 2022\. V1.0
# Class 0: no operand                   NOP
# Class 1: literal                      BEQ  3
# Class 2: register                     INC  r1
# Class 3: register,literal             LDRL r1,5
# Class 4: register,register,           MOV  r1,r2
# Class 5: register,register,literal    ADDL r1,r2,5
# Class 6: register,register,register   ADD  r1,r2,r3
# Class 7: register,[register]          LDRI r1,[r2]
codes = {'NOP':[0],'STOP':[0],'BEQ':[1],'BNE':[1],'BRA':[1],  \
         'INC':[2],'DEC':[2],'CMPL':[3],'LDRL':[3],'MOV':[4],  \
         'CMP':[4],'SUBL':[5],'ADDL':[5],'ANDL':[5],'ADD':[6], \
         'SUB':[6], 'AND':[6],'LDRI':[7],'STRI':[7]}
reg1  = {'r0':0,'r1':1,'r2':2,'r3':3}       # Legal registers
reg2  = {'[r0]':0,'[r1]':1,'[r2]':2,'[r3]':3} # Legal pointer registers
r = [0] * 4                                 # Four registers
r[0],r[1],r[2],r[3] = 1,2,3,4               # Preset registers for testing
m  = [0] * 8                                # Eight memory locations
pc = 0                                      # Program counter initialize to 0
go = 1                                      # go is the run control (1 to run)
z  = 0                                      # z is the zero flag. Set/cleared by SUB, DEC, CMP
while go == 1:                              # Repeat execute fetch and execute loop
    thisLine = sFile[pc]                    # Get current instruction
    pc = pc + 1                             # Increment pc
    pcOld = pc                              # Remember pc value for this cycle
    temp = thisLine.replace(',',' ')        # Remove commas: ADD r1,r2,r3 to ADD r1 r2 r3
    tokens = temp.split(' ')                # Tokenize:  ADD r1 r2 r3 to ['ADD','r1','r2','r3']

在下一节中,我们分析一条指令以提取指令所需的操作数值。这是通过查看指令的 op-class 然后提取适当的信息(例如,寄存器号)来实现的:


    mnemonic = tokens[0]                  # Extract first token, the mnemonic
    opClass = codes[mnemonic][0]          # Extract instruction class
                                          # Process the current instruction and analyze it
    rD,rDval,rS1,rS1val,rS2,rS2val,lit, rPnt,rPntV = 0,0,0,0,0,0,0,0,0 
                                          # Clear all parameters
    if opClass in [0]: pass               # If class 0, nothing to be done (simple opcode only)
    if opClass in [2,3,4,5,6,7,8]:        # Look for ops with destination register rD
        rD     = reg1[tokens[1]]          # Get token 1 and use it to get register number as rD
        rDval  = r[rD]                    # Get contents of register rD
    if opClass in [4,5,6]:                # Look at instructions with first source register rS1
        rS1    = reg1[tokens[2]]          # Get rS1 register number and then contents
        rS1val = r[rS1]
    if opClass in [6]:                    # If class 6, it's got three registers. Extract rS2
        rS2    = reg1[tokens[3]]          # Get rS2 and rS2val
        rS2val = r[rS2]
    if opClass in [1,3,5,8]:              # The literal is the last element in instructions
        lit    = int(tokens[-1])          # Get the literal
    if opClass in [7]:                    # Class 7 involves register indirect addressing
        rPnt   = reg2[tokens[2]]          # Get the pointer (register) and value of the pointer
        rPntV  = r[rPnt]                  # Get the register number
    if mnemonic == 'STOP':                # Now execute instructions. If STOP, clear go and exit
        go = 0
        print('Program terminated')

这是程序中的指令执行部分。我们使用一系列比较助记符与操作码,然后直接执行指令。与 TC1 不同,我们不将助记符转换为二进制代码,然后再通过将二进制代码转换为适当的操作来执行它:


    elif mnemonic == 'NOP':  pass         # NOP does nothing. Just drop to end of loop
    elif mnemonic == 'INC': r[rD] = rDval + 1  # Increment: add 1 to destination register
    elif mnemonic == 'DEC':               # Decrement: subtract 1 from register and update z bit
        z = 0
        r[rD] = rDval - 1
        if r[rD] == 0: z = 1
    elif mnemonic == 'BRA':               # Unconditional branch
        pc = pc + lit - 1
    elif mnemonic == 'BEQ':               # Conditional branch on zero
        if z == 1: pc = pc + lit - 1
    elif mnemonic == 'BNE':               # Conditional branch on not zero
        if z == 0: pc = pc + lit - 1
    elif mnemonic == 'ADD': r[rD]=rS1val+rS2val # Add
    elif mnemonic == 'ADDL': r[rD] = rS1val+lit # Add literal
    elif mnemonic == 'SUB':                     # Subtract and set/clear z
        r[rD] = rS1val - rS2val
        z = 0
        if r[rD] == 0: z = 1
    elif mnemonic == 'SUBL':                    # Subtract literal
        r[rD] = rS1val - lit
        z = 0
        if r[rD] == 0: z = 1
    elif mnemonic == 'CMPL':                    # Compare literal
        diff = rDval - lit
        z = 0
        if diff == 0 : z = 1
    elif mnemonic == 'CMP':                     # Compare
        diff = rDval - rS1val
        z = 0
        if diff == 0: z = 1
    elif mnemonic == 'MOV':  r[rD] = rS1val     # Move, load, and store operations
    elif mnemonic == 'LDRL': r[rD] = lit
    elif mnemonic == 'LDRI': r[rD] = m[rPntV]
    elif mnemonic == 'STRI': m[rPntV] = rDval
    regs = ' '.join('%02x' % b for b in r)      # Format memory locations hex
    mem  = ' '.join('%02x' % b for b in m)      # Format registers hex
    print('pc =','{:<3}'.format(pcOld), '{:<14}'.format(thisLine), \
          'Regs =',regs, 'Mem =',mem, 'z =', z)
    x = input('>>> ')               # Request keyboard input before dealing with next instruction

注意,执行循环以从键盘请求输入结束。这样,在按下Enter/Return键之前,下一个循环不会执行。

以下展示了模拟器在执行嵌入式程序时的输出。更改的寄存器、内存位置和标志值以粗体显示:


pc = 1   LDRL r2,1     Regs = 01 02 01 04 Mem = 00 00 00 00 00 00 00 00 z = 0
pc = 2   LDRL r0,4     Regs = 04 02 01 04 Mem = 00 00 00 00 00 00 00 00 z = 0
pc = 3   NOP           Regs = 04 02 01 04 Mem = 00 00 00 00 00 00 00 00 z = 0
pc = 4   STRI r0,[r2]  Regs = 04 02 01 04 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 5   LDRI r3,[r2]  Regs = 04 02 01 04 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 6   INC r3        Regs = 04 02 01 05 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 7   ADDL r3,r3,2  Regs = 04 02 01 07 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 07 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 06 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 10  BNE -2        Regs = 04 02 01 06 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 06 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 05 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 10  BNE -2        Regs = 04 02 01 05 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 05 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 04 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 10  BNE -2        Regs = 04 02 01 04 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 04 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 03 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 10  BNE -2        Regs = 04 02 01 03 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 03 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 02 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 10  BNE -2        Regs = 04 02 01 02 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 02 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 01 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 10  BNE -2        Regs = 04 02 01 01 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 8   NOP           Regs = 04 02 01 01 Mem = 00 04 00 00 00 00 00 00 z = 0
pc = 9   DEC r3        Regs = 04 02 01 00 Mem = 00 04 00 00 00 00 00 00 z = 1
pc = 10  BNE -2        Regs = 04 02 01 00 Mem = 00 04 00 00 00 00 00 00 z = 1
pc = 11  DEC r3        Regs = 04 02 01 -1 Mem = 00 04 00 00 00 00 00 00 z = 0
Program terminated
pc = 12  STOP          Regs = 04 02 01 -1 Mem = 00 04 00 00 00 00 00 00 z = 0

我们现在将查看 TC1 模拟器的程序。在提供代码之前,我们将简要介绍其一些功能。

TC1 模拟器程序

在本节中,我们提供了 TC1 汇编器和模拟器的完整代码。这将使您能够构建和修改一个可以执行 TC1 支持的代码或您自己的指令集(如果您修改了 TC1)的计算机汇编器和模拟器。

汇编器是更复杂的一部分,因为它涉及到读取文本、分析它并将其格式化为二进制代码。模拟器本身只是读取每个二进制代码然后执行相应的操作。

模拟器包括我们在前面的章节中尚未介绍的功能(例如,调试和跟踪功能)。在本书的第一稿中,TC1 的功能相对更基础,具有最小子集的功能。随着书籍的编辑和程序的修改,功能集得到了增强,使其成为一个更实用的工具。我们首先简要介绍一些这些功能,以帮助理解程序。

单步执行

计算机按顺序执行指令,除非遇到分支或子程序调用。在测试模拟器时,您经常希望一起执行一批指令(即,不打印寄存器值),或者您可能希望在每条指令执行后按Enter/Return键逐条执行指令,或者执行指令直到您按下特定的指令。

在 TC1 的这个版本中,你可以执行并显示一条指令,跳过显示接下来的 n 条指令,或者直到遇到改变流程的指令才显示指令。程序加载后,会显示输入提示。如果你输入回车,模拟器将执行下一条指令并等待。如果你输入一个整数(然后回车),模拟器将执行指定数量的指令而不显示结果。如果你输入 b 然后回车,模拟器将执行指令而不显示它们,直到遇到下一个分支指令。

考虑以下示例。代码只是一组用于演示的随机指令。我使用了无操作(nop)作为填充。我还测试了字面地址格式(十六进制和二进制),并展示了不区分大小写:


@ test trace modes
    nop
    nop
    inc r1
    NOP
    dec r2
    ldrl r6,0b10101010
    bra abc
    nop
    inc R7
    nop
abc ldrl r3,$ABCD
    nop
    inc r3
    INC r4
    nop
    nop
    inc r5
    END!

我已经编辑了它,移除了未访问的内存位置。在提示符>>>之后,你选择要执行的操作:跟踪一条指令,执行n条指令而不停止或显示寄存器,或者执行代码到下一个分支指令而不显示它。在每种情况下,以下输出中都会突出显示程序计数器的值。粗体的文本是我留在当前行操作上的注释(跟踪表示按下了Return/Enter键,这将执行下一条指令):


  >>>  trace
0      NOP           PC= 0 z=0 n=0 c=0 R 0000 0000 0000 0000 0000 0000 0000 0000
>>>3 jump 3 instructions (silent trace)
4      DEC R2        PC= 4 z=0 n=1 c=1 R 0000 0001 ffff 0000 0000 0000 0000 0000
>>>b jump to branch (silent mode up to next branch/rts/jsr)
6      BRA ABC       PC= 6 z=0 n=1 c=1 R 0000 0001 ffff 0000 0000 0000 00aa 0000
>>>  trace Here's the sample run
10 ABC LDRL R3 $ABCD PC=10 z=0 n=1 c=1 R 0000 0001 ffff abcd 0000 0000 00aa 0000
>>>  trace
11     NOP           PC=11 z=0 n=1 c=1 R 0000 0001 ffff abcd 0000 0000 00aa 0000
>>>4 jump 4
16     INC R5        PC=16 z=0 n=0 c=1 R 0000 0001 ffff abce 0001 0001 00aa 0000
>>>  trace
17     END!          PC=17 z=0 n=0 c=1 R 0000 0001 ffff abce 0001 0001 00aa 0000

文件输入

当我们最初开始编写模拟器时,我们通过逐个输入指令的简单方式输入测试程序。这对于最简单的测试是有效的,但很快变得繁琐。后来,程序以文本文件的形式输入。当文件名较短时,例如t.txt,效果很好,但随着文件名的变长(例如,当我在特定目录中存储源代码时),它变得更加繁琐。

然后,我们将文件名包含在实际的 TC1 程序中。当你需要反复运行相同的程序来测试模拟器的各种功能时,这很方便。我们需要的是一种方法,在大多数时候使用我的工作程序(嵌入在模拟器中),但在需要时切换到替代程序。

一个合理的解决方案是生成一个输入提示,提示你按Enter键选择默认文件,或者为替代源程序提供文件名,例如,按 Enter 键选择默认文件输入文件名以选择替代源程序。我们决定使用 Python 的异常机制来实现这一点。在计算机科学中,异常(也称为软件中断)是一种设计用来处理意外事件的机制。在 Python 中,异常处理器使用两个保留字:tryexception

如其名所示,try 会让 Python 执行其后的代码块,而 exception 是在 try 块失败时执行的代码块。本质上,它意味着“如果你不能这样做,就做那件事。”iftry 的区别在于,if 返回 TrueFalse,并在 True 时执行指定的操作,而 try 则尝试运行一个代码块,如果失败(即崩溃),则会调用异常。

try 允许你尝试打开一个文件,如果文件不存在(即避免致命错误),则提供一种退出方式。考虑以下情况:


myProg = 'testException1.txt'                      # Name of default program
try:                                               # Check whether this file exists
    with open(myProg,'r') as prgN:                 # If it's there, open it and read it
        myFile = prgN.readlines()
except:                                            # Call exception if file not there
    altProg = input('Enter source file name: ')    # Request a filename
    with open(altProg,'r') as prgN:                # Open the user file
        myFile = prgN.readlines()
print('File loaded: ', myFile)

这段代码寻找一个名为 testException1.txt 的文件。如果存在(如本例所示),模拟器会运行它,我们得到以下输出:


>>> %Run testTry.py
File loaded:  ['   @ Test exception file\n', ' nop\n', ' nop\n', ' inc\n', ' end!']

在下一个案例中,我们已删除 testException1.txt。现在在提示符后得到以下输出:


>>> %Run testTry.py
Enter source file name: testException2.txt
File loaded:  ['   @ Test exception file TWO\n', ' dec r1\n', ' nop\n', ' inc r2\n', ' end!']

粗体显示的行是备选的文件名。

在 TC1 程序中,我进一步简化了事情,通过在异常中包含文件目录(因为我总是使用相同的目录)并包含文件扩展名 .txt。它看起来如下:


prgN = 'E://ArchitectureWithPython//' + prgN + '.txt'

这个表达式自动提供文件名和文件类型的路径。

记住,Python 允许你使用 + 操作符来连接字符串。

TC1 程序

程序的第一部分提供了指令列表及其编码。这段文本放置在两个 ''' 标记之间,表示它不是程序的一部分。这样可以避免每行都从 # 开始。这种三引号称为文档字符串注释。

TC1 的第一部分是指令列表。这些是为了使程序更容易理解而提供的:


### TC1 computer simulator and assembler. Version of 11 September 2022
''' This is the table of instructions for reference and is not part of the program code
00 00000  stop operation            STOP             00 00000 000 000 000 0  0000
00 00001  no operation              NOP              00 00001 000 000 000 0  0000
00 00010  get character from keyboard  GET  r0         00 00010 rrr 000 000 0  1000
00 00011  get character from keyboard  RND  r0         00 00011 rrr 000 000 L  1001
00 00100  swap bytes in register        SWAP r0         00 00100 rrr 000 000 0  1000
00 01000  print hex value in register     PRT r0          00 01000 rrr 000 000 0  1000
00 11111  terminate program         END!            00 11111 000 000 000 0  0000
01 00000  load register from register     MOVE r0,r1      01 00000 rrr aaa 000 0  1100
01 00001  load register from memory   LDRM r0,L       01 00001 rrr 000 000 L  1001
01 00010  load register with literal       LDRL r0,L       01 00010 rrr 000 000 L  1001
01 00011  load register indirect        LDRI r0,[r1,L]  01 00011 rrr aaa 000 L  1101
01 00100  store register in memory      STRM r0,L       01 00100 rrr 000 000 L  1001
01 00101  store register indirect       STRI r0,[r1,L]  01 00101 rrr aaa 000 L  1101
10 00000  add register to register      ADD  r0,r1,r2   10 00000 rrr aaa bbb 0  1110
10 00001  add literal to register        ADDL r0,r1,L    10 00001 rrr aaa 000 L  1101
10 00010  subtract register from register SUB  r0,r1,r2   10 00010 rrr aaa bbb 0  1110
10 00011  subtract literal from register    SUBL r0,r1,L    10 00011 rrr aaa 000 L  1101
10 00100  multiply register by register   MUL  r0,r1,r2   10 00100 rrr aaa bbb 0  1110
10 00101  multiply literal by register     MULL r0,r1,L    10 00101 rrr aaa 000 L  1101
10 00110  divide register by register     DIV  r0,r1,r2   10 00110 rrr aaa bbb 0  1110
10 00111  divide register by literal       DIVL r0,r1,L    10 00111 rrr aaa 000 L  1101
10 01000  mod register by register      MOD  r0,r1,r2   10 01000 rrr aaa bbb 0  1110
10 01001  mod register by literal        MODL r0,r1,L    10 01001 rrr aaa 000 L  1101
10 01010  AND register to register      AND  r0,r1,r2   10 01000 rrr aaa bbb 0  1110
10 01011  AND register to literal         ANDL r0,r1,L    10 01001 rrr aaa 000 L  1101
10 01100  OR register to register        OR   r0,r1,r2   10 01010 rrr aaa bbb 0  1110
10 01101  NOR register to literal        ORL  r0,r1,L    10 01011 rrr aaa 000 L  1101
10 01110  EOR register to register       OR   r0,r1,r2   10 01010 rrr aaa bbb 0  1110
10 01111  EOR register to literal       ORL  r0,r1,L    10 01011 rrr aaa 000 L  1101
10 10000  NOT register              NOT  r0         10 10000 rrr 000 000 0  1000
10 10010  increment register          INC  r0          10 10010 rrr 000 000 0  1000
10 10011  decrement register         DEC  r0         10 10011 rrr 000 000 0  1000
10 10100  compare register with register CMP  r0,r1      10 10100 rrr aaa 000 0  1100
10 10101  compare register with literal   CMPL r0,L       10 10101 rrr 000 000 L  1001
10 10110  add with carry            ADC              10 10110 rrr aaa bbb 0  1110
10 10111  subtract with borrow        SBC             10 10111 rrr aaa bbb 0  1110
10 11000  logical shift left           LSL  r0,L       10 10000 rrr 000 000 0  1001
10 11001  logical shift left literal       LSLL r0,L       10 10000 rrr 000 000 L  1001
10 11010  logical shift right          LSR  r0,L       10 10001 rrr 000 000 0  1001
10 11011  logical shift right literal      LSRL r0,L       10 10001 rrr 000 000 L  1001
10 11100  rotate left               ROL  r0,L        10 10010 rrr 000 000 0  1001
10 11101  rotate left literal           ROLL r0,L        10 10010 rrr 000 000 L  1001
10 11110  rotate right              ROR  r0,L        10 10010 rrr 000 000 0  1001
10 11111  rotate right literal          RORL r0,L        10 10010 rrr 000 000 L  1001
11 00000  branch unconditionally       BRA  L           11 00000 000 000 000 L  0001
11 00001  branch on zero           BEQ  L           11 00001 000 000 000 L  0001
11 00010  branch on not zero         BNE  L           11 00010 000 000 000 L  0001
11 00011  branch on minus           BMI  L           11 00011 000 000 000 L  0001
11 00100  branch to subroutine       BSR  L           11 00100 000 000 000 L  0001
11 00101  return from subroutine       RTS              11 00101 000 000 000 0  0000
11 00110  decrement & branch on not zero DBNE r0,L       11 00110 rrr 000 000 L  1001
11 00111  decrement & branch on zero  DBEQ r0,L        11 00111 rrr 000 000 L  1001
11 01000  push register on stack       PUSH r0           11 01000 rrr 000 000 0  1000
11 01001  pull register off stack       PULL r0           11 01001 rrr 000 000 0  1000
'''
import random                       # Get library for random number generator
def alu(fun,a,b):                   # Alu defines operation and a and b are inputs
   global c,n,z                     # Status flags are global and are set up here
    if   fun == 'ADD': s = a + b
    elif fun == 'SUB': s = a - b
    elif fun == 'MUL': s = a * b
    elif fun == 'DIV': s = a // b   # Floor division returns an integer result
    elif fun == 'MOD': s = a % b    # Modulus operation gives remainder: 12 % 5 = 2
    elif fun == 'AND': s = a & b    # Logic functions
    elif fun == 'OR':  s = a | b
    elif fun == 'EOR': s = a & b
    elif fun == 'NOT': s = ~a
    elif fun == 'ADC': s = a + b + c # Add with carry
    elif fun == 'SBC': s = a - b – c # Subtract with borrow
    c,n,z = 0,0,0                    # Clear flags before recalculating them
    if s & 0xFFFF == 0: z = 1        # Calculate the c, n, and z flags
    if s & 0x8000 != 0: n = 1        # Negative if most sig bit 15 is 1
    if s & 0xFFFF != 0: c = 1        # Carry set if bit 16 is 1
    return (s & 0xFFFF)              # Return the result constrained to 16 bits

由于左移和右移、变长移位、加移位和旋转操作相当复杂,我们提供了一个实现移位的函数。这个函数接受移位类型、方向和移动位数作为输入参数,以及要移动的单词:


def shift(dir,mode,p,q):   # Shifter: performs shifts and rotates. dir = left/right, mode = logical/rotate
    global z,n,c                    # Make flag bits global. Note v-bit not implemented
    if dir == 0:                    # dir = 0 for left shift, 1 for right shift
        for i in range (0,q):       # Perform q left shifts on p
            sign = (0x8000 & p) >> 15            # Sign bit
            p = (p << 1) & 0xFFFF                # Shift p left one place
            if mode == 1:p = (p & 0xFFFE) | sign # For rotate left, add in bit shifted out
    else:                                        # dir = 1 for right shift
        for i in range (0,q):                    # Perform q right shifts
            bitOut = 0x0001 & p                  # Save lsb shifted out
            sign = (0x8000 & p) >> 15            # Get sign-bit for ASR
            p = p >> 1                           # Shift p one place right
            if mode == 1:p = (p&0x7FFF)|(bitOut<<15) # If mode = 1, insert bit rotated out
            if mode == 2:p = (p&0x7FFF)|(sign << 15) # If mode = 2, propagate sign bit
    z,c,n = 0,0,0                                # Clear all flags
    if p == 0:          z = 1                    # Set z if p is zero
    if p & 0x8000 != 0: n = 1                    # Set n-bit if p = 1
    if (dir == 0) and (sign == 1):   c = 1       # Set carry if left shift and sign 1
    if (dir == 1) and (bitOut == 1): c = 1  # Set carry bit if right shift and bit moved out = 1
    return(0xFFFF & p)               # Ensure output is 16 bits wide
def listingP():                      # Function to perform listing and formatting of source code
    global listing                   # Listing contains the formatted source code
    listing = [0]*128                # Create formatted listing file for display
    if debugLevel > 1: print('Source assembly code listing ')
    for i in range (0,len(sFile)):        # Step through the program
        if sFile[i][0] in codes:          # Is first token in opcodes (no label)?
            i2 =  (' ').join(sFile[i])    # Convert tokens into string for printing
            i1 = ''                       # Dummy string i1 represents missing label
        else:
            i2 = (' ').join(sFile[i][1:]) # If first token not opcode, it's a label
            i1 = sFile[i][0]              # i1 is the label (first token)
        listing[i] = '{:<3}'.format(i) + '{:<7}'.format(i1) + \
                     '{:<10}'.format(i2)  # Create listing table entry
        if debugLevel  > 1:               # If debug  = 1, don't print source program
            print('{:<3}'.format(i),'{:<7}'.format(i1),'{:<10}'.format(i2)) \
                                          # print: pc, label, opcode
    return()

这是处理字面量的函数 getLit。它可以处理多种可能的格式中的字面量,包括十进制、二进制、十六进制和符号形式:


def getLit(litV):                                  # Extract a literal
    if  litV[0]    == '#': litV = litV[1:]         # Some systems prefix literal with '#
    if  litV in symbolTab:                         # Look in sym tab and get value if there
        literal = symbolTab[litV]                  # Read the symbol value as a string
        literal = int(literal)                     # Convert string into integer
    elif  litV[0]   == '%': literal = int(litV[1:],2)
                                                   # If first char is %, convert to integer
    elif  litV[0:2] == '0B':literal = int(litV[2:],2)
                                                   # If prefix 0B, convert binary to integer
    elif  litV[0:2] == '0X':literal = int(litV[2:],16)
                                                   # If 0x, convert hex string to integer
    elif  litV[0:1] == '$': literal = int(litV[1:],16)
                                                   # If $, convert hex string to integer
    elif  litV[0]   == '-': literal = (-int(litV[1:]))&0xFFFF 
                                                   # Convert 2's complement to int
    elif  litV.isnumeric():  literal = int(litV)
                                                   # If decimal string, convert to integer
    else:                    literal = 0           # Default value 0 (default value)
    return(literal)

Print 语句可能有点复杂。因此,我们创建了一个 print 函数,用于显示寄存器和内存内容。我们将在本书的其他地方讨论打印格式。我们生成要打印的字符串数据 mm1m2,然后使用适当的格式打印它们:


def printStatus():                             # Display machine status (registers, memory)
    text = '{:<27}'.format(listing[pcOld])     # Format instruction for listing
    m = mem[0:8]                               # Get the first 8 memory locations
    m1 = ' '.join('%04x' % b for b in m)       # Format memory location's hex
    m2 = ' '.join('%04x' % b for b in r)       # Format register's hex
    print(text, 'PC =', '{:>2}'.format(pcOld) , 'z =',z,'n =',n,'c =',c, m1,\
    'Registers ', m2)
    if debugLevel == 5:
        print('Stack =', ' '.join('%04x' % b for b in stack), \
        'Stack pointer =', sp)
    return()
print('TC1 CPU simulator 11 September 2022 ')  # Print the opening banner
debugLevel = input('Input debug level 1 - 5: ') # Ask for debugging level
if debugLevel.isnumeric():                     # If debug level is an integer, get it
    debugLevel = int(debugLevel)               # Convert text to integer
else: debugLevel = 1                           # Else, set default value to level 1
if debugLevel not in range (1,6): debugLevel = 1 # Ensure range 1 to 5
print()                                        # New line

上述代码块提供了调试功能,旨在演示调试的概念,并提供在汇编阶段显示中间信息以检查汇编过程的功能。在程序开始时,从键盘读取一个变量 debugLevel。这决定了调试功能级别,从 1(无)到 5(最大)。调试信息可以包括源代码、解码操作和其他参数:


global c,n,z                                   # Processor flags (global variables)
symbolTab = {'START':0}             # Create symbol table for labels + equates with dummy entry
c,n,z = 0,0,0                                  # Initialize flags: carry, negative, zero
sFile = ['']* 128                              # sFile holds the source text
memP  = [0] * 128                              # Create program memory of 128 locations
mem   = [0] * 128                              # Create data memory of 128 locations
stack = [0] * 16                               # Create a stack for return addresses
# codes is a dictionary of instructions {'mnemonic':(x.y)} where x is the instruction operand format, and y the opcode
codes = {                                                            \
        'STOP':(0,0),  'NOP' :(0,1),  'GET' :(8,2),  'RND' : (9,3),  \
        'SWAP':(8,4),  'SEC' :(0,5),  'PRT' :(8,8),  'END!':(0,31),  \
        'MOVE':(12,32),'LDRM':(9,33), 'LDRL':(9,34), 'LDRI':(13,35), \
        'STRM':(9,36), 'STRI':(13,37),'ADD' :(14,64),'ADDL':(13,65), \
        'SUB' :(14,66),'SUBL':(13,67),'MUL' :(14,68),'MULL':(13,69), \
        'DIV' :(14,70),'DIVL':(13,71),'MOD' :(14,72),'MODL':(13,73), \
        'AND' :(14,74),'ANDL':(13,75),'OR'  :(14,76),'ORL' :(13,77), \
        'EOR' :(14,78),'EORL':(13,79),'NOT' :(8,80), 'INC' :(8,82),  \
        'DEC' :(8,83), 'CMP' :(12,84),'CMPL':(9,85), 'LSL' :(12,88), \
        'LSLL':(9,89), 'LSR' :(12,90),'LSRL':(9,91), 'ROL' :(12,92), \
        'ROLL':(9,93), 'ROR' :(12,94),'RORL':(9,95), 'ADC':(14,102), \
        'SBC':(14,103),'BRA' :(1,96), 'BEQ' :(1,97), 'BNE' :(1,98),  \
        'BMI' :(1,99), 'BSR' :(1,100),'RTS' :(0,101),'DBNE':(9,102), \
        'DBEQ':(9,103),'PUSH':(8,104),'PULL':(8,105) }
branchGroup = ['BRA', 'BEQ', 'BNE', 'BSR', 'RTS'] # Operations responsible for flow control

以下一小节负责读取要汇编和执行的源文件。此源代码应采用.txt文件的形式。请注意,此代码使用 Python 的tryexcept机制,该机制能够执行一个动作(在这种情况下,尝试从磁盘加载文件)并在动作失败时执行另一个动作。在这里,我们用它来测试默认文件名,如果该文件不存在,则从终端获取一个文件名:


# Read the input source code text file and format it. This uses a default file and a user file if default is absent
prgN = 'E://ArchitectureWithPython//C_2_test.txt' # prgN = program name: default test file
try:                                              # Check whether this file exists
    with open(prgN,'r') as prgN:                  # If it's there, open it and read it
        prgN = prgN.readlines()
except:                                           # Call exception program if not there
    prgN = input('Enter source file name: ')    # Request a filename (no extension needed)
    prgN = 'E://ArchitectureWithPython//' + prgN + '.txt' # Build filename
    with open(prgN,'r') as prgN:                  # Open user file
        prgN = prgN.readlines()                   # Read it
for i in range (0,len(prgN)):                     # Scan source prgN and copy it to sFile
    sFile[i] = prgN[i]                            # Copy prgN line to sFile line
    if 'END!' in prgN[i]: break                   # If END! found, then stop copying
             # Format source code
sFile = [i.split('@')[0] for i in sFile]          # But first, remove comments     ###
for i in range(0,len(sFile)):                     # Repeat: scan input file line by line
    sFile[i] = sFile[i].strip()                   # Remove leading/trailing spaces and eol
    sFile[i] = sFile[i].replace(',',' ')          # Allow use of commas or spaces
    sFile[i] = sFile[i].replace('[','')           # Remove left bracket
    sFile[i] = sFile[i].replace(']','')       # Remove right bracket and convert [R4] to R4
    while '  ' in sFile[i]:                       # Remove multiple spaces
        sFile[i] = sFile[i].replace('  ', ' ')
sFile = [i.upper() for i in sFile]                # Convert to uppercase
sFile = [i.split(' ') for i in sFile if i != '']  # Split the tokens into list items

这一小节处理等价汇编指令,并使用EQU指令将值绑定到符号名称。这些绑定放置在符号表字典中,等价项从源代码中移除:


                                    # Remove assembler directives from source code
for i in range (0,len(sFile)):      # Deal with equates of the form PQR EQU 25
    if len(sFile[i]) > 2 and sFile[i][1] == 'EQU': # If line is > 2 tokens and second is EQU
        symbolTab[sFile[i][0]] = sFile[i][2]       # Put third token EQU in symbol table
sFile = [i for i in sFile if i.count('EQU') == 0]  # Remove all lines with 'EQU'
                                    # Debug: 1 none, 2 source, 3 symbol tab, 4 Decode i, 5 stack
listingP()                          # List the source code if debug level is 1

在这里,我们执行指令解码;也就是说,我们分析每条指令的文本以提取操作码和参数:


                                    # Look for labels and add to symbol table
for i in range(0,len(sFile)):       # Add branch addresses to symbol table
    if sFile[i][0] not in codes:    # If first token not opcode, then it is a label
        symbolTab.update({sFile[i][0]:str(i)})     # Add it to the symbol table
if debugLevel > 2:                  # Display symbol table if debug level 2
    print('\nEquate and branch table\n')           # Display the symbol table
    for x,y in symbolTab.items(): print('{:<8}'.format(x),y) \
                                             # Step through the symbol table dictionary
    print('\n')
            # Assemble source code in sFile
if debugLevel > 3: print('Decoded instructions')   # If debug level 4/5, print decoded ops
for pcA in range(0,len(sFile)):              # ASSEMBLY: pcA = prog counter in assembly
    opCode, label, literal, predicate = [], [], 0, []   # Initialize variables
                                             # Instruction = label + opcode + predicate
    rD, rS1, rS2  = 0, 0, 0                  # Clear all register-select fields
    thisOp = sFile[pcA]                      # Get current instruction, thisOPp, in text form
                                             # Instruction: label + opcode or opcode
    if thisOp[0] in codes: opCode = thisOp[0]      # If token opcode, then get token
    else:                                    # Otherwise, opcode is second token
        opCode = thisOp[1]                   # Read the second token to get opcode
        label = sFile[i][0]                  # Read the first token to get the label
    if (thisOp[0] in codes) and (len(thisOp) > 1): # If first token opcode, rest is predicate
        predicate = thisOp[1:]               # Now get the predicate
    else:                                    # Get predicate if the line has a label
        if len(thisOp) > 2: predicate = thisOp[2:]
    form = codes.get(opCode)                 # Use opcode to read type (format)
                                             # Now check the bits of the format code
    if form[0] & 0b1000 == 0b1000:           # Bit 4 selects destination register rD
        if predicate[0] in symbolTab:        # Check if first token in symbol table
            rD = int(symbolTab[predicate[0]][1:]) # If it is, then get its value
        else: rD = int(predicate[0][1:])     # If not label, get register from the predicate
    if form[0] & 0b0100 == 0b0100:           # Bit 3 selects source register 1, rS1
        if predicate[1] in symbolTab:
            rS1 = int(symbolTab[predicate[1]][1:])
        else: rS1 = int(predicate[1][1:])
    if form[0] & 0b0010 == 0b0010:           # Bit 2 of format selects register rS1
        if predicate[2] in symbolTab:
            rS2 = int(symbolTab[predicate[2]][1:])
        else: rS2 = int(predicate[2][1:])
    if form[0] & 0b0001 == 0b0001:           # Bit 1 of format selects the literal field
        litV = predicate[-1]
        literal = getLit(litV)

这一节是在 TC1 开发之后添加的。我们引入了调试级别的概念。也就是说,在模拟运行开始时,你可以设置一个范围在13之间的参数,以确定在汇编处理过程中显示多少信息。这允许你在测试程序时获取更多关于指令编码的信息:


    if debugLevel > 3:                       # If debug level > 3, print decoded fields
        t0 = '%02d' % pcA                    # Format instruction counter
        t1 = '{:<23}'.format(' '.join(thisOp)) # Format operation to 23 spaces
        t3 = '%04x' % literal                # Format literal to 4-character hex
        t4 = '{:04b}'.format(form[0])        # Format the 4-bit opcode format field
        print('pc =',t0,'Op =',t1,'literal',t3,'Dest reg =',rD,'rS1 =', \
              'rS1,'rS2 =',rS2,'format =',t4)  # Concatenate fields to create 32-bit opcode
    binCode = form[1]<<25|(rD)<<22|(rS1)<<19|(rS2)<<16|literal # Binary pattern
    memP[pcA] = binCode                      # Store instruction in program memory
                                             # End of the assembly portion of the program

我们即将执行指令。在我们这样做之前,有必要初始化一些与当前操作相关的变量(例如,跟踪):


                                           # The code is executed here
r = [0] * 8                                # Define registers r[0] to r[7]
pc = 0                                     # Set program counter to 0
run = 1                                    # run = 1 during execution
sp = 16                                    # Initialize the stack pointer (BSR/RTS)
goCount = 0                                # goCount executes n operations with no display
traceMode    = 0                           # Set to 1 to execute n instructions without display
skipToBranch = 0                           # Used when turning off tracing until a branch
silent = 0                                 # silent = 1 to turn off single stepping

这是主循环,我们在这里解码指令以提取参数(寄存器编号和字面量):


                                           # Executes instructions when run is 1
while run == 1:                            # Step through instructions: first, decode them!
    binCode = memP[pc]                     # Read binary code of instruction
    pcOld = pc                             # pc in pcOld (for display purposes)
    pc = pc + 1                            # Increment the pc
    binOp = binCode >> 25                  # Extract the 7-bit opcode as binOp
    rD    = (binCode >> 22) & 7            # Extract the destination register, rD
    rS1   = (binCode >> 19) & 7            # Extract source register 1, rS1
    rS2   = (binCode >> 16) & 7            # Extract source register 2, rS2
    lit   = binCode & 0xFFFF               # Extract the 16-bit literal
    op0 = r[rD]                            # Get contents of destination register
    op1 = r[rS1]                           # Get contents of source register 1
    op2 = r[rS2]                           # Get contents of source register 2

在下一节中,我们将从 TC1 的原始版本开始。TC1 的第一个版本将操作码解码为二进制字符串,然后进行查找。然而,由于我们有源文件,直接从助记符文本执行会更简单。这使得代码更容易阅读:


# Instead of using the binary opcode to determine the instruction, I use the text opcode
# It makes the code more readable if I use 'ADD' rather than its opcode
    mnemonic=next(key for key,value in codes.items() if value[1]==binOp
                                           # Get mnemonic from dictionary
### INTERPRET INSTRUCTIONS                    # Examine the opcode and execute it
    if   mnemonic == 'STOP': run = 0       # STOP ends the simulation
    elif mnemonic == 'END!': run = 0       # END! terminates reading source code and stops
    elif mnemonic == 'NOP':  pass          # NOP is a dummy instruction that does nothing
    elif mnemonic == 'GET':                # Reads integer from the keyboard
        printStatus()
        kbd = (input('Type integer '))     # Get input
        kbd = getLit(kbd)                  # Convert string to integer
        r[rD] = kbd                        # Store in register
        continue
    elif mnemonic == 'RND':  r[rD] = random.randint(0,lit)
                                           # Generate random number
    elif mnemonic == 'SWAP': r[rD] = shift(0,1,r[rD],8)
                                           # Swap bytes in a 16-bit word
    elif mnemonic == 'SEC':  c = 1         # Set carry flag
    elif mnemonic == 'LDRL': r[rD] = lit   # LDRL R0,20 loads R0 with literal 20
    elif mnemonic == 'LDRM': r[rD] = mem[lit]
                                           # Load register with memory location (LDRM)
    elif mnemonic == 'LDRI': r[rD] = mem[op1 + lit]
                                                   # LDRI r1,[r2,4] memory location [r2]+4
    elif mnemonic == 'STRM': mem[lit] = r[rD]      # STRM stores register in memory
    elif mnemonic == 'STRI': mem[op1 + lit] = r[rD] # STRI stores rD at location [rS1]+L
    elif mnemonic == 'MOVE': r[rD] = op1           # MOVE copies register rS1 to rD
    elif mnemonic == 'ADD':  r[rD] = alu('ADD',op1, op2)
                                                   # Adds [r2] to [r3] and puts result in r1
    elif mnemonic == 'ADDL': r[rD] = alu('ADD',op1,lit) # Adds 12 to [r2] and puts result in r1
    elif mnemonic == 'SUB':  r[rD] = alu('SUB',op1,op2) #
    elif mnemonic == 'SUBL': r[rD] = alu('SUB',op1,lit)
    elif mnemonic == 'MUL':  r[rD] = alu('MUL',op1,op2)
    elif mnemonic == 'MULL': r[rD] = alu('MUL',op1,lit)
    elif mnemonic == 'DIV':  r[rD] = alu('DIV',op1,op2) # Logical OR
    elif mnemonic == 'DIVL': r[rD] = alu('DIV',op1,lit)
    elif mnemonic == 'MOD':  r[rD] = alu('MOD',op1,op2) # Modulus
    elif mnemonic == 'MODL': r[rD] = alu('MOD',op1,lit)
    elif mnemonic == 'AND':  r[rD] = alu('AND',op1,op2) # Logical AND
    elif mnemonic == 'ANDL': r[rD] = alu('AND',op1,lit)
    elif mnemonic == 'OR':   r[rD] = alu('OR', op1,op2) # Logical OR
    elif mnemonic == 'ORL':  r[rD] = alu('OR', op1,lit)
    elif mnemonic == 'EOR':  r[rD] = alu('EOR',op1,op2) # Exclusive OR
    elif mnemonic == 'EORL': r[rD] = alu('EOR',op1,lit)
    elif mnemonic == 'NOT':  r[rD] = alu('NOT',op0,1)   # NOT r1 uses only one operand
    elif mnemonic == 'INC':  r[rD] = alu('ADD',op0,1)
    elif mnemonic == 'DEC':  r[rD] = alu('SUB',op0,1)
    elif mnemonic == 'CMP':  rr    = alu('SUB',op0,op1) # rr is a dummy variable
    elif mnemonic == 'CMPL': rr    = alu('SUB',op0,lit)
    elif mnemonic == 'ADC':  r[rD] = alu('ADC',op1,op2)
    elif mnemonic == 'SBC':  r[rD] = alu('SBC',op1,op2)
    elif mnemonic == 'LSL':  r[rD] = shift(0,0,op0,op1)
    elif mnemonic == 'LSLL': r[rD] = shift(0,0,op0,lit)
    elif mnemonic == 'LSR':  r[rD] = shift(1,0,op0,op1)
    elif mnemonic == 'LSRL': r[rD] = shift(1,0,op0,lit)
    elif mnemonic == 'ROL':  r[rD] = shift(1,1,op0,op2)
    elif mnemonic == 'ROLL': r[rD] = shift(1,1,op0,lit)
    elif mnemonic == 'ROR':  r[rD] = shift(0,1,op0,op2)
    elif mnemonic == 'RORL': r[rD] = shift(0,1,op0,lit)
    elif mnemonic == 'PRT':  print('Reg',rD,'=', '%04x' % r[rD])
    elif mnemonic == 'BRA':             pc = lit
    elif mnemonic == 'BEQ' and  z == 1: pc = lit
    elif mnemonic == 'BNE' and  z == 0: pc = lit
    elif mnemonic == 'BMI' and  n == 1: pc = lit
    elif mnemonic == 'DBEQ':                     # Decrement register and branch on zero
        r[rD] = r[rD] - 1
        if r[rD] != 0: pc = lit
    elif mnemonic == 'DBNE':            # Decrement register and branch on not zero
        r[rD] = alu('SUB',op0,1)        # Note the use of the alu function
        if z == 0: pc = lit
    elif mnemonic == 'BSR':             # Stack-based operations. Branch to subroutine
        sp = sp - 1                     # Pre-decrement stack pointer
        stack[sp] = pc                  # Push the pc (return address)
        pc = lit                        # Jump to target address
    elif mnemonic == 'RTS':             # Return from subroutine
        pc = stack[sp]                  # Pull pc address of the stack
        sp = sp + 1                     # Increment stack pointer
    elif mnemonic == 'PUSH':            # Push register to stack
        sp = sp - 1                     # Move stack pointer up to make space
        stack[sp] = op0                 # Push register in op on the stack
    elif mnemonic == 'PULL':            # Pull register off the stack
        r[rD] = stack[sp]               # Transfer stack value to register
        sp = sp + 1                     # Move stack down

这一小节执行一个名为跟踪的功能,允许我们在执行代码时列出寄存器的内容或关闭列表:


                                        # Instruction interpretation complete. Deal with display
    if silent == 0:                     # Read keyboard ONLY if not in silent mode
       x = input('>>>')                 # Get keyboard input to continue
       if x == 'b': skipToBranch = 1    # Set flag to execute to branch with no display
       if x.isnumeric():                # Is this a trace mode with a number of steps to skip?
           traceMode = 1                # If so, set traceMode
           goCount   = getLit(x) + 1    # Record the number of lines to skip printing
    if skipToBranch == 1:               # Are we in skip-to-branch mode?
        silent = 1                      # If so, turn off printing status
        if mnemonic in branchGroup:     # Have we reached a branch?
            silent = 0                  # If branch, turn off silent mode and allow tracing
            skipToBranch = 0            # Turn off skip-to-branch mode
    if traceMode == 1:                  # If in silent mode (no display of data)
        silent = 1                      # Set silent flag
        goCount = goCount – 1           # Decrement silent mode count
        if goCount == 0:                # If we've reached zero, turn display on
            traceMode = 0               # Leave trace mode
            silent = 0                  # Set silent flag back to zero (off)
    if silent == 0: printStatus()

现在我们已经解释了 TC1 模拟器,我们将演示其使用方法。

TC1 汇编语言程序的示例

在这里,我们演示了一个 TC1 汇编语言程序。这提供了一种测试模拟器和展示其工作方式的方法。我们希望测试一系列功能,因此我们应该包括循环、条件测试和基于指针的内存访问。我们将编写一个程序来完成以下任务:

  1. 将内存从位置 0 到 4 的区域填充为随机数。

  2. 反转数字的顺序。

由于这个问题使用内存和顺序地址,它涉及到寄存器间接寻址,即LDRISTRI指令。通过以下方式创建随机数并将它们按顺序存储在内存中:


Set a pointer to the first memory location (i.e.,0)
Set a counter to 5 (we are going to access five locations 0 to 4)
Repeat
  Generate a random number
  Store this number at the pointer address
  Point to next number (i.e., add 1 to the pointer)
  Decrement the counter (i.e., counter 5,4,3,2,1,0)
  Until counter = 0

在 TC1 代码中,我们可以将其翻译如下:


        LDRL r0,0             @ Use r0 as a memory pointer and set it to 0
        LDRL r1,5             @ Use r1 as the loop counter
  Loop1 RND  r2               @ Loop: Generate a random number in r2
        STRI r2,[r0],0        @ Store the random number in memory using pointer r0
        INC  r0               @ Point to the next location (add 1 to the pointer)
        DEC  r1               @ Decrement the loop counter (subtract 1 from the counter)
        BNE  Loop1            @ Repeat until 0 (branch back to Loop1 if the last result was not 0)

我们已经用随机值填充了一个内存区域。现在我们需要反转它们的顺序。有许多方法可以反转数字的顺序。一种是将数字从源位置移动到内存中的一个临时位置,然后以相反的顺序写回。当然,这需要额外的内存来存储临时副本。考虑另一种不需要缓冲区的解决方案。我们将在目标地址上方写下源地址:

原始(源)        0   1   2   3   4

交换(目标)    4   3   2   1   0

正如你所见,位置0与位置4交换,然后位置1与位置3交换;接着,在位置2,我们达到了中间点,反转完成。为了执行这个动作,我们需要两个指针,每个字符串的一端一个。我们选择字符串两端的两个字符并交换它们。然后,我们将指针向内移动并执行第二次交换。当指针在中点相遇时,任务完成。请注意,这假设要反转的项目数量是奇数:


Set upper pointer to top
Set lower pointer to bottom
Repeat
   Get value at upper pointer
   Get value at lower pointer
   Swap values and store
Until upper pointer and lower pointer are equal

在 TC1 汇编语言中,这看起来如下:


      LDRL r0,0                @ Lower pointer points at first entry in table
      LDRL r1,4                @ Upper pointer points at last entry in table
Loop2 LDRI r2,[r0],0           @ REPEAT: Get lower value pointed at by r0
      LDRI r3,[r1],0           @ Get upper value pointed at by r1
      MOVE r2,r4               @ Save lower value in r4 temporarily
      STRI r3,[r0],0           @ Store upper value in lower entry position
      STRI r4,[r1],0           @ Store saved lower value in upper entry position
      INC  r0                  @ Increase lower pointer
      DEC  r1                  @ Decrease upper pointer
      CMP  r0,r1               @ Compare pointers
      BNE  Loop2               @ UNTIL all characters moved

以下显示了程序逐条指令执行时的输出。为了简化数据的阅读,我们将寄存器和内存值的变化用粗体表示。分支操作被阴影覆盖。比较指令用斜体表示。

第一个块是在指令执行开始前 TC1 打印的源代码:


TC1 CPU simulator 11 September 2022
Input debug level 1 - 5: 4
Source assembly code listing
0           LDRL R0 0
1           LDRL R1 5
2   LOOP1   RND  R2
3           STRI R2 R0 0
4           INC  R0
5           DEC  R1
6           BNE  LOOP1
7           NOP
8           LDRL R0 0
9           LDRL R1 4
10  LOOP2   LDRI R2 R0 0
11          LDRI R3 R1 0
12          MOVE R4 R2
13          STRI R3 R0 0
14          STRI R4 R1 0
15          INC  R0
16          DEC  R1
17          CMP  R0 R1
18          BNE  LOOP2
19          NOP
20          STOP
21          END!
Equate and branch table
START    0
LOOP1    2
LOOP2    10

第二个代码块显示了汇编器在指令解码时的输出。你可以看到各种寄存器、字面量和格式字段:


Decoded instructions
pc=00 Op =       LDRL R0 0        literal 0000 RD=0 rS1=0 rS2=0 format=1001
pc=01 Op =       LDRL R1 5        literal 0005 RD=1 rS1=0 rS2=0 format=1001
pc=02 Op=  LOOP1 RND R2 0XFFFF    literal ffff RD=2 rS1=0 rS2=0 format=1001
pc=03 Op =       STRI R2 R0 0     literal 0000 RD=2 rS1=0 rS2=0 format=1101
pc=04 Op =       INC  R0          literal 0000 RD=0 rS1=0 rS2=0 format=1000
pc=05 Op =       DEC  R1          literal 0000 RD=1 rS1=0 rS2=0 format=1000
pc=06 Op =       BNE  LOOP1       literal 0002 RD=0 rS1=0 rS2=0 format=0001
pc=07 Op =       NOP              literal 0000 RD=0 rS1=0 rS2=0 format=0000
pc=08 Op =       LDRL R0 0        literal 0000 RD=0 rS1=0 rS2=0 format=1001
pc=09 Op =       LDRL R1 4        literal 0004 RD=1 rS1=0 rS2=0 format=1001
pc=10 Op=  LOOP2 LDRI R2 R0 0     literal 0000 RD=2 rS1=0 rS2=0 format=1101
pc=11 Op =       LDRI R3 R1 0     literal 0000 RD=3 rS1=1 rS2=0 format=1101
pc=12 Op =       MOVE R4 R2       literal 0000 RD=4 rS1=2 rS2=0 format=1100
pc=13 Op =       STRI R3 R0 0     literal 0000 RD=3 rS1=0 rS2=0 format=1101
pc=14 Op =       STRI R4 R1 0     literal 0000 RD=4 rS1=1 rS2=0 format=1101
pc=15 Op =       INC  R0          literal 0000 RD=0 rS1=0 rS2=0 format=1000
pc=16 Op =       DEC  R1          literal 0000 RD=1 rS1=0 rS2=0 format=1000
pc=17 Op =       CMP  R0 R1       literal 0000 RD=0 rS1=1 rS2=0 format=1100
pc=18 Op =       BNE  LOOP2       literal 000a RD=0 rS1=0 rS2=0 format=0001
pc=19 Op =       NOP              literal 0000 RD=0 rS1=0 rS2=0 format=0000
pc=20 Op =       STOP             literal 0000 RD=0 rS1=0 rS2=0 format=0000
pc=21 Op =       END!             literal 0000 RD=0 rS1=0 rS2=0 format=0000

以下提供了使用此程序运行时的输出。我们将跟踪级别设置为4以显示源代码(在文本处理之后)、符号表和解码指令。

然后,我们逐行执行了代码。为了使输出更易于阅读并适应页面,我们移除了不改变的寄存器和内存位置,并突出显示了由于指令的结果而改变的值(内存、寄存器和 z 标志)。你可以跟随这些内容,看看内存/寄存器是如何随着每个指令而改变的。

正如你所见,我们在内存位置04中创建了五个随机数,然后反转它们的顺序。这并不匹配打印状态的输出,因为它已经被修改以适应打印:


0         LDRL R0 0         PC =  0 z = 0 0000 0000 0000 0000 0000
                            R  0000 0000 0000 0000 0000
1         LDRL R1 5         PC =  1 z = 0 0000 0000 0000 0000 0000
                            R  0000 0005 0000 0000 0000
2  LOOP1  RND  R2           PC =  2 z = 0 0000 0000 0000 0000 0000
                            R  0000 0005 9eff 0000 0000
3         STRI R2 R0 0      PC =  3 z = 0 9eff 0000 0000 0000 0000
                            R  0000 0005 9eff 0000 0000
4         INC  R0           PC =  4 z = 0 9eff 0000 0000 0000 0000
                            R  0001 0005 9eff 0000 0000
5         DEC  R1           PC =  5 z = 0 9eff 0000 0000 0000 0000
                            R  0001 0004 9eff 0000 0000
6         BNE  LOOP1        PC =  6 z = 0 9eff 0000 0000 0000 0000
                            R  0001 0004 9eff 0000 0000
2  LOOP1  RND  R2           PC =  2 z = 0 9eff 0000 0000 0000 0000
                            R  0001 0004 6d4a 0000 0000
3         STRI R2 R0 0      PC =  3 z = 0 9eff 6d4a 0000 0000 0000
                            R  0001 0004 6d4a 0000 0000
4         INC  R0           PC =  4 z = 0 9eff 6d4a 0000 0000 0000
                            R  0002 0004 6d4a 0000 0000
5         DEC  R1           PC =  5 z = 0 9eff 6d4a 0000 0000 0000
                            R  0002 0003 6d4a 0000 0000
6         BNE  LOOP1        PC =  6 z = 0 9eff 6d4a 0000 0000 0000
                            R  0002 0003 6d4a 0000 0000
2  LOOP1  RND  R2           PC =  2 z = 0 9eff 6d4a 0000 0000 0000
                            R  0002 0003 a387 0000 0000
3         STRI R2 R0 0      PC =  3 z = 0 9eff 6d4a a387 0000 0000
                            R  0002 0003 a387 0000 0000
4         INC  R0           PC =  4 z = 0 9eff 6d4a a387 0000 0000
                            R  0003 0003 a387 0000 0000
5         DEC  R1           PC =  5 z = 0 9eff 6d4a a387 0000 0000
                            R  0003 0002 a387 0000 0000
6         BNE  LOOP1        PC =  6 z = 0 9eff 6d4a a387 0000 0000
                            R  0003 0002 a387 0000 0000
2  LOOP1  RND  R2           PC =  2 z = 0 9eff 6d4a a387 0000 0000
                            R  0003 0002 2937 0000 0000
3         STRI R2 R0 0      PC =  3 z = 0 9eff 6d4a a387 2937 0000
                            R  0003 0002 2937 0000 0000
4         INC  R0           PC =  4 z = 0 9eff 6d4a a387 2937 0000
                            R  0004 0002 2937 0000 0000
5         DEC  R1           PC =  5 z = 0 9eff 6d4a a387 2937 0000
                            R  0004 0001 2937 0000 0000
6         BNE  LOOP1        PC =  6 z = 0 9eff 6d4a a387 2937 0000
                            R  0004 0001 2937 0000 0000
2  LOOP1  RND  R2           PC =  2 z = 0 9eff 6d4a a387 2937 0000
                            R  0004 0001 db95 0000 0000
3         STRI R2 R0 0      PC =  3 z = 0 9eff 6d4a a387 2937 db95
                            R  0004 0001 db95 0000 0000
4         INC  R0           PC =  4 z = 0 9eff 6d4a a387 2937 db95
                            R  0005 0001 db95 0000 0000
5         DEC  R1           PC =  5 z = 1 9eff 6d4a a387 2937 db95
                            R  0005 0000 db95 0000 0000
6         BNE LOOP1         PC =  6 z = 1 9eff 6d4a a387 2937 db95
                            R  0005 0000 db95 0000 0000
7         NOP               PC =  7 z = 1 9eff 6d4a a387 2937 db95
                            R  0005 0000 db95 0000 0000
8         LDRL R0 0         PC =  8 z = 1 9eff 6d4a a387 2937 db95
                            R  0000 0000 db95 0000 0000
9         LDRL R1 4         PC =  9 z = 1 9eff 6d4a a387 2937 db95
                            R  0000 0004 db95 0000 0000
10 LOOP2  LDRI R2 R0 0      PC = 10 z = 1 9eff 6d4a a387 2937 db95
                            R  0000 0004 9eff 0000 0000
11        LDRI R3 R1 0      PC = 11 z = 1 9eff 6d4a a387 2937 db95
                            R  0000 0004 9eff db95 0000
12        MOVE R4 R2        PC = 12 z = 1 9eff 6d4a a387 2937 db95
                            R  0000 0004 9eff db95 9eff
13        STRI R3 R0 0      PC = 13 z = 1 db95 6d4a a387 2937 db95
                            R  0000 0004 9eff db95 9eff
14        STRI R4 R1 0      PC = 14 z = 1 db95 6d4a a387 2937 9eff
                            R  0000 0004 9eff db95 9eff
15        INC  R0           PC = 15 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0004 9eff db95 9eff
16        DEC  R1           PC = 16 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0003 9eff db95 9eff
17        CMP  R0 R1        PC = 17 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0003 9eff db95 9eff
18        BNE  LOOP2        PC = 18 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0003 9eff db95 9eff
10 LOOP2  LDRI R2 R0 0      PC = 10 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0003 6d4a db95 9eff
11        LDRI R3 R1 0      PC = 11 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0003 6d4a 2937 9eff
12        MOVE R4 R2        PC = 12 z = 0 db95 6d4a a387 2937 9eff
                            R  0001 0003 6d4a 2937 6d4a
13        STRI R3 R0 0      PC = 13 z = 0 db95 2937 a387 2937 9eff
                            R  0001 0003 6d4a 2937 6d4a
14        STRI R4 R1 0      PC = 14 z = 0 db95 2937 a387 6d4a 9eff
                            R  0001 0003 6d4a 2937 6d4a
15        INC  R0           PC = 15 z = 0 db95 2937 a387 6d4a 9eff
                            R  0002 0003 6d4a 2937 6d4a
16        DEC  R1           PC = 16 z = 0 db95 2937 a387 6d4a 9eff
                            R  0002 0002 6d4a 2937 6d4a
17        CMP  R0 R1        PC = 17 z = 1 db95 2937 a387 6d4a 9eff
                            R  0002 0002 6d4a 2937 6d4a
18        BNE  LOOP2        PC = 18 z = 1 db95 2937 a387 6d4a 9eff
                            R  0002 0002 6d4a 2937 6d4a
19        NOP               PC = 19 z = 1 db95 2937 a387 6d4a 9eff
                            R  0002 0002 6d4a 2937 6d4a
20        STOP              PC = 20 z = 1 db95 2937 a387 6d4a 9eff
                            R  0002 0002 6d4a 2937 6d4a

在下一节中,我们将展示如何测试 TC1 的操作。我们将涵盖以下内容:

  • 测试汇编器(例如,使用代码自由格式的能力)

  • 测试流程控制指令(分支)

  • 测试移位操作

测试汇编器

由于 TC1 汇编器可以处理多种排版特性(例如,大写或小写和多个空格),测试汇编器的一个简单方法是为它提供一个包含各种条件的文件来汇编,例如多个空格、等价和大小写转换。我的初始测试源代码如下:


     NOP
 BRA eee
      INC r4
alan inc r5
eee    STOP
aa NOP @comment2
bb NOP     1
      LDRL      r0,   12
      LDRL r3,0x123 @ comment1
      LDRL r7,     0xFF
      INC R2
  BRA last
test1     EQU    999
  @comment3
@comment4
  @ qqq EQU 7
www STRI r1,r2,1
abc   equ 25
qwerty  equ   888
last LDRL r5,0xFAAF
  beQ Aa
      STOP 2

这段代码并不十分优雅;它只是随机测试代码。在下面的代码中,我们提供了汇编器在调试模式下的输出。这包括代码的格式化(删除空白行并将小写转换为大写)。第一个列表提供了指令作为令牌列表的数组:


TC1 CPU simulator 11 September 2022
Input debug level 1 - 5: 4
Source assembly code listing
0           NOP
1           BRA EEE
2           INC R4
3   ALAN    INC R5
4   EEE     STOP
5   AA      NOP
6   BB      NOP 1
7           LDRL R0 12
8           LDRL R3 0X123
9           LDRL R7 0XFF
10          INC R2
11          BRA LAST
12  WWW     STRI R1 R2 1
13  LAST    LDRL R5 0XFAAF
14          BEQ AA
15          STOP 2

第二个列表是将符号名称和标签与整数值关联的符号表:


Equate and branch table
START    0
TEST1    999
ABC      25
QWERTY   888
ALAN     3
EEE      4
AA       5
BB       6
WWW      12
LAST     13
LOOP1    18
LOOP2    26

下一个列表主要用于调试,当指令未按预期行为时。它让您确定指令是否被正确解码:


Decoded instructions
pc=0  op=NOP                  literal 000 Dest reg=0 rS1-0 rS2=0 format=0000
pc=00 Op=NOP                  literal 0000 Dest reg=0 rS1=0 rS2=0 format=0000
pc=01 Op=BRA EEE              literal 0004 Dest reg=0 rS1=0 rS2=0 format=0001
pc=02 Op=INC R4               literal 0000 Dest reg=4 rS1=0 rS2=0 format=1000
pc=03 Op=ALAN INC R5          literal 0000 Dest reg=5 rS1=0 rS2=0 format=1000
pc=04 Op=EEE STOP             literal 0000 Dest reg=0 rS1=0 rS2=0 format=0000
pc=05 Op=AA NOP               literal 0000 Dest reg=0 rS1=0 rS2=0 format=0000
pc=06 Op=BB NOP 1             literal 0000 Dest reg=0 rS1=0 rS2=0 format=0000
pc=07 Op=LDRL R0 12           literal 000c Dest reg=0 rS1=0 rS2=0 format=1001
pc=08 Op=LDRL R3 0X123        literal 0123 Dest reg=3 rS1=0 rS2=0 format=1001
pc=09 Op=LDRL R7 0XFF         literal 00ff Dest reg=7 rS1=0 rS2=0 format=1001
pc=10 Op=INC R2               literal 0000 Dest reg=2 rS1=0 rS2=0 format=1000
pc=11 Op=BRA LAST             literal 000d Dest reg=0 rS1=0 rS2=0 format=0001
pc=12 Op=WWW STRI R1 R2 1     literal 0001 Dest reg=1 rS1=2 rS2=0 format=1101
pc=13 Op=LAST LDRL R5 0XFAAF   literal faaf Dest reg=5 rS1=0 rS2=0 format=1001
pc=14 Op=BEQ AA               literal 0005 Dest reg=0 rS1=0 rS2=0 format=0001
pc=15 Op=STOP 2               literal 0000 Dest reg=0 rS1=0 rS2=0 format=0000
>>>

测试流程控制操作

在这里,我们演示如何测试计算机最重要的操作类别之一,即流程控制指令,即条件分支。

需要测试的最重要的一类指令是改变控制流程的指令:分支和子程序调用指令。下面的代码片段也是无意义的(它仅用于测试指令执行)且仅设计用于测试循环。一个循环使用非零操作的分支构建,另一个使用通过递减寄存器并分支直到寄存器递减到零的自动循环机制。DBNE r0,loop,其中r0是被递减的计数器,loop是分支目标地址。

我们首先提供源列表和符号表:


>>> %Run TC1_FinalForBook_V1.2_20220911.py
TC1 CPU simulator 11 September 2022
Input debug level 1 - 5: 4
Source assembly code listing
0           NOP
1           BRA LAB1
2           INC R0
3   LAB1    INC R2
4           NOP
5           BRA LAB6
6           NOP
7   LAB2    LDRL R2 3
8   LAB4    DEC R2
9           NOP
10          BNE LAB4
11          NOP
12          BSR LAB7
13          NOP
14          LDRL R3 4
15  LAB5    NOP
16          INC R7
17          DBNE R3 LAB5
18          NOP
19          STOP
20  LAB6    BRA LAB2
21          NOP
22  LAB7    DEC R7
23          DEC R7
24          RTS
25          END!
Equate and branch table
START    0
LAB1     3
LAB2     7
LAB4     8
LAB5     15
LAB6     20
LAB7     22

以下是在调试会话后的输出。如您所见,分支序列被忠实实现。请注意,我们已经突出显示了分支动作和后果(即,下一个指令):


0         NOP               PC= 0 z=0 n=0 c=0 
                            R  0000 0000 0000 0000 0000 0000 0000 0000
1         BRA LAB1          PC= 1 z=0 n=0 c=0
                            R  0000 0000 0000 0000 0000 0000 0000 0000
3  LAB1   INC R2            PC= 3 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
4         NOP               PC= 4 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
5         BRA LAB6          PC= 5 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
20 LAB6   BRA LAB2          PC=20 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
7  LAB2   LDRL R2 3         PC= 7 z=0 n=0 c=1 
                            R  0000 0000 0003 0000 0000 0000 0000 0000
8  LAB4   DEC R2            PC= 8 z=0 n=0 c=1 
                            R  0000 0000 0002 0000 0000 0000 0000 0000
9         NOP               PC= 9 z=0 n=0 c=1 
                            R  0000 0000 0002 0000 0000 0000 0000 0000
10        BNE LAB4          PC=10 z=0 n=0 c=1 
                            R  0000 0000 0002 0000 0000 0000 0000 0000
8  LAB4   DEC R2            PC= 8 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
9         NOP               PC= 9 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
10        BNE LAB4          PC=10 z=0 n=0 c=1 
                            R  0000 0000 0001 0000 0000 0000 0000 0000
8  LAB4   DEC R2            PC= 8 z=1 n=0 c=0 
                            R  0000 0000 0000 0000 0000 0000 0000 0000
9         NOP               PC= 9 z=1 n=0 c=0 
                            R  0000 0000 0000 0000 0000 0000 0000 0000
10        BNE LAB4          PC=10 z=1 n=0 c=0 
                            R  0000 0000 0000 0000 0000 0000 0000 0000
11        NOP               PC=11 z=1 n=0 c=0 
                            R  0000 0000 0000 0000 0000 0000 0000 0000
12        BSR LAB7          PC=12 z=1 n=0 c=0 
                            R  0000 0000 0000 0000 0000 0000 0000 0000
22 LAB7   DEC R7            PC=22 z=0 n=1 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 ffff
23        DEC R7            PC=23 z=0 n=1 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 fffe
24        RTS               PC=24 z=0 n=1 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 fffe
13        NOP               PC=13 z=0 n=1 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 fffe
14        LDRL R3 4         PC=14 z=0 n=1 c=1 
                            R  0000 0000 0000 0004 0000 0000 0000 fffe
15 LAB5   NOP               PC=15 z=0 n=1 c=1 
                            R  0000 0000 0000 0004 0000 0000 0000 fffe
16        INC R7            PC=16 z=0 n=1 c=1 
                            R  0000 0000 0000 0004 0000 0000 0000 ffff
17        DBNE R3 LAB5      PC=17 z=0 n=1 c=1 
                            R  0000 0000 0000 0003 0000 0000 0000 ffff
15 LAB5   NOP               PC=15 z=0 n=1 c=1 
                            R  0000 0000 0000 0003 0000 0000 0000 ffff
16        INC R7            PC=16 z=1 n=0 c=0 
                            R  0000 0000 0000 0003 0000 0000 0000 0000
17        DBNE R3 LAB5      PC=17 z=1 n=0 c=0 
                            R  0000 0000 0000 0002 0000 0000 0000 0000
15 LAB5   NOP               PC=15 z=1 n=0 c=0 
                            R  0000 0000 0000 0002 0000 0000 0000 0000
16        INC R7            PC=16 z=0 n=0 c=1 
                            R  0000 0000 0000 0002 0000 0000 0000 0001
17        DBNE R3 LAB5      PC=17 z=0 n=0 c=1 
                            R  0000 0000 0000 0001 0000 0000 0000 0001
15 LAB5   NOP               PC=15 z=0 n=0 c=1 
                            R  0000 0000 0000 0001 0000 0000 0000 0001
16        INC R7            PC=16 z=0 n=0 c=1 
                            R  0000 0000 0000 0001 0000 0000 0000 0002
17        DBNE R3 LAB5      PC=17 z=0 n=0 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 0002
18        NOP               PC=18 z=0 n=0 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 0002
19        STOP              PC=19 z=0 n=0 c=1 
                            R  0000 0000 0000 0000 0000 0000 0000 0002

在下一章中,我们将探讨如何增强 TC1 程序以添加错误检查、包含新指令和特殊功能(如可变长度操作数字段)等功能。

测试移位操作

TC1 支持两种移位类型:逻辑旋转。逻辑移位将位向左或向右移动。在一边,空出的位被零替换,在另一边,移出的位被复制到进位标志。在旋转中,从一边移出的位被复制到另一边;也就是说,位串被当作一个环。无论进行多少次移位,都不会丢失任何位。在每次移位时,移到另一边的位也被复制到进位位。

大多数真实计算机还有两种其他移位变体:算术移位,在右移时保留二进制补码数的符号(除以 2 操作),以及带进位的旋转移位,其中从一边移入的位是旧的进位位,移出的位成为新的进位位。本质上,如果寄存器有m位,进位位被包含以创建一个m+1位的字。这个特性用于多精度算术。我们没有在 TC1 中包含这些模式。

除了指定移位类型外,我们还需要指定移位方向(左或右)。大多数计算机允许你指定移位的次数。我们提供了这两种设施,并且移位的次数可以使用寄存器或字面量来指定。在多长度移位中,进位位的当前状态是最后一位移入进位的位。移位操作(附示例)如下:

移位类型 寄存器/字面量 示例
逻辑左移 字面量 LSLL r0,r1,2
逻辑左移 寄存器 LSL r3,r1,r4
逻辑右移 字面量 LSRL r0,r1,2
逻辑右移 寄存器 LSR r3,r1,r2
左旋转 字面量 ROLL r0,r1,2
左旋转 寄存器 ROL r3,r1,r0
右旋转 字面量 RORL r0,r3,2
右旋转 寄存器 ROR r3,r1,r0

表 6.1 – TC1 移位模式

当我们测试这些指令时,我们必须确保移位方向正确,移位的次数正确,末位(那些移出或移入的位)的行为正确,并且标志位被适当地设置。

考虑以下使用 16 位值1000000110000001的一系列移位代码片段:


LDRL r1,%1000000110000001
LSLL r0,r1,1
LSLL r0,r1,2
LSRL r0,r1,1
LSRL r0,r1,1
LDRL r1,%1000000110000001
LDRL r2,1
LDRL r3,2
LSLL r0,r1,r2
LSLL r0,r1,r3
LSRL r0,r1,r2
LSRL r0,r1,r2

以下是从模拟器输出的信息(编辑后仅显示相关信息),显示了在执行前面的代码时寄存器和条件码。寄存器r0的二进制值显示在右侧。这使我们能够通过人工检查来验证操作是否正确执行:


1  LDRL R1 %1000000110000001 z = 0 n = 0 c = 0 
   Regs 0 - 3  0000 8181 0000 0000  R0 =  0000000000000000
2  LSLL R0 R1 1              z = 0 n = 0 c = 1 
   Regs 0 – 3  0302 8181 0000 0000  R0 =  0000001100000010
3  LSLL R0 R1 2              z = 0 n = 0 c = 0 
   Regs 0 - 3  0604 8181 0000 0000  R0 =  0000011000000100
4  LSRL R0 R1 1              z = 0 n = 0 c = 1 
   Regs 0 - 3  40c0 8181 0000 0000  R0 =  0100000011000000
5  LSRL R0 R1 1              z = 0 n = 0 c = 1 
   Regs 0 - 3  40c0 8181 0000 0000  R0 =  0100000011000000
6  LDRL R1 %1000000110000001 z = 0 n = 0 c = 1 
   Regs 0 - 3  40c0 8181 0000 0000  R0 =  0100000011000000
7  LDRL R2 1                 z = 0 n = 0 c = 1 
   Regs 0 - 3  40c0 8181 0001 0000  R0 =  0100000011000000
8  LDRL R3 2                 z = 0 n = 0 c = 1 
   Regs 0 - 3  40c0 8181 0001 0002  R0 =  0100000011000000
9  LSL  R0 R1 R2             z = 0 n = 0 c = 1 
   Regs 0 - 3  0302 8181 0001 0002  R0 =  0000001100000010
10 LSL  R0 R1 R3             z = 0 n = 0 c = 0 
   Regs 0 - 3  0604 8181 0001 0002  R0 =  0000011000000100
11 LSR  R0 R1 R2             z = 0 n = 0 c = 1 
   Regs 0 - 3  40c0 8181 0001 0002  R0 =  0100000011000000
12 LSR  R0 R1 R2             z = 0 n = 0 c = 1 
   Regs 0 – 3  40c0 8181 0001 0002  R0 =  0100000011000000

注意,加载操作不会影响 z 位。一些计算机几乎在每次操作后都会更新 z 位。一些计算机在需要时更新 z 位(例如,我们稍后将要介绍的 ARM),而一些计算机仅在特定操作后更新它。

本章的最后一部分涵盖了向 TC1 添加后缀的内容,我们提供了一个更简单的示例,它执行相同的基本功能,但以不同的方式执行某些操作,例如指令解码。这样做的目的是为了说明构建模拟器有许多方法。

TC1 后缀

这里展示的 TC1 版本是在本书的开发过程中逐渐形成的。当前版本比原型具有更多功能;例如,最初它不包括符号分支地址,并要求用户输入实际的行号。

在这里,我们展示的是 TC1 的一个简化版本,称为 TC1mini,我们在其中做一些不同的事情;例如,不允许自由格式(助记符必须为大写,寄存器为小写,并且不能使用空格和逗号作为可互换的分隔符)。在这个版本中,一个简单的函数检查助记符是否有效,如果无效则终止程序。同样,我们增加了一个功能,检查由指针生成的地址是否在内存空间范围内。下一节提供了一些关于这个版本的注释。

classDecode 函数

TC1 将每个指令与一个 4 位二进制值关联起来,以指示当前指令需要参数;例如,1101表示寄存器rDrS1和一个字面量。TC1mini 版本将一个类号与范围07中的每个指令关联起来,以描述其类型。类别从0(无参数的助记符)到7(具有间接地址的助记符,如LDRI r2,[r4])。与 TC1 不同,TC1mini 汇编语言中的[]括号不是可选的。

两个模拟器 TC1 和 TC1mini 之间的区别在于 4 位二进制代码提供了预解码;也就是说,模拟器不需要计算指令所需的参数,因为代码直接告诉你。如果你使用类号,你必须解码类号以确定实际所需的参数。然而,类号可以非常创新。TC1mini 使用七种不同的指令格式,并需要定义至少七个类别。如果你有,比如说,14 个类别,每个寻址模式类别可以分成两个子类别,以给你更大的控制权来执行指令过程。

classDecode函数接收一个指令的谓词并返回四个谓词值、目标寄存器、源寄存器1、源寄存器2和字面量。当然,指令可能包含从零到四个这样的值。因此,这些参数最初被设置为哑值,要么是null字符串,要么是零。

在继续之前,请记住 Python 的in操作符对于测试一个元素是否是集合的成员非常有用。例如,如果一个操作在类别 2、4、5 和 9 中,我们可以写出以下代码:


if thisThing in [2, 4, 5, 9]:                         # Test for membership of the set
def classDecode(predicate):
    lit,rD,rS1,rS2 = '',0,0,0                         # The literal is a null string initially
    if opClass in [1]:      lit = predicate           # Class 1 is mnemonic plus a literal
    if opClass in [2]:      rD  = reg1[predicate]     # Class 2 is mnemonic plus a literal
    if opClass in [3,4,5,6,7]:                   # Classes 3 to 7 have multiple parameters
        predicate = predicate.split(',')              # So, split predicate into tokens
        rD = reg1[predicate[0]]                       # Get first token (register number)

当前指令的opClass用于提取参数。我们不是使用if结构,而是使用了 Python 的if in [list]结构;例如,if opClass in [3,4,5,6,7]如果指令在类别37中,则返回True。如果是这样,谓词(一个字符串)使用split()函数分割成列表,并读取第一个元素以提取目标寄存器rD。请注意,我们只需要分割谓词一次,因为所有后续的情况也属于这个组。

测试函数testLine

TC1 的另一个限制是缺乏测试和验证;例如,我有时会输入MOVE而不是MOV,程序就会崩溃。通常这不是问题;你只需重新编辑源程序。然而,在调试 TC1 时,我经常假设错误是由于我新代码中的错误造成的,结果发现只是汇编语言程序中的一个误打印。因此,我添加了一些测试。以下提供了测试函数:


def testLine(tokens):                    # Check whether there's a valid instruction in this line
    error = 1                            # error flag = 1 for no error and 0 for an error state
    if len(tokens) == 1:                 # If the line is a single token, it must be a mnemonic
        if tokens[0] in codes: error = 0 # If the token is in codes, there's no error
    else:                                # Otherwise, we have a multi-token line
        if (tokens[0] in codes) or (tokens[1] in codes): error = 0:
    return(error)                        # Return the error code

唯一值得关注的一行是以下内容:


if (tokens[0] in codes) or (tokens[1] in codes): error = 0:

需要考虑两种情况:带有标签的指令和无标签的指令。在前一种情况下,助记符是指令中的第二个令牌,在后一种情况下,助记符是第一个令牌。我们可以通过使用 Python 的if ... in结构来测试令牌是否是助记符。比如说我们有以下结构:

if token[0] in codes

如果第一个令牌是有效的助记符,则返回True。我们可以使用or布尔运算符将两个测试组合起来得到前面的表达式。在程序中,我们使用tokens参数调用testLine,它返回一个错误。我们使用错误来打印消息,并通过sys.exit()函数将其返回给操作系统。

testIndex()函数

此模拟器提供了一种形式为LDRI r1,[r2]的指令,以提供内存间接寻址(即基于指针或索引寻址)。

在这种情况下,寄存器r1被加载了由寄存器r2指向的内存内容。如果指针寄存器包含一个无效值,该值超出了合法地址范围,程序将会崩溃。通过测试索引,我们可以确保检测到越界索引。请注意,只有第一个源寄存器rS1被用作内存指针:


def testIndex():                        # Test for register or memory index out of range
    if (rD > 7) or (rS1 > 7) or (rS2 > 7): # Ensure register numbers are in the range 0 to 7
        print('Register number error')
        sys.exit()                      # Call operating system to leave the Python program
    if mnemonic in ['LDRI', 'STRI']:    # Memory index testing only for memory load and store
        if r[rS1] > len(m) - 1:         # Test rS1 contents are less than memory size
            print(' Memory index error')
            sys.exit()
    return()

一般注释

以下行演示了如何从助记符中提取操作类。由于()[]括号的存在,表达式看起来很奇怪。codes.get(key)操作使用keycodes字典中获取相关值:


opClass = codes.get(mnemonic)[0]     # Use mnemonic to read opClass from the codes dictionary

在这种情况下,键是助记符,返回的值是操作类;例如,如果助记符是'LDRL',相应的值是[3]。请注意,返回的值不是3!它是一个包含单个值3列表。因此,我们必须通过指定第一个项目来从列表中提取值,即mnemonic[0]

构建指令有许多方法。在 TC1 中,我们创建一个二进制值,就像真正的汇编器一样。在 TC1mini 中,我们直接从汇编语言形式执行指令。因此,当我们编译指令时,我们创建一个文本形式的程序。为此,我们需要将标签、助记符、寄存器和立即数组合成一个列表。

实现该功能的代码如下:


thisLine = list((i,label,mnemonic,predicate,opClass))
                                        # Combine the component parts in a list
prog.append(thisLine)                   # Add the new line to the existing program

在本例中,我们使用list()函数将项目组合成一个列表,然后使用append()将此项目添加到现有列表中。注意list()的语法。你可能期望它是list(a,b,c)。不,它是list((a,b,c))list()函数使用括号作为正常操作,但列表本身必须放在括号内。这是因为列表项构成了列表的单个参数。

TC1tiny 代码列表

这是 TC1 简化的版本列表。指令根据操作数的数量和排列分为八个类别。每个指令都在字典codes中,它提供了用于解码操作数的类别编号。指令本身直接从其助记符执行。与 TC1 不同,没有中间的二进制代码。同样,寄存器名称和间接寄存器名称也都在字典中,以简化指令解码:


# Simple CPU instruction interpreter. Direct instruction interpretation. 30 September 2022\. V1.0
# Class 0: no operand                   NOP
# Class 1: literal                      BEQ  3
# Class 2: register                     INC  r1
# Class 3: register,literal             LDRL r1,5
# Class 4: register,register,           MOV  r1,r2
# Class 5: register,register,literal    ADDL r1,r2,5
# Class 6: register,register,register   ADD  r1,r2,r3
# Class 7: register,[register]          LDRI r1,[r2]
import sys                              #NEW
codes = {'NOP':[0],'STOP':[0],'END':[0],'ERR':[0], 'BEQ':[1],'BNE':[1], \
         'BRA':[1],'INC':[2],'DEC':[2],'NOT':[2],'CMPL':[3],'LDRL':[3], \
         'DBNE':[3],'MOV':[4],'CMP':[4],'SUBL':[5],'ADDL':[5],'ANDL':[5], \
         'ADD':[6],'SUB':[6],'AND':[6],'OR':[6],'LDRI':[7],'STRI':[7]}
reg1  = {'r0':0,   'r1':1,   'r2':2,  'r3':3,   'r4':4,   'r5':5, \
         'r6':6,  'r7':7}               # Registers
reg2  = {'[r0]':0, '[r1]':1, '[r2]':2,'[r3]':3, '[r4]':4, \
         '[r5]':5, '[r6]':6,'[r7]':7}   # Pointer registers
symTab = {}                             # Symbol table
r = [0] * 8                             # Register set
m = [0] * 8
prog = [] * 32                          # Program memory
def equates():                          # Process directives and delete from source
    global symTab, sFile
    for i in range (0,len(sFile)):      # Deal with equates
        tempLine = sFile[i].split()
        if len(tempLine) > 2 and tempLine[1] == 'EQU':
                                        # If line > 2 tokens and second EQU
            print('SYMB' , sFile[i])
            symTab[tempLine[0]] = tempLine[2] # Put third token EQU in symbol table
    sFile = [ i for i in sFile if i.count('EQU') == 0] # Remove all lines with 'EQU'
    print('Symbol table ', symTab, '\n')
    return()

本节处理将指令解码为适当的任务,以便正确地使用适当的参数执行它们:


def classDecode(predicate):
    lit,rD,rS1,rS2 = '',0,0,0                         # Initialize variables
    if opClass in [1]:      lit =  predicate
    if opClass in [2]:      rD  = reg1[predicate]
    if opClass in [3,4,5,6,7]:
        predicate = predicate.split(',')
        rD = reg1[predicate[0]]
    if opClass in [4,5,6]:  rS1 = reg1[predicate[1]] \
                                                # Get source reg 1 for classes 4, 5, and 6
    if opClass in [3,5]:    lit = (predicate[-1])     # Get literal for classes 3 and 5
    if opClass in [6]:      rS2 = reg1[predicate[2]]  # Get source reg 2 for class 6
    if opClass in [7]:      rS1 = reg2[predicate[1]]  # Get source pointer reg for class 7
    return(lit,rD,rS1,rS2)

与 TC1 不同,我们在输入上进行了少量测试,例如,内存或寄存器索引是否超出范围。这只是一个数据验证的示例:


def testLine(tokens):   # Check there's a valid instruction in this line
    error = 1
    if len(tokens) == 1:
        if tokens[0] in codes: error = 0
    else:
        if (tokens[0] in codes) or (tokens[1] in codes): error = 0
    return(error)
    def testIndex():                    # Test for reg or memory index out of range
    print('rD,rS1 =', rD,rS1, 'r[rS1] =', r[rS1], 'len(m)', len(m),\
    'mnemonic =', mnemonic)
    if rD > 7 or rS1 > 7 or rS2 > 7:
        print('Register number error')
        sys.exit()                                  # Exit program on register error
    if mnemonic in ['LDRI', 'STRI']:
        if r[rS1] > len(m) - 1:
            print(' Memory index error')
            sys.exit()                              # Exit program on pointer error
    return()
def getLit(litV):                                   # Extract a literal (convert formats)
    if litV == '': return(0)                        # Return 0 if literal field empty
    if  litV in symTab:                    # Look in symbol table and get value if there
        litV = symTab[litV]                         # Read the symbol value as a string
        lit = int(litV)                             # Convert string to integer
    elif  litV[0]    == '%': lit = int(litV[1:],2)  # If % convert binary to int
    elif  litV[0:1]  == '$': lit = int(litV[1:],16) # If first symbol $, convert hex to int
    elif  litV[0]    == '-':
        lit = (-int(litV[1:]))&0xFFFF               # Deal with negative values
    elif  litV.isnumeric():  lit = int(litV)        # Convert decimal string to integer
    else:                    lit = 0                # Default value 0 (if all else fails)
    return(lit)
prgN = 'E://ArchitectureWithPython//NewIdeas_1.txt' # prgN = program name:  test file
sFile = [ ]                                         # sFile source data
with open(prgN,'r') as prgN:                        # Open it and read it
    prgN = prgN.readlines()
for i in range(0,len(prgN)):                        # First level of text-processing
    prgN[i] = prgN[i].replace('\n','')              # Remove newline code in source
    prgN[i] = ' '.join(prgN[i].split())             # Remove multiple spaces
    prgN[i] = prgN[i].strip()                       # First strip spaces
prgN = [i.split('@')[0] for i in prgN]              # Remove comment fields
while '' in prgN: prgN.remove('')                   # Remove blank lines
for i in range(0,len(prgN)):                        # Copy source to sFile: stop on END
    sFile.append(prgN[i])                           # Build new source text file sFile
    if 'END' in sFile[i]: break            # Leave on 'END' and ignore any more source text
for i in range(0,len(sFile)): print(sFile[i])
print()
equates()                                           # Deal with equates
for i in range(0,len(sFile)): print(sFile[i])
print()
for i in range(0,len(sFile)):         # We need to compile a list of labels
    label = ''                        # Give each line a default empty label
    predicate = ''                    # Create default predicate (label + mnemonic + predicate)
    tokens = sFile[i].split(' ')      # Split into separate groups
    error = testLine(tokens)          # Test for an invalid instruction
    if error == 1:                    # If error found
        print('Illegal instruction', tokens, 'at',i)
        sys.exit()                    # Exit program
    numTokens = len(tokens)           # Process this line
    if numTokens == 1: mnemonic = tokens[0]
    if numTokens > 1:
        if tokens[0][-1] == ':':
            symTab.update({tokens[0][0:-1]:i})    # Insert new value and line number
            label = tokens[0][0:-1]
            mnemonic = tokens[1]
        else: mnemonic = tokens[0]
        predicate = tokens[-1]
    opClass = codes.get(mnemonic)[0] # Use the mnemonic to read opClass from codes dictionary
    thisLine = list((i,label,mnemonic,predicate,opClass))
    prog.append(thisLine)            # Program line + label + mnemonic + predicate + opClass
print('Symbol table ', symTab, '\n') # Display symbol table for equates and line labels

下面是实际的指令执行循环。正如你所见,它非常紧凑:


                                             # Instruction execution
run = 1
z = 0
pc = 0
while run == 1:
    thisOp = prog[pc]
    if thisOp[2] in ['STOP', 'END']: run = 0 # Terminate on STOP or END (comment on this)
    pcOld = pc
    pc = pc + 1
    mnemonic  = thisOp[2]
    predicate = thisOp[3]
    opClass   = thisOp[4]
    lit,rD,rS1,rS2 = classDecode(predicate)
    lit = getLit(lit)
    if   mnemonic == 'NOP': pass
    elif mnemonic == 'BRA': pc = lit
    elif mnemonic == 'BEQ':
        if z == 1: pc = lit
    elif mnemonic == 'BNE':
        if z == 0: pc = lit
    elif mnemonic == 'INC': r[rD] = r[rD] + 1
    elif mnemonic == 'DEC':
        z = 0
        r[rD] = r[rD] - 1
        if r[rD] == 0: z = 1
    elif mnemonic == 'NOT': r[rD] = (~r[rD])&0xFFFF  # Logical NOT
    elif mnemonic == 'CMPL':
        z = 0
        diff = r[rD] - lit
        if diff == 0: z = 1
    elif mnemonic == 'LDRL': r[rD] = lit
    elif mnemonic == 'DBNE':
        r[rD] = r[rD] - 1
        if r[rD] != 0: pc = lit
    elif mnemonic == 'MOV':  r[rD] = r[rS1]
    elif mnemonic == 'CMP':
        z = 0
        diff = r[rD] - r[rS1]
        if diff == 0: z = 1
    elif mnemonic == 'ADDL': r[rD] = r[rS1] + lit
    elif mnemonic == 'SUBL': r[rD] = r[rS1] - lit
    elif mnemonic == 'ADD':  r[rD] = r[rS1] + r[rS2]
    elif mnemonic == 'SUB':  r[rD] = r[rS1] - r[rS2]
    elif mnemonic == 'AND':  r[rD] = r[rS1] & r[rS2]
    elif mnemonic == 'OR':   r[rD] = r[rS1] | r[rS2]
    elif mnemonic == 'LDRI':
        testIndex()
        r[rD] = m[r[rS1]]
    elif mnemonic == 'STRI':
        testIndex()
        m[r[rS1]] = r[rD]
    regs = ' '.join('%04x' % b for b in r)           # Format memory location's hex
    mem  = ' '.join('%04x' % b for b in m)           # Format register's hex
    print('pc =','{:<3}'.format(pcOld),'{:<18}'.format(sFile[pcOld]),\
          'regs =',regs,'Mem =',mem,'z =',z)

代码执行循环,就像我们讨论的大多数模拟器一样,非常直接。当前指令被检索并解码为助记符、类别和寄存器号。程序计数器递增,助记符被呈现给一系列的then...elif语句。

许多指令仅用一行代码执行;例如,ADD通过将两个寄存器相加来实现:r[rD] = r[rS1] + r[rS2]。一些指令,如compare,需要从两个寄存器中减去,然后相应地设置状态位。

我们包括了一条相对复杂的指令,即非零减量和分支,该指令会减去一个寄存器的值,然后如果寄存器没有减到0,则跳转到目标地址。

在最后一节,我们将探讨 TC1 的另一种变体。

TC1 后记 II

如果一个后记很好,两个就更好了。我们添加了这个主题的第二种变体来展示不同的做事方式。程序的大部分结构与之前相同。功能如下:

  • 直接执行(回顾)

  • 避免为相同的基本操作使用不同的助记符(例如,ADDADDL)的能力

主要的增强是处理指令和解码指令的方式。在 TC1 中,我们使用 4 位代码来定义每个指令的结构,从其参数的角度来看。当在字典中查找助记符时,它会返回一个代码,该代码给出了所需的参数。

TC1 的一个特性(或问题)是,对于指令的不同变体,我们使用不同的助记符,例如ADDADDL。后缀L告诉汇编器需要一个立即数操作数(而不是寄存器号)。在这个例子中,我们通过将指令分类来避免不同的指令格式,并使用单个助记符。每个类别定义了一种指令格式,从类别0(无参数的指令)到类别9(包含四个寄存器的指令)。

这个例子使用了指令的直接执行。也就是说,我们不是将指令编译成二进制然后执行二进制,而是直接从其助记符执行指令。

这种安排的后果是,一条指令可能属于多个类别;例如,LDR属于三个类别,而不是有LDRLDRLLDRI变体。当遇到一条指令时,它会与每个类别进行比对。如果助记符属于某个类别,则在决定是否找到了正确的类别之前,会检查指令的属性。

考虑ADD指令。我们可以写成ADD r1,r2,5或者ADD r1,r2,r3;也就是说,加到寄存器的第二个数字可以是立即数或寄存器。因此,ADD属于5类和6类。为了解决歧义,我们查看最后一个操作数;如果是立即数,则属于5类,如果是寄存器,则属于6类。

检查寄存器很容易,因为我们已经将寄存器放入了字典中,所以只需要检查最终操作数是否在字典中即可。考虑类别3


if (mnemonic in class3) and (predLen == 2) and (predicate[1] not in regList)

在这里,我们进行三次测试。首先,我们检查助记符是否在类别3中。然后,我们测试谓词长度(对于两个操作数,如CMP r1,5,它是2)。最后,我们通过确保操作数不在寄存器列表中来测试第二个操作数是否为数值。

这个实验的 Python 程序如下。


# Instruction formats
# NOP             # class 0
# BRA 4           # Class 1
# INC r1          # class 2
# LDR r1,#4       # class 3
# MOV r1,r2       # class 4
# ADD r1,r2,5     # class 5
# ADD r1,r2,r3    # class 6
# LDR r1,[r2]     # class 7
# LDR r1,[r2],4   # class 8
# MLA r1,r2,r3,r4 # class 9 [r1] = [r2] + [r3] * [r3]
def getLit(lit):                        # Extract a literal
    if    lit in symTab:    literal = symTab[lit] \
                                        # Look in symbol table and get if there
    elif  lit       == '%': literal = iint(lit[1:],2) \
                                        # If first symbol is %, convert binary to integer
    elif  lit[0:1]  == '$': literal = int(lit[1:],16) \
                                        # If first symbol is $, convert hex to integer
    elif  lit[0]    == '-': literal = i(-int(lit[1:]))&0xFFFF \
                                        # Deal with negative values
    elif  lit.isnumeric():  literal = iint(lit) \
                                        # If number is a decimal string, then convert to integer
    else:                   literal = 0 # Default value 0 if all else fails
    return(literal)
regList = {'r0':0,'r1':1,'r2':2,'r3':3,'r4':4,'r5':5,'r6':6,'r7':7}
iRegList = {'[r0]':0,'[r1]':1,'[r2]':2,'[r3]':3,'[r4]':4,'[r5]':5, \
            '[r6]':6,'[r7]':7}
class0 = ['NOP','STOP','RTS']           # none
class1 = ['BRA','BEQ', 'BSR']           # register
class2 = ['INC', 'DEC']                 # register
class3 = ['LDR', 'STR','CMP','DBNE','LSL','LSR','ROR']  # register, literal
class4 = ['MOV','CMP','ADD']            # register, register Note ADD r1,r2
class5 = ['ADD','SUB']                  # register, register, literal
class6 = ['ADD','SUB']                  # register, register, register
class7 = ['LDR','STR']                  # register, pointer
class8 = ['LDR','STR']                  # register, pointer, literal
class9 = ['MLA']                        # register, register, register, register
inputSource = 0                         # Manual (keyboard) input if 0; file input if 1
singleStep  = 0                         # Select single-step mode or execute all-to-end mode
x = input('file input? type y or n ')   # Ask for file input (y) or keyboard input (any key)
if x == 'y':
    inputSource = 1
    x = input('Single step type y ')    # Type 'y' for single-step mode
    if x == 'y': singleStep = 1
    with open('C:/Users/AlanClements/Desktop/c.txt','r') as fileData:
        fileData = fileData.readlines()
    for i in range (0,len(fileData)):   # Remove leading and trailing spaces
        fileData[i] = fileData[i].strip()
r =     [0] * 8                         # Eight registers
m =     [0] * 16                        # 16 memory locations
stack = [0] * 8                         # Stack for return addresses (BSR/RTS)
prog =  []  * 64                        # Program memory
progDisp = [] * 64                      # Program for display
symTab = {}                             # Symbol table for symbolic name to value binding
run = True
pc = 0                                  # Clear program counter
sp = 7                                  # Set stack pointer to bottom of stack
while run == True:                      # Program processing loop
    predicate = []                      # Dummy
    if inputSource == 1:                # Get instruction from file
        line = fileData[pc]
    else: line = input('>> > ')         # Or input instruction from keyboard
    if line == '':
        run = False
        break
    line = ' '.join(line.split())       # Remove multiple spaces. Uses join and split
    progDisp.append(line)               # Make a copy of this line for later display
    line = line.replace(',',' ')
    line = line.split(' ')              # Split instruction into tokens
    if (len(line) > 1) and (line[0][-1] == ':'): # Look for a label (token 0 ending in :)
        label = line[0]
        symTab[line[0]] = pc            # Put a label in symTab alongside the pc
    else:
        line.insert(0,'    :')          # If no label+, insert a dummy one (for pretty printing)
    mnemonic  = line[1]                 # Get the mnemonic, second token
    predicate = line[2:]                # What's left is the predicate (registers and literal)
    prog.append(line)                   # Append the line to the program
    pc = pc + 1                         # And bump up the program counter
    progLength = pc – 1                 # Record the total number of instructions
for i in range (0,pc-1):
    print('pc =', f'{i:3}', (' ').join(prog[i])) # Print the program
print('Symbol table =', symTab, '\n')   # Display the symbol table
pc = 0
run = True
z = 0
c = 0
classNim = 10
while run == True:                      # Program execution loop
    instruction = prog[pc]
    pcOld = pc
    pc = pc + 1
    if instruction[1] == 'STOP':        # Halt on STOP instruction
        print('End of program exit')
        break
    mnemonic  = instruction[1]
    predicate = instruction[2:]
    predLen   = len(predicate)
    if (predLen > 0) and (mnemonic not in class1): rD = regList[predicate[0]]
                                        # Get rD for classes 2 to 8

在这个模拟器中,我们按类别而不是按助记符处理指令。这个特性意味着相同的助记符可以有不同的寻址方式,例如立即数、寄存器,甚至内存。第一个类别0是为没有操作数的助记符保留的,例如NOP。当然,这种机制使得发明一种新的操作成为可能,比如,比如NOP 4,它以不同的方式执行:


    if mnemonic in class0:              # Deal with instructions by their group (class)
        classNum = 0
        if mnemonic == 'NOP': pass
        if mnemonic == 'RTS':           # Return from subroutine pull address off the stack
            pc = stack[sp]
            sp = sp + 1
    if mnemonic in class1:              # Class deals with branch operations so get literal
        classNum = 1
        literal = getLit(predicate[0])
        if   mnemonic == 'BRA': pc = literal
        elif mnemonic == 'BEQ':
            if z == 1: pc = literal
        elif mnemonic == 'BSR':         # Deal with subroutine call
            sp = sp - 1                 # Push return address on the stack
            stack[sp] = pc
            pc = literal
    if mnemonic in class2:                 # Class 2 increment and decrement so get register
        classNum = 2
        if mnemonic == 'INC': r[rD] = r[rD] + 1
        if mnemonic == 'DEC':
            r[rD] = r[rD] - 1
            if r[rD] == 0: z = 1           # Decrement sets z flag
            else: z = 0
    if (mnemonic in class3) and (predLen == 2) and \
    (predicate[1] not in regList):         
        classNum = 3
        literal = getLit(predicate[-1])
        if mnemonic == 'CMP':
            diff = r[rD] - literal
            if diff == 0: z = 1
            else:         z = 0
        elif mnemonic == 'LDR': r[rD] = literal
        elif mnemonic == 'STR': m[literal] = r[rD]
        elif mnemonic == 'DBNE':
            r[rD] = r[rD] - 1
            if r[rD] != 0: pc = literal        # Note we don't use z flag
        elif mnemonic == 'LSL':
            for i in range(0,literal):
                c = ((0x8000) & r[rD]) >> 16
                r[rD] = (r[rD] << 1) & 0xFFFF  # Shift left and constrain to 16 bits
        elif mnemonic == 'LSR':
            for i in range(0,literal):
                c = ((0x0001) & r[rD])
                r[rD] = r[rD] >> 1
        elif mnemonic == 'ROR':
            for i in range(0,literal):
                c = ((0x0001) & r[rD])
                r[rD] = r[rD] >> 1
                r[rD] = r[rD] | (c << 15)
    if (mnemonic in class4) and (predLen == 2) and (predicate[1]\
    in regList):                           #
        classNum = 4
        rS1 = regList[predicate[1]]        # Get second register
        if mnemonic == 'MOV':              # Move source register to destination register
           r[rD] = r[rS1]
        elif mnemonic == 'CMP':
            diff = r[rD] -  r[rS1]
            if diff == 0: z = 1
            else:         z = 0
        elif mnemonic == 'ADD':            # Add source to destination register
            r[rD] = r[rD] + r[rS1]
    if (mnemonic in class5) and (predLen == 3) and (predicate[2] not\
    in regList):
        classNum = 5                       # Class 5 is register with literal operand
        literal = getLit(predicate[2])
        rS1 = regList[predicate[1]]
        if   mnemonic == 'ADD': r[rD] = r[rS1] + literal
        elif mnemonic == 'SUB': r[rD] = r[rS1] - literal
    if (mnemonic in class6) and (predLen == 3) and (predicate[-1]\
    in regList):
        classNum = 6                       # Class 6 uses three registers
        rS1 = regList[predicate[1]]
        rS2 = regList[predicate[2]]
        if   mnemonic == 'ADD': r[rD] = r[rS1] + r[rS2]
        elif mnemonic == 'SUB': r[rD] = r[rS1] - r[rS2]
    if (mnemonic in class7) and (predLen == 2) and (predicate[1]\
    in iRegList):
        classNum = 7                       # Class 7 uses a pointer register with load and store
        pReg  = predicate[1]
        pReg1 = iRegList[pReg]
        pReg2 = r[pReg1]
        if   mnemonic == 'LDR': r[rD] = m[pReg2]
        elif mnemonic == 'STR': m[pReg2] = r[rD]
    if (mnemonic in class8) and (predLen == 3):
        classNum = 8                       # Class 8 uses a pointer register and a literal offset
        pReg  = predicate[1]
        pReg1 = iRegList[pReg]
        pReg2 = r[pReg1]
        literal = getLit(predicate[2])
        if   mnemonic == 'LDR': r[rD] = m[pReg2 + literal]
        elif mnemonic == 'STR': m[pReg2 + literal] = r[rD]
    if mnemonic in class9:                 # Class 9 demonstrates a 4-operand instruction
        classNum = 9
        if mnemonic == 'MLA':
            rS1 = regList[predicate[1]]
            rS2 = regList[predicate[2]]
            rS3 = regList[predicate[3]]
            r[rD] = r[rS1] * r[rS2] + r[rS3]
    pInst = ' '.join(instruction)          ##############
    Regs = ' '.join('%04x' % i for i in r)
    print('pc {:<2}'.format(pcOld),'Class =', classNum,      \
          '{:<20}'.format(pInst),'Regs: ', regs, 'Mem', m,   \
          'r[0] =', '{:016b}'.format(r[0]),                  \
          'c =', c, 'z =', z, '\n')
    print(progDisp[pcOld])
    if singleStep == 1: input(' >>> ')

之前程序的目的在于演示另一种对指令进行分类和使用操作数数量来区分指令类型的方法,例如ADD r1,r2ADD r1,r2,r3

概述

在本章中,我们介绍了 TC1 模拟器,它可以接受 TC1 汇编语言的文本文件,将其转换为机器代码,然后执行。TC1 的指令集架构接近经典的 RISC 架构,具有寄存器到寄存器的架构(即数据操作发生在寄存器的内容上)。唯一允许的内存操作是从内存(或立即数)中加载寄存器,或将寄存器存储在内存中。

模拟器有两个基本组件:一个汇编器,它将类似于ADD r1,r2,r3的助记符转换为 32 位二进制指令,以及一个解释器,它读取指令,提取必要的信息,然后执行指令。

TC1 的一些元素相当不寻常。提供了源代码的免费格式结构;例如,你可以编写ADD r1,r2,r3adD R1 r2 r3,这两条指令都将被愉快地接受。为什么?首先,这是为了演示 Python 中字符串处理的使用。其次,它使得用户更容易以他们选择的格式输入。所有输入都会自动转换为大写,以使语言不区分大小写。同样,逗号或空格可以作为参数之间的分隔符。最后,去掉了表示间接寻址所需的[]括号。用户可以输入LDRI r0,[r1]LDRI r0,r1

同样,数字可以以不同的形式输入(十进制、二进制或十六进制);例如,基数可以用摩托罗拉格式Python 格式表示。大多数实际的汇编器不允许这种奢侈。

TC1 的早期版本要求所有地址都是数字的;如果你想跳转到行30,你必须写BRA 30。正是 Python 字典结构的非凡功能和易用性使得包含标签变得如此简单。你所要做的就是识别一个标签,将其值与其一起放入字典中,然后,每当遇到该标签时,只需在字典中查找其值即可。

我们还提供了一个示例汇编语言程序来测试 TC1,并对如何测试各种指令进行了简要讨论。

设计了 TC1 之后,我们创建了一个相当简化的版本,并将其称为 TC1mini。这个模拟器在编写指令方面没有提供相同的灵活性,它也没有一个大的指令集。它也没有将指令编码成二进制形式然后再解码并执行。它直接执行汇编指令(再次感谢 Python 的字典机制)。

在本章的结尾,我们提供了另一个简化的计算机模拟器,旨在强调计算机模拟器的结构,并提供一个修改基本设计的方法示例。

在本章的关键部分,我们介绍了 TC1 计算机模拟器并展示了其设计。我们还研究了 TC1 的变体,以帮助创建一个更完整的模拟器和汇编器的图景。在下一章中,我们将更进一步,探讨模拟器的更多方面。我们将描述几个具有不同架构的模拟器。

第七章:扩展 TC1

在本章中,你将学习如何扩展我们在第六章中设计的 TC1 模拟器的功能。我们还探讨了一些模拟器设计元素,包括输入/输出技术和数据验证,并描述了模拟器如何显示处理器在执行指令时的状态。

TC1 是一个折衷方案。最初,它被设计用来支持教授计算机架构的基础知识。它是理解指令编码、指令格式权衡、指令执行、寻址模式和设计并实现指令集的辅助工具。

在这里,我们探讨通过讨论如何添加新指令等方式来扩展 TC1 的方法。最后,我们处理一个迄今为止我们尚未涉及的话题:如何创建具有可变长度指令的计算机架构。也就是说,单个指令可以是基本字长的一个整数倍。

TC1 CPU 模拟器逐条执行指令,并在每条指令执行后打印寄存器、程序计数器和状态标志的内容。你可以使用这些信息来调试汇编级程序。通常,当你查看数据时,你会发现结果并不是你所预期的;例如,你可能想执行循环 9 次,但执行了 10 次,因为你测试循环结束时的错误。

我们有三个问题要处理。第一个是显示数据。我们如何显示数据以及如何格式化它?寄存器的内容应该显示为十进制值、由 1 和 0 组成的二进制字符串,还是以十六进制字符的形式显示?

另一个设计问题是是否存储历史数据。也就是说,我们在每条指令执行后在数组中存储寄存器和标志,以便我们可以查看先前操作中的值?

最后,我们如何进行?TC1 模拟器每次按下Enter键时都会执行一条指令。这为逐步执行程序提供了一个很好的方法,但当循环需要执行 1000 条指令才能到达你感兴趣的程序部分时,这变得不可行。我们将演示如何退出单步模式,执行一批指令,然后返回单步模式。

技术要求

你可以在 GitHub 上找到本章使用的程序:github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter07

再次审视 Python 的输入和输出

在这里,我们更详细地讨论数据输入和输出机制,因为这些主题在模拟器设计中非常重要,因为数据的格式化对于数据的解释至关重要。

让我们再次看看 Python 中的输入。从键盘输入数据非常简单。要输入文本,你写下 x = input(),就这样。当遇到这个语句时,Python 会等待你的输入。你输入文本并使用回车(Enter 键)结束。如果你只是输入一个回车而没有文本,x 的值将是一个空字符串——即 ' '。你输入的数据以文本形式存储。

Python 允许你在接收输入之前显示一个提示;例如,你可以写下以下内容:


x = input('Please enter your age')

因为输入是字符形式,所以在使用之前必须将数值转换为整数形式。像以下示例所示,转换为十进制、二进制或十六进制很容易。你只需将数字基数作为 int() 函数的第二个参数添加即可:


x = input('Enter the constant ')
y = int(x)                        # For a decimal constant
y = int(x,2)                      # For a binary constant
y = int(x,16)                     # For a hexadecimal constant

让我们考虑一个更复杂的例子。我们将输入一个指令,例如 ADD R3,R7,$12FA 并提取三个整数 3, 7, 和 0x12FA。在这个例子中,一个 $ 前缀表示十六进制值(Motorola 使用的约定)。

以下代码执行此操作。输入使用 replace 函数将所有逗号转换为空格。我们将替换操作与输入操作结合起来以创建紧凑的代码。输入后面跟着一个 split 函数,将字符串转换为标记:


inst        = input('Enter operation: >>').replace(',',' ')
p           = inst.split(' ')
t1,t2,t3,t4 = p[0],int(p[1][1:]),int(p[2][1:]),int(p[3][1:],16)

最后,我们依次检查四个标记,并将参数作为整数提取出来(t1t2t3t4)。考虑 t4p[3] 表达式提取了 "$12FA" 字符串。第二个索引 [1:] 提取第一个字符之后的所有字符,给出 "12FA"。这仍然是一个字符串。最后的操作 int(p[3][1:],'16') 将十六进制形式的参数字符串转换为整数 4858。第二个示例 ADD r3,r7,$1102 的输出是 ADD 3 7 4354

正如我们已经看到的,Python 允许你在一行上放置多个等式——例如,a,b,c = p,q,r。这会导致以下结果:

a = p
b = q
c = r

这种简写在与简单等式(通常在初始化过程中)打交道时很有用。一些程序员不喜欢使用这种技术,因为它可能会降低程序的可读性。让我们测试前面代码片段。我们在操作之间添加了一些 print 语句,这样我们就可以在代码执行时跟踪正在发生的事情:


inst = input('Enter operation: >>').replace(',',' ')
print('inst',inst)
p = inst.split(' ')
print ('p',p)
t1,t2,t3,t4 = p[0],int(p[1][1:]),int(p[2][1:]),int(p[3][1:],16)
print(‚t1,t2,t3,t4',t1,t2,t3,t4)

接下来是输出。注意,当我们打印 t1t4 时,十六进制操作数的数值以十进制形式给出:


Enter operation: >>add r1,r2,$FACE
inst add r1 r2 $FACE
p ['add', 'r1', 'r2', '$FACE']
t1,t2,t3,t4 add 1 2 64206

下一节将探讨如何格式化数据,例如数字,以便使读者更容易理解;例如,有时你可能希望将十进制的 42 表示为 101010,有时作为 00101010002A

显示数据

现在,我们更深入地探讨在 Python 中显示数据的方式。当你观察程序执行时,你希望看到在每条指令执行后发生了什么变化。计算机的状态由其寄存器和内存的内容、程序计数器和其状态位以及其内存决定。

我们如何显示数据?由于数据可以代表任何你想要它代表的东西,寄存器中的数据没有固有的意义。按照惯例,CPU 模拟器以十六进制形式表示数据。这部分的理由是每个 16 位寄存器包含 4 个十六进制字符,这为人类处理数据提供了一个相当方便的方式(尝试记住 16 位的 1 和 0 的字符串)。一些模拟器允许以二进制、十六进制或十进制显示,而其他模拟器允许将数据以字符形式显示(即,数据假定是 ASCII 编码的)。

除了选择我们显示数字的基数外,我们还必须选择格式以便于阅读(例如,对齐字符组)。像所有高级计算机语言一样,Python 提供了格式化打印数据的方法。而且,就像大多数其他语言的格式化一样,我倾向于将 Python 的格式化描述为有点不够优雅。

我们没有足够的空间来公正地介绍 Python 的格式化。我们只是提供了一些示例和足够的信息,以便你可以修改它们。

在模拟器中我们需要打印的是指令。这很简单,因为它是一个字符串。所以,我们可以写出以下代码:


print(listing[pcOld])          # Listing contains a list of string instructions

这在pcOld地址查找字符串项(指令)并打印它。由于pc在指令周期中会被修改,所以我们打印当前周期开始时的旧值。

但每个指令的长度都不同(取决于操作数的数量),这意味着在同一行上打印的下一个项目将不会垂直对齐。我们经常需要在预定义的框架内打印指令。我们可以用以下方法做到这一点:


print("{:<23}".format(listing[pcOld]))

我们使用.format()方法。"{:<23}"表达式控制格式。整数23是文本的字符宽度。<符号表示字符串是左对齐的。如果我们想让它右对齐,我们会使用>。format()后面的值是要打印的字符串。

假设我们想在同一行上显示八个寄存器,每个寄存器作为一个六字符的十六进制值。首先,考虑以下 Python 代码:


z = 0x4ace          # The hex data to print
print("%06x" %z)    # Printing it in 6 chars with leading zeros

当代码执行时,它会打印以下内容:

004ace

我们为0x4ace十六进制值设置了一个变量z,并使用print("%06x" %z)来以十六进制字符显示它。打印的值是004ace,因为插入了两个前导零。格式化机制是"%06x" %z。第一个组件"%06x"指定了后续六位十六进制值的格式。第一个%表示格式化。%z表示要打印的值。

转义字符

Python 的print机制易于使用;例如,print('Answer =',a)将引号内的任何内容显示为字面量,并在print语句中出现的任何变量的值。你可以有任意多的引号项和变量。

print语句还允许使用转义字符进行格式化,该转义字符给以下字符赋予特殊含义。在 Python 中,转义字符是\'。如果转义符后面跟着'n',它相当于换行;如果转义符后面跟着't',它相当于制表符。考虑以下:


a = 42
b = 'Test'
print('a is the integer\t',a,'\nb is the string','\t',b)

此代码的输出如下:

a is the integer              42
b is the string               Test

注意这是如何因为'\n'和两个由'\t'制表符分隔的值而打印在两行的。你可以控制制表符的大小,如下例所示。expandtabs()方法将制表符宽度(空格数)设置为提供的参数。在这种情况下,我们将制表符嵌入到字符串中,并将制表符宽度设置为6


print('This is a\ttab\ttest'.expandtabs(6))
This is a   tab   test

其他一些转义序列如下:

  • \’     单引号

  • \\     反斜杠

  • \r     回车(返回)

  • \b     退格

  • \f     换页(移动到下一行的相同位置)

  • \xhh   十六进制字符值(例如,\x41会打印A,因为0x41是字母 A 的 ASCII 值)

转义转义

有时候你希望转义转义字符,并使用反斜杠作为可打印字符。在这种情况下,你需要在要打印的字符串之前加上rR。注意r字母位于字符串引号之外,如下例所示:


print(R'test\n one', 'test\n two')

这会导致以下结果:


test\n one test
 two

在这个例子中,我们使用R来抑制\n作为换行命令,并打印实际的\n。第二个\n没有以R开头,因此它作为换行命令。

从 ASCII 到 ASCII

我很幸运能见证微处理器的诞生,并从单个芯片(包括显示器)构建了一个 Motorola 6000 微处理器系统。当时,我没有任何商业软件,我必须自己将 ASCII 字符和它们的数值进行转换。今天的生活更容易。Python 提供了两个函数,允许你在数值和 ASCII 值之间进行转换。这些是ord()chr()。如果你写x = ord('B')x的值将是它的 ASCII 等效值,0x42或二进制的01000010。同样,如果你写y = chr(0x41)y的值将是'A'

二进制和十六进制字符串

在我们详细查看格式化字符串之前,考虑以下使用 Python 解释器的控制台输入的简单示例。粗体的文本是输出:


>>> x = 12345
>>> y = hex(x)
>>> print(y)
0x3039
>>> z = y + 1
TypeError: can only concatenate str (not "int") to str
>>> z = int(y,16) + 1
>>> print (z)
12346
>>> print(hex(z))
0x303a

我们创建一个x变量,其值为12345,并创建一个新的y值,它是x的十六进制版本。然后,我们打印它,得到预期的0x3039结果。接下来,我们创建一个新的变量z,其中z = y + 1。这会生成一个错误信息,因为y是一个文本字符串,我们不能将其与整数1相加。在下一行,我们再次执行加法操作,但这次,我们使用int(y,16)将十六进制字符串转换为整数形式。现在,我们可以使用print(hex(z))z打印为十进制整数或十六进制字符串。简而言之,Python 使得处理十进制、十六进制和二进制值变得容易,但你必须非常小心,确保在必要时在字符串和整数形式之间进行转换。

由于我们处理的是二进制数,因此以二进制或十六进制格式显示 TC1 的输出是有意义的。假设我们希望将十进制整数值转换为二进制字符串。考虑以下内容,我们将整数p转换为二进制字符串,然后打印它:


p = 1022
q = "{0:b}".format(p)
print('p in decimal is',p, "and in binary, it's",q)

"{0:b}"表达式是格式化的关键。它是一个用大括号括起来的字符串。0告诉它从字符串的第一个字符开始打印,b表示二进制。这会产生以下输出:

p in decimal is 1022 and in binary, it's 1111111110

到目前为止,一切顺利。但如果我们希望输出以固定数量的字符对齐——例如,16 个字符?以下展示了将 26 和2033转换为二进制格式时的这种格式化:


p1, p2 = 26, 2033
q1 = "{0:16b}".format(p1)
q2 = "{0:16b}".format(p2)
print('p1 is',q1, '\nand p2 is',q2)

格式字符串的唯一更改是从"{0:b}""{0:16b}"。也就是说,我们在b之前插入了 16 个字符的字段宽度。16的效果是为字符串定义一个 16 位的宽度。字符串在左侧用空格填充。此代码的输出如下:

p1 is            11010
p2 is      11111110001

在许多计算机文本中,通常会在二进制和十进制值的前面填充前导零,而不是空格。我们可以通过格式的小幅改动来实现这一点。我们在字段宽度之前插入0——即"{0:016b}"。现在考虑以下内容:


p1, p2 = 26, 2033
q1 = "{0:016b}".format(p1)
q2 = "{0:016b}".format(p2)
print('p1 is',q1, '\nand p2 is',q2)

这将产生一个输出,其中数字以 16 位显示,并使用前导零填充,如下所示:

p1 is 0000000000011010
p2 is 0000011111110001

十六进制值通过在format语句中将x替换为b以相同的方式处理:


p1, p2 = 30, 64123
q1 = "{0:8x}".format(p1)
q2 = "{0:8x}".format(p2)
q3 = "{0:08x}".format(p1)
q4 = "{0:08x}".format(p2)
print('\n', q1, '\n', q3, '\n', q2, '\n', q4)

这给出了以下输出。如您所见,它与二进制版本类似:

       1e
 0000001e
     fa7b
 0000fa7b

当然,我们可以结合两种打印格式(即二进制和十六进制),如下例所示:


x, y = 1037, 325
xBin = "{0:016b}".format(x)
yHex = "{0:04x}".format(y)
print("x is",xBin,"y is",yHex)
print("x is","0b" + xBin,"y is","0x" + yHex)

我们打印了两个数字,一个以二进制格式,一个以十六进制格式。在第一种情况下,数字被填充到 16 位,并带有前导零,而在第二种情况下,它被填充到 4 个字符,并带有前导零。

我们打印了两次结果。在第二种情况下,添加了前缀到值以指示基数。如果第一个数字xBin是二进制,我们可以通过使用“+”符号简单地将“0b”添加到二进制字符串之前来连接“0b”。此代码的输出如下:

x is 0000010000001101 y is 0145
x is 0b0000010000001101 y is 0x0145

我们可以将字符串方法格式概括为 "someString".format(<参数列表>)。这个字符串方法将字符串插入到参数列表中出现的参数。您必须在字符串中插入 占位符 的形式 {a:b},当字符串打印时,这些占位符将接收参数。

假设您正在打印整数幂的表格,形式为 x,x²,x³,x⁴。我们可以写出以下内容:


for x in range (1,7):
    print("Table of powers {0:2d}{1:3d}{2:4d}{3:6d}".format(x,x**2,x**3,x**4))

每个参数占位符的形式为 {a:b},其中第一个元素 a 是参数在格式参数列表中的位置。第二个元素 b 决定了参数的打印方式。在这种情况下,它是一个数字和字母 d。数字定义了参数的宽度,而 d 表示它是十进制;例如,最后一个参数指定为 {3:6d},表示第四个参数是一个十进制整数,占六位。以下展示了这段代码的输出:

Table of powers  1   1    1      1
Table of powers  2   4    8     16
Table of powers  3   9   27     81
Table of powers  4  16   64    256
Table of powers  5  25  125    625
Table of powers  6  36  216   1296

为了展示这种方法的通用性,下一个示例将打印相同的幂表,但使用不同的格式。除了十进制,我们还使用了二进制和十六进制。请注意,您只需将 {a:b} 中的 b 改为 b 即可更改基数;例如,{6:x} 告诉 print 语句以十六进制格式打印第七个参数:


for x in range (1,10):
    print('Table of powers {0:2d} binary {1:10b} {2:4d} \
    hex {3:6x}'.format(x, x*x, x*x*x, x*x*x*x))

注意您可以写 xxx,或者 x**3

下面的输出展示了这种格式化技术的结果:


Table of powers  1 binary          1    1 hex      1
Table of powers  2 binary        100    8 hex     10
Table of powers  3 binary       1001   27 hex     51
Table of powers  4 binary      10000   64 hex    100
Table of powers  5 binary      11001  125 hex    271
Table of powers  6 binary     100100  216 hex    510
Table of powers  7 binary     110001  343 hex    961
Table of powers  8 binary    1000000  512 hex   1000
Table of powers  9 binary    1010001  729 hex   19a1

我们对示例进行了一些修改。如果参数要按顺序打印,则不需要给出参数的顺序;例如,第一个参数规范可以写成 {:2d} 而不是 {0:2d}。我们还更改了间距以展示宽度参数如何操作:


for x in range (1,10):
    print('Powers {:2d} binary {:8b}{:4d} hex{:6x}'.format(x,x**2, x**3, x**4))
Powers  1 binary        1    1 hex      1
Powers  2 binary      100    8 hex     10
Powers  3 binary     1001   27 hex     51
Powers  4 binary    10000   64 hex    100
Powers  5 binary    11001  125 hex    271
Powers  6 binary   100100  216 hex    510
Powers  7 binary   110001  343 hex    961
Powers  8 binary  1000000  512 hex   1000
Powers  9 binary  1010001  729 hex   19a1

考虑以下 format 机制的示例。在这里,我们使用符号“<, >, ^”来控制格式化。依次,这些符号强制左对齐、右对齐和在指定的宽度内居中对齐。

以下代码首先以十进制形式打印十进制整数 123,使用三个修饰符,然后以二进制形式使用相同的三个修饰符。在每种情况下,我们都指定了 10 个字符的宽度:


x = 123
print('{:<10d}'.format(x))
print('{:>10d}'.format(x))
print('{:¹⁰d}'.format(x))
print('{:<10b}'.format(x))
print('{:>10b}'.format(x))
print('{:¹⁰b}'.format(x))

由这段代码生成的输出如下:

123                                   Left-justified
       123                            Right-justified
   123                                Centered
1111011                               Left-justified
   1111011                            Right-justified
 1111011                              Centered

现在我们将提供三个示例,说明表示数字的字符串如何打印。第一个示例演示了以整数、十六进制、二进制和实数形式格式化单个数字。第二个示例展示了我们可以如何将寄存器列表合并为一个字符串,并打印它们的值。这在模拟中逐步执行指令时显示数据非常有用。第三个示例演示了将十六进制值处理成所需格式的连续步骤。

示例 1 – 格式化数字

以下演示了这种格式化机制,其中我们打印了几个变量和一个字符串。这使用了一个格式说明符,例如 %05d,它表示带有前导零的五个十进制数字,以及一个占位符,例如 %x,它表示按照 %05d 指定的格式打印 x 的值:


x = 123
y = 0xABCD
p = 13.141592
q ='This is a test'
print("Hex example: %03x" %y, "%05d" %x, 'Alan', "%12.3f" %p, '%-20.14s' %q)

这个 print 语句显示了以下内容(注意十六进制以小写形式出现):

Hex example: abcd 00123 Alan       13.142 This is a test

示例 2 – 以十六进制形式打印寄存器值列表

考虑以下表达式,其中有一个包含各种格式数据的 8 寄存器数组:


R = [0xabcd,2,3,0x123,5,0x11010,7,124]
print('Registers ='," ".join("%04x" % b for b in R))

这个表达式打印了字符串‘寄存器 =,,然后是一个包含八个四字符十六进制值的第二个字符串。为了创建第二个字符串,我们使用了字符串 join() 方法。字符串推导式遍历寄存器,将格式化结构应用于每个元素。也就是说,它读取 r[0],将其转换为字符串格式,然后将其与其左侧邻居(最初是一个空字符串)连接起来。这重复了八次,然后按照以下方式打印该字符串:

Registers = abcd 0002 0003 0123 0005 11010 0007 007c

示例 3 – 依次将十进制值处理成所需的十六进制格式

考虑以下序列,我们依次处理十进制值 44,350,直到它以大写形式显示为没有 0x 指示前缀的十六进制格式:


>>> x = 44350                              Here's a decimal number
>>> print(hex(x))                          Can I see that in hex?
0xad3e                                     Thanks. But I like uppercase hex
>>> print(hex(x).upper())                  OK. We just use the .upper() method on the string.
0XAD3E                                     Nice one. But I don't like 0X at the front.
>>> print(hex(x).upper()[2:])              OK, OK, just use slice notation [2:] to strip 0X.
AD3E                                       Great. Can I have that with fries?

我们简要地描述了如何格式化数字。如果打印的输出要由人类阅读,特别是如果你正在模拟一个其中 1 和 0 的模式很重要的计算机,格式化是必要的。有几种格式化数据的方法,本节仅提供了这个主题的介绍。

输入验证

在本节中,我将介绍数据验证的概念。历史上,一些涉及计算机的主要错误是由未能检查输入数据造成的。TC1 不执行源数据检查;你可以编写 ADDL R1,R2,10ADDL z1,z2,10,结果相同。为什么?因为当汇编器看到 ADDL 时,它会寻找三个参数。它取第一个参数,让我们称它为 p1,并通过 regnum = int(p1[1:]) 读取寄存器号。只有 p1 的第二个和后续字符被记录下来,“R”被忽略。你可以写 R1 或甚至 ?1。这使得汇编语言编程更容易;你可以使用任何字母来表示寄存器。另一方面,它鼓励了不良的编程技术,并增加了与输入错误相关的危险。

验证数据

由于 TC1 汇编器不对输入进行错误检查,如果你犯了错误,程序很可能会崩溃,让你自己进行调试。好的软件会进行错误检查,这从简单的无效指令检测到精确地定位所有错误。

在这里,我们展示了如何读取一行代码并检查几种常见的错误类型——例如,无效的操作码、无效的指令格式(操作数过多或过少)、拼写错误(将 T6 键入为 R6)以及超出范围的寄存器(输入 R9)。

本节的目的在于展示如何向 TC1 添加自己的修改。处理问题的正式方法是为汇编语言构造一个语法,然后构建一个解析器以确定输入是否符合该语法。我们将采取更简单、更具体的方法。

如果当前指令是 x = 'ADD r1 r2 r3',则 y = x.split(' ') 操作将其转换为一系列标记的列表:y = ['ADD', 'R1', 'R2', 'R3']。我们可以使用 jj = y[0] 提取第一个标记,它应该是一个有效的助记符(在这个例子中,我们忽略任何标签)。

首先要进行的测试是检查指令的有效性。假设所有助记符都已定义在名为 codes 的列表或目录中。我们只需使用以下方法在 codes 目录中查找它:


if jj not in codes: error = 1

Python 关键字被阴影显示。此表达式将 error 变量设置为 1,如果此指令不在字典中。然后,我们可以测试 error 并采取必要的行动。

下一步是使用指令的名称来查找其详细信息,然后检查该指令是否需要参数。请记住,我们的字典条目是一个包含两个元素的元组,第一个元素是指令的格式(即所需的操作数数量),第二个是实际的操作码:


form = codes.get(y[0])   # Read the 4-bit format code

它在字典中查找指令(即 y[0]),并返回其值,该值是一个元组,例如 (8:12)。元组的第一个元素 form[0] 描述了指令的操作数,第二个是操作码(在这里不感兴趣)。指令所需的参数由 form[0] 确定。考虑以下代码:


opType = form[0]                                           # Get operand info
if   opType == 0:                  totalOperands = 1       # Just a mnemonic
elif opType == 8  or opType == 1:  totalOperands = 2       # Mnemonic + 1 operand
elif opType == 12 or opType == 9:  totalOperands = 3       # Mnemonic + 2 operands
elif opType == 14 or opType == 13: totalOperands = 4       # Mnemonic + 3
elif opType == 15:                 totalOperands = 5       # Mnemonic + 4 (not used)

格式代码的四位表示 rDrS1rs2 和一个字面量。TC1 指令有几种有效的格式;例如,如果 opType = 0b1001 = 9,则该格式定义了一个具有目标寄存器和字面量(如 LDRL R3 25)的指令。我们使用粗体和阴影来展示格式代码的位与实际指令之间的关系。

之前的代码使用 if…else 来获取每个指令的长度(包括操作码的标记数)。我们接下来只需计算当前指令的标记数,并查看它是否与预期值(即总长度)相同。下面的代码执行此检查:


    totalTokens = len(y)                     # Get the number of tokens in this instruction y
    if totalTokens < totalOperands:          # Are there enough tokens?
        error = 2                            # Error 2: Too few operands
        continue
    if totalTokens > totalOperands:          # Are there too many tokens?
        error = 3                            # Error 3: Too many operands 
        continue

如果令牌的数量与预期值不匹配,我们将错误号设置为23。在两次测试之后,有一个continue语句。continue的效果是跳到当前块的末尾并放弃进一步的错误测试(因为我们知道当前指令是错误的)。

一旦我们确定了一个有效的指令和正确的操作数数量,下一步就是检查每个操作数。操作数必须是R0R7(或字面量)的形式。

我们使用格式信息依次测试每个操作数。在这里,我们只处理第一个操作数,rD(目标寄存器):


    if opType & 0b1000 == 0b1000:             # If the destination register bit is set
        rDname = y[1]                         # Get the register name (second item in string)
        error,q = syntaxTest(rDname)          # Call syntax test to look for errors

此代码的第一行通过将格式代码与0b1000进行 AND 操作并测试0b1000来检查format的最左位是 1 还是 0。如果结果是true,则需要检查第一个寄存器操作数,即第二个令牌——即y[1]

由于我们要测试三个操作数,我们创建了一个syntaxText函数,该函数接受令牌作为参数并返回两个参数:errorqerror的值是返回的错误代码(无错误时为0q是寄存器的编号)。syntaxTest函数的 Python 代码如下:


def syntaxTest(token):                         # Test register for validity (R0 to R7)
    if token[0] != 'R': return(4,0)            # Fail on missing initial R. Return error 4
    if not token[1:].isnumeric(): return(5,0)  # Fail on missing register number. Return 5
    if int(token[1:]) > 7: return(6,0)         # Fail on register number not in 0-7\. Return 6
    return(0,int(token[1:]))                   # OK so return with error 0 and reg number

执行了三次测试,针对我们寻找的每种错误类型。第一次测试是检查令牌的第一个字符是否为'R'。如果不是'R',则返回错误代码4,并将虚拟或默认寄存器号设置为0。第二次测试寻找寄存器的数值('R'后面的字符,即token[1:])。第三次测试检查该数字是否大于7,如果是,则返回错误代码。最后,当到达最后一行时,返回错误代码0和适当的寄存器号。请注意,我们不需要使用elif,因为如果if返回True,代码将通过return()退出。

如果指令的格式代码为0b1110,则此例程会被调用多达三次,这对应于寄存器到寄存器的操作,如ADD R1 R2 R3。在此练习中,我们不检查字面量。如果您想添加此检查,则需要检查范围在 0 到 65,535 或-32,766 到 32,755 之间的整数(如果是二进制,则以%开头;如果是十六进制,则以0x开头)。

使用continue语句

在测试错误时,你是逐个测试语句中的每个错误,还是一旦找到错误就停止?

当发现错误时,代码使用 continue 语句来跳过进一步的测试。不幸的是,continue 会让你跳过循环的末尾并开始下一次迭代;也就是说,你无法打印出错误的性质。解决方案是在循环开始时打印出上一次迭代中发现的任何错误。当然,这会在第一次迭代中引起问题,因为没有先前的错误值。这可以通过在开始循环之前将 error 设置为零来轻松解决。以下代码演示了这种方法:


run = 1
error = 0
while run == 1:
    if error != 0: printError(error)
       .
       .
       <test for error 1>
    if error != 0: continue
        <test for error 2>
    if error != 0: continue
       .
       <test for error n>
    if error != 0: continue

在此代码片段中,error 被测试以确定前一个周期是否发生了错误。如果 error 不是 0,则调用 printError 函数来打印错误编号和类型。使用函数代码进行打印增强了程序的可读性。

下一个给出的是错误测试例程的代码。这不是一个完整的程序,而是演示了如何将错误测试扩展到输入数据中的方法:


# Testing Python parsing # 22 Aug 2020 Version of 29 July 2021
import sys                                    # System library used to exit program
codes = {'NOP':(0,0), 'STOP': (0,1),'BEQ':(1,4), 'INC':(8,2), \
         'MOVE':(12,23), 'LDRL':(9,13), 'ADD':(14,12),'ADDL':(13,12)}
def syntaxTest(token):               # Test the format of a register operand for validity (R0 to R7)
    if token[0] != 'R': return(4,0)           # Fail on missing initial R. Return error 2
    if not token[1:].isnumeric(): return(5,0) # Fail on missing register number. Return error 3
    if int(token[1:]) > 7: return(6,0)  # Fail on register number not in range 0-7\. Return error 4
    return(0,int(token[1:]))            # Success return with error code 0 and register number
def printError(error):
    if error != 0:
        if error == 1: print("Error 1: Non-valid operation")
        if error == 2: print("Error 2: Too few operands")
        if error == 3: print("Error 3: Too many operands")
        if error == 4: print("Error 4: Register operand error- no 'R'")
        if error == 5: print("Error 5: Register operand error - no valid num")
        if error == 6: print("Error 6: Register operand error - not in range")
run = 1
error = 0
while run == 1:
    if error != 0: printError(error)    # if error not zero, print message
    x = input("\nEnter instruction >> ")# Type an instruction (for testing)
    x =  x.upper()                      # Convert lowercase into uppercase
    x = x.replace(',',' ')              # Replace comma with space to allow add r1,r2 or add r1 r2
    y = x.split(' ')                    # Split into tokens. y is the tokenized instruction
    if len(y) > 0:                      # z is the predicate (or null if no operands)
        z = y[1:]
    else: z  = ''
    print("Inst =",y, 'First token',y[0])
    if y[0] not in codes:               # Check for valid opcode
        error = 1                       # Error 1: instruction not valid
        print("Illegal instruction", y[0])
        continue
    form = codes.get(y[0])              # Get the code's format information
    print('Format', form)
    if form[1] == 1:                    # Detect STOP, opcode value 1,and terminate
        print("\nProgram terminated on STOP")  # Say "Goodbye"
        sys.exit()                      # Call OS function to leave
    opType = form[0]
    if   opType == 0:                                  totalOperands = 1
    elif opType == 8  or opType == 4  or opType == 1:  totalOperands = 2
    elif opType == 12 or opType == 9:                  totalOperands = 3
    elif opType == 14 or opType == 13:                 totalOperands = 4
    totalTokens = len(y)                # Compare tokens we have with those we need
    if totalTokens < totalOperands:
        error = 2                       # Error 2: Too few operands
        continue
    if totalTokens > totalOperands:
        error = 3                       # Error 3: Too many operands
        continue
    if opType & 0b1000 == 0b1000:
        rDname = y[1]
        error,q = syntaxTest(rDname)
        if error != 0: continue
    if opType & 0b0100 == 0b0100:
        rS1name = y[2]
        error,q = syntaxTest(rS1name)
        if error != 0: continue
    if opType & 0b0010 == 0b0010:
        rS2name = y[3]
        error,q = syntaxTest(rS2name)
        if error != 0: continue
    if opType & 0b0001 == 0b0001:
        if not y[-1].isnumeric():
            error == 7
            print("Error 7: Literal error")
    if error == 0:
       print("Instruction", x, "Total operands", totalOperands,"Predicate", z)

检查参数——使用字典

下一个示例提供了对参数检查的另一种看法。它检查每条指令的助记符和谓词,并检查它是否代表一个有效的操作。它会在找到错误后停止。换句话说,它将在 INC R9,R2 中检测到两个错误(即操作数过多且第一个操作数超出范围),即使只有一个错误。

我们还扩展了 Python 字典的使用。以前,我们通过检查初始字符是否为 'R' 以及其后是否跟有 0 到 7 范围内的数字来测试有效的寄存器操作数。由于只有八个寄存器名称(R0 到 R7),在错误检查中采用字典是很容易的:


regSet = {'R0':0, 'R1':1, 'R2':2, 'R3':3, 'R4':4, 'R5':5, 'R6':6, 'R7':7}

词典 regSet 包含寄存器名称(键)及其对应的值。由于我们进行了大量的寄存器检查,因此创建一个名为 regTest 的函数来执行检查是方便的。此函数接受两个参数。第一个参数是一个字符串 tokNam,它为寄存器赋予一个名称,第二个参数是要测试的令牌——例如,regTest('rD',predicate[0])。将名称传递给函数的原因是使函数能够打印出错误操作数的名称。

函数返回两个值:一个错误代码和寄存器的编号。如果检测到错误,则默认返回寄存器值 0。此函数如下所示:


def regTest(tokNam,token):           # Test format of a register operand for validity (R0 to R7)
    if token in regSet:              # Is it in the register set?
        return (0,regSet.get(token)) # If it's there, return 0 and token value
    else:                            # If not there, return error code 4 and the token's name
        print("Error in register ",tokNam)
        return (4,0)

检查有效性很容易。if token in regSet: 条件检查此参数是否在寄存器集中。如果是,我们从字典中读取寄存器的值并返回其值。如果令牌不在寄存器集中,则打印错误消息(使用 tokNam 显示错误值),并返回错误消息编号 4

使用regSet.get(token)有点过于直接。我们实际上不需要读取寄存器值。如果它在有效寄存器集中,我们可以使用int(token[1])从名称中提取寄存器号。使用字典机制的优势在于,如果我们想添加新的寄存器,如SPPC等,我们可以修改代码。我们可以重命名寄存器或甚至使用别名;例如,如果我们使用R7寄存器作为临时寄存器,我们可以输入{'. . . 'R6':6, 'R7':7, 'T':7},然后可以写INC R7INC T

我们还尝试了一个新的指令字典。字典中的一些信息是冗余的,因为可以从其他信息中推导出来(例如,长度可以从格式中推导出来)。然而,我们采用了以下系统,因为我们可能在以后改变程序。

在汇编器的上一个版本中,我们使用了一个字典,其中每个条目都有一个键,该键是一个助记符和一个两个元素的元组——例如,'INC':(8,12)。元组的第一个元素是一个格式代码,表示助记符所需的操作数,第二个元素是指令的指令码。

在这个例子中,我们使用一个四个元素的元组来提供以下信息:

  • rD, rS1, rS2, literal(如前所述)。

  • 风格:风格描述了指令的类型——例如,仅助记符、助记符加文字、助记符加寄存器加文字等。格式和风格之间存在直接关系。

  • 长度:长度给出指令中的标记数——即助记符及其操作数。这相当于格式中 1 的数量加 1。

  • 指令的指令码。

指令的初始处理如下所示。在第一部分(浅色阴影)中,从输入标记字符串(即第一个元素)读取助记符。可能或可能不跟有其他参数。

助记符用于访问codes字典以检查其是否有效。错误代码设置为1(无效操作),并且continue语句强制跳转到循环的末尾(由于指令无效,不需要进一步输入测试)。

背景为浅灰色的代码读取与助记符相关的四个数据元素并提取单个参数。

以“if opCode == 1:”开头的三行读取操作以确定指令是否是“STOP”。如果是STOP,则sys.exit()操作终止程序。请注意,我们必须在程序开始时使用import sys导入系统函数库:


    mnemonic = y[0]                    # Get the mnemonic
    if mnemonic not in codes:          # Check for a valid opcode
        error = 1                      # If none found, set error code
        continue                       # and jump to the end of the loop
    opData  = codes.get(mnemonic)      # Read codes to get the data for this instruction
    opForm  =  opData[0]               # Get each of this instruction's parameters
    opStyle =  opData[1]
    opCode  =  opData[2]
    opLen   =  opData[3]
    if opCode == 1:                    # If the op_Code is 1, then it's "STOP", so exit the program
        print("\nProgram terminated on STOP")
        sys.exit()
    totalTokens = len(y)               # How many tokens do we have?
    if totalTokens < opLen:            # Compare with the expected number
        error = 2                      # Error 2: Too few operands
        continue
    if totalTokens > opLen:
        error = 3                      # Error 3: Too many operands
        continue

在前面的代码片段中,有两个带有深灰色背景的最终代码块执行错误检测操作。它们都从指令中获取令牌数量,然后将其与这个指令的值进行比较。在第一种情况下,错误2表示令牌太少,而在第二种情况下,错误3表示令牌太多。

在这个阶段,我们已经确定指令是有效的,并且具有正确的操作数数量。下一个阶段是检查操作数。检查是根据指令的风格进行的。有七种风格。风格 1 没有进一步的检查,因为没有操作数(例如,对于NOP)。我们只看一下风格 6 的检查,它对应于具有助记符、rD1rS1和类似ADD R1,R2,25的常量的指令。

我们首先使用regTest函数的'rD'参数来告诉它我们正在测试目标寄存器,以及predicate[0]令牌,这是第一个参数。这返回一个错误标志和寄存器的值。

因为我们执行了两个测试(寄存器rDrS1),我们必须使用两个错误名称:第一个测试使用e1,第二个测试使用e2。如果我们在这两种情况下都使用error作为变量,非错误第二个结果将清除第一个错误。if (e1 != 0) or (e2 != 0): error = 4这一行返回error,带有适当的错误状态,与哪个寄存器出错无关。在这个代码块末尾的continue跳过对这个指令的进一步错误检查:


# Input error checking - using dictionaries Modified 30 July 2021
# Instruction dictionary 'mnemonic':(format, style, op_code, length)
# Style definition and example of the instruction format
# 0 NOP            mnemonic only
# 1 BEQ L          mnemonic + literal
# 2 INC R1         mnemonic + rD
# 3 MOVE R1,R2     mnemonic + rD1 + rS1
# 4 LDRL R1,L      mnemonic + rD1 + literal
# 5 ADD R1 R2 R3   mnemonic + rD + rS1 + rS2
# 6 ADDL R1 R2 L   mnemonic + rD + rS1 + literal
# 7 LDRI R1 (R2 L) mnemonic + rD + rS1 + literal (same as 6)
import sys                                     # System library used to exit program
             # Dictionary of instructions (format, style, op_code, length)
codes = {'NOP': (0b0000,0,0,1),'STOP':(0b0000,0,1,1),'BEQ': (0b0001,1,2,2), \
         'INC': (0b1000,2,3,2),'MOVE':(0b1100,3,4,3),'LDRL':(0b1001,4,6,3), \
         'LDRI':(0b1101,7,7,4),'ADD': (0b1110,5,8,4),'ADDL':(0b1101,6,9,4)}
regSet = {'R0':0,'R1':1,'R2':2,'R3':3,'R4':4,'R5':5,'R6':6,'R7':7} # Registers
def regTest(token):                            # Test register operand for R0 to R7
    if token in regSet: return (0)             # Return with error 0 if legal name
    else:               return (4)             # Return with error 4 if illegal register name
def printError(error):                         # This function prints the error message
    if error != 0:
        if error == 1: print("Error 1: Non-valid operation")
        if error == 2: print("Error 2: Too few operands")
        if error == 3: print("Error 3: Too many operands")
        if error == 4: print("Error 4: Register name error")
        if error == 5: print("Error 5: Failure in pointer-based expression")
        if error == 6: print("Error 6: Invalid literal")
def litCheck(n):                          # Check for invalid literal format (this is just a demo)
    if n.isnumeric():    error = 0             # Decimal numeric OK
    elif n[0] == '-':    error = 0             # Negative number OK
    elif n[0] == '%':    error = 0             # Binary number OK
    elif n[0:2] == '0X': error = 0             # Hex number OK
    else:                error = 6             # Anything else is an error
    return(error)                              # Return with error number

这是主循环。输入一条指令,然后检查错误。与早期示例一样,首先处理指令的有效性,然后检查助记符是否在代码中:


error = 0
while True:             # Infinite loop
    if error != 0: printError(error)
    error = 0
    x = input(">> ").upper()             # Read instruction and provide limited processing
    if len(x) == 0: continue             # Ignore empty lines and continue
    x = x.replace(',',' ')               # remove commas
    x = x.replace('(','')                # remove (
    x = x.replace(')','')                # remove )
    y = x.split(' ')                     # Create list of tokens (mnemonic + predicate)
    mnemonic = y[0]                      # Get the mnemonic (first token)
    if mnemonic not in codes:            # Check for validity
        error = 1                        # If not valid, set error code and drop out
        continue
    opData = codes.get(mnemonic)         # Read the four parameters for this instruction
    opForm  =  opData[0]                 # opcode format (rDS,rS1,rS2,L)
    opStyle =  opData[1]                 # Instruction style (0 to 7)
    opCode  =  opData[2]                 # Numeric opcode
    opLen   =  opData[3]                 # Length (total mnemonic + operands in range 1 to 4)
    if opLen > 1: predicate = y[1:]      # Get predicate if this is one
    else:         predicate = ''         # If single token, return null
    print("Mnemonic =",mnemonic, "Predicate", predicate, \
          "Format =", bin(opForm),"Style =",opStyle,"Code =",opCode, \
          "Length =",opLen)
    if opCode == 1:                      # Used to terminate this program
        print("\nProgram ends on STOP")
        sys.exit()
    totalTokens = len(y)
    if totalTokens < opLen:
        error = 2                        # Error 2: Too few operands
        continue
    if totalTokens > opLen:
        error = 3                        # Error 3: Too many operands
        continue
    if opStyle == 0:                     # e.g., NOP or STOP so nothing else to do
        continue
    elif opStyle == 1:                   # e.g., BEQ 5 just check for literal
        literal = predicate[0]
        error = litCheck(literal)
        continue
    elif opStyle == 2:                   # e.g., INC r6 check for single register
        error = regTest(predicate[0])
        continue
    elif opStyle == 3:                   # e.g., MOVE r1,r2 check for two registers
        e1 = regTest(predicate[0])
        e2 = regTest(predicate[1])
        if e1 != 0 or e2 != 0:
            error = 4
        continue
    elif opStyle == 4:                   # e.g., LDRL r1,12 Check register then literal
        error = regTest(predicate[0])
        if error != 0: continue
        literal = predicate[1]
        error = litCheck(literal)
        continue
    elif opStyle == 5:                   # e.g., ADD r1,r2,r3 Check for three register names
        e1 = regTest(predicate[0])
        e2 = regTest(predicate[1])
        e3 = regTest(predicate[2])
        if e1 != 0 or e2 != 0 or e3 !=0:
            error = 4
        continue
    elif opStyle == 6:                   # e.g., ADDL R1,R2,4 Check for two registers and literal
        e1 = regTest(predicate[0])
        e2 = regTest(predicate[1])
        literal = predicate[2]
        e3 = litCheck(literal)
        if e1 != 0 or e2 != 0:
            error = 4
        if e1==0 and e2==0 and e3 !=0:   # If registers are OK but not literal
            error = 6                    # report literal error
        continue
    elif opStyle == 7:                   # e.g., LDRI r4,r0,23 or LDRI r4,(r0,23)
        e1 = regTest(predicate[0])
        e2 = regTest(predicate[1])
        literal = predicate[2]
        e3 = litCheck(literal)
        if e1 != 0 or e2 != 0:
            error = 4
        if e1==0 and e2==0 and e3 !=0:   # If registers are OK but not literal
            error = 6                    # report literal error
        continue

在查看输入验证之后,我们现在来看看我们如何在模拟期间控制显示有用的信息。

跟踪和断点

当你模拟计算机时,你必须展示模拟过程中发生的事情。因此,你必须回答以下三个问题:

  • 你何时显示数据?

  • 你如何显示数据?

  • 你显示什么?

当你完成这一部分后,你将能够构建自己的指令跟踪功能。

CPU 模拟器一次执行一条指令。在指令结束时,计算机的状态(即其寄存器、状态标志和内存)可以显示。这种模式称为单步执行。每次你按下Enter键,就会执行一条指令,并在屏幕上显示机器的状态。

逐条顺序执行指令有局限性。如果有一个 3 条指令的循环,比如清除数组中的 1,000 个位置,你会期望有人按 3,000 次Enter键来跟踪这个操作吗?我们需要一种方法来跳过程序中无聊的部分,直接跳到有趣的部分——也就是说,一种机制,允许我们将一些指令作为一个批次执行,而无需在每条指令执行后按return键或打印执行结果。

假设我们创建一个变量,trace,然后在execute循环的末尾,如果trace1,则打印适当的数据;如果trace = 0,则跳到下一条指令而不打印数据:


trace = 1                                    # Trace mode active when trace = 1
run = 1# run = 1 to execute program
pc = 0# Initialize program counter
while run == 1:# Main program loop
    read instruction
    execute instruction
    if trace == 1: displayData()             # When in trace mode, print results

只有当trace = 1时,CPU 状态才会在每条指令后打印。我们如何开启和关闭trace?关闭trace很简单;你只需要在单步执行时读取键盘输入,如果输入了特定的字符或字符串,就关闭trace。然而,一旦trace0,我们就失去了控制,指令会一直执行,直到程序终止。

一种解决方案是设置一个跟踪计数,即再次开启跟踪之前要执行的指令数量;例如,输入T 10,将关闭跟踪,执行 10 条指令而不显示任何内容,然后再次开启跟踪。定义一个固定的指令数量来执行并不总是有帮助,因为它要求程序员在到达感兴趣的点之前计算要执行的指令数量。程序员可能并不总是知道这一点。

一个更好的解决方案是在达到汇编语言程序中的特定点时开启跟踪,这个点被称为断点。断点可以是程序计数器的值、标签或特定的指令。通常,你希望在显示机器状态时程序计数器的值存储在断点表中。执行将继续(没有任何显示),直到遇到断点并显示 CPU 状态。

以下 Python 代码片段展示了这种方法。它不是一个计算机模拟器,只有三条指令(noptesttest1),它们什么都不做,加上stop。该程序旨在展示实现单步执行和断点的可能方法。在每个指令周期结束时,有几种选择:

  • 显示执行此指令后的机器状态

  • 在执行下一个周期之前等待键盘输入

  • 在特定的断点(地址或指令)打印机器状态

以下代码使用不同的字体和背景阴影来突出显示代码的不同功能部分。前两个部分是变量设置和初始化,以及(阴影部分)主程序执行循环。这个循环除了遍历nop(无操作)指令外不做任何事情;testtest1仅用作标记。stop指令用于终止执行。

注意,在跟踪时,我们需要第二个程序计数器pcOld,因为pcfetch周期中会增加,我们需要在它被修改之前显示它:


def display():                                   # Display processor status
    if oldPC in breakTab: print('Breakpoint at %03x' %oldPC)   # if pc in the table
    print("PC = %03x" %oldPC,  ' Op-code = %s' %instruction)
    return()
opCodes = ['nop', 'test', 'test1', 'stop']       # Op-code set
traceCodes = []                                  # List of codes to be traced (initially empty)
mem = ['nop'] * 32                               # Initialize memory to NOPs
mem[10] = 'test'                                 # Dummy operation at 10
mem[20] = 'test'                                 # Dummy operation at 20
mem[25] = 'test1'                                # Dummy operation at 25
r = [0] * 4                                      # Set up 4 registers (not used)
pc = 0                                           # Initialize program counter
oldPC = 0                                        # Initialize previous program counter
run = 1                                          # Set run to 1 to go
trace = 1                                        # Set trace to 1 to single-step
count = 0                                     # Count is the number of cycles not displayed
breakTab = []                                    # Create table for breakpoints
while run == 1:                                  # PROGRAM LOOP
    instruction = mem[pc]                        # read instruction
    oldPC = pc                                   # Save current PC for display
    pc = pc + 1                                  # Increment PC
    # Do processing here                         # For experimentation (add stuff here)
    if pc == 32 or instruction == 'stop': run = 0 # End on stop instruction or max PC
    if trace == 0 and count != 0:                # Test for single-step mode
        count = count - 1                        # If not single-step, decrement counter
        if count == 0:                           # If count zero, return to single step mode
            trace = 1                            # Exit silent mode
            continue                             # Now drop to bottom of the loop
    if trace == 0 and pc in breakTab:            # If not single-step, check for breakpoint
        print('Breakpoint\n')                    # Print status at the breakpoint
        display()
    if trace == 0 and instruction in traceCodes: # If not single-step and opcode in table
        print('Trace Code')                      # Print status info
        display()
    if trace == 1:# If single-step with trace on
        display()                                # Display the status
        c = input('>> ')                         # Wait for keyboard input
        if c == '': continue                     # If it's just a return, continue
        elif c[0]== 't' and len(c) > 2 and c[2:].isdigit():
                                                 # Test for 't' and number
            count = int(c[2:])                   # Set the count for silent mode
            trace = 0                            # Turn off single-step
        elif c[0] == 'b' and len(c) > 2 and c[2:].isdigit():
                                                 # Test for b (set breakpoint)
            breakPoint = int(c[2:])              # Get breakpoint address and add to table
            breakTab.append(breakPoint)
        elif c == 'd':                           # Test for d to display breakpoint info
            print('Display status: breakpoints =', breakTab, \
                  'traced codes =',traceCodes)
        elif c in opCodes: traceCodes.append(c)  # Test for a valid opcode and add to list
print('\nProgram terminated')

初始时,trace被设置为1,表示我们处于单步模式。在显示程序计数器和指令后,程序等待键盘输入。执行此操作的代码如下:


    if trace == 1:                               # If single-step with trace on
        display()                                # Display the status
        c = input('>> ')                         # Wait for input from user
        if c == '': continue                     # If it's just a return, continue

如果输入是回车(即enter),则通过continue终止循环并执行下一个指令周期。如果你输入t后跟一个整数(例如,t 13),则整数会被转移到count变量,而t被设置为0。将t设置为0会关闭单步机制,指令将在不打印机器状态或等待每个周期末的键盘输入的情况下执行。在每个周期末,count变量会递减。当count变为0时,trace被设置为1,并重新进入单步模式。

如果你输入b后跟一个整数(例如,b 21),则在断点表中(一个字典)记录地址21的断点。你可以输入多个断点,并且它们将被保存在字典中。每个断点都是正在执行的程序中的地址。当程序计数器达到该地址时,会显示系统状态。例如,如果你输入序列b 12b 30t 50(每行一个),模拟器会在地址1230设置断点,然后执行 50 个周期而不显示任何数据。然而,如果在这一期间程序计数器变为1230,则会打印机器状态。

类似地,你可以输入一个将被加载到traceCodes表中的指令。这的行为与 PC 断点完全相同。当遇到traceCodes表中的指令时,会显示机器状态。因此,模拟器提供了四种模式:

  • 逐条执行指令

  • 执行n条指令而不显示系统状态(静默模式)

  • 在静默模式下执行指令,但在遇到断点地址时停止并显示

  • 在静默模式下执行指令,但在遇到特定操作码时停止并显示

当然,程序可以扩展到考虑寄存器数据、内存数据或任何事件组合的更奇特形式的断点。例如,你可以允许以下形式的断点:


If PC > 200 or PC < 300 and instruction = 'ADD'

一些模拟器允许你在指令流改变时设置断点——即在任何跳转、分支或子程序调用之后。这对于跟踪复杂程序的执行非常有用。

下面的输出是使用此程序片段的简短会话的结果。请记住,它的目的是展示涉及的原则,而不是一个实际工作的系统:


PC = 000  Op-code = nop           # Hit enter key and trace first instruction
>>
PC = 001  Op-code = nop
>>
PC = 002  Op-code = nop
>> t 4                            # Type t 4 to execute but skip printing 4 instructions
PC = 007  Op-code = nop           # Note how PC jumps from 2 to 7
>>
PC = 008  Op-code = nop
>>
PC = 009  Op-code = nop
>> b 12                           # Type b 12 to insert breakpoint at PC = 12
PC = 00a  Op-code = nop
>> t 6                            # Type t 6 to execute 6 instructions without display
Breakpoint
Breakpoint at 00c                 # No display continues until PC = 12 (0xC)
PC = 00c  Op-code = nop           # Processor printed for PC = 12
PC = 011  Op-code = nop           # Execution continues until PC = 17 (0x11)
>> test1                          # Type 'test1' to make instruction test1 a breakpoint
PC = 012  Op-code = nop
>> t 15                           # Type t 15 to execute but skip printing 15 instructions
Trace Code
PC = 019  Op-code = test1         # Execution continues until 'test1' encountered at PC = 25 (0x19)
Program terminated

下一步是演示如何通过添加新指令来扩展模拟计算机。我们演示了哪些代码部分需要修改,以及如何创建任意复杂性的新指令。例如,如果你正在开发主要用于下棋的计算机,你可以创建一个指令ROOK R1,R2,该指令获取寄存器R2中罗克的位置,并计算它可以移动到且合法的位置,并将它们放入R1

添加新指令

到目前为止,我们已经为 TC1 提供了一套基本的指令。在本节中,我们展示了如何向 TC1 的指令集中添加一条新指令,以便了解扩展指令集所涉及的内容。实际上,这是一个非常直接的任务。

第一步是选择一个助记符和唯一的操作码,并将它们插入到代码表中。我们已经安排了指令集,留出一些未分配的代码(例如,以11开头的代码)。第二步是编写解释新指令的代码。

第一个示例——将两个内存位置按升序排列

让我们创建一条指令,该指令从内存中的两个连续位置获取内容,并将较大的数字放在较低地址的第一个位置(即,对它们进行排序)。这条指令接受一个参数,即指针寄存器,并读取寄存器所指向的数值。我们假设寄存器是r[i]。该指令将这个数字与地址r[i] + 1存储的值进行比较,如果第二个数字比r[i]位置的数字大,则交换它们。在伪代码中,如下所示:


temp ← mem[r[i]]                    # Save first number. Assume ri is the pointer register
if mem[r[i] + 1] > temp             # If the second number is greater than the first
   mem[r[i]]   ← mem[r[i]+1]        # then put the second number in the first location
   mem[r[i]+1] ← temp               # and the first number in the second location

我们将把这个指令命名为ORD(排序数字),并写成ORD r0。二进制代码是1110000 rrr 00…0(其中rrr是 3 位寄存器字段)并分配给这个指令。'ORD':(8,112)被输入到 Python 指令字典中。操作码是112,二进制参数分配代码是1000(即 8),因为只需要一个参数Rd

新指令在程序的执行部分被检测到:

 elif opCode == 0b1110000:           # Test for 'ORD' ( 112 in decimal and in 1110000 binary)

这随后是前面伪代码的 Python 版本。我们可以写出以下内容:


temp = mem[r[dest]]                          # dest is the destination register
if mem[r[dest] + 1] > temp:
    mem[r[dest]] = mem[r[dest]+1]
    mem[r[dest] + 1] = temp

这能有多简单?以下代码提供了一个指令的测试平台。我们用随机数填充内存,然后请求一个内存地址。该地址的数据与下一个地址的数据交换,以创建一个有序对。请注意,此示例不使用全局变量:寄存器和内存作为参数传递给函数。为了简化测试,假设内存地址在 r[0] 寄存器中:


import random                                # System library to generate random numbers
mem = [0] * 32                               # Set up memory
r   = [0] * 8                                # Set up registers
for i in range(32): mem[i] = random.randint(0,256) # Fill memory with random numbers
for i in range(32): print(i, mem[i])
def ord(reg,rD,memory):                      # Pass registers, memory, and register number
    temp = memory[reg[rD]]                   # rD is the destination register
    if memory[reg[rD] + 1] > temp:
        memory[reg[rD]] = memory[reg[rD]+1]
        memory[reg[rD] + 1] = temp
    return()
go = True
r  = [0] * 8
rD = 0
while go:
    x = input('Type address of first: ')
    r[rD] = int(x)
    if r[rD] > 30:                           # Exit on memory address 31 or higher
        print('End of run')
        break
    else:
        print('Before: mem[x] = ',mem[r[rD]], 'next = ',mem[r[rD] + 1])
        ord(r,0,mem)
        print('After:  mem[x] = ',mem[r[rD]], 'next = ',mem[r[rD] + 1])

第二个示例 – 添加位反转指令

让我们在 TC1 指令集中添加一个更复杂的指令。假设你想要反转寄存器中位的顺序,使得 r0 中的二进制代码 1100111000101001 变为 1001010001110011。假设新的指令是 REV r0,它反转 r0 中的 16 位,并将结果返回到 r0

我们如何反转位?考虑四个位 1101,并假设它们在 T1 中(见图 7.1)。假设我们将位向左移动一位,这样离开 T1 左端的位就进入 T2 的右端,然后我们将 T2 向右移动一位。我们重复这个操作四次。图 7**.1显示了我们会得到什么:

图 7.1 – 将一个寄存器的输出移入第二个寄存器的输入以反转字符串

图 7.1 – 将一个寄存器的输出移入第二个寄存器的输入以反转字符串

我们已经反转了位序。如果需要移位的寄存器是 op1,那么我们可以将 Python 代码编写如下。这段代码是一个函数,可以从指令解释器中调用:


def reverseBits(op1):                      # Reverse the bits of register op1
    reversed = 0                           # The reversed value is initialized
    toShift  = r[op1]                      # Read the register contents
    for i in range(0,16):                  # Repeat for all 16 bits
        bitOut   = toShift & 0x8000        # Get msb of word to reverse
        toShift  = toShift << 1            # Shift source word one place left
        reversed = reversed >> 1           # Shift result one place right
        reversed = reversed | bitOut       # Use OR to insert bit in lsb of result
    return(reversed)

我们现在可以修改 TC1 的代码以包含这个功能。这里有三个步骤:

  1. 'REV':(8,113),到 codes 字典中。8 表示二进制的 1000,并通知计算机反向指令需要通过指令指定目标 rD 寄存器。113 是操作码,二进制表示为 0b1110001

  2. 步骤 2:在操作码解释列表中插入新的条目:

    elif 'code ' == 0b1110001: r[op0] = reverseBits(op0)
    

这检查当前指令是否为 0b1110001(即十进制的 161)并调用 reverseBits() 函数来执行所需的操作。

  1. reverseBits 函数添加到 Python 代码中。此指令用反转的位替换 rD 寄存器中的数据。

假设我们想要一个非破坏性指令,该指令不会覆盖包含要反转的位的寄存器 – 即 REV r0,r1。我们需要进行哪些更改?

首先,我们需要一个新的指令格式代码。我们必须指定两个寄存器:源寄存器和目标寄存器。现在目录中的代码将是 'REV': (12,113),因为操作码参数值将是二进制的 1100 或十进制的 12。其他更改将是针对指令解释器的:

elif 'code' == 0b1110010: R[dest] = reverseBits(op1)

注意,我们已经为了最小化代码更改而更改了指令格式(在这种情况下,只是将源寄存器从 op0 更改为 op1)。

一个新的比较操作

假设你正在对一个字符串进行操作,需要找到字符串的中间位置。你可以通过从两端逐步接近中间位置来完成这个操作。但是有两种类型的中间位置。字符数量为奇数的字符串在其中间有一个字符。字符数量为偶数的字符串没有中间字符;它有两个相邻的字符。考虑以下两个例子:

String 1: 1234567字符数量为奇数

String 2: 12345678字符数量为偶数

String 1 有奇数个字符,4 是中心。String 2 有偶数个字符,4 和 5 位于中间的两侧。

假设我们正在使用两个指针遍历一个字符串,每个指针位于一端。当我们从两端逐步接近时,一个指针向上移动,另一个指针向下移动。当我们到达中间时,指针要么相同(奇数长度),要么相差一个(偶数长度)。

如果有一个比较操作可以比较两个值,并在它们相同或第二个值比第一个值大 1 时返回相等,那将会很方便。新的指令CMPT比较一起)就是这样做的。例如,CMPT r4,r6会将z位设置为1,如果r4r6的内容相同,或者如果r4的内容比r6的内容少 1。执行此操作的代码如下:


    if mnemonic == "CMPT":
        z = 0
        if (r[rD] == r[rS1]) or (r[rD] == r[rS1] + 1): z = 1

如你所见,这执行了两个指针测试,一个是等于测试,另一个是加 1 的测试,并使用布尔or运算符组合测试结果;也就是说,如果指针是xy,那么测试为真当x = y为真或者x + 1 = y为真。

这个指令在真实处理器中并未实现。为什么一个很好的想法没有被实现呢?首先,它只会在需要这种特定操作的少数程序中使用。它占据了芯片上几乎从未使用的硅空间,这是一种资源的浪费。其次,机器代码主要是由编译器生成的,设计出能够有效使用这种特殊操作(如本例所示)的编译器并不容易。第三,这个指令执行了三个操作:比较pq,将q加 1,比较pq+1。因此,它的执行时间比单操作指令长。这降低了计算机的效率。

在下一节中,你将了解到具有可变长度字段的指令的概念。真实机器并不具备这种功能。包含这一节的原因是为了展示指令解码和位处理。

可变长度指令

这一小节提供了关于实验指令及其格式的想法,并扩展了你对于指令、其结构和在创建指令集时涉及的权衡的理解。它并不是为了展示一个真实的计算机。

与许多计算机一样,TC1 的指令码中有固定长度的字段;也就是说,每个字段分配的位数是固定的,并且不会随指令而变化。字面量字段始终有 16 位,即使当前指令不需要字面量也是如此。确实很浪费。由于 TC1 的目的是实验,我们展示了如何使寄存器的数量可变(即用户定义)。增加更多的寄存器可以通过减少内存访问次数来加速计算。然而,这也有代价;你从哪里获得指定寄存器所需的额外位?你是从指令码字段中取出额外的寄存器位(减少不同指令的数量),还是从字面量字段中取出(减少单条指令中可以加载的最大字面量大小)?或者你实现多个寄存器组,并临时切换到新的寄存器集(称为分页)?

在这里,我们提供了一些用于实验可变寄存器大小的代码。这不是一个完整的程序。它所做的只是让你输入寄存器字段的尺寸,然后通过创建一个虚拟指令来运行测试。这是一个虚拟指令,因为指令码被设置为 1111110,字面量字段全部为零。它只是测试了在指令中适当位置放置寄存器字段并自动调整字面量字段长度的能力。

图 7.2 提供了该程序单次运行的输出。输入项以粗体显示。你可以看到寄存器字段已被选为 3 位、3 位和 5 位宽。指令是 ADD R7,R2,R31(注意,我们只提取了 7231,因为我们对实际的指令不感兴趣):

图 7.2 – 变长字段演示

图 7.2 – 变长字段演示

最终的二进制指令以不同的样式给出,以便于清晰。你可以看到寄存器字段已放置在指令的正确位置,而剩余的位(字面量字段)用零填充。

似乎拥有不同宽度的寄存器字段有些奇怪。这意味着指令中的某些参数可以访问比其他参数更多的寄存器。这种功能可能很有用;例如,你可以将一些寄存器用作专用寄存器(例如,栈指针),或者它们可以用来存储频繁访问的常量(例如 012):


# 31 Aug 2020 TESTING a variable format instruction set V1
x = input("Enter three width for: rD,rS1,rS2 (e.g., 2,2,3) >> ")
x = x.replace(' ',',')
x = x.split(",")                          # Get register sizes and convert list into tokens
x1 = int(x[0])                            # First register size rD
x2 = int(x[1])                            # Second register size rS1
x3 = int(x[2])                            # Third register size rS2
y = (x1,x2,x3)                            # Convert data size elements into a tuple
z = input("Enter three register operands for: rD,rS1,rS2 (e.g. R1,R3,R2)>> ")
opCode = 0b1111110                        # Dummy 7-bit binary opcode
z = z.replace(' ',',')
z = z.split(",")
t1,t2,t3 = 0,0,0                          # t1,t2,t3 are up to three tokens in the predicate
t1 = int(z[0][1:])                        # Extract three parameters
t2 = int(z[1][1:])
t3 = int(z[2][1:])
print ('Register widths: rD = ',t1, 'rS1 = ',t2,'rS2 = ',t3)   # Print the registers
opCode = opCode << x1 | t1                # Insert the rD field
opCode = opCode << x2 | t2                # Insert the rS1 field
opCode = opCode << x3 | t3                # Insert the rS2 field
intLen = 32 - 7 - x1 - x2 - x3            # Calculate the length of the literal field
opCode = opCode << intLen                 # Shift left by literal size to create 16-bit instruction
print("opCode",bin(opCode))               # Print the result

使用一些示例值运行此代码会得到以下输出(图 7.3)。正如你所见,寄存器文件已被插入到指令码中:


Enter three width for: rD,rS1,rS2 (e.g., 2,2,3) >> 3,4,5
Enter register operands for: rD,rS1,rS2 (e.g.,R1,R3,R2)>> R4,R6,R30
Register widths: rD =  4 rS1 =  6 rS2 =  30
opCode 0b1111110 1000110111100000000000000

图 7.3 – 变长操作数字段演示

图 7.3 – 变长操作数字段演示

变长指令机器

在本文中,我们已经展示了具有固定长度指令字的机器。基于这种范例的计算机通常属于 RISC 类别。然而,经典的 CISC 机器(从朴素的 8080 和 6800 到不那么朴素的 8086 和 68000 微处理器)具有可变长度的指令,正如我们之前已经指出的。考虑以下可变长度指令流的示例,其中1代表 1 个字长的指令,2代表 2 个字长的指令,依此类推(图 7.4):

Figure 7.4 – 指令流的可变长度操作码

图 7.4 – 指令流的可变长度操作码

当指令执行时,它们必须被解码,并将适当数量的字节附加到当前指令。这种方法的一个问题是它使得前瞻处理变得困难,因为你不知道未来的指令从哪里开始和结束,直到你解码了当前指令。

在这里,我们将演示一个非常简单的使用 8 位字和指令长度可以是 8、16、24 或 32 位的可变长度机器。例如,nop指令是 8 位,branch指令是 16 位,move指令是 24 位,add指令是 32 位。指令本身是 8 位(在演示中,我们为了简单起见只使用了 5 位)。指令被读取,并且两个最高有效位确定该指令所需的字节数。

此机器使用的寄存器数量……没有!为了简单和有趣,我们决定使所有指令基于内存。

因此,我们需要两个计数器:一个用于计数指令,另一个用于计数字节。例如,表 7.1中的指令序列演示了指令地址(顺序)和指令第一个字节的内存地址。在这里,指令从 1 字节(stop)到 4 字节(add)不等:

代码 指令地址 内存地址
ld``28,7 0 0
ld``27,2 1 3
ld``26,1 2 6
add 28,28,26 3 9
dec 26 4 13
bne 3 5 15
stop 6 17
下一个空闲空间 7 18

表 7.1 – 可变长度代码的指令和内存地址

在这里,我们使用了简单的数字地址。一些地址是字面字节;例如,ld 28,7表示将内存位置28加载为数字7

以下代码提供了一个实现此类计算机的程序。请注意,实际程序以列表形式提供。此程序没有 TC1 的文本灵活性;它是一个简单的演示。指令以小写文本输入,参数之间用逗号分隔。所有值均为十进制。然而,允许有限的符号名称;例如,abc: equ 12将数字12绑定到符号名称abc:。注意名称后面需要冒号。

所有数据要么是数字,要么是内存地址;例如,add 12,13,20将内存位置13的内容加到内存位置20的内容上,并将和放入内存位置12

一个分支需要一个实际的地址。分支是绝对的(直接)的,而不是程序计数器相关的。要分支到地址16开始的指令,你写下bra 16。然而,支持符号名称,你可以写bra abc:,前提是目标被标记为abc:

在这个模拟器中,每当读取一个新的指令时,指令计数器增加一。然而,每当向这个指令添加一个新的字节时,内存计数器就会增加。添加的字节可能是 1、2、3 或 4。

由于你必须给出一个字节分支地址,你不仅必须计算分支的指令数量,还要计算分支的字节数。为此,我们创建一个映射表,将指令地址映射到字节地址。这个表被称为map[]


print ('Demonstrating multiple length instructions. Version 3 December 8 2022 \n')
mem     = [0] * 128

lookUp{}字典使用二进制键和由助记符组成的值来描述每个指令。allOps{}字典由一个键(助记符)和一个包含指令长度和操作码的元组组成:


lookUp  = {0b00001:'nop',0b00010:'stop',0b01000:'inc',0b01001:'dec',  \
           0b01010:'bra',0b01011:'beq',0b01100:'bne',0b10000:'mov',   \
           0b10001:'cmpl',0b10010:'cmp',0b10011:'ld',0b10100:'st',    \
           0b11000:'add',0b11001:'sub'}
allOps  = {'nop':(1,1),'stop':(1,2),'inc':(2,8),'dec':(2,9),'bra':(2,10),   \
           'beq':(2,11),'bne':(2,12),'mov':(3,16),'ld':(3,19),              \
           'cmpl':(3,17),'cmp':(3,18),'add':(4,24),'sub':(4,25),'test':(0,0)}
# NOTE that progS is the actual program to be executed. It is embedded into the program
progS   = ['this: equ 26','ld this:,7','that: equ 28','ld 27,2', \
           'ld that:,1','loop: add 28,28,26', 'dec 26','bne loop:','stop']
symTab  = {}                                          # Label symbol table
prog    = []                                          # progS is prog without equates
for i in range (0,len(progS)):                        # Process source code for equates
    thisLine = progS[i].split()                       # Split source code on spaces
    if len(thisLine) > 1 and thisLine[1] == 'equ':    # Is this line an equate?
         symTab.update({thisLine[0][0:]:thisLine[2]}) # Store label in symbol table. 
    else: prog.append(progS[i])                  # Append line to prog unless it's an equate

在移除等价项之后的下一步是清理源代码并处理标签:


for i in range (0,len(prog)):             # Process source code (now without equates)
    prog[i] = prog[i].replace(',',' ')    # Remove commas
    prog[i] = prog[i].split(' ')          # Tokenize
    token1 = prog[i][0]                   # Get first token of instruction
    if token1[-1] == ':':                 # If it ends in :, it's a label
        j = str(i)                        # Note: we have to store i as a string not an integer
        symTab.update({token1:j})         # Add label and instruction number to symbol table
        prog[i].pop(0)                    # Remove label from this line. NOTE "pop"
print('Symbol table: ', symTab)
map = [0] * 64                            # Map instruction number to byte address

我们现在遍历代码,但不是在执行模式下。我们创建一个内存计数器,mc,它作用类似于程序计数器,但用于跟踪内存中的指令:


mC  = 0                                 # Memory counter (store code from 0)
for iC in range (0,len(prog)):          # Step through the program
    instruction = prog[iC]              # Read an instruction. iC = instruction counter
    mCold = mC                          # Remember old memory counter (address of first byte)
    map[iC] = mC                        # Map byte address to instruction address
    mnemonic = instruction[0]           # The first token is the mnemonic
    mem[mC] = allOps[mnemonic][1]       # Store opcode in memory
    mC = mC + 1                         # Point to next free memory location
    numOperands = allOps[mnemonic][0] - 1    # Get the number of operands from dictionary
    if numOperands > 0:                 # If one or more operands
        if instruction[1] in symTab:    # See if operand is in symbol table
            instruction[1] = symTab[instruction[1]]   # If it is, convert into as string
        mem[mC] = int(instruction[1])   # Store address in memory as integer
        mC = mC + 1                     # Bump up byte counter
    if numOperands > 1:                 # Do the same for two operands
        if instruction[2] in symTab:    # See if operand is in symbol table
            instruction[2] = symTab[instruction[2]]    # Convert to address as string
        mem[mC] = int(instruction[2])
        mC = mC + 1
    if numOperands > 2:                 # Now deal with 3-operand instructions
        if instruction[3] in symTab:    # See if operand is in symbol table
            instruction[3] = symTab[instruction[3]]   # If it is, convert to string
        mem[mC] = int(instruction[3])
        mC = mC + 1
    instPrint =  ' {0:<15}'.format( (' ').join(instruction)) # reformat instruction
    print('iC=', iC,'\t', 'Op =', mnemonic, '\tNumber of operands =',  \
           numOperands, '\t mC =', mCold, '\tInstruction =',           \
           instPrint, 'memory =', mem[mCold:mC])
print('Memory (in bytes) =', mem[0:40], '\n')
                                               # EXECUTE THE CODE
print('\nCode execution: press enter \n')
pc, iC, z = 0, 0, 0                            # Initialize program and instruction counters

现在我们可以通过使用程序计数器逐步通过内存中的指令来执行代码。然而,程序计数器是在读取当前指令之后根据每个指令的长度增加的:


run = True
while run:                                       # Instruction execution loop
    pcOld  = pc                                  # Remember pc at start of this cycle
    opCode = mem[pc]                             # Read opcode
    opLen  = (opCode >> 3) + 1                   # Get instruction length from opcode
    if opCode == 0b00010:                        # Test for stop
        run = False                              # Terminate on stop instruction
        print('Execution terminated on stop')    # Say 'Goodbye'
        break                                    # and exit the loop
    operand1, operand2, operand3 = '', '', ''    # Dummy operands (null strings)
    if opLen > 1: operand1 = mem[pc + 1]
    if opLen > 2: operand2 = mem[pc + 2]
    if opLen > 3: operand3 = mem[pc + 3]
    pc = pc + opLen
    iC = iC + 1
    mnemonic = lookUp[opCode]

在查找操作码之后,我们使用if...elif结构来检查后续的助记符,以确定当前操作。注意,通过 Python 的 pass 实现的空操作被实现为不执行任何操作:


    if   mnemonic == 'nop': pass
    elif mnemonic == 'inc': mem[operand1] = mem[operand1] + 1
    elif mnemonic == 'dec':
        z = 0
        mem[operand1] = mem[operand1] - 1
        if mem[operand1] == 0: z = 1
    elif mnemonic == 'bra':  pc = map[operand1] # Map instruction address to byte address
    elif mnemonic == 'beq' and z == 1: pc = map[operand1]
                                                 # Map instruction address to byte address
    elif mnemonic == 'bne' and z == 0: pc = map[operand1]
                                                 # Map instruction address to byte address
    elif mnemonic == 'ld':  mem[operand1] = operand2
    elif mnemonic == 'mov': mem[operand1] = mem[operand2]

cmp比较操作从两个操作数中减去,如果结果是零,则将 z 位设置为1。否则,z被设置为0


    elif mnemonic == 'cmp':
        diff = mem[operand1] - mem[operand2]
        z = 0
        if diff == 0: z = 1
    elif mnemonic == 'cmpl':
        diff = mem[operand1] - operand2
        z = 0
        if diff == 0: z = 1
    elif mnemonic == 'add': mem[operand1] = mem[operand2] + mem[operand3]
    elif mnemonic == 'sub':
        mem[operand1] = mem[operand2] - mem[operand3]
        z = 0
        if mem[operand1] == 0: z = 1

execute循环结束时,我们从键盘获取输入。这只是在执行下一个指令之前引入一个等待,直到按下Enter/return键。剩余的 Python 代码格式化输出:


    x = input('... ')
    xxxx =  mnemonic + ' ' + str(operand1) + ' ' + str(operand2) \
    + ' ' + str(operand3)
    instPrint =  ' {0:<15}'.format(xxxx)                   # re-format the instruction
    print ('iC=',iC-1,'\tpc=',pcOld,'\tOp=',mnemonic,'z=',z,      \
           '\tmem 24-35=',mem[24:36],'\tInstruction = ', instPrint)

我们只是简要地触及了可变长度指令的主题。原则上,这是一个非常简单的想法。指令被读取、解码和执行。随着每个指令被装入计算机,它必须被解码,程序计数器必须根据当前指令占用的字数前进。在实践中,这会产生问题,因为分支地址不再只是从分支到指令的数量,还包括这些指令占用的内存位置数量。

摘要

上一章介绍了 TC1,这是一个基于 Python 的计算机模拟器,可用于开发和测试指令集架构。在这一章中,我们更深入地探讨了模拟器设计方面的内容。

我们研究了如何创建新的指令并将它们添加到 TC1 的指令集中。执行大量专用计算的先进指令曾经是经典 CISC 处理器的领域,例如摩托罗拉 68K 系列。然后,随着 RISC 架构的兴起及其对简单性和单周期指令的强调,CISC 处理器似乎即将过时。然而,许多现代计算机已经集成了用于特殊应用(如数据编码、图像处理和人工智能应用)的复杂指令。

我们更深入地研究了如何检查模拟器的输入并确保可以检测到数据和指令中的错误。

我们还探讨了 Python 编程的各个主题,例如数据格式化。原则上,以你想要的方式打印数据是很容易的。实际上,这要复杂一些(至少因为存在几种数据格式化的方法)。

本章扩展了在程序执行时跟踪程序的概念,并演示了在模拟器运行期间打印所需查看的数据所涉及的一些技术。

除了查看新的指令外,我们还探讨了可变长度指令的概念。我们从演示如何通过在运行时更改每个寄存器地址字段中的位数来改变可寻址寄存器的数量开始。这在指令集设计中(目前)不是一个现实的因素,但曾经有一段时间寄存器窗口变得流行,你确实可以扩展寄存器的数量。

当每条指令都可以是计算机字长整数倍时,我们引入了可变长度指令的概念。这种方法允许指令具有无限复杂的级别,但代价是更复杂的解码机制。我们演示了一种原始的可变指令长度机器的设计,它可以很容易地扩展到具有 TC1 完整复杂性的模拟器。

下一章回到模拟器,探讨了不同类型架构的几个模拟器。

第八章:其他架构的模拟器

在本章中,您将学习如何创建不同指令集架构的模拟器,例如基于栈的计算机和经典的 CISC。

在描述了一个简单的基于栈的计算器 TC0 之后,我们将介绍一个单地址格式的模拟器。大多数操作发生在累加器(即寄存器)和内存位置的内容之间;例如,ADD Y意味着将内存位置 Y 的内容加到累加器中。术语累加器表示加法结果累积的位置。早期的微处理器在硅芯片上缺乏多个寄存器的空间,所有数据都必须通过一个或两个累加器。

之后,我们将模拟一个 CISC 架构,它是基于累加器的机器的扩展,其中可以在内存内容和片上寄存器的存储内容上执行操作。

最后,我们将展示 TC4 的代码。这是一个非冯·诺依曼机器的模拟器,具有独立的地址和数据存储器,并且地址和数据字长度不同。

本章我们将涵盖以下主题:

  • TC0:基于栈的计算器

  • TC2:单地址累加器机器

  • TC3:具有寄存器到内存架构的 CISC 机器

  • 完整的 TC3 代码

  • 算术逻辑单元(ALU)

  • 最后一个示例:TC4

技术要求

您可以在 GitHub 上找到本章使用的程序,网址为github.com/PacktPublishing/Practical-Computer-Architecture-with-Python-and-ARM/tree/main/Chapter08

TC0:基于栈的计算器

我们将从一个非常简单的基于栈的计算器开始。在这里,我们将介绍一种零地址机器,它通过在栈上存储数据来避免显式操作数地址。我们包含基于栈的计算机的概念有两个原因。首先,它是许多经典计算器、编程语言(FORTH)和经典计算机(Burroughs B5000)设计的基础。其次,构建基于栈的计算机非常简单,您可以实验这类计算机。实际上,基于栈处理器的元素可以轻松地集成到任何计算机中。在传统计算机中,两个元素通过如ADD A,B,C这样的操作相加。在基于栈的计算机中,两个元素通过ADD相加。不需要操作数地址,因为要相加的元素是栈顶的两个元素。

我们在这里描述的计算机被称为 TC0,以表明它是一个原型模拟器,而不是完整的模拟器(它无法执行条件操作)。

栈是一种以队列形式存在的数据结构。项目从顶部进入队列,并以相反的顺序离开队列,即它们进入的顺序。它被称为,因为它表现得就像一叠纸张。

栈提供了两种操作:push,即将项目添加到栈中,和 pull(或 pop),即将项目从栈中移除。

对单个元素的操作(例如,取反)应用于栈的 顶部 元素。对两个操作数进行操作的操作应用于栈的 顶部元素(TOS);例如,通过从栈中拉出两个操作数,执行加法,然后将结果推回栈中来实现加法。图 8**.1 展示了在评估 P = (A + B)×(C – B – D) 时栈的行为。

图 8.1 – 评估 (A + B)×(C – B – D) 时发生的操作序列

图 8.1 – 评估 (A + B)×(C – B – D) 时发生的操作序列

表 8.1 展示了如何使用 PUSHPULLADDSUBMUL 栈操作来执行 P = (A + B)×(C – B – D) 的计算。除了算术操作外,还有两种常见的栈操作 DUP(复制)和 SWAPDUP 操作会复制 TOS 上的项目并将其推入栈中(即,栈顶被复制)。SWAP 操作交换 TOS 和 Next on Stack (NOS) 的值。


Operation        The stack as a Python list (bold is top of stack)
PUSH A           stack = [x,x,x,x,x,x,x,A]
PUSH B           stack = [x,x,x,x,x,x,B,A]
ADD              stack = [x,x,x,x,x,x,x,A+B]
PUSH C           stack = [x,x,x,x,x,x,C,A+B]
PUSH B           stack = [x,x,x,x,x,B,C,A+B]
SUBTRACT         stack = [x,x,x,x,x,x,C-B,A+B]
PUSH D           stack = [x,x,x,x,x,D,C-B,A+B]
SUBTRACT         stack = [x,x,x,x,x,x,D-C-B,A+B]
MULTIPLY         stack = [x,x,x,x,x,x,x,(D-C-B)(A+B)]
PULL result      stack = [x,x,x,x,x,x,x,x]

表 8.1 – 评估 (A + B)×(C – B – D) 的代码

为了简化模拟器,每个指令都存储在一个包含操作和内存地址(对于 PUSHPULL)的 Python 列表中。这不是一个实用的模拟器;它是一个使用栈来处理算术操作的演示,也是对后续章节中栈的介绍。

一个名为 1231 的寄存器,将一个元素压入栈中会将其存储在地址 1230,因为栈是向低地址增长的。

在某些实现中,栈指针指向栈顶之上的 下一个空闲位置。我们将使用 Python 中的列表 stack[] 来表示栈。栈指针是 sp,将项目 A 压入栈的操作如下:


sp = sp – 1       # Decrement the stack pointer. Point to the next free location above TOS
stack[sp] = A     # Load the new value, A, on the stack in this location

记住,栈指针是递减的,因为栈是向低地址增长的。如果从栈中弹出一个项目,则逆操作如下:


A = stack[sp]     # Retrieve the item at the top of the stack
sp = sp + 1       # Move the stack pointer down

这些是互补操作。一个 pull 操作取消一个 push。考虑评估一个表达式。图 8**.2 展示了在评估 X = (A + B)×(C – D) 时栈的状态。

图 8.2 – 评估 X = (A + B)×(C – D) 时发生的操作序列

图 8.2 – 评估 X = (A + B)×(C – D) 时发生的操作序列

下一步是演示如何实现一个简单的计算器,TC0,它基于栈。

TC0:Python 栈机器

我们可以在 Python 的栈机器上表示加法 y3 = y1 + y2 如下:


y1 = stack[sp]        # Retrieve the item at the top of the stack (y1 and y2 are on the stack)
sp = sp + 1           # Move the stack pointer down
y2 = stack[sp]        # Retrieve the item at the top of the stack
y3 = y1 + y2          # Add the two values
stack[sp] = y3        # Store the result on the stack

我们采取了一个捷径。我们本可以从栈中取出两个元素,将它们相加,然后将结果压入栈中。相反,我们将结果放回到第二个操作数的位置,并节省了两个栈指针的移动。以下 Python 代码演示了一个非常简单的栈机解释器。它不实现分支操作,因此它不是一个现实的计算机器。由于栈机通常在栈顶及其下面的元素上操作,第二个元素通常被称为 NOS。请注意,程序存储为列表的列表,每个指令由一个包含两个元素的列表(例如,['push', '2']))或一个包含单个元素的列表(例如,['``mul'])组成:


                                       # Stack machine simulator
prog = [['push',0],['push',1],['add'],   ['push',2],['push',1],           \
        ['sub'],   ['push',3],['sub'],   ['mul'],   ['push',4],           \
        ['swap'],  ['dup'],['pull',4],   ['stop']]
stack = [0] * 8                        # 8-location stack. Stack grows to lower addresses
mem   = [3,2,7,4,6,0]                  # Data memory (first locations are preloaded 3, 2,7, 4, 6)
run = True                             # Execution continues while run is true
pc = 0                                 # Program counter - initialize
sp = 8                                 # Initialize stack pointer to 1 past end of stack
while run:                             # Execute MAIN LOOP until run is false (STOP command)
    inst = prog[pc]                    # Read the next instruction
    pc = pc + 1                        # Increment program counter
    if   inst[0] == 'push':            # Test for push operation
         sp = sp - 1                   # Pre-decrement stack pointer
         address = int(inst[1])        # Get data from memory
         stack[sp] = mem[address]      # Store it on the stack
    elif inst[0] == 'pull':            # Test for a pull instruction
         address = int(inst[1])        # Get destination address
         mem[address] = stack[sp]      # Store the item in memory
         sp = sp + 1                   # Increment stack pointer
    elif inst[0] == 'add':             # If operation add TOS to NOS and push result
         p = stack[sp]
         sp = sp + 1
         q = stack[sp]
         stack[sp] = p + q
    elif inst[0] == 'sub':             # sub
         p = stack[sp]
         sp = sp + 1
         q = stack[sp]
         stack[sp] = q - p
    elif inst[0] == 'mul':             # mul
         p = stack[sp]
         sp = sp + 1
         q = stack[sp]
         stack[sp] = p * q
    elif inst[0] == 'div':             # div (note floor division with integer result)
         p = stack[sp]
         sp = sp + 1
         q = stack[sp]
         stack[sp] = p//q
    elif inst[0] == 'dup':             # dup (duplicate top item on stack)
         p = stack[sp]                 # get current TOS
         sp = sp - 1                   # and push it on the stack to duplicate
         stack[sp] = p
    elif inst[0] == 'swap':            # swap (exchange top of stack and next on stack)
         p = stack[sp]
         q = stack[sp+1]
         stack[sp] = q
         stack[sp+1]=p
    elif inst[0] == 'stop':            # stop
         run = False
    if sp == 8: TOS = 'empty'          # Stack elements 0 to 7\. Element 8 is before the TOS
    else: TOS = stack[sp]
    print('pc =', pc-1,'sp =',sp,'TOS =',TOS,'Stack',stack,'Mem',mem,'op',inst)

以下是从该程序输出的结果,显示了程序计数器、栈顶、NOS、栈本身、数据和正在执行的指令码。在周期之间发生变化的值以粗体显示:


pc=0 sp=7  TOS=3 Stack [0,0,0,0,0,0,0,3] Mem [3,2,7,4,6,0] op ['push',0]
pc=1 sp=6  TOS=2 Stack [0,0,0,0,0,0,2,3] Mem [3,2,7,4,6,0] op ['push',1]
pc=2 sp=7  TOS=5 Stack [0,0,0,0,0,0,2,5] Mem [3,2,7,4,6,0] op ['add']
pc=3 sp=6  TOS=7 Stack [0,0,0,0,0,0,7,5] Mem [3,2,7,4,6,0] op ['push',2]
pc=4 sp=5  TOS=2 Stack [0,0,0,0,0,2,7,5] Mem [3,2,7,4,6,0] op ['push',1]
pc=5 sp=6  TOS=5 Stack [0,0,0,0,0,2,5,5] Mem [3,2,7,4,6,0] op ['sub']
pc=6 sp=5  TOS=4 Stack [0,0,0,0,0,4,5,5] Mem [3,2,7,4,6,0] op ['push',3]
pc=7 sp=6  TOS=1 Stack [0,0,0,0,0,4,1,5] Mem [3,2,7,4,6,0] op ['sub']
pc=8 sp=7  TOS=5 Stack [0,0,0,0,0,4,1,5] Mem [3,2,7,4,6,0] op ['mul']
pc=9 sp=6  TOS=6 Stack [0,0,0,0,0,4,6,5] Mem [3,2,7,4,6,0] op ['push',4]
pc=10 sp=6 TOS=5 Stack [0,0,0,0,0,4,5,6] Mem [3,2,7,4,6,0] op ['swap']
pc=11 sp=5 TOS=5 Stack [0,0,0,0,0,5,5,6] Mem [3,2,7,4,6,0] op ['dup']
pc=12 sp=6 TOS=5 Stack [0,0,0,0,0,5,5,6] Mem [3,2,7,4,6,5] op ['pull',5]
pc=13 sp=6 TOS=5 Stack [0,0,0,0,0,5,5,6] Mem [3,2,7,4,6,5] op ['stop']

在下一节中,我们将查看一个更现实的机器,该机器实现了早期 8 位微处理器时代的简单累加器机器。

TC2:一个单地址累加器机器

在本节中,你将了解一种实现内存到寄存器架构的计算机。这是一台非常简单的机器,实现了单地址指令格式(类似于 20 世纪 70 年代的 8 位 CISC 微处理器)。

TC2 模型可以用来模拟在低成本计算机系统中发现的经典 8 位微处理器(例如,机械设备的控制器)。它还教会你关于计算机的简单性(计算机的)和由原始架构限制的软件的复杂性之间的权衡。

与现代 RISC 架构不同,这种计算机在累加器中的一个操作数和另一个操作数(可以是文字或内存内容)之间实现二元操作;例如,ADD M将内存位置M的内容加到累加器中,而ADD #5将一个文字加到累加器的内容中。这台计算机没有大量的一般用途寄存器。

单地址机器允许在累加器中的数据和内存中的数据之间进行操作。这与只允许在寄存器之间进行数据处理操作的 RISC 架构形成对比。加载和存储是 RISC 架构允许的唯一内存操作。这台计算机 TC2 实现了一个最小的指令集,以展示其操作。表 8.2 描述了指令集:

指令 操作 内存形式 文字形式 指令码
LDA 加载累加器 [A][``M] [A]L 0
STA 存储累加器 [M][``A] [M]L 0
ADD 向累加器中加 [A][A] + [``M] [A][A] + L 1
SUB 从累加器中减去 [A][A] - [``M] [A][A] – L 2
CLR 将累加器/内存加载为零 [A]0 [M]0 3
BRA 无条件分支到 L [PC]L 4
BEQ 零分支到 L 如果 Z = 1,则[PC]L 5
BNE 非零分支到 L 如果 Z = 0,则[PC]L 6
STOP 停止 7

表 8.2 – 寄存器到内存计算机的典型操作

在这里,[A]是累加器的内容,[M]是内存位置M的内容,L是一个立即数,如果减法的结果为零,则设置 Z 位。ML代表指令的立即数字段,它们是互斥的。你不能有一个同时包含ML操作数的指令。

模拟计算机教给我们很多关于将指令分割成各种字段以及如何实现指令的知识。在这个例子中,我们使用一个 3 位操作码,一个 1 位方向标志(用于LDASTA),它定义了数据移动的方向(向或从内存),以及一个 1 位模式标志,它选择是直接内存访问还是立即数。一个 5 位数字字段提供一个 0 到 31 范围内的整数,或一个内存地址。指令大小为 10 位,格式为CCC D M LLLLL,其中CCC是操作码字段,D是方向位,M是模式位,LLLLL是立即数或内存地址(图 8**.3)。这种极端的简单性使得编写一个微型的模拟器变得容易,同时也给用户留下了很多将代码扩展成更真实机器的机会。

图 8.3 – TC2 指令格式

图 8.3 – TC2 指令格式

TC2 模拟了一个存储程序计算机,它有一个单一内存,既可以存储程序也可以存储数据。32 位位置内存通过memory = [``0]*32初始化。

TC2 代码有一个设置部分和一个包含取指令和执行指令部分的while循环。代码中while循环部分的(指令取/执行周期)结构如下:

while run == True:
   operation         # Body of while loop operation
   .
   .
statement            # Next operation after the while loop

while循环中,我们有一个fetch阶段,随后是执行阶段。fetch阶段与我们已经描述的 CPU 相同。指令解码包含在这个阶段。指令解码通过移位和位掩码操作将 OpCode、Dir(即向或从内存的方向)、Mode 和 Immediate 分开:


    MAR = PC                     # PC to Memory Address Register
    PC = PC + 1                  # Increment PC
    MBR = Memory[MAR]            # Read instruction, copy to Memory Buffer register
    IR = MBR                     # Copy instruction to Instruction Register
    OpCode = IR >> 7             # Extract Op-Code frominstruction (bits 7 to 10)
    Dir  = (IR >> 6) & 1         # Extract data direction from instruction (0 = read, 1 = write)
    Mode = (IR >> 5) & 1         # Extract address mode from instruction (0 = literal, 1 = memory)
    Lit = IR & 0x1F              # Extract the literal/address field from the instruction

右移和 AND 操作从指令中提取字段;例如,从 10 位的CCCDMLLLLL指令中提取 3 位操作码,通过左移七位得到0000000CCC。方向位,Dir,通过执行六次左移得到000000CCCD,然后与 1 进行 AND 操作得到000000000D。这两个操作可以合并并写成如下:

(IR >> 6) & 1      # 使用>>进行 6 位右移,并使用 AND 操作符&与 1 进行 AND 操作

同样,我们通过执行Mode = (IR >> 5) & 1来提取模式位。最后,立即数已经就位,所以我们只需通过 AND 操作与0b0000011111(即IR & 0x1F)进行 AND 操作来清除其他位。

execute阶段,三个操作码位OpCode选择八种可能指令之一。当然,使用if … elif会更合适:


if   OpCode == 0:
     Code for case 0
elif OpCode == 1:
     Code for case 1
.
.
elif OpCode == 7:
     Code for case 7

每个操作码都由一个if语句保护。以下是加载和存储累加器指令的代码。我们将这视为一个操作,并使用方向标志Dir来选择LDA(从内存到累加器的方向)和STA(从累加器到内存的方向):


    if OpCode == 0:              # Test for Load A or Store A instruction
      if Dir  == 0:              # If direction bit is 0, then it's a load accumulator
         if Mode == 0:           # Test for literal or direct memory operand
            Acc  = Lit           # If mode is 0, then it's a literal operand
         else:                   #If mode is 1, then it's a memory access
            MAR = Lit            #Copy field (address) to MAR
            MBR = Memory[MAR]    #Do a read to get the operand in MBR
            Acc  = MBR           #and send it to the accumulator
      else:
          MAR = Lit              # If direction is 1 then it's a store accumulator
          MBR = Acc              # Copy accumulator to MBR
          Memory[MAR] = MBR      # and write MBR to memory

为了使代码更容易阅读,我们将其分为两个块(一个深灰色阴影,一个浅灰色阴影),由if Dir == 0语句保护。当方向标志为 0 时,指令是*加载累加器*,地址被复制到MAR,执行读取操作,然后将数据复制到MBR和累加器。如果方向标志为 1,指令是*存储累加器*,累加器被复制到MBR`并执行写入操作。

注意Mode标志的使用。当从内存加载累加器LDA时,模式标志用于将累加器加载为字面量或内存内容。当执行STA(存储累加器)时,模式标志被忽略,因为只能进行内存存储。

我们不需要描述ADDSUB操作,因为它们仅仅是加载和存储操作的扩展。我们包含了一个清晰的操作CLR,它根据Mode标志将累加器设置为 0 或将内存内容设置为 0。

现在我们将展示完整的模拟器代码。Memory[MAR]表示法意味着地址在MAR中的内存内容,并且方便地与我们所使用的 RTL 相同。在执行指令块中,交替的操作码以灰色和蓝色阴影显示,以方便阅读。

我们在内存中包含了一个小程序,包括测试几个指令的数据,包括加载和存储、加法、减法和分支。

TC2 有一个清除操作CLR,根据模式标志将累加器或内存内容设置为 0。这个简化的计算机只有一个 Z 位(没有 N 和 C 位)。

指令组(BRABEQBNE)将一个字面量加载到程序计数器中,以强制跳转。BRA执行无条件分支,而BEQ/BNE则根据 Z 位的状态(由加法和减法操作设置/清除)执行。分支目标地址是由字面量字段提供的绝对地址。

我们已经预留了最后一个指令操作码111作为停止(暂停)指令,它会跳出while循环并终止执行。一般来说,实际的 CPU 不需要暂停指令,尽管暂停指令可以用来强制将其置于待机模式,直到被外部事件(如键盘/鼠标输入或屏幕触摸)唤醒:

                                # The TC2: A primitive accumulator machine
mnemonics = {0:'LDA/STR', 1:'ADD', 2:'SUB', 3:'CLR', 4:'BRA', 5: \
               'BEQ', 6:'BNE', 7:'STOP'}
def progSet():
    global mem
    mem = [0] * 32              # The memory holds both instructions and data
  # Format  CCCDMLLLLL          # 000 LDA/STR, 001 ADD, 010 SUB, 011 CLR, 100 BRA, \
                                  101 BEQ, 110 BNE, 111 STOP
    mem[0]  =  0b0000110000     # LDA 16  [A]   = M[16]
    mem[1]  =  0b0010110001     # ADD 17  [A]   = [A] + M[17] 
    mem[2]  =  0b0001110010     # STA 18  M[18] = [A]
    mem[3]  =  0b0100000011     # SUB #3  [A]   = [A] - 3
    mem[4]  =  0b1010001000     # BEQ 8
    mem[5]  =  0b0000010010     # LDA #18 [A]   = 18
    mem[6]  =  0b0001110010     # STA 18  M[18] = [A]
    mem[7]  =  0b0110000000     # CLR     [A]   = 0  
    mem[8]  =  0b0000000010     # LDA #2  [A]   = 2  
    mem[9]  =  0b0100000010     # SUB #2  [A]   = [A] - 3
    mem[10] =  0b1010001101     # BEQ 12
    mem[11] =  0b0000001111     # LDA #15 LDA #18 [A] = 18 Dummy not executed  
    mem[12] =  0b1110000000     # STOP
    mem[16] =  0b0000000100     # 4 Data for test
    mem[17] =  0b0000000101     # 5 Data for test  
    mem[31] =  0b1110000000     # Ensure STOP operation
    return(mem)
run = True                  # run is True for code execution. Setting run to False stops the computer
PC  = 0                     # The program counter points to the next instruction to execute. Initially 0
z = 0                       # Initialize z-bit (note no n and c bits implemented)
mem = progSet()

现在我们已经将程序加载到内存中并设置了一些变量,我们可以进入fetch execute循环:


                                 # MAIN LOOP – FETCH/EXECUTE
while run:                   # This is the fetch/execute cycle loop that continues until run is False
    MAR    = PC                  # FETCH PC to mem Address Register
    pcOld  = PC                  # Keep a copy of the PC for display
    PC     = PC + 1              # Increment PC
    MBR    = mem[MAR]            # Read the instruction, copy it to the mem Buffer Register
    IR     = MBR                 # Copy instruction to Instruction Register – prior to decoding it
    OpCode = (IR >> 7) & 0x7     # Extract Op-Code from instruction bits 7 to 10 by shifting masking
    Dir    = (IR >> 6) & 1       # Extract data direction from instruction (0 = read, 1 = write)
    Mode   = (IR >> 5) & 1       # Extract address mode from instruction (0 = literal, 1 = mem)
    Lit     = IR & 0x1F          # Extract literal/address field (0 = address, 1= literal)
                             # EXECUTE The EXECUTE block is an if statement, one for each opcode
    if OpCode == 0:          # Test for LDA and STA (Dir is 0 for load acc and 1 for store in mem)
        if Dir == 0:             # If Direction is 0, then it's a load accumulator, LDA
            if Mode == 0:        # Test for Mode bit to select literal or direct mem operand
                Acc = Lit        # If mode is 0, then the accumulator is loaded with L
            else:                # If mode is 1, then read mem to get operand
                MAR = Lit        # Literal (address) to MAR
                MBR = mem[MAR]   # Do a read to get operand in MBR
                Acc = MBR        # and send it to the accumulator
        else:
            MAR = Lit            # If Direction is 1, then it's a store accumulator
            MBR = Acc            # Copy accumulator to MBR
            mem[MAR] = MBR       # and write MBR to mem
    elif OpCode == 1:              # Test for ADD to accumulator
        if Mode == 0:              # Test for literal or direct mem operand
            total = Acc + Lit      # If mode is 0, then it's a literal operand
            if total == 0: z = 1   # Deal with z flag
            else: z = 0
        else:                      # If mode is 1, then it's a direct mem access
            MAR = Lit              # Literal (address) to MAR
            MBR = mem[MAR]         # Do a read to get operand in MBR
            total = MBR + Acc      # And send it to the accumulator
        if Dir == 0: Acc = total   # Test for destination (accumulator)
        else: mem[MAR] = total     # Or mem
    elif OpCode == 2:              # Test for SUB from accumulator
        if Mode == 0:              # Test for literal or direct mem operand
            total = Acc – Lit      # If mode is 0 then it's a literal operand
        else:                      # If mode is 1 then it's a direct mem access
            MAR = Lit              # Literal (address) to MAR
            MBR = mem[MAR]         # Do a read to get operand in MBR
            total = Lit – MBR      # and send it to the accumulator
        if total == 0: z = 1       # Now update z bit (in all cases)
        if Dir == 0: Acc = total   # Test for destination (accumulator)
        else: mem[MAR] = total     # Or mem

以下块(深色阴影)实现了清除操作。这个指令不是严格必要的,因为你可以始终加载零或从 X 中减去 x。因此,一些计算机不包含清除指令。一些计算机允许你写入CLR然后替换为操作,如SUB X,X

    elif OpCode == 3:              # Test for CLR (clear Accumulator or clear mem location)
        if Mode == 0:              # If Mode = 0 Then clear accumulator
            Acc = 0
        else:
            MAR = Lit              # If Mode = 1
            mem[MAR] = 0           # Then clear mem location mem[Literal]
    elif OpCode == 4:              # Test for BRA Branch unconditionally
        PC = Lit - 1          # Calculate new branch target address (-1 because PC auto increment)
    elif OpCode == 5:              # Test for BEQ Branch on zero
        if z == 1: PC = Lit - 1    # If z bit = 1 then calculate new branch target address
    elif OpCode == 6:              # Test for BNE Branch on not zero
        if z == 0: PC = Lit - 1    # If z bit = 0 calculate new branch target address
    elif OpCode == 7:               # Test for STOP
        run = False                 # If STOP then clear run flag to exit while loop and stop

你可以争论说我们应该在这里插入一个中断或退出,因为如果我们没有在execute循环结束时遇到有效的操作码,源代码必须是无效的:


# End of main fetch-execute loop
    mnemon = mnemonics.get(OpCode)  # Get the mnemonic for printing
    print('PC',pcOld, 'Op ',OpCode, 'Mode = ', Mode, 'Dir = ',Dir, \
          'mem', mem[16:19], 'z',z, 'Acc', Acc, mnemon)

我们现在运行这个程序。运行此程序时的输出如下:


PC 0  OpCode  0 Mode =  1 Dir =  0 mem [4, 5, 0]  z 0 Acc 4  LDA/STR
PC 1  OpCode  1 Mode =  1 Dir =  0 mem [4, 5, 0]  z 0 Acc 9  ADD
PC 2  OpCode  0 Mode =  1 Dir =  1 mem [4, 5, 9]  z 0 Acc 9  LDA/STR
PC 3  OpCode  2 Mode =  0 Dir =  0 mem [4, 5, 9]  z 0 Acc 6  SUB
PC 4  OpCode  5 Mode =  0 Dir =  0 mem [4, 5, 9]  z 0 Acc 6  BEQ
PC 5  OpCode  0 Mode =  0 Dir =  0 mem [4, 5, 9]  z 0 Acc 18 LDA/STR
PC 6  OpCode  0 Mode =  1 Dir =  1 mem [4, 5, 18] z 0 Acc 18 LDA/STR
PC 7  OpCode  3 Mode =  0 Dir =  0 mem [4, 5, 18] z 0 Acc 0  CLR
PC 8  OpCode  0 Mode =  0 Dir =  0 mem [4, 5, 18] z 0 Acc 2  LDA/STR
PC 9  OpCode  2 Mode =  0 Dir =  0 mem [4, 5, 18] z 1 Acc 0  SUB
PC 10 OpCode  5 Mode =  0 Dir =  0 mem [4, 5, 18] z 1 Acc 0  BEQ
PC 12 OpCode  7 Mode =  0 Dir =  0 mem [4, 5, 18] z 1 Acc 0  STOP

增强 TC2 模拟器

累加器基础机器的简单示例说明了指令实现、指令集设计和位分配的几个方面。TC2 有一个 3 位操作码,给我们提供了八个操作。或者不是吗?

方向位,Dir,仅由LDA/STA指令使用。如果我们从操作码字段中移除此位,我们将有一个 4 位操作码,提供 16 条指令。由于LDASTA现在将是独立的指令,我们的 8 条指令计算机将拥有 9 条指令,留下 16 - 9 = 7 个新的(即未分配的)操作码。我们还可以使用方向标志与ADDSUB指令一起使用,允许目标为累加器或内存。考虑以下示例。当前的 TC2 模拟器可以使用以下代码增加变量xy


      LDA  x
      ADD  #1
      STA  x
      LDA  y
      ADD  #1
      STA  y

通过扩展加法操作(ADDA用于累加器加和ADDM用于内存加和),我们现在可以编写以下代码:


      LDAA #1   ; Load accumulator with 1
      ADDM x    ; Add accumulator to memory location x
      ADDM y    ; Add accumulator to memory location y

这个增强将指令数量减半,因为我们只将累加器加载为文字一次,然后将其添加到两个不同的内存位置。ADD操作的新代码如下:

 if OpCode == 1:                 # Test for ADDA or ADDM instruction
    if Dir == 0:                 # Test for add to accumulator (Dir=0) or add to memory (Dir =1)
       if Mode == 0:             # Test for ADDA literal or direct memory operand
          Acc = Acc + Lit        # If mode is 0, then it's a literal operand
       else:                     # If mode is 1, then it's a direct memory access
          MAR = Lit              # Literal (address) to MAR
          MBR = Memory[MAR]      # Do a read to get operand in MBR
          Acc = MBR + Acc        # and send it to the accumulator
    if Dir == 1:                 # ADDM: add to memory version of ADD
       MAR = Lit                 # Set up the memory address
       MBR = Memory[MAR]         # Read memory contents
       MBR = MBR + Acc           # Add accumulator to memory
       Memory[MAR] = MBR         # And write back the result

我们还能做什么来扩展指令集?我们为分支组分配了三个操作码。这非常浪费。由于这些分支指令的方向位和模式位都是未使用的,我们可以将这些位投入使用(即重新定义它们的含义)。考虑表 8.3的排列:

操作 方向 模式
BRA 0 0
未定义 0 1
BEQ 1 0
BNE 1 1

表 8.3 – 重新利用方向和模式位

我们已经使用了DirMode指令位来选择分支类型。作为额外的好处,我们有一个标记为未定义的备用操作。分支组的代码如下。我们使用了阴影来帮助识别块。注意,在这个例子中,我们展示了如何使分支程序计数器相关:


if OpCode == 3:                                # Test for the branch group
   if Dir == 0:                                # Direction 0 for unconditional
      if Mode == 0: PC = PC + Lit - 1          # If Mode is zero then unconditional branch
      else: run = 0                            # If Mode is 1 then this is undefined so stop
   else:
      if Dir == 1:                             # If direction is 1, it's a conditional branch
         if Mode == 0:                         # If mode is 0 then we have a BNE
            if Z == 0: PC = PC + Lit - 1       # Branch on Z = 0 (not zero)
         else:                                 # If Mode is 1 we have a BEQ
            if Z == 1: PC = PC + Lit - 1       # Branch on Z = 1 (zero)

这段代码看起来比实际要复杂,因为我们测试操作码、方向、模式和 Z 位时嵌套了四个if语句。然而,这个例子展示了如何通过重用指令位来增加指令数量,但代价是解码复杂度增加。

在指令集中仍有空间进行操作和提取更多功能。看看CLR指令。我们使用模式位来清除内存或累加器。为什么不稍微发挥一下创意,使用方向位来提供另一种操作呢?增加寄存器或内存是一个常见的操作,所以让我们提供这个功能。我们可以使用Dir == 0来表示CLRDir == 1来表示内存/累加器的INC操作。灰色阴影的部分是原始的清除操作,蓝色阴影的部分是新的增加操作:


if OpCode == 6:                    # Test for clear mem/Acc or increment mem/Acc
    if Dir == 0:                   # Direction = 0 for clear operation
        if Mode == 0:              # If Mode = 0
           Acc = 0                 # Then clear accumulator
        else:
           MAR = Lit               # If Mode = 1
           Memory[MAR] = 0         # Then clear memory location
    else:                          # Direction = 1 for increment
        if Mode == 0:              # If Mode = 0
           Acc = Acc + 1           # Then increment accumulator
        else:
           MAR = Lit               # If Mode = 1
           MBR = Memory[MAR]       # Then increment memory location
           MBR = MBR + 1           # Increment memory in MBR
           Memory[MAR] = MBR       # Write back incremented memory value

最后,考虑带有111DMLLLLL操作码的STOP(停止)指令。在这里,我们有 7 位没有做任何事情。这是(2⁷ = 128)种组合。如果我们为停止操作保留一个代码,比如1110000000,那么我们可以将代码11100000011111111111分配给新的指令。下一节将扩展这个架构以创建一个更逼真的模拟器。

TC3:具有寄存器到内存架构的 CISC 机器

在本节中,你将了解一个模拟器的结构设计,该模拟器实现了 CISC 风格的指令集架构,提供寄存器到寄存器和寄存器到内存的操作。TC3 是 TC2 的一个更复杂的版本,具有更实用的架构。

TC3 支持寄存器直接、寄存器间接、内存直接和立即数寻址模式。例如,AND [R2], #129执行的是寄存器R2指向的内存位置的值与二进制值10000001的逻辑AND操作。

我们包括了内存直接操作。这些操作旨在展示计算机的特性,而不是实用性。早期的 8 位微处理器,如摩托罗拉 6800,允许你直接操作内存。大多数现代处理器都不支持这一点。TC3 可以通过MOV R2,M:12来访问,例如,内存位置 12 的内容。注意语法。TC3 指令提供了一个单一的立即数字段,它可以作为立即数或内存地址,但不能同时作为两者。我使用#来表示立即数,M:来表示内存地址;考虑MOV R2,M:12MOV R2,#12。前者将内存位置12的内容加载到寄存器R2中,后者将整数12加载到R2中。由于指令中只有一个立即数字段,TC3 不能支持像MOV M:12,#127`这样的指令。

TC3 指令集架构

TC3 模拟器是一个具有 24 位指令和 8 位数据字长的一个半地址 CISC 处理器。这使得它成为一个哈佛机器,因为它有独立的数据和程序内存。我们采取这种方法的两个原因是:首先,从教育角度来看,8 位数据字容易处理;其次,24 位指令提供了功能,既没有使用大的 32 位字,也没有采用像某些 CISC 处理器那样的可变长度指令。

图 8**.4描述了 TC3 的指令格式,它包含一个指令类别字段和一个操作码字段、一个寻址模式字段、两个寄存器字段和一个立即数字段。所有指令的格式都是相同的。

图 8.4 – TC3 指令格式

图 8.4 – TC3 指令格式

我们使用 8 位寄存器、8 位地址和 8 位立即数来简化设计。数据空间限制在 2⁸ = 256 个位置,因为立即数只能访问 256 个位置。将指令宽度改为 32 位并将立即数扩展到 16 位将提供 65,536 个位置的数据空间。

TC3 拥有八个通用寄存器,R0R7。它需要 6 位来提供源寄存器和目标寄存器字段。指令字段宽度为 6 位,分为一个 2 位的指令类别字段和一个 4 位的操作码字段。这允许最多有 64 条指令,每个类别最多 16 条。我们采用这种(指令类别和操作码)方法来简化设计。从指令空间使用效率的角度来看,这是一个相当低效的方法,因为大多数指令都位于一个类别中,而其他类别几乎是空的。

4 位模式字段定义了指令的属性(例如,寻址模式)。TC3 支持表 8.4中定义的寻址模式,即无操作数指令、单寄存器指令、带立即数的指令和双操作数指令。尽管 TC3 只支持两个操作数(寄存器+寄存器和寄存器+立即数),但指令中有三个字段。因此,计算机可以很容易地修改以提供三操作数指令。我们选择这种方法来简化指令编码和解码。另一种方法是为两个操作数字段提供两个字段——一个寄存器字段和一个寄存器或立即数字段:

模式 地址 示例 RTL 类别
0 无操作数 STOP 0
1 单个寄存器 INC R1 [R1][R1] + 1 1
2 立即数偏移量 BEQ 123 [pc]123 2
3 保留
4 立即数到寄存器 MOV R1,#M [R1]M 3
5 寄存器到寄存器 MOV R1,R2 [R1][``R2] 3
6 寄存器间接到寄存器 MOV R1,[R2] [R1][[``R2]] 3
7 寄存器到寄存器间接寻址 MOV [R1],R2 [[R1]][``R2] 3
8 寄存器间接到寄存器间接 MOV [R1],[R2] [[R1]][[``R2]] 3
9 寄存器到内存 MOV M:123,R2 M[123] ← [``R2] 3
10 寄存器间接到内存 MOV M:123,[R2] M[123][[``R2]] 3
11 内存到寄存器 MOV R1,M:123 [R1]M[123] 3
12 内存到寄存器间接 MOV [R1],M:123 [[R1]]M[123] 3
13-15 保留

表 8.4 – TC3 处理器寻址模式

考虑以下在此计算机上运行的汇编语言程序。我们希望将两个向量加上一个整数相加,即zi = xi + yi + 5,其中i = 0 到 3。以下代码应该大部分是自解释的。立即数前面加有#,指令中的标签以冒号结束。代码的前一部分使用RND R5指令用随机数填充向量XY,以帮助测试。


      Code          @ Comment                        Instruction encoding
      MOV  R0,#     @ Point to memory location 8
Next: RND  R5       @ REPEAT: generate random value in r5
      MOV  [R0],R5  @ store r5 at location pointed at by r0
      DEC  R0       @ decrement r0 pointer
      BNE  Next     @ UNTIL zero
      EQU  X,#1     @ Vector X memory 1 to 4
      EQU  Y,#5     @ Vector Y memory 5 to 8
      EQU  Z,#9     @ Vector Z memory 9 to 12
      MOV  R0,#X    @ r0 points to array X                    00 0000 0010 000 000 00000001
      MOV  R1,#Y    @ r1 points to array Y                   00 0000 0010 001 000 00000101
      MOV  R2,#Z    @ r2 points to array Z                      00 0000 0010 010 000 00001001
      MOV  R3,#4    @ r3 number of elements to add in r3        00 0000 0010 011 000 00000100
Loop: MOV  R4,[R0]  @ Get xi                            00 0000 0000 100 000 00000000
      ADD  R4,#5    @ Add 5 to xi                         00 0001 0010 100 000 00000101
      ADD  R4,[R1]  @ Add xi + 5 to yi Memory to reg operation    00 0001 0001 100 001 00000000
      MOV  [R2],R4  @ Store result in array Z                 00 0000 0100 010 100 00000000
      INC  R0       @ Increment pointer to array X             00 1100 0000 000 000 00000000
      INC  R1       @ Increment pointer to array Y               00 1100 0000 001 000 00000000
      INC  R2       @ Increment pointer to array Z             00 1100 0000 010 000 00000000
      DEC  R3       @ Decrement loop counter                00 1101 0000 011 000 00000000
      BNE  Loop     @ Continue until counter 0              01 0011 0000 000 000 00000100

本例使用了直接寻址、寄存器直接寻址和寄存器间接寻址(基于指针)的寻址方式。我们为每个指令提供了二进制代码,包括类别、操作码、寻址模式、寄存器和立即数字段。

初始时,我们没有为这个模拟器构建汇编器。然而,手动编写指令非常痛苦,因此加入了汇编器。汇编器和模拟器的关键是指令的mode字段,它指示寻址模式。

当以助记符形式读取指令时,会对其进行检查,并使用其寻址模式和操作数来确定指令所需的四个模式位。当执行指令时,执行相反的操作,并使用模式位来实现适当的寻址模式。例如,如果指令是LDR R6,#5,则模式为4,汇编器将6存储在第一个寄存器字段中,将5存储在立即数字段中。当执行指令时,模拟器使用模式位0100来确定目标寄存器为110,立即数为00000101

TC3 模拟器的第一部分如下。我们创建两个列表:一个用于程序存储器,一个用于数据存储器(pMem 和 dMem)。程序存储器中的指令是从文件导入的。数据存储器设置为 16 个位置,初始化为 0。包含源程序的文本文件是 src,处理以重新格式化指令和删除汇编指令。

代码的阴影部分是为了检测源代码中的'END'指令,它终止汇编处理,并在代码执行时作为STOP指令。我添加它是为了方便。有时我想测试一两个指令,但不想编写新的源代码程序。我可以将测试的代码放在现有程序的最上面,然后跟随着ENDEND之后的所有代码都将被忽略。稍后,我可以删除新的代码和END


sTab = {}                             # Symbol table for equates and labels name:integerValue
pMem = []                             # Program memory (initially empty)
dMem = [0]*16                         # Data memory. Initialized and 16 locations
reg  = [0]*8                          # Register set
z,c,n = 0,0,0                         # Define and status flags: zero, carry, negative
testCode = "E:\\AwPW\\TC3_NEW_1.txt"  # Source filename on my computer
with open(testCode) as src:           # Open the source file containing the assembly program
    lines = src.readlines()           # Read the program into lines
src = [i[0:-1].lstrip() for i in lines ]
                                      # Remove the /n newline from each line of the source code
src = [i.split("@")[0] for i in src]           # Remove comments in the code
src = [i for i in src if i != '']     # Remove empty lines
for i in range(0,len(src)):           # Scan source code line-by-line
    src[i] = src[i].replace(',',' ')  # Replace commas by a space
    src[i] = src[i].upper()           # Convert to upper-case
    src[i] = src[i].split()           # Split into tokens (label, mnemonic, operands)
src1 = []                             # Set up dummy source file, initially empty
for i in range (0,len(src)):          # Read source and stop on first END instruction
    src1.append(src[i])               # Append each line to dummy source file
    if src[i][0] == 'END': break      # Stop on 'END' token
src = src1                            # Copy dummy file to source (having stopped on 'END')
for i in range (0,len(src)):          # Deal with equates of the form EQU PQR 25
    if src[i][0] == 'EQU':            # If the line is 3 or more tokens and first token is EQU
        sTab[src[i][1]] = getL(src[i][2])      # Put token in symbol table as integer
src = [i for i in src if i.count('EQU') == 0]  # Remove lines with "EQU" from source code

以下代码将汇编语言形式的指令进行标记化,并将其转换为指令的位模式。在以下代码中,我们使用 ic 作为指令计数器,它逐行遍历源程序。

我们必须处理的一个问题是标签。一些指令有一个标签,而另一些则没有。这意味着,对于没有标签的指令,助记符是令牌 0,如果有标签,则是令牌 1。Python 代码检查标签(以冒号结尾)。如果找到标签,j 被设置为 1,如果没有找到,j 被设置为 0。然后我们使用 j 来计算指令中令牌的位置。tLen 变量是一个指令中令牌的数量:


for ic in range(0,len(src)):           # ASSEMBLY LOOP (ic = instruction counter)
    t0,t1,t2 = '', '', ''              # Prepare to assign tokens. Initialize to null string
    if src[ic][0][-1] != ':':          # If the first token doesn't end in colon, it's an instruction
        j = 0                          # j = 0 forline starting with mnemonic
    else:                              # If the first token ends in a colon it's a label
        j = 1                          # j = 1 if mnemonic is second token
    t0 = src[ic][j]                    # Set t0 to mnemonic j selects first or second token
    if len(src[ic]) > 1+j: t1 = src[ic][j+1]  # Set t1 to ingle operand
    if len(src[ic]) > 2+j: t2 = src[ic][j+2]  # Set t2 to second operand
    tLen = len(src[ic]) - j - 1         # tLen is the number of tokens (adjusted for any label)

汇编器的下一部分做所有的工作。在这里,我们生成二进制代码。与我们所开发的其它模拟器不同,我们使用目录和列表来检测寄存器,如下(部分)代码所示:


rName   = {'R0':0,'R1':1,'R2':2,'R3':3}  # Relate register name to numeric value (lookup table)
rNamInd = {'[R0]':0,'[R1]':1,'[R2]':2,'[R3]':3}
                                         # Look for register indirect addressing (lookup table)
iClass0 = ['STOP', 'NOP', 'END']         # Instruction class 00 mnemonic with no operands
iClass1 = ['BRA',  'BEQ', 'BNE','CZN' ]  # Instruction class 01 mnemonic with literal operand

现在,我们可以取一个令牌并询问它是否在 rName 中以检测 R0R7,或者是否在 rNamInd 中以检测它是否是 [R0][R7]。此外,我们可以使用指令中的助记符并依次询问它是否在每个类别中,以确定指令的两个类别位;例如,如果 t0 是第一个令牌(对应于助记符),我们可以写出以下内容:


if t0 in iClass0: mode = 0.

同样,我们可以使用 if t1 in rNamInd`` 来判断第二个标记是否是一个用作指针的寄存器(例如,[R4])。

指令中最复杂的类别是 iClass3,它处理双操作数指令,例如 ADD [R3],R4。在这种情况下,令牌 t0 将是 'ADD',令牌 t1 将是 '``[r3]',而令牌 t2 将是 'R4'。为了识别这个指令的类别,我们寻找第一个操作数,它是一个间接寄存器,以及第二个操作数,它是一个寄存器,如下所示:

 if (t1 in rNamInd) and (t2 in rName): mode = 7

决定指令模式的代码如下:


binC = 0                                  # Initialize binary code for this instruction to all zeros
opCode = mnemon[t0]                       # Look up op-code in table mnemon using token t0
iClass = opCode >> 4                      # Get two most significant bits of op-code (i.e., class)
if   t0 in iClass0:                       # If in iClass0 it's a single instruction, no operands
    mode = 0                              # The mode is 0 for everything in this class
    binC = (mnemon[t0] << 18)             # All fields zero except op_code
elif t0 in iClass1:                       # If in iClass1 it's an 0p-code plus offset (e.g., branch)
    mode = 1                              # All class 1 instruction are mode 1 (op-code plus literal)
    binC = (mnemon[t0] << 18)+(mode << 14)+getL(t1)
                                          # Create binary code for Class1 instruction
elif t0 in iClass2:                       # If in iClass2 it's an op-code plus register number
    mode = 2                              # All iClass2 instructions are mode 2
    binC = (mnemon[t0] << 18)+(mode << 14)+(rName[t1] << 11
                                          # Construct binary code of instruction
elif t0 in iClass3:                       # All data-processing and movement ops in iClass3
    if   (t1 in rName) and (t2[0] == '#'): # Look for register name and literal for mode 4
        mode = 4
    elif (t1 in rName) and (t2 in rName): # Look for register name and register name for mode 5
        mode = 5
    elif (t1 in rName) and (t2 in rNamInd):   # Look for R0,[R2]) format
        mode = 6
    elif (t1 in rNamInd) and (t2 in rName):   # Look for instruction format [R1],R2
        mode = 7
    elif (t1 in rNamInd) and (t2 in rNamInd): # Look for two register indirect names [R1],[R2]
        mode = 8
    elif (t1[0:2] == 'M:') and (t2 in rName):
                                      # Look for memory address M: and reg name M:12,r4
        mode = 9
    elif (t1[0:2] == 'M:') and (t2 in rNamInd): # Look for M:12,[R4] format
        mode = 10
    elif (t1 in rName) and (t2[0:2] == 'M:'): # Look for register name and literal prefixed by M:
        mode = 11
    elif (t1 in rNamInd) and (t2[0:2] == 'M:'):
                                      # Look for register indirect name and literal prefixed by M:
        mode = 12

在提取指令类别、操作码和模式之后,最后一步是获取实际的寄存器编号和任何字面量。在以下代码片段中,我们分别定义了两个寄存器字段和字面量字段。这些是 rField1rField2lField,并且它们都被初始化为 0,因为不带三个字段的指令对应的位被设置为 0

在这里,我们使用列表作为提取字段的一个非常方便的方法,而不是使用组合的 if 和 or 操作符。例如,寄存器字段 1 被模式 45611 使用。我们可以写出以下内容:


 if (mode == 4) or (mode == 5) or (mode == 6) or (mode == 11):

然而,我们可以用以下更易读的方式来写:


 if mode in [4,5,6,11]:

以下代码显示了如何评估三个寄存器/字面量字段:


binC = (mnemon[t0] << 18) + (mode << 14)  # Insert op_Code and mode fields in instruction
rField1, rField2, lField = 0, 0, 0        # Calculate register and literal fields. Initialize to zero
if mode in [4,5,6,11]: rField1 = rName[t1] # Convert register names into register numbers
if mode in [7,8,12]:   rField1 = rNamInd[t1]
if mode in [5,7,9]:    rField2 = rName[t2] # rField2 is second register field
if mode in [6,8,10]:   rField2 = rNamInd[t2]
if mode in [4,11,12]:  lField  = getL(t2)
                                  # if (mode==4) or (mode==11) or (mode==12): lField = getL(t2)
if mode in [9,10]:     lField  = getL(t1)
                                  # if (mode == 9) or (mode == 10): lField = getL(t1) Literal field

以下两行代码生成逻辑通过移位和执行位或操作来插入寄存器/字面量字段,并将当前二进制指令 binC 添加到程序内存 pMem 中:


binC = binC + (rField1 << 11) + rField2 << 8) + lField
                                  # Binary code with register and literal fields
pMem.append(binC)                 # Append instruction to program memory

模拟器的特点

TC3 的模拟器部分相对简单。在这里,我将简单地提供一些关于其特性的注释,以帮助理解代码。

1. 打印数据

在模拟器开发过程中,要显示的项目数量增加了。因此,我们创建了一个字符串列表,每个要打印的项目一个,然后将这些项目连接起来。例如,这是我在汇编过程中显示数据的打印机制:


### Display assembly details of each instruction for diagnostics
pcF  = "{0:<20}".format(" ".join(src[ic]))    # 1\. instruction
icF  = 'pc = ' + "{:<3}".format(ic)           # 2\. pc
binF = format(binC, "024b")                   # 3\. Binary encoding
iClF = 'Class = '+ str(iClass)                # 4\. instruction class
modF = 'mode = ' + str(mode)                  # 5\. instruction mode. Convert mode to string
t0F  = "{:<5}".format(t0)                     # 6\. token 0 (mnemonic)
t1F  = "{:<5}".format(t1)                     # 7\. token 1 (register field 1)
t2F  = "{:<10}".format(t2)                    # 8\. token 2 (register field 2 or literal)
print(pcF,icF,binF,iClF,modF,t0F,'t1 =',t1F,t2F)   # Print these fields

displayLevel参数包括确定在汇编过程中打印了什么信息。例如,我们可以编写以下内容:


if displayLevel > 4: print('Binary code =', xyz)

这将在需要调试时(将变量设置为5或更大)仅打印二进制代码。

2. 实现带进位的加法

当我实现带有编号功能的 ALU 时,我最初忘记了包括ADC,带进位的加法。而不是重新编号函数,我通过首先执行加法将ADD转换为双重ADD/ADC操作。然后,如果操作码是ADC,则添加进位位:


elif fun == 1:                             # ADD:
        res = (op1 + op2)                  # Perform addition of operands
        if thisOp == 'ADC': res = res + c  # If operation ADC then add carry bit

3. 处理简单的指令类

这里是处理第 1 类指令的代码。我们不必担心解码模式,因为此类只有一个模式。当然,该类可以通过添加其他模式(在未来)进行扩展。


elif opClass == 1:                         # Class 1 operation instructions with literal operand
    if    thisOp == 'BRA': pc = lit        # BRA Branch unconditionally PC = L
    elif (thisOp == 'BEQ') and (z == 1): pc = lit    # BEQ Branch on zero
    elif (thisOp == 'BNE') and (z == 0): pc = lit    # BNE Branch on not zero
    elif  thisOp == 'CZN':                 # Set/clear c, z, and n flags
        c = (lit & 0b100) >> 2             # Bit 2 of literal is c
        z = (lit & 0b010) >> 1             # Bit 1 of literal is z
        n = (lit & 0b001)                  # Bit 0 of literal is n

第 1 类指令包含操作码和字面量,通常用于实现分支操作。请注意,我们比较当前指令与一个名称(例如,'BRA')而不是操作码,就像我们在其他模拟器中所做的那样。使用反向操作码到助记符翻译表使得生活变得更加容易。

我们添加了一个CZN(进位零负)指令,允许我们预设条件码;例如,CZN #%101cn设置为1,将z设置为0。计算机通常有一个操作,允许你测试条件码、清除它们、设置它们,以及切换(翻转)它们。

4. 处理字面量

TC3 的数值可以用几种格式表示,例如二进制,其中我们用%1000表示8。TC3 中的字面量处理还必须处理特殊格式,例如 M:12,它表示一个内存地址。以下函数执行所有字面量处理并处理几种格式。它还可以处理必须是符号名称的字面量,这些名称必须在符号表中查找:


def getL(lit8):                               # Convert string to integer
    lit8v = 9999                              # Dummy default
    if lit8[0:2]   == 'M:': lit8  = lit8[2:]  # Strip M: prefix from memory literal addresses
    if lit8[0:1]   == '#':  lit8  = lit8[1:]  # Strip # prefix from literal addresses
    if   type(lit8) == int: lit8v = lit8      # If integer, return it
    elif lit8.isnumeric():  lit8v = int(lit8) # If decimal in text form convert to integer
    elif lit8 in sTab:      lit8v = sTab[lit8]       # If in symbol table, retrieve it
    elif lit8[0]   == '%':  lit8v = int(lit8[1:],2)  # If binary string convert to int
    elif lit8[0:2] == '0X': lit8v = int(lit8[2:],16) # If hex string convert it to int
    elif lit8[0]   == '-':  lit8v = -int(lit8[1:]) & 0xFF
                                               # If decimal negative convert to signed int
    return(lit8v)                              # Return integer corresponding to text string

5. 结果写回

执行 ALU 操作或数据移动后,必须将结果操作数写回计算机。然而,因为我们指定了双操作数 CISC 风格格式,计算结果可以写入寄存器(就像任何 RISC 操作一样),可以写入由寄存器指向的内存位置,或者可以写入由其地址指定的内存操作。以下代码片段演示了 TC3 的写回操作:


op3 = alu(fun,op1,op2)                          # Call ALU to perform the function
if mode in [4,5,6,11]: reg[reg1] = op3          # Writeback ALU result in op3 to a register
elif mode in [7,8,12]: dMem[reg[reg1]] = op3    # Writeback result to mem pointed at by reg
elif mode in [9,10]:   dMem[lit]       = op3    # Writeback result to memory

样本输出

以下是从模拟器输出的示例,展示了整数处理。我们编写了一个程序,有六种不同的输入字面量的方式。在每种情况下,我们将字面量加载到寄存器r0中。源程序如下:


 EQU www,#42
 MOV r0,#12
 MOV r0,#%11010
 MOV r0,#0xAF
 MOV r0,#-5
 MOV r0,M:7
 MOV r0,#www
 NOP
 STOP
 END

在以下代码块中,我们有 TC3 的输出。这个输出是为了开发和测试模拟器(例如,在汇编过程之后)而设计的:


Source code                     This is the tokenized source code
['MOV', 'R0', '#12']
['MOV', 'R0', '#%11010']
['MOV', 'R0', '#0XAF']
['MOV', 'R0', '#-5']
['MOV', 'R0', 'M:7']
['MOV', 'R0', '#WWW']
['NOP']
['STOP']
['END']
Equate and branch table          This is the symbol table. Only one entry
WWW      42

以下是在汇编和分析阶段的输出:


Assembly loop
MOV R0 #12     pc=0 110000010000000000001100 Class=3 mode=4 MOV   t1=R0 #12
MOV R0 #%11010   pc=1 1110000010000000000011010   Class=3  mode=4   MOV   t1=R0 #%11010
MOV R0 #0XAF   pc=2 110000010000000010101111 Class=3 mode=4  MOV  t1=R0 #0XAF
MOV R0 #-5     pc=3 110000010000000011111011 Class=3 mode=4  MOV  t1=R0 #-5
MOV R0 M:7     pc=4 110000101100000000000111 Class=3 mode=11 MOV  t1=R0 M:7
MOV R0 #WWW    pc=5 110000010000000000101010 Class=3 mode=4  MOV  t1=R0 #WWW
NOP            pc=6 000000000000000000000000 Class=0 mode=0  NOP  t1 =
STOP           pc=7 001110000000000000000000 Class=0 mode=0  STOP t1 =
END            pc=8 001111000000000000000000 Class=0 mode=0  END  t1 =
110000010000000000001100      This is the program in binary form
110000010000000000011010
110000010000000010101111
110000010000000011111011
110000101100000000000111
110000010000000000101010
000000000000000000000000
001110000000000000000000
001111000000000000000000

在以下代码块中,我们有单步执行输出。为了帮助适应页面,我们已编辑它。我们只为每一行打印了两个内存位置。指令中的文字和其在r0中的值以粗体显示:


EXECUTE
MOV R0 #12     pc=0  110000010000000000001100 Class=3 mode=4  
Reg=0c 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=0
MOV R0 #%11010 pc=1  110000010000000000011010 Class=3 mode=4  
Reg=1a 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=0
MOV R0 #0XAF   pc=2  110000010000000010101111 Class=3 mode=4  
Reg=af 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=1
MOV R0 #-5     pc=3  110000010000000011111011 Class=3 mode=4  
Reg=fb 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=1
MOV R0 M:7     pc=4  110000101100000000000111 Class=3 mode=11 
Reg=07 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=0
MOV R0 #WWW    pc=5  110000010000000000101010 Class=3 mode=4  
Reg=2a 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=0
NOP            pc=6  000000000000000000000000 Class=0 mode=0  
Reg=2a 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=0
STOP           pc=7  001110000000000000000000 Class=0 mode=0  
Reg=2a 00 00 00 00 00 00 00 Mem=00 C=0 Z=0 N=0

完整的 TC3 代码

我们已经讨论了 TC3 的设计。在这里,我们展示了完整模拟器的代码。与我们之前章节中描述的代码描述片段相比,这个更完整的模拟器有一些细微的差异。随后是一个模拟器样本运行的示例。代码的第一部分定义了指令模式,并提供了将执行的简单源程序:


### TC3 CISC machine
### Demonstration register-to-memory architecture Designed 22 January 2022.
### Instruction formats and addressing modes
### Mode 0:  NOP, STOP        No operand length 1
### Mode 1:  INC R1           Single register operand
### Mode 2:  BEQ XXX          Literal operand
### Mode 3:  Reserved
### Mode 4:  MOV r1,literal   Two-operand, register and literal
### Mode 5:  MOV r1,r2        Two-operand, register to register
### Mode 6:  MOV r1,[r2]      Two-operand, register indirect to register
### Mode 7:  MOV [r1],r2      Two-operand, register to register indirect
### Mode 8:  MOV [r1],[r2]    Two-operand, register indirect to register indirect
### Mode 9:  MOV M,r2         Two-operand, register to memory address
### Mode 10: MOV M,[r2]       Two-operand, register indirect to memory address
### Mode 11: MOV r1,M         Two-operand, memory address to register
### Mode 12: MOV [r1],M       Two-operand, memory address to register indirect
### The sample test code
###       MOV  r0,#8      @ Memory locations 1 to 8 with random numbers
### Next: RND  r5
###       MOV  [r0],r5
###       DEC  r0
###       BNE  Next
###       EQU   X,#1      @ Vector 1
###       EQU   Y,#5      @ Vector 5
###       EQU   Z,#9      @ Vector 9
###       MOV   r1,#X     @ r0 points to array X              11 0000 0100 000 000 00000001
###       MOV   r2,#Y     @ r1 points to array Y              11 0000 0100 001 000 00000101
###       MOV   r3,#Z     @ r2 points to array Z               11 0000 0100 010 000 00001001
###       MOV   r4,#6     @ r4 number of elements to add      11 0000 0100 011 000 00000100
### Loop: MOV   r5,[r1]   @ REPEAT: Get xi                 11 0000 0110 100 000 00000000
###       ADD   r5,#6     @ Add 6 to xi                   11 0001 0100 100 000 00000101
###       ADD   r5,[r2]   @ Add xi + 5 to yi                 11 0001 0110 100 001 00000000
###       MOV   [r3],r5   @ Store result in array Z            11 0000 0111 010 100 00000000
###       INC   r1        @ Increment pointer to array X        10 0000 0010 000 000 00000000
###       INC   r2        @ Increment pointer to array Y        10 0000 0010 001 000 00000000
###       INC   r3        @ Increment pointer to array Z          10 0000 0010 010 000 00000000
###       DEC   r4        @ Decrement loop counter           10 0001 0010 011 000 00000000
###       BNE   Loop      @ Continue until counter zero         01 0010 0001 000 000 00000100
###       STOP                                        00 1111 0000 000 000 00000000

以下块包含指令解码和寄存器查找的字典。我们提供了反向查找,以便你可以通过查找助记符来获取其代码,或者通过查找代码来获取助记符。同样,我们提供了对寄存器(如R0R2)和间接寄存器(如[R0][R1])的查找:


import random                                 # Get library of random number operations
### Dictionaries and variables
mnemon  = {'MOV':48,'MOVE':48,'ADD':49,'SUB':50,'CMP':51,'NOT':52,'AND':53, \
           'OR':54,'EOR':55,'ONES':56, 'MRG':57,'FFO':58,'LSL':59,'LSR':60, \
           'ADC':61,'INC':32,'DEC':33,'RND':34,'CZN':19,'TST':36,'NOP':0,   \
           'BRA':16,'BEQ':17,'BNE':18,'STOP':14,'END':15}
mnemonR = {48:'MOV',49:'ADD',50:'SUB',51:'CMP',52:'NOT',53:'AND',54:'OR',   \
           55:'EOR',56:'ONES',57:'MRG',58:'FFO',59:'LSL',60:'LSR',61:'ADC', \
           32:'INC',33:'DEC', 34:'RND',19:'CZN',36:'TST',0:'NOP',16:'BRA',  \
           17:'BEQ',18:'BNE',14:'STOP',15:'END'}
rName   = {'R0':0,'R1':1,'R2':2,'R3':3,'R4':4,'R5':5,'R6':6,'R7':7} # Register tab
rNamInd = {'[R0]':0,'[R1]':1,'[R2]':2,'[R3]':3,'[R4]':4,' \
           '[R5]':5,'[R6]':6,'[R7]':7}                       # Indirect registers
iClass0 = ['STOP', 'NOP','END']         # class 00 mnemonic with no operands
iClass1 = ['BRA','BEQ','BNE','CZN']     # class 01 mnemonic with literal operand
iClass2 = ['INC','DEC','RND','TST']     # class 10 mnemonic with register operand
iClass3 = ['MOV','MOVE','ADD','ADC','SUB','CMP', 'NOT','AND','OR', \
           'EOR','ONES','MRG','FFO','LSL','LSR']   # class 11 mnemonic two operands
sTab = {}                              # Symbol table for equates and labels name:integerValue
pMem = []                              # Program memory (initially empty)
dMem = [0]*16                          # Data memory
reg  = [0]*8                           # Register set
z,c,n = 0,0,0                          # Define and clear flags zero, carry, negative

以下两个函数提供了以各种格式读取整数操作数的能力,以及执行算术和逻辑运算的 ALU。这两个函数都可以扩展以提供额外的功能:


def getL(lit8):                                # Convert string to integer
    lit8v = 9999                               # Dummy default
    if lit8[0:2]   == 'M:': lit8  = lit8[2:]  # Strip M: prefix from memory literal addresses
    if lit8[0:1]   == '#':  lit8  = lit8[1:]   # Strip # prefix from literal addresses
    if   type(lit8) == int: lit8v = lit8       # If integer, return it
    elif lit8.isnumeric():  lit8v = int(lit8)  # If decimal in text from convert to integer
    elif lit8 in sTab:      lit8v = sTab[lit8] # If in symbol table, retrieve it
    elif lit8[0]   == '%':  lit8v = int(lit8[1:],2)  # If binary string convert to int
    elif lit8[0:2] == '0X': lit8v = int(lit8[2:],16) # If hex string convert to int
    elif lit8[0]   == '-':  lit8v = -int(lit8[1:]) & 0xFF
                                               # If decimal negative convert to signed int
    return(lit8v)                              # Return integer corresponding to text string
def alu(fun,op1,op2):             # Perform arithmetic and logical operations on operands 1 and 2
    global z,n,c                               # Make flags global
    z,n,c = 0,0,0                              # Clear status flags initially
    if   fun == 0: res = op2             # MOV: Perform data copy from source to destination
    elif fun == 1:                       # ADD: Perform addition - and ensure 8 bits plus carry
        res = (op1 + op2)                      # Do addition of operands
        if thisOp == 'ADC': res = res + c      # If operation ADC then add carry bit
    elif fun == 2: res = (op1 - op2)           # SUB: Perform subtraction
    elif fun == 3: res = op1 - op2        # CMP: Same as subtract without writeback
    elif fun == 4: res = op1 & op2        # AND: Perform bitwise AND
    elif fun == 5: res = op1 | op2        # OR
    elif fun == 6: res = ~op2             # NOT
    elif fun == 7: res = op1 ^ op2        # XOR
    elif fun == 8:
        res = op2 << 1                    # LSL: Perform single logical shift left
    elif fun == 9:
        res = op2 >> 1                    # LSR: Perform single logical shift right
    elif fun == 10:                       # ONES (Count number of 1s in register)
       onesCount = 0                      # Clear the 1s counter
       for i in range (0,8):        # For i = 0 to 7 (test each bit) AND with 10000000 to get msb
           if op2 & 0x80 == 0x80:         # If msb is set
               onesCount = onesCount + 1  # increment the 1s counter
           op2 = op2 << 1                 # shift the operand one place left
       res = onesCount                    # Destination operand is 1s count
    elif fun == 11:                       # MRG (merge alternate bits of two registers)
         t1 = op1 & 0b10101010            # Get even source operand bits
         t2 = op2 & 0b01010101            # Get odd destination operand bits
         res = t1 | t2                    # Merge them using an OR
    elif fun == 12:                       # FFO (Find position of leading 1)
        res = 8                           # Set default position 8 (i.e., leading 1 not found)
        for i  in range (0,8):            # Examine the bits one by one
          temp = op2 & 0x80               # AND with 10000000 to get leading bit and save
          op2 = op2 << 1                  # Shift operand left
          res = res - 1                   # Decrement place counter
          if temp == 128: break           # If the last tested bit was 1 then jump out of loop
    if res & 0xFF == 0:        z = 1      # TEST FLAGS z = 1 if bits 0 to 7 all 0
    if res & 0x80 == 0x80:     n = 1      # If bit 7 is one, set the carry bit
    if res & 0x100 == 0x100:   c = 1      # carry bit set if bit 8 set
    if (thisOp == 'LSR') and (op2 & 1 == 1): c = 1
                                          # Deal with special case of shift right (carry out is lsb)
    return(res & 0xFF)                    # Return and ensure value eight bits

trace()函数在程序执行时打印处理器的状态。这可以修改以改变数据量、布局和格式:


def trace():                                          # Function to print execution data
    cF   = "{0:<20}".format(" ".join(src[pcOld]))     # 1\. instruction
    icF  = 'pc = ' + "{:<3}".format(pcOld)            # 2\. pc
    binF = format(inst, "024b")                       # 3\. binary code
    iClF = 'Class = '+ str(iClass)                    # 4\. instruction class
    modF = 'mode = ' + str(mode)   # 5\. instruction mode NOTE we have to convert mode to string
    t0F  = "{:<5}".format(t0)                        # 6\. token 0 (mnemonic)
    t1F  = "{:<5}".format(t1)                        # 7\. token 1 (register field 1)
    t2F  = "{:<10}".format(t2)                       # 8\. token 2 (register field 2 or literal)
    rF   = 'Reg = '+ ' '.join('%02x' % b for b in reg)  # 9\. Registers in hex format
    m    = dMem[0:11]                                # 10\. First 10 memory locations
    mF   = 'Mem = '+ " ".join("%02x" % b for b in m) # 11\. Hex-formatted memory values
    ccrF = 'C = '+ str(c) + ' Z = ' + str(z) +' N = ' + str(n) # 12\. Condition codes
    x = input('>>> ')                               # 13\. Wait for keyboard input (return)
    print(cF,icF,binF,iClF, modF, rF, mF,ccrF)      # 14\. Print the computer status data
    return()
testCode = "E:\\AwPW\\TC3_NEW_1.txt"  # Source filename on my computer
with open(testCode) as src:           # Open source file with assembly language program
    lines = src.readlines()           # Read the program into lines
src.close()                           # Close the source file
src = [i[0:-1].lstrip()  for i in lines ]
                                      # Remove the /n newline from each line of the source code
src = [i.split("@")[0] for i in src]  # Remove comments in the code
src = [i for i in src if i != '']     # Remove empty lines
for i in range(0,len(src)):           # Scan source code line by line
    src[i] = src[i].replace(',',' ')  # Replace commas by a space
    src[i] = src[i].upper()           # Convert to upper-case
    src[i] = src[i].split()           # Split into tokens (label, mnemonic, operands)
src1 = []                             # Set up dummy source file, initially empty
for i in range (0,len(src)):          # Read source and stop on first END operation
    src1.append(src[i])               # Append line to dummy source file
    if src[i][0] == 'END': break      # Stop on 'END' token
src = src1                            # Copy dummy file to source (having stopped on 'END')
for i in range (0,len(src)):          # Deal with equates of the form EQU PQR 25
    if src[i][0] == 'EQU':            # If the line is 3 or more tokens and first token is EQU
        sTab[src[i][1]] = getL(src[i][2])
                                      # Put token in symbol table as integer
src = [i for i in src if i.count("EQU") == 0]
                            # Remove lines with 'EQU' from source code (these are not instructions)
for i in range(0,len(src)):           # Add label addresses to symbol table
    if src[i][0][-1] == ':':          # If first token is a label with : terminator
        sTab.update({src[i][0][0:-1]:i}) # add it to the symbol table.
xLm = 0                               # Length of maximum instruction (for printing)
for i in range (0,len(src)):          # Step through source array
    xL = len(' '.join(src[i]))       # Get the length of each line after joining tokens
    if xL > xLm: xLm = xL            # If xL > xLm  NOTE: This facility is not used in this version
print('Source code')                 # Display tokenized source code
for i in range(0,len(src)): print(src[i])
print("\nEquate and branch table\n") # Display the symbol table
for x,y in sTab.items():             # Step through the symbol table dictionary structure
    print("{:<8}".format(x),y)       # Display each line as label and value
print('\nAssembly loop \n')
for ic in range(0,len(src)):         # ASSEMBLY LOOP (ic = instruction counter)
    t0,t1,t2 = '','',''              # Prepare to assign tokens. Initialize to null string
    if src[ic][0][-1] != ':':        # If the first token doesn't end in colon, its an instruction
        j = 0                        # j = 0 for line starting with mnemonic
    else:                            # If the first token ends in a colon it's a label
        j = 1                        # j = 1 if mnemonic is second token
    t0 = src[ic][j]                  # Set t0 to mnemonic
    if len(src[ic]) > 1+j: t1 = src[ic][j+1]   # Set t1 to single operand
    if len(src[ic]) > 2+j: t2 = src[ic][j+2]   # Set t2 to second operand
    tLen = len(src[ic]) - j - 1      # tLen is the number of tokens (adjusted for any label)
    binC = 0                         # Initialize binary code for this instruction to all zeros
    opCode = mnemon[t0]              # Look up op-code in table mnemon using token t0
    iClass = opCode >> 4             # Get two most significant bits of op-code (i.e., class)
    if   t0 in iClass0:              # If in iClass0 it's a single instruction, no operands
        mode = 0                     # The mode is 0 for everything in this class
        binC = (mnemon[t0] << 18)    # All fields zero except op_code
    elif t0 in iClass1:              # If in iClass1 it's an op-code plus offset (e.g., branch)
        mode = 1                     # All class 1 instruction are mode 1 (op-code plus literal)
        binC = (mnemon[t0] << 18) + (mode << 14)  + getL(t1)
                                     # Create binary code with operation plus address (literal)
    elif t0 in iClass2:              # If in iClass2 it's an op-code plus register number
        mode = 2                     # All instruction are mode 2
        binC = (mnemon[t0] << 18) + (mode << 14)  + (rName[t1] << 11)
                                     # Create binary code
    elif t0 in iClass3:        # Two-operand inst. All data-processing and movement ops in iClass3
        if   (t1 in rName) and (t2[0] == '#'):
                               # Look for register name and literal for mode 4
            mode = 4
        elif (t1 in rName) and (t2 in rName):
                               # Look for register name and register name for mode 5
            mode = 5
        elif (t1 in rName) and (t2 in rNamInd):
                               # Look for register name and register indirect name (r1,[r2])
            mode = 6
        elif (t1 in rNamInd) and (t2 in rName):
                                     # Look for register indirect name and register ([r1],r2)
            mode = 7
        elif (t1 in rNamInd) and (t2 in rNamInd):
                                     # Look for two register indirect names ([r1],[r2])
            mode = 8
        elif (t1[0:2] == 'M:') and (t2 in rName):
                                     # Look for literal prefixed by M: and register name (M:12,r4)
            mode = 9
        elif (t1[0:2] == 'M:') and (t2 in rNamInd):
                               # Look for literal prefixed by M: and register indirect name (M:12,[r4])
            mode = 10
        elif (t1 in rName) and (t2[0:2] == 'M:'):
                                     # Look for register name and literal prefixed by M:
            mode = 11
        elif (t1 in rNamInd) and (t2[0:2] == 'M:'):
                                     # Look for register indirect name and literal prefixed by M:
            mode = 12
        binC = (mnemon[t0] << 18) + (mode << 14)
                                     # Insert op_Code and mode fields in the instruction
        rField1, rField2, lField = 0, 0, 0  # Calculate register and literal fields. Initialize to zero
        if mode in [4,5,6,11]: rField1 = rName[t1]
                               # Convert register names into register numbers rField1is first register
        if mode in [7,8,12]:   rField1 = rNamInd[t1]
        if mode in [5,7,9]:    rField2 = rName[t2]   # rField2 is second register field
        if mode in [6,8,10]:   rField2 = rNamInd[t2]
        if mode in [4,11,12]:  lField  = getL(t2)
                                  # if (mode == 4) or (mode == 11) or (mode == 12): Get literal
        if mode in [9,10]:     lField  = getL(t1)
                                  # if (mode == 9) or (mode == 10):  lField = getL(t1) Literal field
        binC = binC+(rField1 << 11)+(rField2 << 8)+lField
                                     # Binary code with register and literal fields added
    pMem.append(binC)                # Append instruction to program memory in pMem
### Display the assembly details of each instruction (this is for diagnostics)
    pcF  = '{0:<20}'.format(' '.join(src[ic])) # 1\. instruction
    icF  = 'pc = ' + '{:<3}'.format(ic)        # 2\. pc
    binF = format(binC, '024b')                # 3\. binary code
    iClF = 'Class = '+ str(iClass)             # 4\. instruction class
    modF = 'mode = ' + str(mode)           # 5\. instruction mode NOTE convert mode to string
    t0F  = '{:<5}'.format(t0)              # 6\. token 0 (mnemonic)
    t1F  = '{:<5}'.format(t1)              # 7\. token 1 (register field 1)
    t2F  = '{:<10}'.format(t2)             # 8\. token 2 (register field 2 or literal)
    print(pcF,icF,binF,iClF,modF,t0F,'t1 =',t1F,t2F) # Print these fields
print('\nEXECUTE \n')
### EXECUTE LOOP   # reverse assemble the binary instruction to recover the fields and execute the instruction
pc = 0                                     # Reset the program counter to 0
run = True                         # run flag: True to execute, False to stop (stop on END or STOP)
while run == True:                         # MAIN LOOP
    op1, op2, op3 = 0,0,0                  # Initialize data operands
    inst = pMem[pc]        # Fetch current instruction. inst is the binary op-code executed in this cycle
    pcOld = pc                             # Remember current pc for printing/display
    pc = pc + 1                            # Increment program counter for next cycle
    iClass = inst >> 22                    # Extract operation class 0 to 3 (top two bits)
    opCode = (inst >> 18)   & 0b111111     # Extract the current op-code
    mode   = (inst >> 14)   & 0b1111       # Extract the addressing mode
    reg1   = (inst >> 11)   & 0b0111       # Extract register 1 number
    reg2   = (inst >>  8)   & 0b0111       # Extract register 2 number
    lit    = inst           & 0b11111111   # Extract the 8-bit literal in the least significant bits

以下是程序中的指令执行部分。请注意,指令按其类别顺序执行:


### EXECUTE THE CODE
    thisOp = mnemonR[opCode]               # Reverse assemble. Get mnemonic from op-code
    if iClass == 0:                        # Class 0 no-operand instructions
        if thisOp == 'END' or thisOp == 'STOP': run = False
                                           # If END or STOP clear run flag to stop execution
        if opCode == 'NOP': pass           # If NOP then do nothing and "pass"
    elif iClass == 1:                      # Class 1 operation 
                                           # Class 1 branch and instr with literal operands
        if    thisOp == 'BRA': pc = lit    # BRA Branch unconditionally PC = L
        elif (thisOp == 'BEQ') and (z == 1): pc = lit  # BEQ Branch on zero
        elif (thisOp == 'BNE') and (z == 0): pc = lit  # BNE Branch on not zero
        elif thisOp == 'CZN':                          # Set/clear c, z, and n flags
            c = (lit & 0b100) >> 2                     # Bit 2 of literal is c
            z = (lit & 0b010) >> 1                     # Bit 1 of literal is z
            n = (lit & 0b001)              # Bit 0 of literal is c
    elif iClass == 2:                      # Class 0 single-register operand
        if   thisOp == 'INC': reg[reg1] = alu(1,reg[reg1],1)
                                           # Call ALU with second operand 1 to do increment
        elif thisOp == 'DEC': reg[reg1] = alu(2,reg[reg1],1)   # Decrement register
        elif thisOp == 'RND': reg[reg1] = random.randint(0,0xFF)
                                           # Generate random number in range 0 to 0xFF
        elif thisOp == 'TST':              # Test a register: return z and n flags. Set c to 0
            z, n, c = 0, 0, 0                      # Set all flags to 0
            if reg[reg1] == 0:           z = 1     # If operand 0 set z flag
            if reg[reg1] & 0x80 == 0x80: n = 1     # If operand ms bit 1 set n bit
    elif iClass == 3:                      # Class 3 operation: Two operands.
        if   mode in [4,5,6,11]: op1 = reg[reg1]
                                           # Register, literal e.g. MOVE r1,#5 or ADD r3,#0xF2
        elif mode in [7,8,12]:   op1 = dMem[reg[reg1]]
                                           # Register, literal e.g. MOVE r1,#5 or ADD r3,#0xF2
        elif mode in [9,10]:     op1 = lit # MOV M:12,r3 moves register to memory
        if   mode in [4,11,12]:  op2 = lit # Mode second operand literal
        elif mode in [5,7,9]:    op2 = reg[reg2]
                                           # Modes with second operand contents of register
        elif mode in [6,8,10]:   op2 = dMem[reg[reg2]]
                                           # Second operand pointed at by register
        if thisOp == 'MOV' : fun = 0       # Use mnemonic to get function required by ALU
        if thisOp == 'ADD' : fun = 1       # ADD and ADC use same function
        if thisOp == 'ADC' : fun = 1
        if thisOp == 'SUB' : fun = 2
        if thisOp == 'AND' : fun = 4
        if thisOp == 'OR'  : fun = 5
        if thisOp == 'NOT' : fun = 6
        if thisOp == 'EOR' : fun = 7
        if thisOp == 'LSL' : fun = 8
        if thisOp == 'LSR' : fun = 9
        if thisOp == 'ONES': fun = 10
        if thisOp == 'MRG' : fun = 11
        if thisOp == 'FFO' : fun = 12
        op3 = alu(fun,op1,op2)             # Call ALU to perform the function
        if   mode in [4,5,6,11]: reg[reg1]       = op3
                                           # Writeback ALU result in op3 result to a register
        elif mode in [7,8,12]:   dMem[reg[reg1]] = op3
                                            # Writeback result to mem pointed at by reg
        elif mode in [9,10]:     dMem[lit]       = op3
                                            # Writeback the result to memory
    trace()                                 # Display the results line by line

TC3 的样本运行

以下是 TC3 样本运行的输出。我们提供了执行的源代码、等式和分支表、汇编代码,以及运行输出:

  1. 源代码

['MOV', 'R0', '#8']
['NEXT:', 'RND', 'R5']
['MOV', '[R0]', 'R5']
['DEC', 'R0']
['BNE', 'NEXT']
['MOV', 'R1', '#X']
['MOV', 'R2', '#Y']
['MOV', 'R3', '#Z']
['MOV', 'R4', '#6']
['LOOP:', 'MOV', 'R5', '[R1]']
['ADD', 'R5', '#6']
['ADD', 'R5', '[R2]']
['MOV', '[R3]', 'R5']
['INC', 'R1']
['INC', 'R2']
['INC', 'R3']
['DEC', 'R4']
['BNE', 'LOOP']
['STOP', '00', '1111', '0']
  1. 等式和分支表

X        1
Y        5
Z        9
NEXT     1
LOOP     9
  1. 汇编循环

MOV R0 #8    pc=0   110000010000000000001000 Class=3 mode=4 MOV t1=R0 #8
NEXT: RND R5 pc=1   100010001010100000000000 Class=2 mode=2 RND   t1=R5
MOV [R0] R5  pc=2   110000011100010100000000 Class=3 mode=7 MOV   t1=[R0]  R5
DEC R0       pc=3   100001001000000000000000 Class=2 mode=2 DEC   t1=R0
BNE NEXT     pc=4   010010000100000000000001 Class=1 mode=1 BNE   t1=NEXT
MOV R1 #X    pc=5   110000010000100000000001 Class=3 mode=4 MOV   t1=R1    #X
MOV R2 #Y    pc=6   110000010001000000000101 Class=3 mode=4 MOV   t1=R2    #Y
MOV R3 #Z    pc=7   110000010001100000001001 Class=3 mode=4 MOV   t1=R3    #Z
MOV R4 #6    pc=8   110000010010000000000110 Class=3 mode=4 MOV   t1=R4   #6
LOOP: MOV R5 [R1]  pc=9   110000011010100100000000 Class=3 mode=6 MOV   t1=R5    [R1]
ADD R5 #6    pc=10  110001010010100000000110 Class=3 mode=4 ADD   t1=R5   #6
ADD R5 [R2]  pc=11  110001011010101000000000 Class=3 mode=6 ADD   t1=R5   R2]
MOV [R3] R5  pc=12  110000011101110100000000 Class=3 mode=7 MOV   t1=[R3]  R5
INC R1       pc=13  100000001000100000000000 Class=2 mode=2 INC   t1=R1
INC R2       pc=14  100000001001000000000000 Class=2 mode=2 INC   t1=R2
INC R3       pc=15  100000001001100000000000 Class=2 mode=2 INC   t1=R3
DEC R4       pc=16  100001001010000000000000 Class=2 mode=2 DEC   t1=R4
BNE LOOP     pc=17  010010000100000000001001 Class=1 mode=1 BNE   t1=LOOP
STOP 00 1111 0  pc=18  001110000000000000000000 Class=0 mode=0 STOP  t1=00    1111
  1. 执行

我们只提供了追踪输出的几行,并将它们重新格式化以适应页面。


>>> 
MOV R0 #8            pc = 0   110000010000000000001000 
Class = 3 mode = 4 
Reg = 08 00 00 00 00 00 00 00 
Mem = 00 00 00 00 00 00 00 00 00 00 00 
C = 0 Z = 0 N = 0
NEXT: RND R5         pc = 1   100010001010100000000000 
Class = 2 mode = 2 
Reg = 08 00 00 00 00 8f 00 00 
Mem = 00 00 00 00 00 00 00 00 00 00 00 
C = 0 Z = 0 N = 0
MOV [R0] R5         pc = 2   110000011100010100000000 
Class = 3 mode = 7 
Reg = 08 00 00 00 00 8f 00 00 
Mem = 00 00 00 00 00 00 00 00 8f 00 00 
C = 0 Z = 0 N = 1
DEC R0             pc = 3   100001001000000000000000 
Class = 2 mode = 2 
Reg = 07 00 00 00 00 8f 00 00 
Mem = 00 00 00 00 00 00 00 00 8f 00 00 
C = 0 Z = 0 N = 0
BNE NEXT             pc = 4   010010000100000000000001 
Class = 1 mode = 1 
Reg = 07 00 00 00 00 8f 00 00 
Mem = 00 00 00 00 00 00 00 00 8f 00 00 
C = 0 Z = 0 N = 0
NEXT: RND R5         pc = 1   100010001010100000000000 
Class = 2 mode = 2 
Reg = 07 00 00 00 00 35 00 00 
Mem = 00 00 00 00 00 00 00 00 8f 00 00 
C = 0 Z = 0 N = 0

注意

为了节省空间,未显示输出

在下一节中,我们将更详细地研究模拟器的一个组件,即 ALU。

算术逻辑单元 (ALU)

现在让我们来点不同的。我们已经在所有模拟器中使用了 ALU。在这里,你将更详细地了解 ALU 及其测试。

下面的 Python 代码演示了一个 8 位、16 功能的 ALU 的实现。我们添加了一些计算机提供的当代操作,例如取模、最小值和最大值。alu函数通过opabcIndisplay参数调用。op参数的范围是 0 到 15,定义了函数。ab参数是范围在 0 到 255 之间的两个 8 位整数,cin是进位输入,display是一个标志。当display0时,不打印任何数据。当display1时,函数会打印输入和结果。这个特性用于调试。

此代码演示了使用 Python 的 if...elif 结构来解码算术操作。我们还包含了一个字典结构,使我们能够通过名称打印出操作码。在这种情况下,字典是allOps,如下所示:

AllOps = {0:'clr', 1:'add',2:'sub',3:'mul'}    # just four entries to make easy reading.

另一个特点是,我们可以轻松地以二进制形式打印数据。print(bin(c))操作以二进制形式打印c。然而,因为我们使用 8 位算术并且希望看到前导零,我们可以通过使用zfill强制 8 位输出;即print(bin(c).zfill(8))方法。

或者,我们可以使用print('Result', format(c,'08b'))来以 8 位二进制字符串的形式打印c变量。

Python 函数可以返回多个值作为元组。元组是 Python 不可变值的列表,不能更改;例如,如果你写return (c, z, n, v, cOut),你正在返回一个由我们计算出的函数和znvcOut标志组成的元组。这些不能更改,但可以在调用程序中分配给变量;以下是一个示例:


result,Zero,Neg,oVerflow,carry = alu(0,A,B,0,1)

注意溢出的计算。如果两个操作数的符号位相同且结果的符号位不同,则设置v-bit。溢出仅对加法和减法有效。如果输入参数在二进制补码中为负值,则取模函数返回正值。我们通过翻转位并加 1 来实现这一点。


# This function simulates an 8-bit ALU and provides 16 operations
# It is called by alu(op,a,b,cIn,display). Op defines the ALU function
# a,b and cIn are the two inputs and the carry in
# If display is 1, the function prints all input and output on the terminal
# Return values: q, z, n, v, cOut) q is the result
def alu(op,a,b,cIn,display):
    allOps = {0:'clr', 1:'add',2:'sub',3:'mul',4:'div',5:'and',6:'or', \
              7:'not', 8:'eor', 9:'lsl',10:'lsr', 11:'adc',12:'sbc',   \
              13:'min',14:'max',15:'mod'}
    a, b = a & 0xFF, b & 0xFF             # Ensure the input is 8 bits
    cOut,z,n,v = 0,0,0,0                  # Clear all status flags
    if   op == 0:   q = 0                 # Code 0000 clear
    elif op == 1:   q = a + b             # Code 0001 add
    elif op == 2:   q = a - b             # Code 0010 subtract
    elif op == 3:   q = a * b             # Code 0011 multiply
    elif op == 4:   q = a // b            # Code 0100 divide
    elif op == 5:   q = a & b             # Code 0100 bitwise AND
    elif op == 6:   q = a | b             # Code 0100bitwise OR
    elif op == 7:   q = ~a                # Code 0111 bitwise negate (logical complement)
    elif op == 8:   q = a ^ b             # Code 0100 bitwise EOR
    elif op == 9:   q = a << b            # Code 0100 bitwise logical shift left b places
    elif op == 10:  q = a >> b            # Code 0100 bitwise logical shift right b places
    elif op == 11:  q = a + b + cIn       # Code 0100 add with carry in
    elif op == 12:  q = a - b - cIn       # Code 0100 subtract with borrow in
    elif op == 13:                        # Code 1101 q = minimum(a,b)
       if a > b: q = b
       else:     q = a
    elif op == 14:                        # Code 1110 q = maximum(a,b)
       if a > b: q = a                    # Note: in unsigned terms
       else:     q = b
    elif op == 15:                        # Code 1111 q = mod(a)
       if a > 0b01111111: q = (~a+1)&0xFF # if a is negative q = -a (2s comp)
       else:     q = a                    # if a is positive q =  a
# Prepare to exit: Setup flags
    cOut = (q&0x100)>>8                   # Carry out is bit 8
    q    =  q & 0xFF                      # Constrain result to 8 bits
    n    = (q & 0x80)>>7                  # AND q with 10000000 and shift right 7 times
    if q == 0: z = 1                      # Set z bit if result zero
    p1 = ( (a&0x80)>>7)& ((b&0x80)>>7)&~((q&0x80)>>7)
    p2 = (~(a&0x80)>>7)&~((b&0x80)>>7)& ((q&0x80)>>7)
    if p1 | p2 == True: v = 1             # Calculate v-bit (overflow)
    if display == 1:                      # Display parameters and results
       a,b = a&0xFF, b&0xFF               # Force both inputs to 8 bits
       print('Op =',allOps[op],'Decimals: a =',a,' b =',b, \
             'cIn =',cIn,'Result =',q)
       print('Flags: Z =',z, 'N =',n, 'V =',v, 'C =',cOut)
       print('Binaries A =',format(a,'08b'), 'B =',format(b,'08b'), \
             'Carry in =',format(cIn,'01b'), 'Result =',format(q,'08b'))
       print ()
    return (q, z, n, v, cOut)             # Return c (result), and flags as a tuple

测试 ALU

我们现在将演示测试 ALU 的过程。创建了一个while循环,并使用 Python 的键盘输入函数和.split()方法将输入字符串分割成子字符串,输入两个整数。例如,你可以输入add 3 5来执行 3 和 5 的加法操作。空输入(即回车)将结束序列。

我已经安排了代码,以便你只能看到操作所需输入的参数,例如,add 3,7mod 5sbc 3 4 1。为了更容易测试逻辑函数,你可以以二进制(%10110)或十六进制($3B)格式输入参数。

测试代码的一个特点是,我使用了一个反向字典。这允许你通过名称而不是数字来输入一个函数。

下面是我用来测试 ALU 的代码:


#### MAIN BODY
def literal(lit):
    if   lit.isnumeric(): lit =  int(lit)        # If decimal convert to integer
    elif lit[0]  == '%': lit =  int(lit[1:],2)   # If binary string convert to int
    elif lit[0:1]== '$': lit =  int(lit[1:],16)  # If hex string convert to int
    elif lit[0]  == '-': lit = -int(lit[1:])&0xFF # If negative convert to signed int
    return(lit)
opsRev = {'clr':0,'add':1,'sub':2,'mul':3,'div':4,'and':5,'or':6,     \
          'not':7,'eor':8,'lsl':9,'lsr':10,'adc':11,'sbc':12,         \
          'min':13,'max':14,'mod':15}
x,y,op1,op2,cIn = 0,0,0,0,0                      # Dummy value prior to test in while loop
while True:
    x = input('Enter operation and values ')
    if x == '': break                            # Exit on return
    y = x.split()                                # Divide into tokens
    print (y)                                    # Show the input
    fun = opsRev[y[0]]                           # Convert function name into number
    if len(y) > 1: op1 = literal(y[1])           # One parameter
    if len(y) > 2: op2 = literal(y[2])           # Two parameters
    if len(y) > 3: cIn = literal(y[3])           # Three parameters
    q, z, n, v, cOut  = alu(fun,op1,op2,cIn,1)   # Call the ALU function
                                                 # Repeat until return entered

下面是一些测试运行中的示例输出:


Enter operation and values add 25 $1F
['add', '25', '$1F']
Operation =  add Decimals: a = 25  b = 31 cIn = 0 Result = 56
Flags: Z = 0 N = 0 V = 0 C = 0
Binaries A = 00011001 B = 00011111 Carry in = 0 Result = 00111000
Enter operation and values add %11111111 1
['add', '%11111111', '1']
Operation =  add Decimals: a = 255  b = 1 cIn = 0 Result = 0
Flags: Z = 1 N = 0 V = 0 C = 1
Binaries A = 11111111 B = 00000001 Carry in = 0 Result = 00000000
Enter operation and values add 126 2
['add', '126', '2']
Operation =  add Decimals: a = 126  b = 2 cIn = 0 Result = 128
Flags: Z = 0 N = 1 V = 1 C = 0
Binaries A = 01111110 B = 00000010 Carry in = 0 Result = 10000000
Enter operation and values add 7 -2
['add', '7', '-2']
Operation =  add Decimals: a = 7  b = 254 cIn = 0 Result = 5
Flags: Z = 0 N = 0 V = 0 C = 1
Binaries A = 00000111 B = 11111110 Carry in = 0 Result = 00000101
Enter operation and values add 128 -2
['add', '128', '-2']
Operation =  add Decimals: a = 128  b = 254 cIn = 0 Result = 126
Flags: Z = 0 N = 0 V = 1 C = 1
Binaries A = 10000000 B = 11111110 Carry in = 0 Result = 01111110
Enter operation and values and $A7 %11110001
['and', '$A7', '%11110001']
Operation =  and Decimals: a = 167  b = 241 cIn = 0 Result = 161
Flags: Z = 0 N = 1 V = 0 C = 0
Binaries A = 10100111 B = 11110001 Carry in = 0 Result = 10100001
Enter operation and values lsl %11100011 2
['lsl', '%11100011', '2']
Operation =  lsl Decimals: a = 227  b = 2 cIn = 0 Result = 140
Flags: Z = 0 N = 1 V = 0 C = 1
Binaries A = 11100011 B = 00000010 Carry in = 0 Result = 10001100
Enter operation and values

最后一个示例:TC4

在这个例子中,我们提供了一个新的模拟器,它介绍了 Python 的一些新元素,例如包含日期和时间的能力。这个计算机模拟器的最终例子将我们讨论的一些内容结合起来,创建了一个具有 32 位指令内存和 16 位数据内存的模拟器。因此,这不是冯·诺依曼机器,因为它有不同的程序和数据内存。TC4 包含了一些修改,以展示简化和添加。

我们首先展示代码,然后通过标签添加一些注释,这些标签指示感兴趣的点。代码的阴影部分后面跟着代码的注释:

import re                          # Library for regular expressions for removing spaces  (See 1)
from random import  *              # Random number library
import sys                         # Operating system call library
from datetime import date          # Import date function                      (See 2)
bPt = []                           # Breakpoint table (labels and PC values)
bActive = 0
today = date.today()               # Get today's date                         (See 2)
print('Simulator', today, '\n')
deBug, trace, bActive  = 0, 0, 0   # Turn off debug, trace and breakpoint modes       (See 3)
x1 = input('D for debug >>> ')     # Get command input
if x1.upper() == 'D': deBug = 1    # Turn on debug mode if 'D' or 'd' entered
x2 = input('T or B')               # Get command input
x2 = x2.upper()                    # Convert to upper-case
if x2 == 'T': trace = 1            # Turn on trace mode if 'T' or 't' entered
elif x2 == 'B':                    # If 'B' or 'b' get breakpoints until 'Q' input          (See 4)
    next = True
    bActive = 1                    # Set breakpoint active mode
    while next == True:            # Get breakpoint as either label or PC value
        y = input('Breakpoint ')
        y = y.upper()
        bPt.append(y)              # Put breakpoint (upper-case) in table
        if y == 'Q': next = False
    if deBug == 1:                 # Display breakpoint table if in debug mode
        print ('\nBreakpoint table')
        for i in range (0,len(bPt)): print(bPt[i])
        print()
print()

memProc()函数处理数据内存,并允许你在内存中存储数据,甚至 ASCII 代码。这个函数处理汇编指令:


def memProc(src):                                # Memory processing   
    global memPoint, memD                        # Deal with directives
    for i in range(len(src)):                    # and remove directives from source code
        if src[i][0] == '.WORD':                 # Test for .word directive
            lit = get_lit(src[i],2)              # Get the literal value
            sTab.update({src[i][1]:memPoint})    # Bind literal name to the memory address
            memD[memPoint] = lit                 # Store the literal in memory
            memPoint = memPoint + 1              # Move the memory pointer on one word
        if src[i][0] == '.ASCII':                # .ASCII: test for an ASCII character
            sTab.update({src[i][1]:memPoint})    # Bind name to memory address
            character = ord(src[i][2])           # Convert character to numeric form
            memD[memPoint] = character        # Store the character in memory as ASCII code
            memPoint = memPoint + 1           # Move the memory pointer on
        if src[i][0] == '.DSW':               # Test for .DSW to reserve locations in memory
            sTab.update({src[i][1]:memPoint}) # Save name in table and bind to memory address
            memPoint = memPoint + int(src[i][2]) # Move memory pointer by space required
    src = [i  for i in src if i[0] != '.WORD']   # Remove .word from source
    src = [i  for i in src if i[0] != '.ASCII']  # Remove .ASCII from source
    src = [i  for i in src if i[0] != '.DSW']    # Remove .DSW from source
    memD[memPoint] = 'END'                    # Add terminator to data memory (for display)
    return(src)

get_reg()函数确定寄存器的编号。它首先在符号表中查找以确定名称是否为符号。如果不是,它从谓词中提取寄存器编号:


def get_reg(pred,p):                     # Extract a register number from predicate
    reg = pred[p]                        # Read token p is the predicate
    if reg in sTab:                      # Check if this is a symbolic name
        reg = sTab.get(reg)              # If symbolic name read it from symbol table
        reg = int(reg[1:])               # Convert register name into number
    else: reg = int(reg[1:])             # If not symbolic name convert name into number
    return(reg)                          # Otherwise return the register number

get_lit()函数从谓词中提取一个字面量。与寄存器名称的情况一样,它能够通过首先在符号表中查找名称来处理符号值。如果没有符号名称,文本将被转换为适当的整数,通过观察和处理任何前缀来完成:


def get_lit(pred,p):                              # Extract literal from place p in predicate
    global sTab                                   # We need the symbol table
    lit = pred[p]                                 # Read the literal from the predicate
    if lit in sTab:                               # If literal is in symbol table, look it up
        lit = int(sTab.get(lit))
    else:                                         # Convert literal format to an integer
        if   lit[0]   == "%": lit = int(pred[-1][1:],2)   # If prefix % then binary
        elif lit[0:2] == "0X": lit = int(pred[-1][2:],16)
                                                     # If prefix 0X then hexadecimal
        elif lit[0].isnumeric(): lit = int(pred[-1]) # If numeric get it
        elif lit[0].isalpha(): lit = ord(lit)        # Convert ASCII character to integer
        elif lit[0:2] == "0X": lit = int(pred[-1][2:],16)
                                                     # If prefix 0X then hexadecimal
        else:  lit = 0                               # Default (error) value 0
    return(lit)

display()函数负责在每条指令执行后显示数据。在这种情况下,数据值被转换为十六进制格式并转换为字符串:


def display():                                # Print the state after each instruction
    thisOp = ' '.join(src[pcOld])             # Join this op-code's tokens into a string
    a =[format(x,'04x') for x in r]           # Format registers into hex strings
    b = (' ').join(a)                         # Join the hex strings with a space
    f1 = f'{pcOld:<4}'                        # Format the PC as a string
    f2 = f'{thisOp:<18}'                      # Format the instruction to fixed width
    print('PC =',f1,'Reg =',b,'Z =',z,'N =',n,'C =',c,f2)       # Print the data
    return()

alu()函数执行算术运算。这个例子非常基础,只提供了基本操作。因为我们已经在其他地方介绍了 ALU,所以没有必要全面介绍。你可以轻松地添加新功能:


def alu(a,b,f):    # ALU for addition/subtraction and flag calculation                    (See 6)
# a and b are the numbers to add/subtract and f the function
    global z,c,n                      # Make flags global
    z,c,n = 0,0,0                     # Clear flags initially
    if f == 1: s = a + b              # f = 1 for add
    if f == 2: s = a - b              # f = 2 for subtract
    s = s & 0x1FFFF                   # Constrain result to 17 bits
    if s > 0xFFFF: c = 1              # Carry set if 17th bit 1
    if 0x8000 & s == 0x8000 : n = 1   # Bit 15 set to 1 for negative
    if s & 0xFFFF == 0: z = 1         # Zero flag set to 1 if bits 0-15 all 0
    s = 0xFFFF & s                    # Ensure 16-bit result
    return(s)
codes = {"STOP":(0,0),"NOP":(0,1),"RND":(1,4),"BRA":(2,5),"BEQ":(2,6),      \
         "BNE":(2,7),"MOV":(3,8),"LDRM":(4,9),"LDRL":(4,10),"LDRI":(7,11),  \
         "LDRI+":(7,12),"STRM":(4,13),"STRI":(7,14),"STRI+":(7,15),         \
         "ADD":(5,16),"ADDL":(6,17),"SUB":(5,18),"SUBL":(6,19),             \
         "AND":(5,20),"ANDL":(6,21),"OR":(5,22),"ORL":(6,23), "EOR":(5,24), \
         "EORL":(6,25),"CMP":(3,26),"CMPL":(4,27),"LSL":(3,28),             \
         "LSR":(3,29),"ROL":(3,30),"ROR": (3,31), "BSR":(2,32),             \
         "RTS":(0,33),"PUSH":(1,34),"POP":(1,35),"BL":(2,36),"RL":(0,37),   \
         "INC":(1,48), "DEC":(1,49), "PRT":(1,3), "BHS": (2,71)}
# Style Code Format (a,b) where a is the instruction style and b is the actual op-code
# 0     Zero operand               STOP
# 1     Destination register operand    INC  R0
# 2     Literal operand              BEQ  5
# 3     Two registers: Rd, Rs1          MOV  R2,R4
# 4     Register and literal Rd L         LDR  R6,23
# 5     Three registers: Rd, Rs1, Rs2       ADD  R1,R2,R3
# 6     Two registers, literal Rs, Rd1, L    ADDL R1,R2,9
# 7     Indexed, Rd, Rs, L            LDRI R4,(R6,8)
# 8     UNDEFINED
testFile = 'E:/ArchitectureWithPython/TC4_test.txt'  # Source filename on my computer
with open(testFile) as myFile:        # Open source file with assembly language program
    lines = myFile.readlines()        # Read the program into lines
myFile.close()                        # Close the source file (not actually needed)
lines = [i[0:-1]  for i in lines ]    # Remove the /n newline from each line of the source code
src = lines                        # Copy lines to variable scr (i.e., source code)
if deBug == 1:                     # If in debug mode print the source file           (See 3)
    print('Debug mode: original source file')
    for i in range(0,len(src)): print(i, src[i])    # Listing file

在这里,我们执行汇编语言文件中源文本的常规清理工作,并为后续的解析和分析准备文本。请注意,我们使用正则表达式来删除多个空格。这是一个我们在这本书中不使用的功能,但如果你在进行大量的文本处理,那么调查它是值得的:


for i in range(0,len(src)):              # Remove comments from source
   src[i] = src[i].split('@',1)[0]       # Split line on first occurrence of @ and keep first item
src = [i.strip(' ') for i in src ]          # Remove leading and trailing spaces
src = [i for i in src if i != '']           # Remove blank lines
src = [i.upper() for i in src]              # Convert lower- to upper-case
src = [re.sub('+', ' ',i) for i in src ]    # Remove multiple spaces     1
src = [i.replace(', ',' ') for i in src]    # Replace commas space by single space
src = [i.replace('[','') for i in src]      # Remove [ in register indirect mode
src = [i.replace(']','') for i in src]      # Remove [
src = [i.replace(',',' ') for i in src]     # Replace commas by spaces
src = [i for i in src if i[0] != '@']       # Remove lines with just a comment
src = [i.split(' ')  for i in src]          # Tokenize
if deBug == 1:                              # If in debug mode print the source file
    print('\nProcessed source file\n')
    [print(i) for i in src]
# Initialize key variables
# memP program memory, memD data memory
sTab = {}                                   # Set up symbol table for labels and equates
memP = [0] * 64                             # Define program memory
memD = [0] * 64                             # Define data memory
memPoint = 0                                # memPoint points to next free  location
[sTab.update({i[1]:i[2]}) for i in src if i[0] == '.EQU']
                                            # Scan source file and deal with equates
src = [i  for i in src if i[0] != '.EQU']   # Remove equates from source
src = memProc(src)                          # Deal with memory-related directives
for i in range (0,len(src)):                # Insert labels in symbol table
    if src[i][0][-1]== ':': sTab.update({src[i][0][0:-1]:i})
                                            # Remove the colon from labels
print('\nSymbol table\n')
for x,y in sTab.items(): print("{:<8}".format(x),y)    # Display symbol table
if deBug == 1:
    print("\nListing with assembly directives removed\n")
    for i in range(0,len(src)):             # Step through each line of code
        z = ''                              # Create empty string for non-labels
        if src[i][0][-1] != ':': z = '        '
                                            # Create 8-char empty first spaced
        for j in range(0,len(src[i])):      # Scan all tokens of instruction
            y = src[i][j]                   # Get a token
            y = y.ljust(8)                  # Pad it with spaces with a width of 8 characters
            z = z + y                       # Add it to the line
        print(str(i).ljust(3),z)            # Print line number and instruction
if deBug == 1:                              # Display data memory for debugging
    print("\nData memory")
    [print(memD[i]) for i in range(0,memPoint+1)]  # print pre-loaded data in memory
    print()
#### MAIN ASSEMBLY LOOP
if deBug == 1: print('Assembled instruction\n')    # If in debug mode print heading 4

现在,我们执行代码。程序计数器首先初始化为 0。当然,我们可以在任何任意点开始,甚至提供汇编语言指令来预置 pc。在设置 pc 之后,我们读取一条指令并解析它。


pc = 0
for pc in range(0,len(src)):
    rD,rS1,rS2,lit = 0,0,0,0                      # Initialize operand fields
    if src[pc][0][-1] != ':':                     # Extract mnemonic and predicate
        mnem  = src[pc][0]
        if len(src[pc]) > 1: pred = src[pc][1:]   # Check for single mnemonic only
        else: pred = '[]'                         # If only mnemonic with no predicate
    else:
        mnem  = src[pc][1]                        # For lines with a label
        if len(src[pc]) > 2: pred = src[pc][2:]   # Get predicate if one exists
        else: pred = '[]'                         # If only mnemonic, no pred
    if mnem in codes:
       opFormat = codes.get(mnem)                 # Read of op-code format of mnemonic
    else: print('Illegal opcode ERROR, mnem')     # Display error message

现在,我们可以使用opFormat从谓词中提取所需的参数:


# OP-CODE FORMATS
    if opFormat[0] == 1:            # Type 1 single register rD: inc r0
        rD = get_reg(pred,0)
    if opFormat[0] == 2:            # Type 2 literal operand: BEQ 24
        lit = get_lit(pred,-1)
    if opFormat[0] == 3:            # Type 3 two registers dD, rS1: MOV r3,R0
        rD  = get_reg(pred,0)
        rS1 = get_reg(pred,1)
    if opFormat[0] == 4:            # Type 4 register and literal Rd, lit: LDRL R1,34
        rD  = get_reg(pred,0)
        lit = get_lit(pred,-1)
    if opFormat[0] == 5:            # Type 5 three registers Rd, Rs1 Rs2: ADD  R1,R2,R3
        rD  = get_reg(pred,0)
        rS1 = get_reg(pred,1)
        rS2 = get_reg(pred,2)
    if opFormat[0] == 6:            # Type 6 two registers and lit Rd, Rs1 lit: ADD  R1,R2,lit
        rD  = get_reg(pred,0)
        rS1 = get_reg(pred,1)
        lit = get_lit(pred,-1)
    if opFormat[0] == 7:            # Type 7 two registers and lit Rd, Rs1 lit: LDR  R1,(R2,lit)
        rD  = get_reg(pred,0)
        pred[1] = pred[1].replace('(','')    # Remove brackets
        pred[2] = pred[2].replace(')','')
        rS1 = get_reg(pred,1)
        lit = get_lit(pred,-1)
    if opFormat[0] == 8:                     # Type 8 UNDEFINED
        pass

在这个模拟器的例子中,我们创建要执行的二进制代码。从谓词中提取的各种参数必须移动到适当的位置以创建最终的二进制代码binCode


    opCd     = opFormat[1] << 25    # Move op-code to left-most 7 bits
    rDs      = rD          << 22    # Move destination reg into place
    rS1s     = rS1         << 19    # Move source reg 1 in place
    rS2s     = rS2         << 16    # Move source reg 2 in place
    binCode=opCd|rDs|rS1s|rS2s|lit  # Assemble the instruction by combining fields
    memP[pc] = binCode              # Store 32-bit binary code in program memory
    if deBug == 1:                  # If in debug mode show the binary output of the assembler
        a1 = f'{pc:<4}'             # Format for the PC (4 chars wide)
        a2 = format(binCode,'032b') # Create 32-bit binary string for op-code
        a3 = f'{mnem:<5}'           # Format the mnemonic to 5 places
        a4 = f'{rD:<4}'             # Format source register to 4 places
        a5 = f'{rS1:<4}'
        a6 = f'{rS2:<4}'
        a7 = f'{lit:<6}'
        print('PC =',a1,a2,a3,a4,a5,a6,a7,src[pc]) # Assemble items and print them
# CODE EXECUTE LOOP
print('\nExecute code\n')

在我们进入代码执行循环之前,这个块初始化变量、寄存器、内存和栈指针。注意我们创建了一个包含 16 个条目的栈。栈指针被设置为 16,这是栈底以下的一个位置。当第一个项目被推入时,栈指针预先递减到 15,这是可用栈区域的底部:


r = [0] * 8                                   # Register set
stack = [0] * 16                              # stack with 16 locations           See 7
sp = 16                                       # stack pointer initialize to bottom of stack + 1
lr = 0                                        # link register initialize to 0
run = 1                                       # run = 1 to execute code
pc = 0                                        # Initialize program counter
z,c,n = 0,0,0                                 # Clear flag bits. Only z-bit is used
while run == 1:                               # Main loop
    instN = memP[pc]                          # Read instruction
    pcOld = pc                                # Remember the pc (for printing)
    pc = pc + 1                               # Point to the next instruction
    op  = (instN >> 25) & 0b1111111           # Extract the op-code (7 most-significant bits)
    rD  = (instN >> 22) & 0b111               # Extract the destination register
    rS1 = (instN >> 19) & 0b111               # Extract source register 1
    rS2 = (instN >> 16) & 0b111               # Extract source register 2
    lit = (instN      ) & 0xFFFF              # Extract literal in least-significant 16 bits
    rDc = r[rD]                               # Read destination register contents)
    rS1c = r[rS1]                             # Read source register 1 contents
    rS2c = r[rS2]                             # Read source register 2 contents

在这里,执行指令。注意我们为每个指令使用了一个 if 语句。这在初始开发阶段被使用过。在实践中,一个 if...elif 结构会更合适:


# Instruction execution
    if op == 0b0000001:           # NOP   Nothing to see here ... it's NOP so just drop out
        pass
    if op == 0b0000100:           # RND     # RND r0 generates random number in r0
       r[rD] = randint(0,0xFFFF)
    if op == 0b0000101:           # BRA     # Branch to the label or literal. Absolute address
        pc = lit
    if op == 0b0000110:           # BEQ     # Branch on zero flag
        if z == 1: pc = lit
    if op == 0b0000111:           # BNE     # Branch on not zero
        if z != 1: pc = lit
    if op == 0b1000111:           # BHS    # Branch on unsigned higher or same     (See 8)
        if c == 0 : pc = lit
    if op == 0b0001000:           # MOV    # Copy one register to another
        r[rD] = rS1c
    if op == 0b0001001:           # LDRM    # Load register from address in memory
        r[rD] = memD[lit]
    if op == 0b0001010:           # LDRL    # Load register with a literal
        r[rD] = lit
    if op == 0b0001011:           # LDRI    # Load register indirect with offset; LDRI r1,[r2,4]
        r[rD] = memD[rS1c + lit]
    if op == 0b0001100:           # LDRI+   # Auto-indexed. Increment pointer after use (See 9)
        r[rD] = memD[rS1c + lit]
        r[rS1] = rS1c + 1
    if op == 0b0001101:           # STRM    #
        memD[lit] = rDc
    if op == 0b0001110:           # STRI     # Store register indexed
        memD[rS1c + lit] = rDc
    if op == 0b0001111:           # STRI+    # Auto indexed
        memD[rS1c + lit] = rDc
        r[rS1] = rS1c + 1
    if op == 0b0010000:           # ADD     # r1 = r2 + r3
        r[rD] = alu(rS1c,rS2c,1)
    if op == 0b0010001:           # ADDL    # r1 = r2 + literal
        r[rD] = alu(rS1c,lit,1)
    if op == 0b0010010:                    # SUB
        r[rD] = alu(rS1c,rS2c,2)
    if op == 0b0010011:                    # SUBL
        r[rD] = alu(rS1c,lit,2)
    if op == 0b0010100:                    # AND
        r[rD] = (rS1c & rS2c) & 0xFFFF
    if op == 0b0010101:                    # ANDL
        r[rD] = (rS1c & lit) & 0xFFFF
    if op == 0b0010110:                    # OR
        r[rD] = (rS1c | rS2c) & 0xFFFF
    if op == 0b0010111:                    # ORL
        r[rD] = (rS1c | lit) & 0xFFFF
    if op == 0b0011000:                    # EOR (XOR)
        r[rD] = (rS1c ^ rS2c) & 0xFFFF
    if op == 0b0011001:                    # EORL (XORL)
        r[rD] = (rS1c ^ lit) & 0xFFFF
    if op == 0b0011010:                    # CMP
        diff = alu(rDc,rS1c,2)
    if op == 0b0011011:                    # CMPL
        diff = alu(rDc,lit,2)
    if op == 0b0011100:                    # LSL
        r[rD] = (rS1c << 1) & 0xFFFF
    if op == 0b0011101:                    # LSR
        r[rD] = (rS1c >> 1) & 0xFFFF
    if op == 0b0011110:                    # ROL
        bitLost = (rS1c & 0x8000) >> 16
        rS1c = (rS1c << 1) & 0xFFFF
        r[rD] = rS1c | bitLost
    if op == 0b0011111:                    # ROR
        bitLost = (rS1c & 0x0001)
        rS1c = (rS1c >> 1) & 0xFFFF
        r[rD] = rS1c | (bitLost << 16)

在下面的代码中,我们包括基于栈的操作是为了演示栈的使用和多功能性:


    if op == 0b0100000:    # BSR
        sp = sp - 1
        stack[sp] = pc
        pc = lit
    if op == 0b0100001:                    # RTS
        pc = stack[sp]
        sp = sp + 1
    if op == 0b0100010:                    # PUSH                       (See 7)
        sp = sp - 1
        stack[sp] = rDc
    if op == 0b0100011:                    # POP                        (See 7)
        r[rD] = stack[sp]
        sp = sp + 1
    if op == 0b0100100:                    # BL branch with link               (See 10)
        lr = pc
        pc = lit
    if op == 0b0100101:                    # RL return from link
        pc = lr
    if op == 0b0110000:                    # INC
        r[rD] = alu(rDc,1,1)
    if op == 0b0110001:                    # DEC
        r[rD] = alu(rDc,1,2)
    if op == 0b0000011:    # PRT r0 displays the ASCII character in register r0             See 11
        character = chr(r[rD])
        print(character)
    if op == 0b0000000:                    # STOP
        run = 0
# END OF CODE EXECUTION Deal with display
    if bActive ==1:                        # Are breakpoints active?
        if src[pcOld][0] in bPt:           # If the current label or mnemonic is in the table
            display()                      # display the data
        if str(pcOld) in bPt:              # If the current PC (i.e., pcOld) is in the table display
            display()
    if trace == 1:                         # If in trace mode, display registers
        x = input('<< ')                   # Wait for keyboard entry (any key will do)
        display()                          # then display current operation
    elif bActive != 1: display()           # If not trace and not breakpoints, display registers
    if run == 0:                           # Test for end of program             See 12
        print('End of program')            # If end, say 'Goodbye'
        sys.exit()                         # and return

关于 TC4 的注释

我们没有对这个程序提供详细的讨论,因为它遵循了早期模拟器相同的模式。然而,我们已经突出了一些其主要特性。以下数字对应于代码中阴影行的注释字段末尾的数字:

  1. 调用正则表达式库。这是一个处理正则表达式的库,提供了非常强大的文本处理手段。在这个例子中,我们只使用了一个简单的正则文本处理示例。

  2. src = [re.sub(' +', ' ',i) for i in src ] 这个 Python 表达式使用正则表达式从一个文本字符串中移除多个空格。我们包括这个例子是为了指导你了解正则表达式在更复杂的文本操作中的应用。

  3. from datetime import date 操作导入了一个来自 datetime 的 date 方法,可以用来显示日期和时间。这在运行过程中标记输出时非常有用。

  4. TC4 有一个可选的调试功能。我们可以选择三个选项。输入‘D’通过打印正在处理的源程序来提供调试功能。我们可以看到原始源代码、不带注释和汇编指令的源代码,以及经过处理的版本,包括汇编后的二进制输出。‘T’选项提供了一个按行跟踪功能,每次按下 Enter 键时执行一行代码。‘B’选项支持断点,只有当输出在断点处时才会打印。断点可能是一个标签、助记符或 PC 值。注意,在断点模式下,只显示断点行。

  5. 在提示符处输入 TB 可以用来设置跟踪模式或输入指令或 PC 值到断点表中。不寻常的是,这仅在程序开始时执行,而不是在执行阶段。

  6. 该函数处理源代码并处理与设置数据内存相关的汇编指令,例如,使用 .WORD 指令加载数据值。它还支持在内存中存储 ASCII 字符并为数据保留一个命名的内存位置。

  7. 最重要的指令是.WORD,它在内存中存储一个数值,并给该地址一个符号值。例如,如果下一个空闲的数据内存位置是20,那么.WORD TIME 100表达式会将数字100存储在内存位置20,并将名称time绑定到100.DSW(定义存储字)指令简单地为未来的数据访问预留内存位置,并命名第一个位置的地址;例如,如果当前内存位置是10,那么.DSW XYZ 5会释放五个内存位置(1011121314),并将名称XYZ绑定到10。内存指针移动到15,下一个空闲位置。memPoint变量是内存指针,在汇编阶段跟踪数据在数据内存中的存储位置。.ASCII指令是为了演示目的而存在的。.ASCII PQR指令会在内存中存储字符'T'的 ASCII 代码。

  8. 这些指令在其任务完成后从源代码中移除。

  9. 我们创建了一个非常简单的 ALU,仅实现加法和减法。这样做是为了保持程序小巧,并专注于在这个最终示例中更有趣的指令。简单的逻辑操作直接在程序的代码执行中实现,风格如下

if thisOpcode == 'AND': result = a & b

  1. TC4 提供了几种栈操作(压栈和出栈)。我们最初创建了一个单独的栈。TC4 的栈不使用数据内存。这个特性是为了演示,可以扩展。

  2. 实际的计算机通常包含比我们在这本书中展示的更广泛的条件分支。在这里,我们演示了一种这样的分支操作,BHS,表示如果更高或相同则分支。这个操作在比较两个值时强制分支,如果x > yx = y。请注意,这适用于无符号数(即,不是二进制补码)。如果比较后的进位位c0,则满足此条件。BHSBCC(在进位为 0 时分支)是同义词。例如,如果x = 1000y = 0100,则无符号数时x > y(4 > -8),如果是有符号数则y > x(8 > 4)。

  3. LDRI+操作执行基于指针的加载寄存器操作,然后增加指针寄存器。

  4. 我们提供了一个类似于 ARM 的带链接的分支和返回操作。BL操作跳转到目标地址,并将返回地址保存在一个称为链接寄存器rl的特殊寄存器中。在子例程结束时,RL(从链接返回)指令返回到调用之后的指令。这种机制只允许一个调用,因为第二个调用会覆盖链接寄存器中的返回地址。

  5. 为了演示直接打印,PRT操作显示寄存器中 ASCII 代码对应的字符;例如,如果 R1 包含 0x42,则PRT R2操作会在控制台上显示B

  6. 当程序执行完毕后,sys.exit()库函数将退出程序。

  7. 这里是一个可以被 TC4 执行的代码示例。它被错误地设置,以便测试 TC4 处理文本的能力:

    
    @ TC4_test
    @ 31 Oct 2021
          .equ abc 4
    .word aaa abc
    .word bbb 5
    .dsw   dataA 6             @ data area to store numbers
    .word  end 0xFFFF
     ldrl r0,0xF
     addl r1,r7,2
     bl lk
    back: rnd r0
           ldrl r3,dataA       @ r3 points at data area
           ldrm r4,bbb         @ r4 contains value to store
     ldrl r5,4                 @number of words to store
    loop: nop                  @
          bsr sub1
          dec r5
     bne loop
     stop
    sub1: stri r4,[r3,0]
       inc r3
       addl r4,r4,2
       cmpl r4,9
       bne skip
       addl r4,r4,6
    skip:  rts
    lk: ldrl r6,%11100101
            andl r7,r6,0xF0
     rl
    

这个例子结束了本书关于在 Python 中设计模拟器这一部分。在下一部分,我们将研究一台真正的计算机。

摘要

在本章中,我们扩展了我们关于模拟器设计的概述。我们从所有中最简单的模拟器之一开始,即零地址机;也就是说,堆栈计算机,TC0。这个模拟器不是一个真正的计算机,因为它不包括条件和分支操作。然而,它展示了使用堆栈作为执行链式计算的手段。

我们接着研究了经典 8 位计算机的指令集架构(IAS),它具有简单的单地址指令格式,其中所有操作都应用于单个累加器(即寄存器)和内存位置的内容或一个字面量。

单地址机之后是模拟一个多寄存器 CISC 指令集架构(ISA),它允许在两个寄存器之间或寄存器与内存位置的内容之间进行操作。我们开发的模拟器有一个 22 位的地址,只是为了演示你可以有任意宽度的指令。

我们还研究了 ALU 的模拟,以进一步展示算术操作是如何被模拟的。

最后,我们展示了一个具有独立的数据和指令存储器的寄存器到寄存器机。

在下一章中,我们将改变方向,介绍基于 ARM 的 Raspberry Pi 微处理器,它可以用来编写 Python 程序,并学习如何用汇编语言编程一个真正的 32 位 ARM 微处理器。

第二部分:使用树莓派研究真实计算机架构

现在,我们将把注意力转向一台真实的计算机——树莓派单板计算机核心中的 ARM。我们首先看看树莓派本身,并解释如何进入汇编程序,执行它,并在执行指令的过程中通过检查寄存器和内存来观察其执行情况。然后,我们将更详细地研究 ARM 计算机。首先,我们检查 ARM 的指令集,然后展示其寻址模式和如何访问内存。最后,我们深入探讨 ARM 处理子程序的方式。

本节包含以下章节:

  • 第九章**,树莓派——入门

  • 第十章**,深入探讨 ARM

  • 第十一章**,ARM 寻址模式

  • 第十二章**,子程序和堆栈

第九章:Raspberry Pi:简介

在前面的章节中,我们介绍了数字计算机并解释了其在指令集级别的操作。现在你将了解一个为教育目的而设计的真实、低成本计算机。

在本章中,我们介绍基于流行的 ARM 微处理器的 Raspberry Pi。我们描述了其指令集架构,并演示了如何使用它以调试模式运行汇编语言程序。本章的亮点如下:

  • Raspberry Pi 操作系统简介

  • 使用 GCC ARM 汇编器和链接器

  • 调试 ARM 汇编语言程序

这不是一本 Raspberry Pi 的手册。我们只对用它来编写汇编语言程序、运行它们并观察它们的行为感兴趣。我们不涵盖 Raspberry Pi 的 Windows 风格 GUI,因为它与相应的 PC 和 macOS 用户界面非常相似。此外,Raspberry Pi 操作系统包括实用程序和网页浏览器。

技术要求

本章基于 Raspberry Pi 4。我们使用的软件也应与早期的 3B 型号兼容。为了使用 Raspberry Pi,你需要以下设备:

  • Raspberry Pi 4(提供 2 GB、4 GB 和 8 GB DRAM)

  • Raspberry Pi 5V 3A 电源

  • USB 鼠标

  • USB 键盘

  • Wi-Fi 互联网连接

  • 带有微型 HDMI 线的视频显示器

  • 预装 NOOBS(32 GB Class 10 micro SD 卡)(见本节末尾的注释)

所有这些物品都可以在 Amazon 或 Raspberry Pi 供应商处购买。你可以将操作系统预装到微型 SD 卡上,或者下载操作系统并将其预装到自己的卡上,使用 PC 或 Mac。Raspberry Pi 网页 www.raspberrypi.org/ 提供了关于此计算机的详细信息,包括入门、设置和将操作系统加载到自己的卡上。

文本是用 NOOBS新开箱即用软件)编写的。Raspberry Pi 基金会不再支持 NOOBS,并建议你使用运行在 macOS、Windows 和 Ubuntu 下的 Raspberry Pi Imager 下载操作系统的最新版本。你可以在 www.raspberrypi.org/ 找到必要的信息。

本书所使用的 ARM 代码是为在配备 32 位操作系统的 Raspberry Pi 上运行而设计的。

Raspberry Pi 基础

微型计算机自 20 世纪 70 年代以来一直存在。在 20 世纪 70 年代,基于 Z80、6502 和 6809 8 位微处理器的几个针对爱好者的系统出现了。当时还没有操作系统、应用程序和互联网。

然后,在 20 世纪 70 年代末,Intel 推出了 8086,Motorola 推出了其 68000 16 位 CPU(实际上,68000 微处理器具有 32 位指令集架构,但 Motorola 最初将其作为 16 位机器进行营销。在我看来,这是一个灾难性的营销错误。16 位计算机相对于它们的 8 位前辈是一个巨大的飞跃,原因有两个。首先,技术进步,使得设计师能够在芯片上放置更多的电路(即,更多的寄存器、更强大的指令集等),其次,由于特征尺寸的减小(即,更小的晶体管),处理器速度更快。最后,内存成本的下降意味着人们可以运行更大、更复杂的程序。

在 20 世纪 60 年代,IBM 这家大型企业以其大规模数据处理机而闻名。然而,IBM 希望改变方向,IBM 的工程师决定围绕 Motorola 的 68000 处理器构建一台 PC。不幸的是,那个芯片的版本尚未投产。Intel 发布了 8088,这是其 16 位 8086 处理器的 8 位版本,具有 8 位数据总线,这使得使用 8 位外围设备和内存组件创建低成本微机变得容易。8088 仍然具有 16 位架构,但能够与 8 位内存和 I/O 设备接口。

IBM 与 Intel 建立了合作关系,并在 1981 年推出了所有米色系列的 IBM PC。与 Apple 不同,IBM 创建了一个开放架构,任何人都可以使用而不必支付版税。随后,数百万台 PC 克隆机应运而生。其余的都是历史。然而,PC 和 Apple 的 Mac 在市场上留下了一个空缺:一种超低成本的电脑,年轻人、学生、实验者和爱好者都可以玩。树莓派填补了这一空缺。

低成本计算已经存在很长时间了。只需几美元,你就可以买到一张当你打开时播放“生日快乐”的贺卡。高性能计算更昂贵。电脑的成本往往不在于处理器,而在于将微处理器转换为电脑系统所需的辅助组件和系统——特别是图形和显示接口、内存接口和通信接口(输入/输出)。这就是为什么树莓派如此成功的原因。在一块小巧、低成本的板上,你拥有创建一个与 PC 相当(尽管在性能方面不相上下)的完整系统所需的所有外围设备和接口。

要将板子变成一个功能齐全的微型计算机,你只需要一个低成本电源和与 PC 相同的鼠标和键盘。事实上,许多人使用从 PC 和其他他们放在那里的计算机中遗留的周边设备。我购买了一个 2 到 1 的 HDMI 切换器,将我的 4K 显示器连接到我的 PC 和 Raspberry Pi。你只需按下一个按钮,显示屏就会从 PC 切换到 Raspberry Pi。Raspberry Pi 使用开源操作系统,拥有大量的免费软件。不再需要为 Office 或 Photoshop 贷款。

Raspberry Pi 取得了卓越的成功,并迅速创造了一个庞大而热情的追随者群体。它在从幼儿园到博士的所有教育层次上都有应用。多年来,推出了 Raspberry Pi 的改进版本,以及真正最小化的版本,可以用几美元的价格作为专用嵌入式处理器使用。

图 9.1 展示了在撰写本书时使用的 Raspberry Pi 4。第一个 Raspberry Pi Model B 于 2012 年出现,具有 256 MB 的 DRAM、USB 端口和以太网,但没有无线通信。到 2019 年,Raspberry Pi 4 配备了 2 个 USB 2.0 和 2 个 USB 3.0 端口,板载 Wi-Fi 802.11ac、蓝牙 5 和千兆以太网,以及通过 2 个支持 4K 显示的 micro HDMI 端口的双显示器支持。RPi 4 的主要特点如下:

  • 强大的 ARM CPU(四核 Cortex-A72 64 位架构)

  • 音频(声音处理系统)

  • 视频显示和图形逻辑系统(你只需将卡插入显示器)

  • DRAM 主存储器(2、4 或 8 GB)

  • 带操作系统的非易失性闪存(通常不包括)

  • 鼠标和键盘 USB 端口

  • Wi-Fi(2.4 和 5.0 GHz 频段)

  • 蓝牙 5.0

  • 以太网端口

  • 一个通用 I/O 端口,可以直接与外部硬件接口

图 9.1 – Raspberry Pi 4(图片由 Laserlicht / Wikimedia Commons / CC BY-SA 4.0 提供)

图 9.1 – Raspberry Pi 4(图片由 Laserlicht / Wikimedia Commons / CC BY-SA 4.0 提供)

Raspberry Pi 板通常不附带操作系统。它有一个微 SD 端口,你必须插入一个包含合适操作系统的内存卡。你可以购买一个已经安装了操作系统的卡,或者从网上下载一个免费可用的变体(使用你的 PC 或 Mac)并将其加载到卡上,然后插入 Raspberry Pi。

学术界计算机科学家使用的经典操作系统是 Unix,该系统于 20 世纪 60 年代末在 AT&T 的贝尔实验室开发,由包括 Ken Thomson 和 Dennis Ritchie(计算机科学史上两位最杰出的玩家)在内的团队开发。Unix 是第一个成为可移植的操作系统之一——也就是说,可以在不同类型的计算机上运行。

计算机科学史上的一个强有力线索是开源软件——即由一个个人社区开发的免费软件,如 Python 编程语言和 LibreOffice 应用程序包,后者提供了微软 Office 套件的大部分功能。

在 20 世纪 80 年代,由理查德·斯蒂尔曼领导的自由软件基金会推动了 GNU 操作系统的开发,该系统旨在提供 Unix 的开源版本。1991 年,林纳斯·托瓦兹发布了 GNU 的一个开源组件,即其内核,称为 Linux。

今天,Linux 内核加上 GNU 工具和编译器已成为 Windows 等专有操作系统的免费、开源替代品。GNU/Linux 有不同的风味(由不同组编写的具有相同基本结构但不同功能的发行版)。原始官方树莓派操作系统被称为Raspbian,基于为树莓派优化的 Debian Linux 版本。

Unix 和 Linux 以命令行模式运行——即操作系统指令以文本形式输入(就像微软的 DOS 一样)。现在,Unix、Linux 和 DOS 都有用户友好的图形输入,利用鼠标作为主要输入设备。其中最著名的是微软的 Windows 操作系统。

树莓派现在包括基于 Windows 的 Linux 版本和基于文本的命令行界面,用于调用汇编和执行 ARM 汇编语言程序所需的工具。本章提供了对树莓派 Linux 操作系统的简要介绍。

树莓派操作系统包括几个与本书非常相关的软件包。例如,Thonny Python IDE 为 Python 提供了一个集成开发环境IDE),以及用于编辑、汇编、调试和运行 Python 程序的软件。

另一个有用的软件包是 Geany 编辑器,它内置了对 50 多种编程语言的支持。您可以在www.geany.org/获取 Geany。

还有一个终端模拟器窗口,允许您在 Linux 命令行模式下操作——当使用 ARM 汇编语言工具时,这是一个有用的功能。图 9.2显示了在 4K 显示器上打开多个窗口的树莓派屏幕。

图 9.2 – 树莓派多个窗口的截图

图 9.2 – 树莓派多个窗口的截图

在编写这本书的过程中,我还接触到了 Visual Studio Code,这是一个编辑和调试平台。Visual Studio Code 是免费的,可在 Linux、macOS 和 Windows 平台上使用。图 9.3显示了使用 Visual Studio Code 编写 Python 程序的一个示例会话。

图 9.3 – 开发 Python 程序时的 VS Code 会话

图 9.3 – 开发 Python 程序时的 VS Code 会话

我必须感谢 Graeme Harker 鼓励我使用 VS Code。如果我能早点发现 VS Code,我可能就会坚持使用它。

现在我们已经介绍了无处不在的 Raspberry Pi,它可以与鼠标、键盘和显示器一起组成一个计算机系统的基础,我们将介绍其操作系统。

然而,我们不会深入探讨。为了使用 Raspberry Pi 进入、运行和调试 ARM 汇编语言,您只需要了解操作系统的一些基本元素。此外,尽管 Raspberry Pi 有一个基于 Unix 的命令行操作系统,但它包括一个与 Windows 或 macOS 非常相似的图形界面。

Raspberry Pi 操作系统基础

在本节中,您将学习如何使用 Raspberry Pi 创建 ARM 汇编语言程序,将其汇编成可执行的代码,然后在 Raspberry Pi 上运行它。在下一章中,我们将更深入地探讨 ARM 架构。

我们不会花太多时间讨论 RPi 的操作系统,因为世界上充满了专注于 Linux 的网站。我们将涵盖绝对最小内容,以帮助您使用一些可能有用的命令。大多数读者将使用图形界面进行编辑、网络搜索和运行 Python 等程序。我们将介绍 Linux 文件系统的基本概念以及用于组装和运行用 ARM 汇编语言编写的源文件的命令行指令。不幸的是,Unix/Linux 命令的名称并不直观。

图 9**.4 展示了 Linux 分层操作系统的基本概念,每个级别都有一个节点可以支持较低级别的子节点;例如,Desktoppi节点的子节点。

图 9.4 – Raspberry Pi 文件结构

图 9.4 – Raspberry Pi 文件结构

最高级文件夹是/,称为根文件夹/反斜杠用于在文件系统中导航,与 Windows 的等效方式非常相似。Linux 和 Windows 之间的一大区别是,在 Linux 中,您不需要指定文件所在的磁盘(例如,Windows 始终使用c:/作为操作系统文件)。在图 9**.4中,MyFile.doc文件是一个文本文件,其位置是/home/pi/Documents/MyFile.doc.

目录导航

如果您按下回车键,Raspberry Pi 会响应一个“我在这里”提示,如本例所示:

pi@raspberrypi:/var/log/apt $

此提示显示了设备名称和当前目录的路径(在本例中以粗体字体显示)。您可以使用cd更改目录)命令更改活动目录,如本例所示:


cd ..         # This means change directory to parent (the node above)
cd home
cd pi
cd Desktop

要列出当前目录中的文件和子目录,您可以使用ls命令(列出文件)。

ls -l /home/pi 命令生成以下输出,显示权限(你可以做什么),大小和创建日期。图 9.5 展示了这个命令。

图 9.5 – Raspberry Pi 文件结构

图 9.5 – Raspberry Pi 文件结构

ls 命令有多个选项;例如,ls ~ 表示列出主目录中的内容。在 Linux 中,波浪号 ~ 表示主目录。同样,ls –t 表示按日期和时间创建的文件列表。

文件操作

现在我们介绍一些 Linux 的基本文件命令。pwd 命令看起来像应该意味着 密码。实际上,它意味着 打印工作目录 并显示当前目录的内容。这是一个“我在哪里?”命令。输入 pwd 将生成类似 /home/pi 的响应。

要创建一个新的子目录,你使用 mkdir 命令。输入 mkdir newFolder 将在当前目录下创建一个名为 newFolder 的子目录。

如果你输入 mkdir /home/pi/newFolder,它将在 pi 目录下创建一个子目录。

Linux 中比较容易混淆的命令名之一是 cat,它列出文件的内容。cat 的意思是 catalog;例如,cat /home/pi/firstExample.txt 将在控制台上以文本文件的形式显示 firstExample.txt 文件的内容。

要删除或移除一个文件,你使用 rm;例如,rm tempData.py 删除当前子目录中的 tempData.py 文件。你可以使用 rm -r 删除整个目录。这将删除当前目录且不可逆。这是一个危险的命令。另一种选择是 rm –d,它仅当目录为空时(即,你必须首先删除其内容)才会删除当前目录。

Linux 有一个帮助命令 man(即手册),它提供另一个命令的详细信息;例如,man ls 将提供 ls 命令的详细信息。

通常,当使用 Raspberry Pi 时,大多数用户都会使用图形界面。然而,我们将使用命令行输入来设置 Raspberry Pi 并组装、调试和执行汇编语言程序。

安装和更新程序和软件包

设置好 RPi 后,你需要维护它(即安装和更新软件)。以下两个命令检索新软件包并更新你的软件。偶尔运行它们以获取新更新是个好主意。注意,sudo 代表 超级用户执行,对于可能仅限于管理员操作的操作是必需的,因为如果不当使用可能会损害系统。术语 aptget 获取并安装软件包(apt = Advanced Package Tool):


sudo apt-get update        # Downloads the packages you have in your configuration source files
sudo apt-get upgrade       # Updates the packages

注意

sudo apt-get update 更新软件包但不安装它们。

要在 Raspberry Pi 上安装新软件包,你使用 apt-get install 命令;例如,

sudo apt-get install vim 安装 vim 编辑器软件包。

Linux 提供了一个关机命令,可以有序地结束会话:

sudo shutdown -h now 停止并进入挂起状态

-h参数表示进入停止状态,而now参数表示立即停止。关闭系统的命令是sudo shutdown -r now。要重启 Raspberry Pi,你可以输入以下两个命令中的任意一个。这些命令在单用户系统上具有相同的效果。在多用户系统上,你会使用shutdown -r


sudo shutdown -r now
sudo reboot

然而,大多数 Raspberry Pi 用户将使用鼠标从其 GUI 关闭 Raspberry Pi。确实,只有在汇编、链接和调试汇编语言程序时才需要使用基于文本的输入模式。

你可以应用延迟关闭;例如,sudo shutdown -h 30将在 30 分钟后关闭 RPi。你可以使用,例如,sudo shutdown -h 22:30在特定时钟时间关闭,这将在大约晚上 10:30 关闭。可以通过sudo shutdown -c撤销(取消)延迟关闭命令。

创建和编辑汇编语言程序

在我们更详细地查看 ARM 处理器之前,让我们通过在 Raspberry Pi 上创建并执行 ARM 程序所需的步骤来了解一下。尽管我们还没有介绍 ARM 汇编语言,但我们使用的指令的动作是显而易见的。

首先,你必须以文本形式创建一个.s文件类型的汇编语言程序。有许多文本编辑器,你选择的编辑器是个人喜好。我最初使用 Geany,它是一个用于 C 语言等语言的 IDE。后来我在我的台式 PC 上使用了 Thonny。Geany 和 Thonny 都是优秀的工具。如果你在台式 PC(或任何其他设备)上创建一个文本文件,你只需将.txt扩展名更改为.s,使其与 RPi 的汇编器兼容。

图 9**.6 展示了初始的 Geany 屏幕:

图 9.6 – Geany 文本编辑器窗口

图 9.6 – Geany 文本编辑器窗口

图 9**.7 展示了创建 ARM 汇编语言程序后的 Geany 窗口:

图 9.7 – Geany 窗口 – 注意到是将加到上,并将和存储在中

图 9.7 – Geany 窗口 – 注意到add r3,r0,r1是将r0加到r1上,并将和存储在r0

在以下程序中,粗体字体的文本表示一个汇编器指令,它告诉汇编器有关程序的环境以及如何处理内存空间的分配:


.section .text       @ .text indicates a region of code (not data)
.global _start       @ .global indicates that this label is visible to external objects
_start:              @ _start is the entry point to this program
   mov r0,#1         @ Load r0 with 1\. mov copies an 8-bit value into a register
   mov r1,#2         @ Load r1 with 2
   add r3,r0,r1      @ Add r0 and r1 and put result in r3
.end                 @ .end indicates the end of the program. It is optional

注意到 ARM 使用mov来加载一个字面量,而不是ldr(正如你可能预期的)。

汇编 ARM 代码

在我们能够深入查看 ARM 架构之前,我们将演示如何编写程序并运行它。基于 Debian 的 Raspberry Pi 操作系统包括 GCC,即GNU 编译器集合,它可以编译 C 程序和汇编语言程序。我们在这里不处理 C。

我们需要使用以下两个 GCC 命令来汇编源文件myProg.s文本:


as –o myProg.o myProg.s
ld –o myProg myProg.o

第一个命令 as 会将汇编语言源文件 myProg.s 转换为目标代码文件 myProg.o。第二个命令 ld 会调用一个链接器,该链接器使用目标文件创建一个可执行的二进制代码文件 myProg,它可以被执行。-o 选项是构建输出文件所必需的。然后你可以通过输入 ./myProg 来运行汇编的二进制代码程序。

虽然单个汇编语言程序可以被汇编成二进制代码,但程序通常是以模块(文件)的形式编写的,这些模块被组合在一起。这些文件可能由不同的程序员编写,甚至可能是库程序。它们被单独汇编成文件,然后链接器将这些文件组合起来创建最终的二进制代码,该代码可以被执行。在本文本中,我们没有充分利用链接器的功能;所有汇编语言程序都是单个文件。

在链接操作之后,会创建一个可执行的二进制程序。现在让我们输入以下内容:

./myProg ; echo $?

myProg 二进制代码被执行,并显示一条消息。在 Linux 中,分号允许将两个命令放在同一行上;这两个操作是 执行程序显示消息

echo $? 命令会打印出执行程序的输出信息。打印命令是 echo,而 $? 表示要打印的实际信息。在这种情况下,$? 命令返回的是上一个命令的退出状态。你也可以打印其他信息;例如,$3 会打印寄存器 r3 的内容。

注意,如果程序崩溃或进入无限循环(无响应),你可以输入 control-C 来退出并返回到操作系统级别。

汇编语言调试

我们现在将介绍 ARM 调试器,它允许你监控程序的执行并跟踪代码,就像我们在 第六章第七章 的模拟器中所做的那样。

我们想要的是监控汇编语言程序执行的能力,这项任务我们在运行 Python 计算机模拟器时已经完成。我们可以使用一个名为 gdb 的调试器来完成这项任务,它是 GCC 编译器套件的一部分。考虑以下示例:


as -g –o myProg.o myProg.s       # Assemble myProg.s Note the -g parameter
ld –o myProg myProg.o            # Link myProg.o to create source code myProg
gdb myProg                       # Run the debugger

汇编器部分的 -g 参数会生成供后续 gdb 调试器使用的调试信息。

gdb是一个功能强大的工具,具有调试程序所需的设施。我们将只查看这些允许我们运行 ARM 汇编语言程序并逐行观察其执行的设施。我们首先列出一些gdb的命令。这些命令可以缩写;例如,quit命令可以输入为 q。在表 9.1中,我们将命令的必要部分用粗体表示,可以省略的部分用灰色表示,例如 quit。注意nextstep之间的区别。这两个命令除了遇到函数时外是相同的。Step 跟踪函数中的所有操作,而 next 将函数视为一条单独的指令,并且不跟踪它。

当你将程序加载到gdb中时,似乎没有任何事情发生。如果你尝试查看你的汇编语言代码或寄存器,你会得到一个错误信息。你必须首先明确地运行程序。

命令 效果
quit 退出:离开 gdb 调试器并返回到 shell。Ctrl + D 也可以退出 gdb。
list 列出:列出正在调试的程序。
run 运行:执行程序。程序将运行至完成或遇到断点。
break 断点:执行遇到断点时停止。断点可以是行号、地址或标签。
info r 信息:显示寄存器。此命令显示寄存器内容。
info b 信息:显示断点。此显示断点。
continue 继续:在断点后恢复执行。
delete 删除:移除断点。输入d <number>以移除特定的断点。
next 单步执行(执行一条指令)。这不会跟踪函数。
step 单步执行包括函数中的所有操作。
file 将二进制代码文件加载到 gdb 中进行调试。

表 9.1 – 基本 gdb 命令

让我们在树莓派上编写和调试一个 ARM 程序。我们还没有介绍 ARM 架构。你不需要对 ARM 了解很多,因为示例与我们在第六章中模拟的 RISC 架构相似。ARM 是一个 32 位计算机,具有 RSIC 架构;也就是说,它是一个加载/存储计算机,允许的唯一内存访问是加载和存储。数据操作使用三个操作数在寄存器之间进行——例如,add r1, r2, r3。ARM 有 16 个寄存器,r0r15。寄存器r0r12可以被视为通用寄存器(即,它们的行为方式相同)。寄存器r13r14r15具有特定的功能。

伪指令——一个关键概念

ARM 汇编器包括不是 ARM 的clr r0指令的一部分的伪指令,该指令将r0加载为0。可以设计一个伪指令clr r0,汇编器可以自动将其替换为sub r0, r0操作,它具有相同的效果。

伪指令使程序员的编程生活更轻松;例如,ARM 的adr r0,abc伪指令将 32 位地址 ABC 加载到r0。这样的指令并不存在;汇编器将 adr 替换为适当的 ARM 指令。

一个 ARM 程序的示例

假设我们希望生成从 1 到 10 的数的立方和。以下 ARM 代码实现了这个算法。它还使用了 ARM 的四操作数乘法和累加指令mla


       mov   r0,#0           @ Clear the total in r0
       mov   r1,#4           @ FOR i = 1 to 4 (count down)
Next:  mul   r2,r1,r1        /* Square a number */
       mla   r0,r2,r1,r0     @ Cube the number and add to total
       subs  r1,r1,#1        @ Decrement counter (and set condition flags)
       bne   Next            @ END FOR (branch back on count not zero)

标签字段从第一列开始(如前述代码中加粗所示),提供用户定义的标签,必须以冒号结束。标签字段后面是包含操作和任何所需操作数的指令。在参数列表中逗号后面的空格数量无关紧要。@符号后面的文本是注释字段,由汇编器忽略。GCC 编译器还支持 C 语言风格的注释:由/* */字符分隔的文本,如本例所示。

表 9.2描述了 ARM 的一些指令。这里只有一个惊喜;mla 乘法和加法指令指定了四个寄存器。它将两个寄存器相乘,然后加上第三个寄存器,并将和放入第四个寄存器中;也就是说,它可以计算A = B + C.D

指令 ARM 助记符 定义
加法 add r0,r1,r2 [r0] ← [r1] + [r2]
减法 sub r0,r1,r2 [r0] ← [r1] - [r2]
AND and r0,r1,r2 [r0] ← [r1] ∧ [r2]
OR orr r0,r1,r2 [r0] ← [r1] ∨ [r2]
独异或 eor r0,r1,r2 [r0] ← [r1] ⊕ [r2]
乘法 mul r0,r1,r2 [r0] ← [r1] × [r2]
寄存器到寄存器移动 mov r0,r1``mov r0,#0xAB [r0] ← [r1][r0] ← 0xAB 移动 8 位立即数
比较 cmp r1,r2 [r1] – [r2]
零分支到标签 beq label [PC] ← label (如果 z = 1 则跳转到标签)
非零分支到标签 bne label [PC] ← label (如果 z = 0 则跳转到标签)
乘法和加法 mla r0,r1,r2,r3 [r0] ← [r1] x [r2] + [r3]
从内存加载寄存器 ldr r0,[r1] [r0] ← [[r1]]
将寄存器存储到内存 str r0,[r1] [[r1]] ← [r0]
调用操作系统 svc 0 从操作系统请求操作

表 9.2 – ARM 数据处理、数据传输和比较指令

一些计算机在操作后总是更新条件码。ARM 在操作后不会自动更新其状态标志;您必须通过在适当的助记符后附加 s 来命令状态更新。例如,add r1,r2,r3执行加法而不更新状态标志,而addsr1,r2,r3则更新状态标志。这还不是程序。以下提供了在 Raspberry Pi 上运行所需的代码和汇编语言指令:


        .global _start        @ Provide entry point
_start: mov   r0,#0           @ Clear the total in r0
        mov   r1,#10          @ FOR i = 1 to 10 (count down)
Next:   mul   r2,r1,r1        /* Square a number. Note the C style comment */
        mla   r0,r2,r1,r0     @ Cube the number and add to total
        subs  r1,r1,#1        @ Decrement counter (set condition flags)
        bne   Next            @ END FOR (branch back on count not zero)
        mov   r7,#1           @ r7 contains 1 to indicate a leave request
        svc   #0              @ Call operating system to exit this code

我们添加了一个汇编语言指令 .global,它将 _start 标签声明为在此代码片段外部是 可见的。GCC 链接过程将分别汇编的模块链接在一起,并在代码中插入标签的适当地址。

通过将标签声明为全局的,你是在告诉链接器这个标签对其他模块是可见的,它们可以引用它。没有全局指令的标签是当前模块的局部变量,对其他所有模块不可见;也就是说,你可以在两个模块中使用相同的标签,而不会发生冲突。

_start 标签表示执行开始的点。链接器和操作系统处理将程序存储在内存中的问题;也就是说,你不必担心它将在计算机的物理内存中实际存储在哪里。

最后,最后两个操作(阴影部分)提供了一种在代码执行后返回操作系统级别的手段。ARM 有一个 svc 指令,代表 服务调用 并用于调用操作系统。大多数计算机都有一个类似于 svc 的操作,并且它有许多名称——例如,软件中断。这个指令调用操作系统并传递一个或多个参数。参数可以是指令本身的一部分,也可以被加载到寄存器中。当操作系统检测到服务调用时,会读取参数并执行适当的操作。这一行为完全取决于系统;也就是说,它是操作系统的一部分,而不是计算机架构的一部分。

在这种情况下,服务调用所需的具体函数已预先加载到 r7 寄存器中。这种机制是 Raspberry Pi 操作系统的一部分。

关于汇编语言程序需要注意的关键点如下:

  • 注释由 @ 符号(或 C 语言的 /* */ 书签)开头

  • 汇编指令由点号开头

  • 标签从第一列开始,并以冒号结束

  • 可以使用 .end 指令来终止汇编语言(这是可选的)

  • .global 指令提供了一个表示程序入口点的标签

使用 Raspberry Pi 调试器

我们现在可以运行一个 ARM 汇编语言程序,并使用 gdb 逐行观察其执行。当你将程序加载到 gdb 中时,似乎没有发生任何事情。如果你尝试查看你的汇编语言代码或寄存器,你会得到一个错误消息。你必须首先明确运行程序。

考虑到 图 9**.8 中的代码片段。这是程序加载到 Geany 编辑器后的截图。它只是一组指令和指令,旨在演示创建和测试汇编语言程序所涉及的步骤。在这里,我们使用 Geany 编辑器。该程序演示了以下汇编语言指令:

  • .data    定义了一个存储变量和常数的内存区域。

  • .text 定义了一个代码区域(即汇编语言)。

  • .word 允许你在内存中存储一个数字,并给该位置一个符号地址。

  • .balign 在 4 字节边界上对齐代码和文本。由于指令是 32 位,因此是必需的。

  • .asciz 在内存中创建一个以零终止的 ASCII 文本字符串,并给它一个名称。

  • .global 使标签对链接器可见。否则,它是此模块的私有标签。

balign 操作是必需的,因为内存是按字节寻址的,ARM 指令是 4 字节长。因此,指令地址必须是 0、4、8、12、16、20 等等。如果你在指令之间放置不是 4 字节(32 位)倍数的内存数据元素,程序将会崩溃。balign 指令用零填充你存储的任何数据,以确保下一个空闲地址在 4 字节边界上。

注意,图 9**.8 中的代码使用了伪指令。指令 ldr r4,=Alan3将 32 位文字Alan3装载到r4` 中。汇编器将自动替换执行此操作所需的 ARM 代码。

下一步是汇编和链接代码,我们称之为 a4.s(我厌倦了输入长名称,将源程序命名为 a4.s)。我们可以用以下方式完成:


pi@raspberrypi:~ $ cd Desktop                     # Change to Desktop directory
pi@raspberrypi:~/Desktop $ as -g -o a4.o a4.s     # Assemble the program a4.s
pi@raspberrypi:~/Desktop $ ld -o a4 a4.o          # Now link it to create executable
pi@raspberrypi:~/Desktop $ ./a4 ; echo $?         # Run the executable program a4

粗体文本是我的输入。这些行将工作目录更改为桌面,我的源程序就在那里,然后汇编和链接源程序。最后一行,./a4 ; echo $? 运行程序并打印其返回值(通过打印 4,即 r0 中的值)来显示它已被成功执行。

图 9.8 – Geany 编辑器中的演示程序

图 9.8 – Geany 编辑器中的演示程序

以下四行演示了如何调用 gdb 调试器并设置断点。粗体字体的文本表示从键盘输入的行。其他文本是调试器的输出:


pi@raspberrypi:~/Desktop $ gdb a4
Reading symbols from a4...done.
(gdb) b _start
Breakpoint 1 at 0x10074: file a4.s, line 14.

通过输入 b 来设置断点,其中断点是行号或标签。这里,它是 _start。如果我们运行代码,它将执行到断点为止的指令。接下来的几行如下:


(gdb) run
Starting program: /home/pi/Desktop/a4
Breakpoint 1, _start () at a4.s:14
14    _start: mov r0,#4             @ Enter here (first instruction)

注意,你可以通过输入 c(即继续)从断点继续执行,执行将继续到下一个断点。

输入运行命令后,调试器开始执行并打印下一行要执行的指令 – 即,由 _start 标记的行。gdb 指令 i r(寄存器信息)显示 ARM 寄存器如下:


(gdb) i r
r0             0x0                 0
r1             0x0                 0
r2             0x0                 0
r3             0x0                 0
r4             0x0                 0
r5             0x0                 0
r6             0x0                 0
r7             0x0                 0
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
sp             0xbefff390          0xbefff390
lr             0x0                 0
pc             0x10074             0x10074 <_start>
cpsr           0x10                16
fpscr          0x0                 0

cpsrfpscr 都是状态寄存器,包含有关处理器状态的信息。

所有寄存器都由系统软件自动初始化为零,除了 r13、r15(sp, pc)和两个状态寄存器。我们现在可以开始跟踪代码,使用 s 1 步进命令逐条执行指令。您可以简单地按 enter 键来跟踪下一个指令,如下面的输出所示。如果您输入 si 2(或 s 2),则会执行两条指令:


(gdb) si 1
15            mov r1,#9
(gdb)
16            add r2,r0,r1
(gdb)
17            sub r3,r2,#3
(gdb)
18            ldr r4,=Alan3         @ Load an address into r4
(gdb)
19            ldr r5,[r4]           @ And pick up a word from memory

让我们使用 i r 命令查看寄存器。我们已从列表中删除了未更改的寄存器,以使其更容易阅读。寄存器内容以十六进制和十进制格式给出:


(gdb) i r
r0             0x4                 4
r1             0x9                 9
r2             0xd                 13 Note: r2 is expressed in hex and decimal
r3             0xa                 10
r4             0x200a0             131232
r5             0x0                 0
pc             0x10088             0x10088 <_start+20> Note: 20 bytes from start

最后,我们将继续逐步执行,直到代码执行完毕。您只需在第一个 si 1 命令之后使用 enter 键即可逐步执行:


(gdb) si 1
21            mov r7,#1             @ Prepare to exit
(gdb)
22            svc 0                 @ Go
(gdb)
[Inferior 1 (process 1163) exited with code 04]

gdb 执行了移动和系统调用指令并退出了模拟。我们学到了什么?这个例子演示了以下内容:

  • 如何创建 ARM 汇编语言程序

  • 如何汇编和链接

  • 如何将其加载到 gdb 调试器中

  • 如何设置断点并运行代码,直到达到断点

  • 如何在任何程序点显示寄存器的内容

  • 如何逐行执行代码

跟踪执行和显示内存

调试器的一个重要功能是能够在执行程序时逐步执行代码并显示寄存器。这允许您跟踪程序的执行并检测错误。gdb 调试器通过其 layout regs 功能实现这一功能。

图 9**.9 展示了具有三个面板的 TUI 示例。上面的窗口是寄存器窗口(已更改的寄存器被突出显示;在这种情况下,是 r7)。中间面板显示程序代码,下一个要执行的指令被突出显示。每一行都包括代码的内存地址,以十六进制形式表示,以及从 start 标签的距离。

注意,代码在我的最后一条指令 svc 之后继续执行。这是因为反汇编器读取一块内存并将其显示为代码(即使它不是您的程序的一部分)。在这种情况下,我们使用 .word 指令在内存中输入的数据被读取并显示为相应的 ARM 指令。请记住,调试器不知道内存中的二进制值是指令还是用户数据。如果它读取与指令操作码相对应的数据值,它会打印出该操作码。

反汇编的指令以十六进制形式显示其在内存中的地址;例如,第一条指令位于 0x10074。这个地址由 Raspberry Pi 的操作系统确定。如您所见,每个指令的地址比前一个指令大 4 个字节,因为 ARM 是一个 32 位机器,具有 32 位或 4 字节的指令;因此需要对齐指令,强制下一个指令或数据放置在特定的边界上。

最低面板包含您输入的命令。在这种情况下,我使用了 si 1 来逐步执行指令。

图 9.9 – 显示寄存器和内存内容的 TUI

图 9.9 – 显示寄存器和内存内容的 TUI

让我们看看汇编语言程序及其调试的另一个示例。这个例子仅用于演示目的。它没有任何有用的功能。我们的目的是演示在 GCC 汇编器和 gdb 调试器下运行的 ARM 汇编语言程序的功能。表 9.3给出了 GCC 的一些最常见汇编指令。

GCC 汇编指令 操作
.text 指示包含代码的程序段
.data 指示包含数据的程序段
.global label 使标签对链接器可见
.extern label 使标签在文件外部可见
.bytebyte1,byte2, … 定义 1 个或多个字节的数据并将其存储在内存中
.hwordhw1,hw2, … 定义 1 个或多个 16 位半字并将其存储在内存中
.wordw1,w2, … 定义 1 个或多个 32 位字并将其存储在内存中
.space bytes,fill 使用给定的值填充内存块(例如,.fill 64,0xFF
.balign 4 将下一个地址对齐到 4 字节边界(也可以使用 2、4、8 和 16)
.ascii "``any string" 在内存中存储 ASCII 字符串
.asciz "``any string" 在内存中存储一个以 0 结尾的 ASCII 字符串
.equsymbol, value 将符号名称与其值等价(例如,.equ hours 24
.end 标记程序的结束

表 9.3 – GCC ARM 汇编器指令

我们对程序的一些功能进行了更详细的解释;这些功能的形式为@ PRINT STRING @


                                @ Test function calls and memory access
         .text                  @ Program (code) area
         .global  _start        @ Provide entry point and make it known to linker
         .equ     v1,0xAB       @ Test equate directive
_start:   mov     r0,#23        @ Just three dummy operations for debugging
          mov     r1,#v1        @ ARM assembler uses # to indicate a literal
          add     r2,r0,r1

@ 在控制台上打印字符串 @


          ldr     r1, =banner   @ Test display function (this is a pseudo-instruction)
          mov     r2,#15        @ Number of characters to print 13 plus two newlines)
          mov     r0,#1         @ Tell the OS we want to print on console display
          mov     r7,#4         @ Tell the OS we want to perform a print operation
          svc     0             @ Call the operating system to do the printing

@ 使用地址 @


          adr     r3,v3              @ Load address of v3 into register r3 (a pseudo-instruction)
          ldr     r4,[r3]            @ Read contents of v3 in memory
                                     @ Read from memory, increment and store in next location
          ldr     r0,adr_dat1        @ r0 is a pointer to dat1 in memory
          ldr     r1,[r0]            @ Load r1 with the contents of dat1
          add     r2,r1,#1           @ Add 1 to dat1 and put in r2
          add     r3,r0,#4           @ Use r3 to point to next memory location after dat1
          str     r2,[r3]            @ Store new data in location after dat1

@ 程序退出 @


          mov     r0,#0              @ Exit status code (0 indicates OK)
          mov     r7,#1              @ Tell the OS we want to return from this program
          svc     0                  @ Call the operating system to return

@ 地址向量 @


adr_dat1: .word   dat1               @ Generate an address for dat1 in the memory area
v2:       .word   0xFACE             @ Dummy data
banner:   .ascii "\nTest printing\n" @ String to print. Note newlines "\n"
                                     @ This string has 15 characters (13 ASCII two newlines)
          .balign 4                  @ Align data (0 inserted because the string is 15 chars)
v3:       .word   0x1111             @ Dummy test data
          .space  4,0xAB             @ Reserve 8 bytes of storage and fill with 0xAB
          .word   0x2222
          .data                      @ Data segment
test:     .word 0xbbbb
dat1:     .word 0x1234
          .end                       @ End of this program

这段代码说明了几个要点——例如,使用汇编指令如.equ,将符号名称绑定到值。我已经用阴影标记了有趣的代码块,以便我们可以讨论它们。

我们使用了 ARM 的伪指令。这些是adr r3,v3ldr r1,=banner。这两个指令都将 32 位地址加载到寄存器中。这样的指令并不存在。ARM 汇编器选择实际的指令来执行所需操作。

@ 打印字符串 @

第一个示例演示了如何从汇编程序中打印数据。实际上,我们无法打印数据,但我们可以请求操作系统为我们完成。大多数处理器都有一个名为软件中断(或系统调用、陷阱、异常或额外代码)的指令。所有这些术语都指同一件事:程序员插入的指令,用于调用操作系统。在 ARM 的情况下,它是 svc 指令(以前称为 swi)。当 Linux 使用时,这个指令使用参数0——即svc 0

系统调用完全依赖于操作系统,并通过在寄存器中传递参数来告诉操作系统它需要什么。我们将打印一串字符到显示设备。Raspberry Pi OS 需要将字符串在内存中的位置传递到寄存器 r1,打印的字符数传递到寄存器r2,显示类型传递到r0,以及要执行的操作(打印)传递到r7

需要打印的文本地址banner通过ldr r1,=banner指令加载到寄存器r1中。这个伪指令通过=<address>指定一个地址。在程序中,我们使用了.ascii指令将打印的字符串存储在内存中。字符串以\n结尾,对应于换行符。请注意,换行符的代码是 1 字节,虽然在程序中用\n表示。除非内存中存储的字符串或其他数据项是 4 字节的倍数,否则你必须跟一个.balign 4来确保下一个指令落在字边界上。

@ 使用 ADR @

第二个示例演示了使用adr伪指令与adr r3,v3。我们将把一个名为v3的变量的地址加载到寄存器r3中,并通过.word指令将其加载到内存中。一个实际考虑是,当你反汇编代码时,你将看不到adr;你将看到 ARM 汇编器将其转换成的实际代码。

v3变量的地址放入寄存器意味着我们可以使用该寄存器作为指针,并通过加载指令使用它;例如,ldr r4,[r3]将变量的值(即0x1111)加载到r4中。如果你希望修改该变量,你可能认为可以用str r5,[r3]将其存储回内存。遗憾的是不行!adr指令生成的代码只允许你访问程序的当前段。该段是只读的,因为它包含代码。你无法更改该段中的内存。如果你希望修改内存,你必须使用不同的技术,正如我们很快将看到的。

@ 程序退出 @

在汇编语言程序执行完成后,需要返回到操作系统级别。将退出代码1加载到寄存器r7中,并执行svc 0指令来调用操作系统。按照惯例,程序员在退出之前将他们的退出代码加载到寄存器 r0 中。退出代码0通常用来表示一切顺利,而退出代码1表示出现了问题。

@ 地址向量 @

你不能使用 adr 伪指令将数据写入与程序代码部分位于不同段的读写内存。这种困境存在于所有代码开发系统中,并不仅限于 ARM GCC 环境。ARM 处理器将允许你在逻辑地址空间内的任何位置读取内存和写入内存。然而,ARM 操作系统 不允许你写入只读内存区域或其他禁止区域。技巧是创建一个指向变量的指针,并将该指针存储在代码段中。

考虑以下汇编指令。这个指令将 32 位的 dat1 值存储在内存中的 adr_dat1 位置。按照惯例,一些程序员通过在其名称前加上标记(通常是 adr)来表示一个项是一个指针(即地址)。这并不是一个规则,而是一种惯例:

图 9.10 - 创建指向数据值的指针

图 9.10 - 创建指向数据值的指针

我们创建了一个名为 adr_dat1 的名称,它是我们目标变量的 地址 的地址。存储的值是实际变量的地址,dat1。因此,当我们写入指令 ldr r0,adr_dat1 时,dat1地址 被加载到寄存器 r0 中。也就是说,寄存器 r0 现在指向 dat1

在以 .data 为标题的数据部分,我们有以下内容:


dat1:     .word 0x1234   @ The value 0x1234 is stored in memory at address dat1

这将 0x1234 值存储在内存中,并给它命名为 dat1。正如我们所见,该名称用于通过以下方式在代码部分创建变量的地址:


adr_dat1: .word   dat1

下一步是运行代码。我们已经这样做,并在 列表 9.1 中提供了一个会话的编辑输出(删除操作之间的空提示行和一些文本):


pi@raspberrypi:~ $ cd Desktop
pi@raspberrypi:~/Desktop $ as -g -o t1a.o t1a.s
pi@raspberrypi:~/Desktop $ ld -o t1a t1a.o
pi@raspberrypi:~/Desktop $ gdb
GNU gdb (Raspbian 8.2.1-2) 8.2.1
(gdb) file t1a
Reading symbols from t1a...done.
(gdb) b _start
Breakpoint 1 at 0x10074: file t1a.s, line 7.
(gdb) r 1
Starting program: /home/pi/Desktop/t1a 1
Breakpoint 1, _start () at t1a.s:7
7    _start: mov     r0,#23        @ Just three dummy operations for debugging
(gdb) si 1
8            mov     r1,#v1
9            add     r2,r0,r1
11           ldr     r1,=banner   @ Test display function (r1 has address of a string)
12           mov     r2,#15        @ Number of characters to print 13 plus two newlines)
13           mov     r0,#1         @ Tell the OS we want to print on console display
14           mov     r7,#4         @ Tell the OS we want to perform a print operation
15           svc     0             @ Call the operating system to do the printing
Test printing
17           adr     r3,v3         @ Load address of v3
18           ldr     r4,[r3]       @ Read its contents in memory
21           ldr     r0,adr_dat1   @ r0 is a pointer to dat1 in memory
22           ldr     r1,[r0]       @ Load r1 with the contents of data1
23           add     r2,r1,#1      @ Add 1 to dat1 and put in r2
24           add     r3,r0,#4      @ Use r3 to point to next memory location after dat1
25           str     r2,[r3]       @ Store new data in location after dat1
(gdb) i r r0 r1 r2 r3
r0           0x200e8             131304
r1           0x1234              4660
r2           0x1235              4661
r3           0x200ec             131308
(gdb) si 1
28          mov    r0,#0           @ Exit status code (indicates OK)
29          mov    r7,#1           @ Tell the OS we want to return from this program
30          svc    0               @ Call the operating system to return
(gdb) x/2xw 0x200e8
0x200e8:    0x00001234    0x00001235
(gdb) si 1
[Inferior 1 (process 7601) exited normally]

列表 9.1 – 调试会话

访问内存

我们已经展示了如何逐条执行程序并显示寄存器的内容。例如,gdb 允许你使用 i r r0 r1 r2 r3 命令显示寄存器 r0r3 的内容。现在我们将演示如何显示内存位置的内容。

列表 9.1 中,我们逐条执行代码的前几条指令(内存访问和存储操作),然后在第 25 行之后,我们可以看到 dat3 变量的地址是 0x200e8。假设我们想检查其值是否为 0x1234,以及下一个字位置 4 字节之后的 0x2008c 包含 0x1235 值。

你可能会合理地期望读取内存位置的 gdb 命令是 m 0x200c。如 列表 9.1 所示,命令相当难以记忆:


x/2xw 0x2208    Read the contents of two memory locations

内存访问命令是 x/,并且需要三个参数:2xw。这些如下:

  • 2   要显示的内存位置数量。

  • x   数据的格式。x 表示十六进制。

  • w   数据的宽度(字节数)。w 表示 4 字节的 32 位字。

可用的格式如下:

  • o   八进制

  • d   十进制

  • x   十六进制

  • u   无符号整数

  • s   字符串

  • b   字节

数据显示大小如下:

  • b   字节

  • h   半字(16 位)

  • w   字(32 位)

  • g   双字(8 字节或 64 位的大字)

考虑以下示例:

  • x/1xw 0x1234   在地址 0x1234 以十六进制形式打印一个 4 字节字

  • x/6xh 0x1234   在地址 0x1234 以十六进制形式打印六个 2 字节值

  • x/3db 0x1234   在地址 0x1234 以十进制形式打印三个单字节值

  • x/9sb 0x1234   在地址 0x1234 以字符串形式打印九个单字节字符

在下一节中,我们将更详细地探讨 ARM GCC 汇编器。例如,我们将介绍控制程序内存分配的 ARM 汇编指令。

GCC ARM 汇编器的特性

我们将从这个部分开始,看看如何为常量和变量保留内存空间。我们已经看到,ARM 汇编语言中的字面量前面有一个 # 符号。数字默认为十进制,除非前面有 0x 前缀,表示十六进制——例如,mov r0,#0x2C。ASCII 字符使用单引号表示,如下例所示:


      cmp   r0,#'A'            @ Was it a letter 'A'?

两个重要的汇编指令是 .equ,它将一个名称绑定到一个值,以及 .word,它允许你在程序运行之前预加载内存中的数据。.equ 指令非常容易理解;它将一个数值绑定到一个名称。考虑以下示例:


      .equ  Tuesday, 2

这个汇编指令将名称 Tuesday 绑定到值 2。每次你写入 Tuesday,汇编器都会将其替换为 2。GCC ARM .word 汇编指令为常量和变量保留内存空间;也就是说,它声明了一个变量(或常量)并初始化它。考虑以下示例:


      .equ    Value1,12        @ Associate name Value1 with 12
      .equ    Value2,45        @ Associate name Value2 with 45
      .word   Table,Value1     @ Store the 32-bit word 12 in memory at address Table
      .word   Value2           @ Store the word 45 in memory
      .word   Value2 + 14      @ Store  45 + 14 = 59 in memory (you can use expressions)

.word 指令在内存中保留一个 32 位字(即 4 字节)的存储空间,并将 .word 右侧的表达式得到的值加载到该位置。在这种情况下,我们将 Value1 绑定到数字 12,因此二进制值 00000000000000000000000000001100 将存储在这个位置。下一个使用的内存位置是下一个空闲位置(即存储指令按顺序存储数据在内存中)。

位置计数器 前进了四个字节,以便下一个 .word 或指令将放置在内存中的下一个字。位置计数器 指的是程序汇编时内存中下一个位置的指针,在概念上与程序计数器相似。

你不必在 ARM 程序中使用 32 位值。.byte.hword 汇编指令分别将一个字节和一个 16 位半字存储在内存中,如下例所示:


Q1:    .byte        25                  @ Store the byte 25 in memory
Q2:    .byte        42                  @ Store the byte 42 in memory
Tx2:   .hword       12342               @ Store the 16-bit halfword 12,342 in memory

虽然你可以使用 .byte 在内存中存储文本字符串,但这会很笨拙,因为你必须查找每个字符的 ASCII 值。GCC ARM 汇编器提供了一个更简单的机制。.ascii 指令接受一个字符串,并将每个字符作为连续内存位置中的 8 位 ASCII 编码的字节存储。.asciz 命令执行相同的功能,但插入一个全零的 8 位二进制字节作为终止符:


Mess1: .ascii    "This is message 1"    @ Store string memory
Mess2: .asciz    "This is message 2"    @ Store string followed by 0
       .balign  4                       @ Align code on word boundary

因为 ARM 将所有指令对齐到 32 位单词边界,所以需要 .balign 4 指令来对齐下一个单词边界(4 表示 4 字节边界)。换句话说,如果你在内存中存储三个 8 位字符,.balign 4 命令会跳过一个字节,迫使下一个地址达到 32 位边界。请注意,.balign 2 强制对齐到半字边界(你可以使用 .balign 16 或任何其他 2 的幂,以强制下一次内存访问适当地对齐)。

下面的 ARM 代码演示了存储分配和 .balign 4 指令的使用:


        .global  _start                 @ Tell the linker where we start from
        .text                           @ This is a text (code) segment
_start: mov     r0,#XX                  @ Load r0 with 5 (i.e., XX)
        mov     r1,#P1                  @ Load r1 with P1 which is equated to 0x12 or 18 decimal
        add     r2,r0,r1                @ Just a dummy instruction
        add     r3,r2,#YY               @ Test equate to ASCII byte (should be 0x42 for 'B')
        adr     r4,test                 @ Let's load an address (i.e., location of variable test)
        ldr     r5,[r4]                 @ Now, access that variable which should be 0xBB)
Again:  b       Again                   @ Eternal endless loop (terminate here)
       .equ     XX,5                    @ Equate XX to 5
       .equ     P1,0x12                 @ Equate P1 to 0x12
       .equ     YY,'B'                  @ Equate YY to the ASCII value for 'B'
       .ascii   "Hello"                 @ Store the ASCII byte string "Hello"
       .balign  4                       @ Ensure code is on a 32-bit word boundary
       .ascii   "Hello"                 @ Store the ASCII byte string "Hello"
       .byte    0xAA                    @ Store the byte 0xAA in memory
test:  .byte    0xBB                    @ Store the byte 0xBB in memory
       .balign  2                       @ Ensure code is on a 16-bit halfword boundary
       .hword   0xABCD                  @ Store the 16-bit halfword 0xABCD in memory
last:  .word    0x12345678              @ Store a 32-bit hex value in memory
       .end

让我们在 Raspberry Pi 上使用 gdb 汇编、链接和运行此代码。终端窗口的前几行显示了程序的加载、设置断点和单步执行模式:


pi@raspberrypi:~ $ cd Desktop
pi@raspberrypi:~/Desktop $ as -g -o labels.o labels.s
pi@raspberrypi:~/Desktop $ ld -o labels labels.o
pi@raspberrypi:~/Desktop $ gdb labels
GNU gdb (Raspbian 8.2.1-2) 8.2.1
Reading symbols from labels...done.
(gdb) b _start
Breakpoint 1 at 0x10054: file labels.s, line 3.
(gdb) run 1
Starting program: /home/pi/Desktop/labels 1
Breakpoint 1, _start () at labels.s:3
3    _start: mov     r0,#XX             @ Load r0 with 5 (i.e., XX)
(gdb) si 1
4            mov     r1,#P1             @ Load r1 with 0x12 (i.e., P1)
5            add     r2,r0,r1           @ Dummy instruction (r2 is 5+0x12=0x17)
6            add     r3,r2,#YY          @ Dummy instruction (r3 is 0x17+0x42=0x59)
7            adr     r4,test
8            ldr     r5,[r4]
Again () at labels.s:9
9    Again:  b       Again              @ Eternal endless loop (enter control-C to exit)

到目前为止,一切顺利。让我们看看寄存器中有什么。我们删除了不感兴趣的寄存器行,以使输出更易于阅读:


(gdb) i r
r0             0x5                 5
r1             0x12                18
r2             0x17                23
r3             0x59                89 Note 0x59 = 0x17 + 0x42
r4             0x1007e             65662
r5             0xabcd00bb          2882339003
pc             0x1006c Current pc     0x1006c <Again>

寄存器 r0r3 包含我们预期的内容(r3 中的 0x17 加上 'B'0x42 代码,即 0x59)。

寄存器 r4 包含 0x1007e,这是名为 test 的数据的地址(即,0xBB)在内存中。该地址用于将 0xBB 常量加载到 r5 中,现在 r5 包含 0xABCD00BB,而不是我们预期的 0x000000BB。出了什么问题?

问题在于 ldr 从内存中将 32 位值加载到寄存器中。0xABCD000xBB 后面的单词加上由于 .balign 2 语句引起的空字节。我们应该使用特殊的 “加载一个字节” 指令,加载四个字节并将三个字节清零,或者在内存中正确对齐字节。计算机的巨大优势在于它会按照你的指示去做。唉,它的巨大弱点是……它 完全 按照你的指示去做。

接下来,我们使用 x/7xw 0x1006c 命令查看内存中存储的数据,该命令以十六进制形式显示从地址 0x1006c 开始的 7 个单词的内存(我们从寄存器转储中的 pc 获取了该地址)。记住,是 ARM 的操作系统最初设置了程序计数器:


(gdb) x/7xw 0x1006c
0x1006c <Again>:    0xeafffffe 0x6c6c6548 0x0000006f 0x6c6c6548
0x1007c <Again+16>: 0x00bbaa6f 0x5678abcd 0x00001234

我们也可以使用 x/28xb 0x1006c 来查看存储在内存中的数据,它以十六进制形式显示从地址 0x1006c 开始的 7 个单词(4 x 7 = 28 字节)的内存:


(gdb) x/28xb 0x1006c
0x1006c <Again>:      0xfe   0xff   0xff   0xea   0x48   0x65   0x6c   0x6c
0x10074 <Again+8>:    0x6f   0x00   0x00   0x00   0x48   0x65   0x6c   0x6c
0x1007c <Again+16>:   0x6f   0xaa   0xbb   0x00   0xcd   0xab   0x78   0x56
0x10084 <last+2>:     0x34   0x12   0x00   0x00

图 9**.10 提供了一个内存映射,展示了内存的分配情况。粗体的十六进制地址是 4 字节单词边界。你可以看到 .balign 指令如何在内存中插入零作为填充,以形成所需的边界。

在下一节中,我们将探讨现代计算机设计的关键方面之一——如何将 32 位值加载到 32 位字长的计算机寄存器中:


000000010070     48             ASCII H start of the sequence Hello
000000000071     65             ASCII e
000000000072     6C             ASCII l
000000000073     6C             ASCII l
000000000074     6F             ASCII o
000000000075     00             Padded zero due to align
000000000076     00             Padded zero due to align
000000000077     0              Padded zero due to align
000000010078     48             ASCII H
000000000079     65             ASCII e
00000000007A     6C             ASCII l
00000000007B     6C             ASCII l
00000000007C     6C             ASCII o
00000000007D     0xAA           Byte 0xAA
00000000007E     0xBB           Byte 0xBB
00000000007F     00             Padded zero due to align
000000000080     0xAB           First byte of 0xABCD
000000000081     0xCD           Second byte of 0xABCD

图 9.10 – 分配数据到内存 – 内存映射

接下来,我们将探讨影响所有计算机的一个困境:如何加载与指令字大小相同的常量(立即数)?

处理 32 位立即数

在这里,你将学习 ARM 如何使用 32 位指令加载 32 位立即数。立即数不能与操作码组合,就像我们在模拟器中所做的那样。我们将演示 ARM 如何使用几种技术使用 32 位指令来访问 32 位立即数。

ARM 有 32 位数据字和指令。你无法在一个指令中将 32 位立即数加载到 ARM 寄存器中,因为你无法在一个指令中同时指定操作和数据。CISC 处理器将两个或更多指令链接在一起;例如,一个 16 位机器可能需要 2 个指令字来创建一个包含 16 位操作和 16 位立即数的 32 位指令。一些处理器使用一个指令加载 16 位立即数(加载高位),然后使用第二个指令加载第二个 16 位立即数(加载低位)。然后计算机将高位和低位 16 位值连接成一个 32 位立即数。

ARM 有两种伪指令可以将 32 位值加载到寄存器中,允许汇编器生成执行此操作所需的实际代码。伪指令adr(加载地址)的格式为adr rdestination,label,其中label表示程序中的一行(地址)。adr允许汇编器生成适当的机器代码,并减轻程序员的某些家务adr使用 ARM 的addsub指令与PC 相对寻址来生成所需的地址。程序计数器相对寻址通过其与当前指令的距离来指定地址。以下代码片段演示了adr的使用:


          adr    r0,someData    @ Setup r1 to point to someData in memory
          ldr    r1,[r0]        @ Read someData using the pointer in r0
          .        .
someData: .word  0x12345678     @ Here's the data

伪指令adr r0,someData使用汇编器生成的适当代码将 32 位地址someData加载到寄存器r0中。通常你不需要知道汇编器如何生成实现adr的实际代码。

另一个有用的 ARM 伪指令是ldr r1,=value`。在这种情况下,编译器生成的代码允许寄存器 r1 加载指定的值,如下例所示:


      ldr r2, =0x12345678       @ Load a 32-bit literal into r2

这将 1234567816 加载到 r2 中。如果汇编器能够做到,它将使用movmvn指令。ARM 的异常移动not指令接受一个 8 位立即数,反转位,并将其移动到寄存器。例如MVN r1,#0xF00x0F复制到 r1。或者,汇编器使用ldr r2,[pc,#offset]指令来访问存储在内存中某个所谓的立即数池常量池中的适当常量 1234567816。立即数池是嵌入在代码中的一或多个数据项。

让我们看看 GCC 汇编器开发系统如何处理伪指令。考虑以下代码片段:


        .text
        .global _start
_start: ldr    r0,=0x12345678   @ Load r0 with a 32-bit constant
        adr    r1,Table1        @ Load r1 with the address of Table1
        adr    r2,Table2        @ Load r2 with the address of Table2
        ldr    r3,[r1]          @ Load r3 with data in Table1
        ldr    r4,[r2]          @ Load r4 with data in Table2
        ldr    r5, =0xAAAAAAAA  @ Load r5 with a 32-bit constant
wait:   mov    r0,#0            @ Goodbye message
        mov    r7,#1            @ Goodbye command in r7
        svc    0                @ Call operating system to return
Table1: .word  0xABCDDCBA       @ Dummy data
Table2: .word  0xFFFFFFFF

以下是一个 gdb 调试会话的编辑输出。代码已执行完成,寄存器内容如下。右侧列显示十进制形式的数据:


r0             0x0                 0
r1             0x10078             65656
r2             0x1007c             65660
r3             0xabcddcba          2882395322
r4             0xffffffff          4294967295
r5             0xaaaaaaaa          2863311530
pc             0x10074             0x10074 <wait+8>

指针寄存器 r1r2 已加载了内存中两个数据元素的地址(即,Table1 和 Table2)。这些指针已被用来检索这两个元素,你可以从调试器中看到操作是成功的。

以下调试输出提供了代码的反汇编。这不是所写的代码。汇编器已将三个伪操作转换为实际的 ARM 代码(粗体字体):


Dump of assembler code from 0x10054 to 0x10086:
   0x00010054 <_start+0>:    ldr    r0, [pc, #36]    ; 0x10080 <Table2+4>
   0x00010058 <_start+4>:    add    r1, pc, #24
   0x0001005c <_start+8>:    add    r2, pc, #24
   0x00010060 <_start+12>:   ldr    r3, [r1]
   0x00010064 <_start+16>:   ldr    r4, [r2]
   0x00010068 <_start+20>:   ldr    r5, [pc, #20]    ; 0x10084 <Table2+8>

第一次加载指令将寄存器 r0 从当前程序计数器内存的 36 个字节处加载数据。在那个位置,汇编器存储了要加载的 0x12345678 常量。

两个 adr 操作通过添加 pc 和内存中数据之间的距离来生成一个地址。这被称为 程序计数器相对寻址,我们将在稍后更详细地探讨它。

让我们看看内存中的数据。我们使用 x/6xw 0x10080 gdb 命令从地址 0x10080 显示六个字的内存:


(gdb) x/6xw 0x10080
0x10080 <Table2+4>:    0x12345678    0xaaaaaaaa    0x00001141    0x61656100
0x10090:               0x01006962    0x00000007

这显示了在程序之后加载到内存中的 0x12345678 常量,以及我们加载的其他常量。

关于字节序的说明

我们还没有提到一个主题 - 字节序。这个术语是从 格列佛游记 中借用的,在那里世界被分为那些从大端吃煮鸡蛋的人和那些从小端吃鸡蛋的人。这把世界分成了相互敌对的大端序者和小端序者(当然,这是讽刺)。

计算机做类似的事情。假设你在内存中存储了 32 位的十六进制值 0x12345678。如果你在字内存中存储它,其中每个字的地址相差 1,那么生活就会很简单。但是因为计算机内存是 字节组织 的,每个内存位置都有一个独立的字节地址,所以连续的字节地址是 0,1,2,3,4,5,6…,而连续的字地址是 0,4,8,12,16,20,24…

字节寻址的一个后果是,字 0 占据字节地址 0,1,2,3。假设我们在地址 0 存储了 0x12345678。我们首先放哪个数字的端?它是存储在字节 0 到 3 作为 12 34 56 78,还是作为 78 56 34 12

图 9**.4 展示了三个内存系统。在所有三种情况下,内存都是字节寻址的。在 32 位版本中,我们在地址 c 和 0x1014 存储了代表 0x12345678 的两个 32 位值。注意存储的字的各个字节有不同的字节地址。小端序数字的安排使得最高有效字节 0x12 存储在字的最低地址 0x1010。大端序数字存储时,最高有效字节存储在最低地址 0x1013

图 9.11 – 内存组织

图 9.11 – 内存组织

有些计算机是大端字节序,有些是小端字节序。摩托罗拉微处理器是大端字节序,英特尔是小端字节序。ARM 最初是小端字节序,但现在它在 CPSR 状态寄存器中有一个位可以用来选择所需的字节序版本。默认情况下,ARM 是小端字节序。

字节序重要吗?有两种方式很重要。如果你正在构建系统或与具有混合字节序的系统进行接口,那么它很重要,因为当你将字节从一个系统传递到另一个系统时,你必须确保字节顺序是正确的。例如,TCP/IP 协议是大端字节序。同样,如果你在对数据进行字节和字操作,你必须意识到字节地址和字地址之间的关系。如果你将 ASCII 字符串“Mike”存储在字地址 0x1000,并且你想要“e”,在大端系统中它将是 0x1000,而在小端系统中它将是 0x1003

将一切组合在一起 – 一个最终示例

为了结束本章,我们提供了一个使用 Raspberry Pi 进入 ARM 汇编语言并使用 gdb 在调试模式下运行的最终示例。与之前一样,此示例不执行有用的功能。其目的是演示寻址模式、使用汇编指令在内存中加载数据、字节序的性质以及声明变量并在内存中修改它们的能力。

此示例还演示了如何显示内存数据以及如何使用内存显示功能来读取数据。我们在调试过程中使用了 gdb 并复制了各种屏幕。以下将这些内容组合在一起。我们删除了一些材料(例如,状态寄存器和未访问的寄存器),并对格式进行了轻微编辑以提高可读性。

源程序

源程序在内存中设置数据并访问它。我们在演示中使用了 ASCII 文本和数字文本。请注意,使用 GCC 汇编器,ASCII 字符串或字符需要双引号。我们还使用 ldrb 读取字节,使用 ldrh 读取半字(16 位)。

程序使用 ldrb r5,[r3,#1] 来演示从 r5 的字基址开始的 32 位字中读取字节。

程序包含存储在内存中的虚拟数据,例如 0xAAAAAAAA。我们这样做是为了演示数据是如何存储的,但主要是为了帮助调试。当你显示内存中的数据内容时,这些虚拟值提供了很好的标记,帮助你读取内存数据。

注意,我们在读写内存中访问的字 testRW 位于 .data 部分。它被初始化为 0xFFFFFFFF,后来被修改:


        .global _start
_start: adr     r0,mike        @ r0 points to ASCII string "Mike"
        ldr     r1,[r0]        @ Read the string into r1
        ldrb    r2,[r0,#3]     @ Read byte 3 of "Mike"
        adr     r3,test        @ Point to data value 0x2468FACE in memory
        ldrb    r4,[r3]        @ Read single byte of test. Pointer in r0
        ldrb    r5,[r3,#1]     @ Read single byte 1 offset
        ldrh    r6,[r3]        @ Read halfword of test
        ldr     r7,a_adr       @ r7 points at address of testRW
        ldr     r8,=0x12345678 @ r8 loaded with 32-bit literal 0x12345678
        str     r8,[r7]        @ Store r8 in read/write memory at testRW
        ldrh    r9,[r7]        @ Read halfword of testRW
        mvn     r10,r9         @ Logically negate r9
        strh    r10,[r7,#4]    @ Store halfword in next word after testRW
        nop                    @ Just a place to stop
        mov     r0,#1          @ Raspberry Pi exit sequence
        mov     r7,#7
        svc     0
mike:   .ascii  "Mike"         @ Store string as 4 ASCII characters
test:   .word   0x2468FACE     @ Save a word in memory
a_adr:  .word   testRW         @ Pointer to data in read/write memory
        .data                  @ Data section in read/write memory
nines:  .word   0x99999999     @ 0x99999999 Just dummy data
testRW: .word   0xFFFFFFFF     @
        .word   0x77777777     @ More dummy data
        .word   0xBBBBBBBB     @ More dummy data
        .end

第一步是汇编和加载程序(称为 endian)并调用 gdb 调试器。我们使用粗体字来表示来自键盘的输入:


alan@raspberrypi:~/Desktop $ as g o endian.o endian.s
alan@raspberrypi:~/Desktop $ ld o endian endian.o
alan@raspberrypi:~/Desktop $ gdb endian
GNU gdb (Raspbian 10.11.7) 10.1.90.20210103git

我们可以使用 gdb 在 _start 处设置断点,然后运行程序到该断点:


(gdb) b _start
Breakpoint 1 at 0x10074: file endian.s, line 2.
(gdb) r
Starting program: /home/alan/Desktop/endian
Breakpoint 1, _start () at endian.s:2
2    _start: adr     r0,mike   @ r0 points to ASCII string "Mike"

让我们看看实际加载到内存中的程序。它与我们所写的程序略有不同,因为伪操作已被实际代码所取代。请注意,adr 被翻译成 add 指令,通过将程序计数器与所需变量的距离相加,以生成其地址。

ldr r8,=0x12345678 被翻译成一条加载程序计数器相对指令,因为所需的常量已在程序结束之后加载到内存中:


(gdb) disassemble
Dump of assembler code for function _start:
=> 0x00010074 <+0>:     add    r0, pc, #60    ; 0x3c
   0x00010078 <+4>:     ldr    r1, [r0]
   0x0001007c <+8>:     ldrb   r2, [r0, #3]
   0x00010080 <+12>:    add    r3, pc, #52    ; 0x34
   0x00010084 <+16>:    ldrb   r4, [r3]
   0x00010088 <+20>:    ldrb   r5, [r3, #1]
   0x0001008c <+24>:    ldrh   r6, [r3]
   0x00010090 <+28>:    ldr    r7, [pc, #40]   ; 0x100c0 <a_adr>
   0x00010094 <+32>:    ldr    r8, [pc, #40]   ; 0x100c4 <a_adr+4>
   0x00010098 <+36>:    str    r8, [r7]
   0x0001009c <+40>:    ldrh   r9, [r7]
   0x000100a0 <+44>:    mvn    r10, r9
   0x000100a4 <+48>:    strh   r10, [r7, #4]
   0x000100a8 <+52>:    nop                    ; (mov r0, r0)
   0x000100ac <+56>:    mov    r0, #1
   0x000100b0 <+60>:    mov    r7, #7
   0x000100b4 <+64>:    svc    0x00000000
End of assembler dump.

这与我们所写的代码不同,因为伪操作已被实际的 ARM 代码所取代。请注意,adr 被翻译成 add 指令,通过将程序计数器与所需变量的距离相加,以生成其地址。

ldr r8,=0x12345678 被翻译成一条加载程序计数器相对指令,因为所需的常量已在程序结束之后加载到内存中。

让我们看看内存。以下显示了从 0x100b4 开始的 8 个连续字的内存内容,这是 svc 0 指令的地址。

在继续执行程序之前,我们将查看程序设置的内存内容。这些数据在哪里?它遵循最后一个可执行指令 svc 0,该指令的地址为 0x000100B4。我们以十六进制格式显示 svc 指令中的八个字:


(gdb) x/8wx 0x100b4
0x100b4 <_start+64>:    0xef000000    0x656b694d    0x2468face    0x000200cc
0x100c4 <a_adr+4>:      0x12345678    0x99999999    0xffffffff    0x77777777

您可以看到汇编器加载的 0x12345678 常量和一些标记。

我们将逐步执行前几条指令:


(gdb) si 1
3          ldr   r1,[r0]       @ Read the string into r1
4          ldrb  r2,[r0,#3]    @ Read byte 3 of "Mike"
6          adr   r3,test       @ Point to data value 0x2468FACE in memory

下一步是查看程序运行完成之前的寄存器状态。我们使用 gdb 的 i r 命令来完成此操作。目前没有太多可看的内容(这是一个部分列表),因为我们只执行了前几条指令。然而,r0 现在包含指向地址 0x100B8 的 ASCII 文本字符串 “Mike” 的指针。如果您查看该地址,您会看到它包含 0x656b694d,这是 ekiM。这就是小端序的作用!


(gdb) i r
r0             0x100b8             65720
r1             0x656b694d          1701538125
r2             0x65                101
r3             0x0                 0
r4             0x0                 0
sp             0x7efff360          0x7efff360
lr             0x0                 0
pc             0x10080             0x10080 <_start+12>

继续单步执行:


(gdb) si 1
7          ldrb    r4,[r3]          @ Read byte of test Pointer in r0
8          ldrb    r5,[r3,#1]       @ Read single byte 1 offset
9          ldrh    r6,[r3]          @ Read halfword of test
11         ldr     r7,a_adr         @ r7 points at address of testRW
12         ldr     r8,=0x12345678   @ r8 loaded with 32-bit 0x12345678
(gdb) i r
r0             0x100b8             65720
r1             0x656b694d          1701538125
r2             0x65                101
r3             0x100bc             65724
r4             0xce                206
r5             0xfa                250
r6             0xface              64206
r7             0x200cc             131276
sp             0x7efff360          0x7efff360
lr             0x0                 0
pc             0x10094             0x10094 <_start+32>

让我们看看数据段的内存。寄存器 r7 指向读写数据区域。它位于 testRW 指针之前 4 个字节处,在 r7 中;即 0x200CC - 4 = 0x200C8。从该地址开始的四个字如下:


(gdb) x/4xw 0x200c8
0x200c8:    0x99999999    0xffffffff    0x77777777    0xbbbbbbbb

最后,我们逐步执行指令,直到遇到最后的 nop 指令:


(gdb) si 1
13          str    r8,[r7]       @ Store r8 in read/write memory at testRW
14          ldrh   r9,[r7]       @ Read halfword of testRW
15          mvn    r10,r9        @ Logically negate r9
16          strh   r10,[r7,#4]   @ Store halfword in next word after testRW
17          nop                  @ Just a place to stop

让我们最后看看寄存器的状态:


(gdb) i r
r0             0x100b8             65720
r1             0x656b694d          1701538125
r2             0x65                101
r3             0x100bc             65724
r4             0xce                206
r5             0xfa                250
r6             0xface              64206
r7             0x200cc             131276
r8             0x12345678          305419896
r9             0x5678              22136
r10            0xffffa987          4294945159
sp             0x7efff360          0x7efff360
lr             0x0                 0
pc             0x100a8             0x100a8 <_start+52>

这里是我们对数据内存的最终查看。注意,0xFFFFFFFF 已被替换为我们写入内存的值 0x12345678。这展示了如何使用 ARM 访问数据内存。

还要注意 0x200D0 处的数据值;即 0x7777a987。我们使用半字加载更改了半个字:


(gdb) x/4xw 0x200c8
0x200c8:    0x99999999    0x12345678    0x7777a987    0xbbbbbbbb

摘要

在本章中,我们介绍了一台真正的计算机,树莓派。我们不是设计自己的计算机指令集,而是查看树莓派和大多数智能手机核心中的 ARM 微处理器。

我们介绍了 Raspberry Pi 的基础知识,并展示了如何编写可以在其上运行的 ARM 汇编语言程序。这需要理解 ARM 汇编器和链接器的使用。我们演示了如何使用 gdb 调试器逐条指令运行您的 ARM 程序。

我们遇到的一个重要的 Raspberry Pi 架构特点是内存中数据修改的方式。您不能使用 str(存储)指令来修改内存中的数据。您必须通过指向您想要更改的内存地址的指针间接地完成。以下简短的程序演示了这一关键点。内存中的数据项直接使用 ldr 读取,但在内存中使用指向指针来修改。关键操作以粗体显示:


         .text           @ Code section
         .global _start
_start:  ldr r1,=xxx     @ Point to data xxx
         ldr r2,[r1]     @ Read the data
         adr r3,adr_xxx  @ Point to the address of the address of xxx
         ldr r4,[r3]     @ Point to the data xxx
         ldr r5,[r4]     @ Read the value of xxx
         add r5,r5,#1    @ Increment the data
         str r5,[r4]     @ Change xxx in memory
         mov r0,#0       @ Exit procedure
         mov r7,#1
         svc 0
adr_xxx: .word           @ Pointer to data xxx
         .data           @ Data section
xxx:     .word  0xABCD   @ Initialize variable xxx
         .end

为了演示 ARM 程序,我们介绍了 ARM 的汇编语言。幸运的是,这并不太远离一些模拟器所采用的编程语言。实际上,ARM 的汇编语言并不难学,尽管它确实包含了一些非常有趣的特点,这些特点我们将在后面的章节中描述。

在下一章中,我们将回到 ARM 架构及其最重要的方面之一:寻址以及数据如何从内存中传输到和从内存中传输出来。

第十章:深入了解 ARM

我们已经介绍了 ARM 处理器。现在,我们将更深入地研究它。ARM 处理器系列可能是教授计算机架构的最佳工具。特别是,由于它的指令集精简和与许多其他微处理器相比的简单寄存器模型,它非常容易学习。此外,ARM 还有一些非常有趣的功能,例如,当一条指令可以根据处理器状态执行或忽略时,预测执行。计算机教育中引入树莓派恰逢其时,因为它使学生能够亲身体验引人注目的 ARM 架构。

在本章中,我们将做以下几件事:

  • 介绍 ARM

  • 描述其寄存器集

  • 检查加法和减法操作的变化

  • 涵盖 ARM 的乘法指令

  • 介绍逻辑操作和移位操作

  • 解释流程控制和 ARM 的条件执行

技术要求

因为这一章节是前一章的扩展,所以不需要新的硬件或软件。你只需要配置为通用计算机的树莓派。所需的唯一软件是一个文本编辑器来创建汇编语言程序,以及 GCC 汇编器和加载器。

介绍 ARM

ARM 处理器系列是一个引人注目的成功故事,不仅仅是因为许多其他微处理器在几年内变得流行,然后又逐渐消失在人们的视野中(例如,6502、Cyrix 486 和安腾)。在其发布时,摩托罗拉的 68K 被认为比英特尔 8086 更加优雅和强大。事实上,68K 在 8086 还是 16 位机器的时候,就已经是一个真正的 32 位机器了。68K 被苹果的 Mac、雅达利和 Amiga 电脑所采用——它们都是家用电脑市场的主要玩家。那么英特尔的 8086 如何可能与之竞争呢?好吧,IBM 选择了 8086 系列作为其新型个人电脑,其余的就是历史了。后来,摩托罗拉退出了半导体业务。

在 20 世纪 80 年代末,一家名为 Advanced RISC Machines 的新公司成立,旨在创建高性能微处理器。它们的机器架构遵循 RISC 架构的寄存器到寄存器范式,而不是英特尔和摩托罗拉更复杂的 CISC 指令集。ARM 应运而生。

ARM 不仅在与许多早期微处理器失败时幸存下来,而且还繁荣发展,并成功地将目标对准了移动设备的世界,如上网本、平板电脑和手机。ARM 集成了许多有趣的架构特性,使其在竞争对手中具有竞争优势。

事实上,ARM 是一家无晶圆厂公司——也就是说,它开发计算机架构,并允许其他公司制造这些计算机。术语“无晶圆厂”来源于“fab”(制造的首字母缩写)。

在我们描述 ARM 指令之前,我们将讨论其寄存器集,因为所有 ARM 数据处理指令都操作于其寄存器的内容(RISC 计算机的一个主要特性)。

由于 ARM 的架构在多年中不断发展,并且因为正在使用不同版本的 ARM 架构,因此 ARM 的教师面临一个问题。应该使用哪个版本来展示计算机架构课程?在本章中,我们将使用具有 32 位指令的 ARMv4 32 位架构。一些 ARM 处理器可以在 32 位和 16 位指令状态之间切换(16 位状态称为 Thumb 状态)。Thumb 状态旨在在嵌入式控制系统中运行非常紧凑的代码。我们在这里不会介绍 Thumb 状态。

Raspberry Pi 4 中的 ARM 拥有与早期 32 位 ARM 非常不同的 64 位架构。然而,由于 32 位 ARM 架构被大多数教学文本所采用,并且 Raspberry Pi 4 支持 32 位架构,因此我们在这里将使用 32 位架构。为了与其他使用 ARM 来说明计算机架构的书籍保持一致,这里的大部分材料都是基于 ARMv4T 32 位架构。ARM 的 32 位架构现在被称为 AArch32,以区别于 ARM 的新 64 位架构,AArch64。

ARM 架构概述

ARM 的架构很有趣,因为它结合了传统的 CISC 架构(如摩托罗拉的 68K 和英特尔 32/64 位架构)的元素,以及像 MIPS 和 RISC-V 这样的处理器更激进的精简 RISC 架构。

在这里,我们将检查以下内容:

  • ARM 的寄存器集

  • 算术指令

  • 特殊加法和减法指令

  • 乘法和 ARM 的乘法与加法指令

  • 位操作指令

  • 移位操作

我们在这里不详细覆盖数据移动操作。我们已经遇到了可以用来将立即数加载到寄存器的 mov 操作,例如,mov r1,#12。同样,strldr 指令分别从内存中加载寄存器和将寄存器存储到内存中。一个典型的例子是 ldr r4,[r5]str r0,[r9]。这两个指令使用 寄存器间接寻址,我们将在下一章中详细介绍。

ARM 寄存器集

与拥有 32 个通用寄存器的流行 MIPS 处理器不同,ARM 只有 16 个寄存器,r0r15,以及一个 状态寄存器。值得注意的是,ARM 的寄存器并不完全相同——也就是说,有些是专用寄存器。图 10**.1 展示了 ARM 的寄存器集。

图 10.1 – ARM 的寄存器集

图 10.1 – ARM 的寄存器集

四个寄存器,r0r13,在真正意义上是通用的,因为它们都以相同的方式工作——例如,你可以用 r5 做的事情,用 r10 也可以做。寄存器 r13r14r15 不是通用的,因为它们有额外的功能。

严格来说,r13 是一个通用寄存器,但按照惯例,它被保留用作堆栈指针。如果你在一个团队项目中工作,你应该遵守这个惯例。否则,你不必遵循这个惯例,你可以按你希望的方式使用 r13

寄存器 r14 由硬件强制赋予了一个额外的功能。它是一个 链接寄存器,在汇编程序中可以写作 lrr14。ARM 有一个指令,带链接的分支 (bl),允许你跳转到程序中的某个点(即分支)并将后续指令的地址保存到链接寄存器中。换句话说,该指令将下一个 pc 地址保存到 r14 中,然后跳转到指定的目标。稍后,你可以通过将链接寄存器中的地址复制到 pc 来返回,使用 mov pc,lrmov 15,r14。这是一个比传统的使用堆栈存储返回地址的 bsrrts 指令对更快的子程序调用和返回机制。

寄存器 r15 是一个真正与其他寄存器不同的寄存器,永远不能用作通用寄存器(即使你可以应用一些指令到它,就像它是通用寄存器一样)。寄存器 r15程序计数器,包含下一个要执行的指令的地址,在 ARM 代码中通常写作 pc 而不是 r15。在计算机体系结构的世界中,将程序计数器放入通用寄存器是非常罕见的。请注意,在实际中,由于 ARM 内部的组织方式,pc 包含一个比当前 pc 高 8 字节的地址。

我们将首先查看 ARM 的数据处理指令,而不是数据移动操作。我们采取这种方法是因为数据移动指令更复杂,因为它们涉及到复杂的寻址模式。

算术指令

让我们从 ARM 的算术指令开始,这些指令对表示 数值 的数据进行操作:

  • 加法          add

  • 减法        sub

  • 比较操作   cmp(技术上讲,“比较”不是一个数据处理操作)

  • 乘法              mul

  • 移位            lsl, lsr, asl, asr, ror, rrx

加法、减法和比较

加法是一个简单的操作,它将两个操作数相加生成一个和和一个进位。在十进制算术中,4 + 5 得到 9。4 + 9 得到 13;也就是说,结果是 3,进位是 1。计算机通过将进位存储在进位位中来处理这种情况。ARM 指令要求你在操作后更新条件码标志时添加后缀 s——也就是说,你需要写 adds r1,r2,r3

ARM 是一个 32 位机器。你如何加 64 位数字?假设有两个 64 位数字AB,其中 AL 是A的低位 32 位,AU 是A的高 32 位。同样,BL 是B的低位 32 位,BU 是B的高 32 位。

我们首先将 AL 加到 BL 上,并记录进位。然后我们将 AU 和 BU 相加,加上从低位对加法产生的任何进位。在 ARM 汇编语言中,这如下所示:


adds CL,AL,BL         @ CL,AL,BL are registers, each holding the 32 lower-order bits of a word
adc  CU,AU,BU         @ Add the two upper-order 32-bit registers together with the carry bit

第二次加法操作adc意味着带进位加法,并添加前一次加法产生的任何进位。我们使用 CL、AL、BL 等,而不是r1r2r3来演示这些是分布在两个寄存器之间的数字的高位和低位部分。我们可以将这个原则扩展到执行任意长度的整数的扩展精度算术。

ARM 还提供了一个简单的减法操作sub,以及一个sbc带进位减法指令来支持扩展精度减法,它们的工作方式与相应的adc类似。

除了subsbc,ARM 还有一条反向减法操作,其中rsc r1, r2, r3执行从r3减去r2的操作。这条指令可能看起来很奇怪且不必要,因为你完全可以简单地颠倒后两个寄存器的顺序,对吧?然而,ARM 缺少一条取反指令,该指令从零减去一个数;例如,r0的负数是0 – [r0]。反向减法操作可以用来完成这个任务,因为rsb r1, r1, #0等价于neg r1

比较操作通过从另一个值中减去一个值来比较两个值——例如,我们可以比较 3 和 5。假设正在比较的两个元素是AB。如果你执行A – B,并且结果是零,那么AB是相等的。如果结果是正数,则A > B,如果是负数,则A < B。比较是一种减法,你不在乎结果;只关心它的符号,是否为零,是否产生了进位。考虑以下情况:


mov r1,#6                @ Load r1 with 6
mov r2,#8                @ Load r2 with 8
cmp r1,r2                @ Compare r1 and r2

操作cmp r1, r2评估[r1] – [r2]并更新ZCNV位。然后我们可以执行如beq next之类的操作,如果r1r2相等,则跳转到标签next。我们说过你需要追加s来更新条件码。比较操作是例外,因为设置条件码正是它们所做的事情。如果你想写cmps也可以,因为它和cmp是相同的。

有两种整数比较类型。考虑(在 8 位中)A = 00000001B = 11111111 的二进制值。哪个更大?你可能认为它是 B,因为 B = 255A = 1。这是真的。然而,如果这些被分配为 2s 补码数,A 将是 1B 将是 -1;因此,A 是更大的。像所有处理器一样,ARM 提供了两套分支操作,一套用于无符号算术,一套用于有符号算术。程序员必须根据他们是否使用有符号或无符号算术来选择适当的分支。我们之前构建的模拟器都只提供了无符号分支。

乘法

ARM 的乘法指令 mul Rd,Rm,Rs 生成 64 位乘积 Rm x Rs 的低阶 32 位。当使用 mul 时,你应该确保结果不会超出范围,因为两个 m 位数的乘积会产生一个 2m-位结果。此指令不允许你将寄存器的内容乘以一个常数——也就是说,你不能执行 mul r9,r4,#14。此外,你不能使用相同的寄存器来指定 Rd 目的和 Rm 操作数。这些限制是由于该指令在硬件中的实现。以下代码演示了使用 ARM 的乘法来乘以 23 和 25:


       mov   r4,#23      @ Load register r4 with 23
       mov   r7,#25      @ Load register r7 with 25
       mul   r9,r4,r7    @ r9 is loaded with the low-order 32-bit product of r4 and r7

我们已经看到 ARM 有一个乘累加指令mla,具有四操作数格式 mla Rd,Rm,Rs,Rn,其 RTL 定义为 [Rd][Rm] x [Rs] + [Rn]。32 位乘以 32 位的乘法被截断为低阶 32 位。像乘法一样,Rd 不能与 Rm 相同(尽管在 ARMv6 及以后的架构中取消了此限制)。

ARM 的乘累加指令通过每条指令执行一次乘法和加法来支持内积的计算。内积用于多媒体应用——例如,如果向量 a 由 n 个组件 a1, a2, … an 组成,而向量 b 由 n 个组件 b1, b2, ... , bn 组成,那么 a 和 b 的内积是标量值 s = a·b = a1·b1 + a2·b2 + … + an·bn。

我们现在将演示乘累加操作的用途。尽管我们还没有介绍 ARM 的寻址模式,但以下示例包括指令 ldr r0,[r5],#4,该指令将寄存器 r0加载为数组中由寄存器r5指向的元素,然后更新r5` 以指向下一个元素(4 字节):


       mov   r4,#n        @ r4 is the counter
       mov   r3,#0        @ Clear the inner product
       adr   r5,V1        @ r5 points to v1
       adr   r6,V2        @ r6 points to v2
Loop:  ldr   r0,[r5],#4   @ REPEAT read a component of v1 and update the pointer
       ldr   r1,[r6],#4   @ Get the second element in the pair from v2
       mla   r3,r0,r1,r3  @ Add new product term to the total (r3 = r3 + r0·r1)
       subs  r4,r4,#1     @ Decrement  the counter and set the (CCR)
       bne   Loop         @ UNTIL all done

关于除法?ARMv4 架构作为其基本架构的一部分缺少除法指令(一些 ARM 的变体,如 ARMv7 架构,则包含除法指令)。如果你想在 ARM 上进行除法,你必须编写一个短程序,该程序使用涉及移位和减法的迭代循环来执行除法(类似于纸和笔的长除法)。

位运算逻辑

ARM 提供了大多数处理器支持的ANDORNOTEOR(异或)基本位逻辑操作。这些操作用于设置、清除和切换字中的单个位,就像我们在汇编指令时已经看到的那样。还有一个不寻常的位清除操作bic,它将第一个操作数与第二个操作数的对应位的补码进行AND操作——即 ci = ai ∧ bi。

ARM 的 NOT 操作写为mvn rd, rs。这个移动指令否定、反转源寄存器的位,并将它们复制到目标寄存器。

以下示例说明了在 r1 = 11001010 和 r0 = 00001111 上的逻辑操作:

逻辑指令 操作 r2 中的最终值
and r2, r1, r0 11001010 ∧ 00001111 00001010
or r2, r1, r0 11001010 + 00001111 11001111
mvn r2, r1 īōōīōīō 00110101
eor r2, r1, r0 11001010 ⊕ 00001111 11000101
bic r2, r1, r0 11001010 ∧ ōōōōīīīīī 11000000

当你设计指令集时,主要任务之一是为指令构造二进制代码。这些操作使得位操作变得容易实现。例如,假设变量sR1指定源寄存器 1,sR2指定源寄存器 2,我们必须构造一个 16 位的二进制代码C,其格式为xxxxxaaaxxbbbxxx。源位asR1中,源位bsR2`的低位三位中。

我们必须在适当的位置插入sR1sR2的位,而不改变C的任何其他位。在 Python 中,我们可以这样做:


C = C & 0b1111100011000111       # Clear the two fields for sR1 and sR2
sR1 = sR1 << 8                   # Move sR1 into position by shifting left 8 times
C = C | sR1                      # Insert sR1
sR2 = sR2 << 3                   # Move sR2 into position by shifting left 3 times
C = C | sR2                      # Insert sR2

我们可以使用ANDOR和移位操作轻松地将这个操作翻译成 ARM 汇编语言。假设sR1r1中,sR2r2中,C在寄存器r0中。此外,假设寄存器位已经在其各自的寄存器中就位:


   ldr r3,=0b1111100011000111    @ Load r3 with 1111100011000111 mask
   and r0,r0,r3                  @ Mask r0 to get xxxxx000xx000xxx
   or  r0,r0,r1                  @ Insert r1 to get xxxxxaaaxx000xxx
   or  r0,r0,r2                  @ Insert r2 to get xxxxxaaaxxbbbxxx

移位操作

Python 可以使用<<运算符向左移位,或使用>>运算符向右移位。ARM 的汇编语言缺少像LSRLSL这样的显式指令,这些指令可以右移或左移位。然而,它确实有一些伪指令,如lsl r1, r3, #4,可以将r3的内容左移四位,并将结果转移到r1。ARM 的实际移位方法相当不寻常、复杂且多功能

ARM 将移位操作作为常规数据操作的一部分。考虑add r1, r2, r3,它将r3加到r2上,并将结果放入r1。ARM 允许你在数据操作中使用第二个操作数之前对其进行移位。你可以写add r1, r2, r3, lsl r4(见图 10.2 解释第二个目标字段)。

图 10.2 – 动态移位操作的结构

图 10.2 – 动态移位操作的结构

此指令将第二个源操作数 r3 进行逻辑左移。左移的位数由 r4 的内容确定。您也可以使用以下常数实现固定移位:


add r1,r2,r3,lsl #3

在这种情况下,寄存器 r3 在加到r2之前左移了三位。如果移位的位数由寄存器指定,则称为动态移位,因为您可以通过更改移位计数在运行时更改移位次数。如果移位的位数由文字(常数)给出,则无法在运行时更改。这是一个静态移位。

ARM 的移位将数据处理操作与移位(加速数据处理)结合起来,并允许您在一个指令中指定四个寄存器。如果您需要不带数据处理指令的普通移位,可以使用MOV指令,如下所示:


mov r1,r1,lsr #1         @ Shift contents of register r1 a single bit place right before moving

今天,ARM 汇编器允许您编写伪指令lsl r1,r3,#4并自动替换为移位操作指令mov r1,r1,lsr #4,使用移位后的第二个操作数。

位移类型

所有移位从位串的中间看起来都一样 – 即,位向左或向右移动一个(或多个)位置。然而,位在末尾会发生什么?当位在寄存器中移位时,在一边,一个位会掉落。这个位可以消失在虚无中,进入进位位,或者以循环方式移动到另一边。在位被腾出的末端,新位可以设置为01、与进位位相同,或者从另一端掉落的位。

计算机处理移入位的方式的变化对应于特定的移位类型 – 逻辑算术循环带进位循环。让我们看看一些移位操作(表 10.1):

源字符串 方向 移位 目标字符串
0110011111010111 左移 1 1100111110101110
0110011111010111 左移 2 1001111101011100
0110011111010111 左移 3 0011111010111000
0110011111010111 右移 1 0011001111101011
0110011111010111 右移 2 0001100111110101
0110011111010111 右移 3 0000110011111010

表 10.1 – ARM 的逻辑移位操作

在斜体中的目标字符串中的位是移入的位,而在粗体中的源字符串中的位是在移位后丢失(丢弃)的位。这种移位是一种逻辑移位:

  • 逻辑移位:移位位移动一个或多个位置向左或向右。位在一边掉落,另一边进入零。最后移出的位被复制到进位标志。图 10**.3说明了逻辑左移和逻辑右移。

图 10.3 – 逻辑移位

图 10.3 – 逻辑移位

  • 算术移位:这种算术移位将正在移位的数视为有符号的二进制补码值。算术左移与逻辑左移相同。对于右移,最高有效位向右传播。这种移位将操作数视为有符号值,要么除以二(右移一位),要么乘以二(左移一位),如 10**.4所示。

算术移位的目的在于在表示除以 2 的幂的移位操作中,保留二进制补码数的符号。例如,8 位值10001111在逻辑右移时变为01000111,但在算术右移时变为11000111

图 10.4展示了算术左移和右移。ARM 有asr操作但没有asl,因为aslLSL相同——也就是说,你使用逻辑左移,因为它与asl完全相同。

图 10.4 – 算术移位

图 10.4 – 算术移位

  • 01101110逐位左旋转:

图 10.5 – 连续旋转操作示例

图 10.5 – 连续旋转操作示例

旋转操作是非破坏性的——也就是说,操作过程中没有位丢失或改变。它在诸如计算位串中1的个数等操作中非常有用。图 10.6展示了旋转操作。请注意,ARM 没有实现右旋转指令——也就是说,没有ror。由于旋转是一个循环操作,将m位字右移p位可以通过左移32-p位来实现;因此,ror r0,r1,#4可以通过rol r0,r1,#28来实现。

图 10.6 – 旋转操作(ARM 不实现 ROL)

图 10.6 – 旋转操作(ARM 不实现 ROL)

旋转操作的变体是带进位的旋转,其中进位位被视为要移位的字的一部分——也就是说,n位字变成了n+1位字。图 10.7演示了带进位的旋转操作,其中移出的进位被复制到进位位,而进位位的旧值成为新移入的位。这种操作用于链式算术(它是带进位加法和带借位减法操作的类似物)。

图 10.7 – 带进位的旋转

图 10.7 – 带进位的旋转

ARM 仅实现了以下五种移位操作(程序员可以合成其余的操作):


lsl     logical shift left
lsr     logical shift right
asr     arithmetic shift right
ror     rotate right
rrx     rotate right through carry (1-bit shift only)

rrx,它通过带进位右旋转位(图 10.7),与其他移位操作表现不同。首先,只允许一个方向的移位;没有带进位的左移位。其次,ARM 支持所有其他移位操作的所有静态动态移位,而rrx只允许一次移位。

虽然没有左旋操作,但你可以通过右旋操作轻松实现它。以下示例演示了 4 位值的左旋和右旋。经过四次旋转后,数字保持不变。正如你所见,左旋和右旋之间存在对称性。对于 32 位值,n 位左移等同于 32-n 位右移:

右旋 左旋
1101 开始
1110 右旋 1
0111 右旋 2
1011 右旋 3
1101 右旋 4

表 10.2 - 比较连续的左旋和右旋

考虑 adcs r0,r0,r0(带进位加并设置状态标志)。这会将 r0 的内容加上 r0 的内容,再加上进位位,生成 2 x [r0] + C。左移等同于乘以 2。将进位位移入最低有效位置等同于加上进位位以得到 2 x [r0] + C。在指令后添加 S 强制更新 CCR,这确保任何进位都加载到 C 位。因此,adcs r0,r0,r0rlx r0 是等效的。

使用移位操作合并数据

在以下示例中,我们从每个三个寄存器中提取最低有效字节,并将它们组合成一个新的字。字面量以十六进制格式表示。假设寄存器最初如下所示:

  • r1 = XXXXXXAA   r1 是源 1,Xs 代表无关紧要的值

  • r2 = XXXXXXBB   r2 是源 2

  • r3 = XXXXXXCC   r3 是源 3

  • r4 = 00CCBBAA   r4 是最终结果


      mov r4,#0                @ Clear r4
      and r1,r1,#0x000000FF    @ Clear r1 except least-significant byte
      and r2,r2,#0x0000FF00    @ Clear r2 except second byte
      and r3,r2,#0x00FF0000    @ Clear r3 except third byte
      or  r4,r4,r1             @ Copy r1 to r4\. No shifting
      or  r4,r4,r2 lsl #8      @ Copy r2 to r4\. Shifting to second byte
      or  r4,r4,r3 lsl #16     @ Copy r3 to r4\. Shifting to third byte

前面的代码是一个暴力方法。一个更好的替代方案如下:


      and r4,r1,#0x000000FF    @ Clear r4 and insert r1 0x000000AA
      or  r4,r4,r2 lsl #8      @ Insert r2, shifting left into place 0xXXXXBBAA
      or  r4,r4,r3 lsl #16     @ Insert r3, shifting left into place 0xXXCCBBAA
      and r4,r4,#0x00FFFFFF    @ Insert most-significant byte 0x00CCBBAA

现在,考虑 r0 = 0x0000AA, r1 = 0x000000BB, 和 r2 = 0x12345678。我们希望合并这四个寄存器以得到 0xAABB5678。我们可以用三条指令来完成这个操作:


add r2,r1,r2,lsl #16           @ r2 = 567800BB
add r2,r2,r0,lsl #8            @ r2 = 5678AABB,
mov r2,r2,ror #16              @ r2 = AABB5678

add r2,r1,r2,lsl #16r2 左移 16 次后将 r1 添加到 r2。16 位的左移将 r2 的低阶 16 位移动到高阶 16 位,并通过移入零来清除低阶 16 位。我们保留了 r2 的旧的低阶半部分,并清除了新的低阶 16 位,以便插入来自 r0r1 的字节。add r2,r2,r0,lsl #8r0的低阶字节插入到r2的第 8 位到第 15 位,因为r0首先左移 8 位。由于r0中移入了零,这个操作不会影响r2的第 0 位到第 7 位。执行mov r2,r2,ror #16` 执行 16 位的旋转。

下一个移位示例演示了如何实现 if x < 0: x = 0。这个结构在 x 为负数(即,msb1)时将 x 变量设置为零;否则,x 保持不变。ARM 代码如下:


bic r0,r0,r0,asr #31

算术右移将符号位 31 次,对于正数留下0x00000000,对于负数留下0xFFFFFFFFbic操作将第一个操作数与第二个操作数的补码进行AND操作。如果r0是正数,所有位都会进行AND操作,1 留下r0不变。如果r0是负数,位会进行AND操作,0 留下0。因此,正数x保持不变,负数x被设置为0

下一节将探讨一类不移动数据或处理数据的指令;它确定下一个要执行的指令。

流控制指令

计算机按顺序执行指令,除非分支导致跳转到非顺序指令,或者当调用子程序时中断指令流。当发生中断时,指令流也会改变(我们这里不处理中断)。

在本节中,我们将查看以下内容:

  • 无条件分支

  • 条件分支

无条件分支

ARM 的无条件分支表示为b target,其中target表示分支目标地址(即将执行的下一条指令的地址)。无条件分支强制从程序的一个点跳转到另一个点。这与我们之前介绍的无条件分支完全相同。以下 ARM 代码演示了如何使用无条件分支:


      ..   do this       @ Some code
      ..   then that     @ Some other code
      b    Next          @ Now skip past the next instructions and jump to Next:
      ..                 @ …the code being skipped past
      ..                 @ …the code being skipped past
Next: ..                 @ Target address for the branch, denoted by label Next

ARM 的分支指令使用一个 24 位的立即数来提供一个二进制补码相对偏移。这个偏移量左移两次以创建一个 26 位的字节偏移量,然后将其加到当前程序计数器上以获得 32 位的目标地址。分支范围是从当前 PC(任一方向)的 32MB。记住,连续的指令地址相差四。

条件分支

ARM 的条件分支由一个助记符 Bcc 和一个目标地址组成。下标定义了必须满足的 16 个条件之一,以便分支被执行。如果条件为true,则执行继续在分支目标地址。如果条件不为true,则执行序列中的下一条指令。考虑以下 ARM 汇编语言示例,它实现了以下内容:


if x == y: y = y + 1
else:      y = y + 2
       cmp r1,r2         @ Compare x and y (r1 contains y and r2 contains x)
       bne plus2         @ If not equal, then branch to the else part
       add r1,r1,#1      @ If equal, fall through to here and add one to y
       b   leave         @ Now, skip past the else part
plus2: add r1,r1,#2      @ ELSE add 2 to y
leave:  …                @ Continue from here

条件分支指令检查处理器条件码寄存器中的标志位,然后如果测试条件为真则执行分支。由于条件码寄存器包括一个零位(Z)、负位(N)、进位位(C)和溢出位(V),因此基于单个位的状态有八个条件分支(四个在true时分支,四个在false时分支)。表 10.3定义了所有 ARM 的条件分支。注意,有一个总是分支和一个从不分支指令。

分支指令可以应用于有符号无符号数据。考虑四位值 x = 0011y = 1001。我们希望如果 y 大于 x 则分支。使用无符号算术,x = 3y9,所以 y > x。然而,如果我们认为这些是有符号值,那么 x = 3y = -7,所以 y < x。显然,我们必须为无符号算术选择无符号比较,为有符号算术选择有符号比较。

| 编码 | 助记符 | 基于 标志状态 执行 条件 |
| --- | --- | --- | --- |
| 0000 | EQ | Z set | 等于(即零) |
| 0001 | NE | Z clear | 不相等(即非零) |
| 0010 | CS or HS | C set | 无符号更高或相同 |
| 0011 | CC or LO | C clear | 无符号更低 |
| 0100 | MI | N set | 负 |
| 0101 | PL | N clear | 正或零 |
| 0110 | VS | V set | 溢出 |
| 0111 | VC | V clear | 无溢出 |
| 1000 | HI | C set and Z clear | 无符号更高 |
| 1001 | LS | C clear or Z set | 无符号更低或相同 |
| 1010 | GE | N set and V set, or N clear and V clear | 签名大于或等于 |
| 1011 | LT | N set and V clear, or N clear and V set | 有符号小于 |
| 1100 | GT | Z clear, and either N set and V set, or N clear and V clear | 有符号大于 |
| 1101 | LE | Z set, or N set and V clear, or N clear and V set | 有符号小于或等于 |
| 1110 | AL | 无条件 | 总是(默认) |
| 1111 | NV | | 从不(保留) |

表 10.3 - ARM 的条件执行和分支控制助记符

一些微处理器有条件分支操作的同义词——也就是说,一个分支条件有两个助记符。例如,带进位设置的分支(bcs)可以写成带更高或相同(bhs)的分支,因为 C = 1 在无符号算术中实现了(>)操作。同样,bcc 可以写成带更低(blo),因为进位清除实现了无符号比较,即更低。

条件分支的使用最好的例子之一是在重复结构中。考虑 while 循环:


Loop:       cmp   r0,#0         @ Perform test at start of loop (exit on zero)
            beq   whileExit     @ Exit on test true
            Code  ...           @ Body of the loop
            b     Loop          @ Repeat WHILE true
whileExit:  Post-loop code...   @ Exit

本章的最后部分将探讨 ARM 的 条件执行机制,该机制提供了一种取消或取消指令的方法——也就是说,您可以在运行时选择是否执行指令。这是一个在非常非常少的处理器上发现的特性。然而,这种机制通过创建紧凑的代码为 ARM 提供了一种非常有趣的加速执行的方法。

条件执行

在这里,我们将只处理一个主题,即条件执行,我们将展示如何忽略不满足指定标准(与条件控制状态位相关)的指令。这种机制使程序员能够编写更紧凑的代码。

考虑add指令。当计算机从内存中读取它时,它会执行,就像几乎每台计算机一样。ARM 不同;它的每条指令都是条件执行的——也就是说,只有当满足特定条件时,指令才会执行;否则,它会被绕过(取消或压扁)。每个 ARM 指令都与一个逻辑条件相关联(表 10.3中的 16 个之一)。如果声明的条件为真,则执行该指令。

后缀通过附加condition来表示条件执行,例如,add eq r1, r2, r3指定只有在 CCR 中的 Z 位被设置时才执行加法。这个操作的 RTL 形式如下:


IF Z = 1 THEN [r1] ← [r2] + [r3]

当然,没有任何东西阻止你结合条件执行和移位,因为指令的分支和移位字段是独立的。你可以写出以下代码:


addcc  r1,r2,r3, lsl r4

这被解释如下:


 IF C = 0 THEN [r1] ← [r2] + [r3] x 2[r4].

为了展示条件执行的力量,考虑以下 Python 语句:


if A == B: C = D – E;

使用条件执行将以下代码翻译成 ARM 代码,我们可以写出以下代码:


      cmp    r1,r2         @ Compare A == B
      subeq  r3,r1,r4      @ If (A== B) then C = D - E

测试之后,操作要么执行,要么不执行,这取决于测试的结果。现在,考虑一个复合谓词的结构:


if ((a == b)AND(c == d)): e = e + 1
      cmp    r0,r1         @ Compare a == b
      cmpeq  r2,r3         @ If a == b then test c == d
      addeq  r4,r4,#1      @ If a == b AND c == d THEN increment e

第一行,cmp r0, r1,比较ab。下一行,cmp eq r2, r3,仅在第一行的结果为真时(即a == b)执行条件比较。第三行,add eq r4, r4, #1,仅在上一行结果为真时(即c == d)执行,以实现e = e + 1。没有条件执行,我们可能会写出以下代码:


      cmp    r0,r1           @ Compare a == b
      bne    Exit            @ Exit if a =! b
      cmp    r2,r3           @ Compare c == d
      bne    Exit            @ Exit if c =! d
      add    r4,r4,#1        @ Else increment e
Exit

这种传统的复合逻辑条件方法需要五条指令。你也可以用多个条件处理一些测试。考虑以下:


if (a == b) e = e + 4;
if (a < b)  e = e + 7;
if (a > b)  e = e + 12;

使用与之前相同的寄存器分配,我们可以使用条件执行如下实现:


      cmp    r0,r1           @ Compare a == b
      addeq  r4,r4,#4        @ If a == b then e = e + 4
      addle  r4,r4,#7        @ If a < b  then e = e + 7
      addgt  r4,r4,#12       @ If a > b  then e = e + 12

使用传统的非条件执行,我们需要编写以下代码来实现这个算法。这比之前的版本要简陋得多:


        cmp    r0,r1         @ Compare a == b
        bne    Test1         @ Not equal try next test
        add    r4,r4,#4      @ a == b so e = e+4
        b      ExitAll       @ Now leave
Test1:  blt    Test2         @ If a < b then
        add    r4,r4,#12     @ If we are here a > b so e = e + 12
        b      ExitAll       @ Now leave
Test2:  add    r4,r4,#7      @ If we are here a < b so e = e + 7
ExitAll:

在下一个例子中,我们使用条件执行来获取有符号整数的绝对值——也就是说,如果整数是负数,它会被转换成相应的正数。例如(在 8 位中),-2 是11111110,这将转换成00000010(即+2)。

我们可以使用 ARM 的teq指令。teqCMP类似,但teq在测试期间不设置VC标志。teq在测试负数时很有用,因为如果测试的数字是负数,N 位会被设置为 1:


       teq    r0,#0          @ Compare r0 with zero
       rsbmi  r0,r0,#0       @ If negative then 0 - [r0]

在这里,r0中的操作数被测试,如果它是负数,N 位被设置,如果是正数,N 位被清除。条件指令rsb mi在测试的操作数为正数时(不需要更改)不执行。如果数字是负数,反向减法执行0 – r0,这会反转它的符号并使其变为正数。

顺序条件执行

由于比较或算术操作会更新CNVZ位,我们可以在一次比较之后执行多达四个条件操作。以下示例将大写 ASCII 编码字符转换为小写字符——例如,'M'将被转换为'm'。ASCII 字符的位 5 对于大写字母为零,对于小写字母为一。考虑以下代码,它首先检查一个字符是否在'A'到'Z'的范围内,如果是,则将其转换为小写:


      cmp    r0,#'A'         @ Compare character with letter "A"
      rsbges r1,r0,#'Z'      @ Check less than Z if greater than A  Update flags
      orrge  r0,r0,#0x20     @ If in "A" to "Z," then set bit 5 to force lowercase

第一条指令,cmp,通过减去字符'A'的 ASCII 码来检查字符是否大于或等于'A'。如果是,rsbges 会检查字符是否小于'Z'。这个测试仅在r0中的字符大于或等于'A'时执行。我们使用反向减法,因为我们想测试 Z 的 ASCII 码减去字符的 ASCII 码是否为正。如果我们处于范围内,则执行条件orr,并通过设置位 5 来执行大写转小写的转换。

在下一章中,我们将探讨如何指定操作数——也就是说,我们将探讨寻址方式。

摘要

在本章中,我们扩展了我们对 ARM 的了解,超出了我们在上一章中遇到的基本数据处理指令。

我们从 ARM 的寄存器集开始,这与几乎所有其他处理器都不同。RISC 处理器通常有 32 个通用寄存器。ARM 只有 16 个寄存器。

ARM 的两个寄存器具有特殊用途。寄存器r14被称为链接寄存器,用于带有链接的分支指令来恢复返回地址。否则,它是一个通用寄存器。寄存器r15是程序计数器,这确实非常不寻常。这使得 ARM 成为一个非常有趣的设备,因为你可以通过操作r15来更改程序计数器。

我们还研究了移位操作。移位只是涉及位向左或向右移动一个或多个位置。然而,由于位位于寄存器或内存位置中,移位涉及位从一个位置移动到另一个位置,并从另一个位置掉落。不同类型的移位取决于在数字两端移入或移出的位发生了什么。

我们发现 ARM 还有一个不寻常的特性,因为它不提供纯移位指令。相反,它可以在传统的数据处理操作中将移位应用于第二个操作数。ARM 可以执行add r0, r1, r2, lsl r3指令,该指令将寄存器r2的内容左移r3中的值。然后,移位值被加到r1上,结果被转移到r0。这种机制提供了一种免费移位,因为你可以进行移位而无需为执行它支付任何代价。

可能 ARM 最引人入胜的特性是其执行条件指令的能力——也就是说,在执行指令之前,会检查条件码位。例如,addeq r0,r1,r2 仅当 z 位设置为 1 时才执行加法操作。这是一个非常强大的操作,你可以用它来编写紧凑的代码。

可惜的是,条件执行似乎是一种聪明的技术,但其时代已经过去。如今,它不是一个成本效益高的操作。条件执行减少了程序中的分支数量和执行程序所需的时钟周期数。计算机技术的进步使得条件执行在新 CPU 设计中变得多余。

在下一章中,我们将探讨 ARM 的寻址模式——这是这款处理器的一个亮点。

第十一章:ARM 寻址模式

寻址模式是计算机体系结构的基本部分,它关注的是你如何表达操作数的位置。我们在前面的章节中介绍了寻址模式。现在,我们将研究 ARM 相当复杂的寻址模式集。

将讨论的主题如下:

  • 字面量寻址

  • 缩放字面量

  • 寄存器间接寻址

  • 使用双指针寄存器

  • 自动递增指针

字面量寻址

最简单的寻址模式是字面量寻址。你不需要说操作数在内存中的位置,而是在指令中提供操作数(即这实际上是值)。其他寻址模式要求你指定操作数在内存中的位置。考虑以下 Python 表达式,它有两个字面量,30 和 12:

if A > 30: B = 12

我们可以将以下 Python 代码片段表示为 ARM 汇编语言,如下所示:


                         @ Register r0 is A and r1 is B
      cmp    r0,#30      @ Compare A with 30
      ble    Exit        @ If A ≤ 30, skip the next operation
      mov    r1,#12      @ else B = 12
Exit:

我们可以通过使用条件执行来简化此代码,如下所示:


      cmp    r0,#30      @ Compare A with 30
      movgt  r1,#12      @ If A > 30, then B = 12

缩放字面量

ARM 以非传统方式实现了 12 位字面量,借鉴了浮点数领域的技巧。字面量的 12 位中有 4 位用于缩放一个 8 位常数。也就是说,8 位常数通过 4 位缩放字段中的数字进行两次右旋转。字面量字段中最显著的 4 位指定了字面量在 32 位字中的对齐方式。如果 8 位立即值是N,4 位对齐是n,则字面量的值由N右旋转2n位得到。例如,如果 8 位字面量是0xABn4,则生成的 32 位字面量是0xAB000000,因为进行了八位的右旋转(2 x 4)。记住,八个右旋转位置相当于 32 - 8 = 24 位左移。图 11**.1展示了某些 32 位字面量和生成它们的 12 位字面量代码。

图 11.1 – ARM 的字面量操作数编码

图 11.1 – ARM 的字面量操作数编码

你可能会觉得这相当奇怪。为什么 ARM 没有使用 12 位字面量字段来提供一个 0 到 4,095 范围内的数字,而不是一个 0 到 255 范围内通过 2 的幂缩放的数字?答案是 ARM 的设计者认为缩放字面量在现实世界的应用中比未缩放数字更有用。例如,假设你想清除 32 位字中除了 8 到 15 位之外的所有位。你需要用字面量0b00000000000000001111111100000000或十六进制的0x0000FF00进行AND操作。使用缩放机制,我们可以取 8 位0x11111111并将它们左移 8 位(即右移 24 位)以得到所需的常数。然而,缩放因子n需要两倍的旋转次数才能实现这一点。即(32 – 8)/2,这是 12。因此,存储在 12 位指令字段中的字面量是 12,255,或十六进制的 CFF。

幸运的是,计算缩放因子是程序员不必总是担心的事情。ARM 编译器会自动生成生成所需的最佳指令(或指令集)。

寄存器间接寻址

我们已经遇到了这种寻址模式,其中操作数的位置存储在寄存器中。它被称为寄存器 间接寻址,因为指令指定了可以找到指向实际操作数指针的寄存器。在 ARM 文献中,这种寻址模式被称为 索引寻址。有些人称之为 基址寻址

寄存器间接寻址模式需要三个读取操作来访问操作数:

  • 读取指令以找到指针寄存器

  • 读取指针寄存器以找到操作数地址

  • 在操作数地址读取内存以找到操作数

寄存器间接寻址很重要,因为包含指向实际操作数指针的寄存器的内容可以在运行时修改,因此地址是变量。因此,我们可以通过改变指针来遍历诸如表格之类的数据结构。

图 11.2 – 寄存器间接寻址 – 执行 ldr r1,[r0]

图 11.2 – 寄存器间接寻址 – 执行 ldr r1,[r0]

图 11.2 展示了 ldr r1,[r0] 的效果,其中 r0 是指针,包含值 n

ldr r1,[r0] 将寄存器 r0 指向的内存位置的内容加载到寄存器 r1 中。

执行 add r0,r0,#4 将指针寄存器 r0 的内容增加 4,以便指向下一个字位置(记住,连续的字地址相差 4)。

考虑以下内容:


   ldr r1,[r0]                   @ Get data pointed at by r0
   add r0,r0,#4                  @ Advance pointer to next word location

第一条指令将 r1 寄存器加载为 r0 指向的 32 位字。第二条指令将 r0 寄存器增加 4,以便指向内存中的下一个字节。重复执行这对指令将允许你逐个元素地遍历值表。我们很快就会看到 ARM 包含一个自动增加或减少指针的机制。

下一个代码片段演示了如何将表格的元素相加。假设你有一个包含四周内每日支出的表格。每个条目都连续存储在包含 4 x 7 = 28 个条目的表格中:


      adr r0,table               @ r0 points to the table of data (pseudo-instruction)
      add r3,r0,#28 * 4          @ r3 points to the end of the table (28 x 4 bytes)
      mov r1,#0                  @ Clear the sum in r1
loop: ldr r2,[r0]                @ REPEAT: Get the next value in r2
      add r1,r1,r2               @ Add the new value to the running total
      add r0,r0,#4               @ Point to the next location in the table (4 bytes increment)
      cmp r0,r3                  @ Are we at the end of the table?
      bne loop                   @ UNTIL all elements added
table: .word 123                 @ Data for day 1
       .word 456                 @ Data for day 2
       .word 20                  @ Data for day 28

在这个简单的例子中,我们设置了一个循环,并从第一个元素遍历到最后一个元素。在每次循环中,我们读取一个元素并将其加到总数中。阴影线表示动作发生的地方——获取一个元素并指向下一个元素。

下一个示例演示了基于指针的间接寻址和字节操作(即,对 8 位值而不是整个字的操作)。假设我们想在字符串中找到一个特定的字符。以下代码使用 字节加载指令ldrb,它将 8 位加载到目标寄存器。我们通过遍历字符串时指针增加 1,而不是 4,因为值位于单字节边界上:


      ldr       r0,=String        @ r0 points at the string (using a pseudo-instruction)
loop: ldrb      r1,[r0]          @ REPEAT Read a byte character
      add       r0,r0,#1          @ Update character pointer by 1 (not by 4)
      cmp       r1,#Term          @ UNTIL terminator found
      bne      loop

带偏移的基于指针的寻址

假设有人问你,“药店在哪里?”你可能会回答,“它在银行左边两个街区。”这就是日常生活中的基于指针的偏移寻址。我们通过给出相对于其他事物的位置来指向银行。

ARM 允许你使用指针寄存器加上一个提供偏移的 12 位立即数来指定地址。请注意,这是一个真正的 12 位立即数,而不是用作立即操作数的 8 位缩放值。立即数可以是正数或负数(表示它是要加到或从基指针中减去的)。考虑 ldr r5,[r2,#160],其中加载到 r5 的操作数地址是 r1 的内容加上 160。

假设你想要将一个 24 个字的块移动到内存中比当前位置再远 128 字节的地方。假设要移动的块的地址在 0x400。我们可以编写以下代码:


      mov  r0,#0x400         @ r0 points to the block of data
      ldr  r1,#24            @ r1 contains the number of words to move
next: ldr  r2,[r0]           @ REPEAT: Read word and put in r2
      add  r0,r0,#4          @ Point to next word
      str  r2,[r0,#128]      @ Store the word 128 bytes on
      subs r1,#1             @ Decrement the counter and set the status bits
      bne  next              @ UNTIL all elements added

不幸的是,你无法直接在如我们描述的基于 ARM 的 Raspberry Pi 上运行此代码,因为你不允许在代码段中修改内存。这是一个操作系统限制。为了避免这个问题,你必须使用指针,正如我们在第九章中演示的那样。我们将回到这一点。

两个指针比一个好

寄存器间接寻址模式允许你访问线性数据结构中的元素,例如一列表。有时,你将有一个更复杂的多维数据结构,例如具有行和列的矩阵。在这种情况下,两个指针可以简化编程——一个指针用于行,一个用于列。

ARM 提供了一种基于指针的寻址模式,允许你指定两个指针寄存器的和的地址,如下所示:


ldr r7,[r0,r1]               @ Load r7 with the contents of the location pointed at by r0 plus r1

你可以对第二个操作数应用位移,如下所示:


ldr r2,[r0,r1,lsl #3]        @ Load r7 with the contents of the location pointed at by r0 plus r1 x 8

在这种情况下,寄存器 r1 被缩放 8 倍。缩放因子必须是 2 的幂(即,2、4、8、16...)。

图 11**.3 展示了使用两个索引寄存器的基于指针的寻址。

图 11.3 – 使用寄存器偏移的索引寻址 – ldr r0,[r1,r2]

图 11.3 – 使用寄存器偏移的索引寻址 – ldr r0,[r1,r2]

这是一种概念性的表示,因为我们已经展示了其中一个寄存器指向内存(r1)和另一个寄存器提供从 r1 的偏移(即,r2)。然而,由于最终地址是 r1r2,我们也可以反过来画。

指针寄存器的自动索引

当使用指针寄存器时,它们通常用于遍历数据结构,并在每次内存访问后经常增加或减少。因此,将此动作作为指令的一部分是有意义的。

事实上,CISC 处理器通常包括自动索引。当 RISC 处理器以每时钟周期一条指令的目标出现时,自动索引被从指令集中删除。然而,这种机制已被纳入 ARM 架构。

ARM 的自动索引有四种变体。您可以在使用指针之前或之后执行它。您可以向上索引到更高地址或向下索引到更低地址。考虑以下涉及索引寄存器 r0 和增量 4(在字节寻址机器上为一个字)的操作。每个选项由一个内存访问和一个指针调整动作组成;在前两种情况下,指针首先调整(预索引),在其他两种情况下,首先访问内存(后索引)。内存访问以粗体表示:

索引类型 第一个动作 第二个动作
预索引上升 [r1] [r1] + 4 [r0] ← [[r1]]
预索引下降 [r1] [r1] - 4 [r0] ← [[r1]]
后索引上升 [r0] ← [[r1]] [r1] [r1] + 4
后索引下降 [r0] ← [[r1]] [r1] [r1] – 4

ARM 通过在方括号内包含偏移量并在其后附加感叹号来表示预索引,如下所示:


    str r4,[r0,#4]!   @ Store r4 at the address given by [r0] + 4 and then increment r0 by 4

指针 r0 的值在使用之前改变。假设我们希望使用后索引并在使用后增加指针。在这种情况下,格式如下:


    str r4,[r0],#4    @ Store  r4 at the address given by [r0] and then increment r0 by 4.

在这里,指针 r0 的值在使用作为偏移量之后改变。

图 11.411.6 展示了 ARM 在索引寻址上的变体。在每种情况下,基址寄存器是 r1,偏移量是 12,目标寄存器是 r0。这些图在此总结:

类型 格式 基址 基址 操作数地址
11.4 寄存器间接 ldr r0,[r1,#12] [r1] [r1] [r1] + 12
11.5 预索引 ldr r0,[r1,#12]! [r1] [r1] + 12 [r1] + 12
11.6 后索引 ldr r0,[r1],#12 [r1] [r1] + 12 [r1]

图 11.4 – 带偏移的寄存器间接寻址

图 11.4 – 带偏移的寄存器间接寻址

图 11.5 – 带预索引的寄存器间接寻址

图 11.5 – 带预索引的寄存器间接寻址

图 11.6 – 带后索引的寄存器间接寻址

图 11.6 – 带后索引的寄存器间接寻址

考虑以下示例,我们使用后索引将数据块从一个内存区域移动到另一个内存区域。在这种情况下,我们通过后索引四个字节,因为我们移动的是 4 字节字:


       adr  r0,table1       @ Source array pointer in r0\. Use pseudo instruction
       adr  r1,table2       @ Destination array pointer in r1
       mov  r2,#8           @ Eight elements (words) to move
loop:  ldr  r3,[r0],#4      @ REPEAT: Get element from table 1 (post-indexing by 4)
       str  r3,[r1],#4      @ Store in table 2 (post-indexing 4)
       subs r2,r2,#1        @ Decrement counter
       bne  loop            @ UNTIL all done

该程序的两大关键行是加载和存储指令(阴影部分),其中数据从源读取并复制到目标。正如我们所言,由于变量分配内存空间的方式,你无法在不修改的情况下直接在 Raspberry Pi 上运行此代码。以下代码演示了适用于 Raspberry Pi 的可运行版本。

这基本上是相同的代码。除了处理内存问题外,我们还添加了汇编指令和虚拟数据(包括允许你更容易观察内存中数据的标记)。还有一个nop指令。请注意,ARM 的一些版本有真正的nop指令,而另一些则使用伪指令。我在测试时添加了这个虚拟指令以“定位”到某个位置。记住,实际数据的地址存储在程序区域,然后使用该地址加载一个指针:

图 11.7 – 访问读写内存时使用指针

图 11.7 – 访问读写内存时使用指针

我们使用gbd作为调试工具运行了前面的程序,以下输出展示了这一点。为了压缩文本,我们已从显示中删除了不必要的数据——例如,未访问或修改的寄存器。

图 11.8 – 使用 gdb 跟踪图 11.7 的程序

图 11.8 – 使用 gdb 跟踪图 11.7 的程序

字符串复制的示例

下一个示例,图 11.9,使用后索引将字符串从一处复制到另一处,并按反向顺序移动一个指针向下,另一个指针向上。目标指针通过len-1递增,最初指向字符串的末尾。以下代码包括汇编语言指令,使其能够在 RPi 上运行:


         .equ    len,5            @ Length of string to reverse
         .text                    @ Program (code) area
         .global _start
_start:  mov    r0,#len           @ Number of characters to move
         adr    r1,adr_st1        @ r1 points at source address 1
         adr    r2,adr_st2        @ r2 points at source address 2
         ldr    r1,[r1]           @ Register r1 points to source
         ldr    r2,[r2]           @ Register r2 points to destination
         add    r2,r2,#len-1      @ r2 points to bottom of destination
Loop:    ldrb   r3,[r1],#1        @ Get char from source, increment pointer (note ldbr)
         strb   r3,[r2],#-1       @ Store char in destination, decrement pointer
         subs   r0,r0,#1          @ Decrement char count
         bne    Loop              @ REPEAT until all done
         nop                      @ Stop here
adr_st1: .word  str1
adr_st2: .word  str2
         .data
str1:    .ascii "Hello"           @ Source string
str2:    .byte  0,0,0,0,0         @ Destination string
         .end

图 11.9 – 反转字符串

图 11.9 – 反转字符串

下一节将探讨一种基于指针寻址的变体,其中指针本身是程序计数器。因此,所有数据和程序都参照当前程序的地址进行引用。

程序计数器相对寻址

ARM 处理器在许多方面都是不寻常的。你可以使用任何寄存器(即,r0r15)作为指针寄存器。然而,r15是 ARM 的程序计数器。如果你使用r15作为带索引的指针寄存器,你就是在说,“操作数距离我这里有多远。”这里的“我这里”指的是指令本身。图 11.10说明了程序计数器相对寻址。

想想看。你给出的是数据相对于使用它的程序地址,而不是内存中的绝对地址。如果你在内存中移动程序,数据与访问它的指令的距离仍然是相同的,使用程序计数器相对寻址。程序计数器相对寻址的引入是计算机架构中的一个重大进步。顺便说一句,大多数分支指令都使用程序计数器相对寻址,因为分支的目标是相对于当前指令指定的。

ARM 使用程序计数器相对寻址来加载 32 位常量。回想一下,你只能使用ldr指令加载 12 位常量。然而,汇编器可以在内存中预先加载一个 32 位常量,然后使用程序计数器相对寻址来访问它。换句话说,你将一个 32 位值存储在程序附近(或其中)的内存中,然后使用程序计数器相对寻址来访问它。这就是伪指令存在的原因。如果没有伪指令,你将不得不计算当前pc和所需操作数之间的相对地址。伪指令会这样做,并且使地址对程序员不可见。

执行ldr r0,[r15,#0x100]指令将一个 32 位操作数加载到r0寄存器中,该操作数位于程序计数器r15的内容0x100字节(0x40 或 64 个字)处。加载到r0的操作数距离程序计数器pc0x108字节。为什么是0x108而不是指令中指定的0x100`,这是为什么呢?额外的 8 个字节存在是因为程序计数器在每条指令执行后增加 8 个字节,因此它比当前地址领先 8 个字节。

那么程序计数器相对存储指令呢?这些指令不能使用。在 ARM 中,不支持使用程序计数器寻址模式的存储操作。这种限制可能是因为(a)大量代码位于只读内存中且无法更改,以及(b)它将允许修改运行时程序。

图 11.10 – 程序计数器相对寻址

图 11.10 – 程序计数器相对寻址

程序计数器相对寻址演示

考虑以下文本,其中我们使用了两种类型的伪指令,这两种伪指令都是为了将 32 位值加载到寄存器中。一个是adr(加载地址)和另一个ldr(加载 32 位立即数):


        .text
        .global   _start
_start: ldr r0,=pqr
        ldr r1,=abc                @ Note the special format of the pseudo ldr
        adr r2,pqr
        ldr r3,=0x11111111
        nop
        mov r0,#0
        mov r7,#1
        svc 0
abc:   .word 0x22222222
pqr:   .word 0x33333333
       .end

有三个ldr指令。两个将地址加载到寄存器中,第三个ldr指令加载一个 32 位立即数0x11111111。还有一个adr指令将 32 位地址加载到寄存器r2中。当这些代码执行时会发生什么?

首先,让我们看看在gdb中查看的源代码:

0x10054 <_start>      ldr r0, [pc, #32]   ; 0x1007c <pqr+4>
0x10058 <_start+4>    ldr r1, [pc, #32]   ; 0x10080 <pqr+8>
0x1005c <_start+8>
0x10060 <_start+12>   ldr r3, [pc, #28]   ; 0x10084 <pqr+12>

注意,这些与源代码不同。这是因为源代码使用的是翻译后的伪指令。例如,ldr r0,=pqr 被翻译成 ldr r0,[pc,#32]。源代码不能指定 32 位指令。然而,翻译版本使用常规加载来指定实际操作数的 32 位位置,这是程序计数器当前值的 32 位。

add r2,pc,#20 的处理方式不同。在这里,32 位立即数是通过将 20 加到当前 pc 的值来生成的,因为要定位的地址距离当前 PC 值 20 字节。

让我们使用 gdb 来查看代码执行到 nop 时的寄存器。我们将检查程序结束时的寄存器内容,然后查看内存位置。你可以看到存储在内存中的数据以及通过程序计数器相对寻址访问的常量:


r0             0x00010078
r1             0x00010074
r2             0x00010078
r3             0x11111111
(gdb) x/8xw 0x10074
0x10074 <abc>:          0x22222222    0x33333333    0x00010078      0x00010074
0x10084 <pqr+12>:       0x11111111    0x00001141    0x61656100      0x00010069

在下一章中,我们将继续探讨寻址的主题,看看 ARM 如何实现子程序,以及你如何使用堆栈来跟踪子程序返回地址。

摘要

寻址模式包括表示内存中项目位置的所有方式。寻址模式在汇编语言编程中既是最容易的也是最难的主题。概念很简单,但使用指针的间接寻址模式可能需要一些努力来可视化。

在本章中,我们学习了立即寻址或直接寻址,其中操作数是一个实际值(它是事物本身,而不是位置)。立即值用于指定常量——例如,在 x + 5 中,数字 5 是一个立即值。这是最简单的寻址模式,因为数据是指令的一部分,所以不访问任何内存位置。

我们还研究了 ARM 指定立即数的一种相当不寻常的方式,即提供一个 0 到 255 范围内的值和一个可以将其乘以 2 的偶数次幂的乘数。你可以指定 5 并存储 5、20、80 等等。

本章的大部分内容都是关于寄存器间接寻址,它有许多其他名称(索引和基于指针的)。在这种情况下,地址由寄存器的内容给出。指令指定的是操作数的实际地址,而不是指向它的寄存器。因为你可以操作寄存器中的数据,所以你可以操作地址并访问数组、表格等数据结构。

ARM 提供了 RISC 风格的自动增量和减量。这意味着你可以使用指针并在之前(或之后使用)对其进行增减。

我们还研究了寄存器间接寻址的一种特殊形式,即相对寻址,其中指针是程序计数器本身——也就是说,操作数的地址是相对于访问它的指令来指定的。这意味着使用程序计数器相对寻址的代码可以在内存中移动,而无需重新计算任何地址。

在下一章中,我们将探讨编程基础中的一个主题——子程序和栈。这个主题也与寻址模式密切相关。

第十二章:子程序和栈

子程序是一段从程序中的某个点调用并执行的代码,然后返回到调用点之后的指令。所有计算机都使用子程序,但有些计算机为程序员提供了比其他计算机更多的实现子程序的功能。

在本章中,我们将探讨 ARM 的子程序处理机制——特别是以下内容:

  • 特殊的分支和链接指令

  • 子程序调用和返回

  • 块移动指令

带链接指令的分支

首先,我们讨论 ARM 的branch and link(分支和链接)指令,bl,它提供了一种快速简单的方法来调用子程序,而不使用栈机制。

实现子程序调用和返回有两种基本方法。经典的 CISC 方法是BSR(跳转到子程序)和RTS(从子程序返回)。典型的代码可能如下所示:


      bsr abc    @ Call the subroutine on the line labeled abc
      . . .
      . . .
abc:  . . .      @ Subroutine abc entry point
      . . .
      rts        @ Subroutine abc return to calling point

这就是简单在行动中的体现。你调用一段代码,执行它,然后返回到调用点之后的指令。大多数 RISC 处理器拒绝这种机制,因为子程序调用和返回是复杂的指令,在调用期间将返回地址保存在栈上,然后在返回时从栈中取出返回地址。这对程序员来说非常方便,但需要几个 CPU 时钟周期来执行,并且不符合 RISC 处理器每条指令一周期的工作模式。

基于栈的子程序调用/返回的巨大优势在于,你可以嵌套子程序,并从其他子程序中调用子程序,而栈机制会自动处理返回地址。

你很快就会看到你可以在 ARM 上实现这种机制,但不是通过使用两个专用指令。你必须编写自己的代码。

如果你想要一个简单的子程序调用和返回(子程序被称为叶子),你所需要做的只是将返回地址保存在一个寄存器中(不需要外部内存或栈)。然后,要返回,你只需将返回地址放入程序计数器——简单快捷。然而,一旦你进入子程序,你就不能再做同样的事情并调用另一个子程序。这样做会破坏你现有的已保存的返回地址。

ARM 的子程序机制称为branch with link,其助记符为bl target,其中target是子程序的符号地址。实际地址是程序计数器相对的,是一个 24 位有符号字,提供了从当前 PC 开始的 223 个字的分支范围。范围是从 PC 的任意方向 223 个字(即向前分支和向后分支)。

带有链接指令的分支行为类似于分支指令,但它还会复制返回地址(即返回后要执行的下一个指令的地址到链接寄存器 r14。假设你执行以下操作:


        bl     sub_A          @ Branch to sub_A with link and save return address in r14

ARM 执行一个跳转到由标签 sub_A 指定的目标地址的分支。它还将程序计数器(保存在寄存器 r15 中)复制到链接寄存器 r14 以保留返回地址。在子程序结束时,您通过将 r14 中的返回地址转移到程序计数器来返回。您不需要特殊的返回指令;您只需写下以下内容:


        mov    pc,lr          @ We can also write this as mov r15,r14

让我们看看子程序使用的一个简单例子。假设您需要在程序中多次评估 if x > 0 then x = 16x + 1 else x = 32x 函数。假设 x 参数在寄存器 r0 中,我们可以编写以下子程序:


Func1:  cmp    r0,#0          @ Test for x > 0
        movgt  r0,r0, lsl #4  @ If x > 0 then x = 16x
        addgt  r0,r0,#1       @ If x > 0 then x = 16x + 1
        movle  r0,r0, lsl #5  @ ELSE if x < 0 THEN x = 32x
        mov    pc,lr          @ Return by restoring saved PC

创建子程序所需的一切只是一个入口点(标签 Func1)和一个返回点,该返回点通过在链接寄存器中 bl 恢复保存的地址。

我们已经描述了栈。我们在这里再次讨论它,因为它可能是计算中最重要的数据结构。栈是一个堆,您在顶部添加东西,也从顶部取出东西。如果您从栈中取出东西,它就是最后添加到栈中的东西。因此,栈被称为 后进先出队列LIFO),其中项目从一端进入,以相反的顺序离开。

计算机通过使用指针寄存器来指向栈顶来实现栈。ARM 使用 r13 作为栈指针,或者更准确地说,ARM 强制 使用 r13 作为栈指针。如果您愿意,可以使用 r0r13 作为栈指针。使用 r13 是一种 约定,旨在使代码更易于阅读和共享。

栈有四种变体。它们都做同样的事情,但实现方式不同。ARM 支持所有四种变体,但为了简单起见,我们在这里只使用一种。栈存储在内存中,在正常的人类意义上没有上下之分。当项目添加到栈中时,它们可以添加到具有较低地址的下一个位置或具有较高地址的下一个位置。按照惯例,大多数栈都是这样实现的,即下一个项目存储在 较低地址。我们说栈向 较低 地址 增长(这是因为我们按从上到下的顺序给书的行编号,第一行在顶部)。

栈排列的其他变体是栈指针可以指向栈顶的项目,TOS,或者指向该栈上的下一个空闲项目。我将介绍栈指针指向栈顶的项目(再次强调,这是最常见的约定)。

图 12**.1 展示了一个用于保存子程序返回地址的栈。

图 12.1 – 使用栈保存返回地址,N

图 12.1 – 使用栈保存返回地址,N

栈指针指向栈顶元素,当向栈中添加一个项目(推送)时,栈指针首先减少。当从栈中移除一个项目时,它是在栈指针指示的位置取出的,然后栈指针增加(即向下移动)。我们可以根据 栈指针SP)定义推送和拉取(弹出)操作如下:


PUSH:    [SP]   ← [SP] – 4    @ Move stack pointer up one word (up toward lower addresses)
         [[SP]] ← data        @ Push data onto the stack. Push uses pre-decrementing.
PULL:    data  ← [[SP]]       @ Pull data off the stack by reading TOS
         [SP]  ← [SP] + 4     @ Move stack pointer down one word (pull uses post-incrementing)

栈指针以四的倍数减少和增加,因为我们遵循 ARM 习惯,即内存是字节寻址的,栈项是一字长(四个字节)。下一节将更详细地探讨子程序的调用和有序返回到调用点。

子程序调用和返回

要调用子程序,你需要将返回地址压入栈中。CISC 处理器使用 bsr target 实现子程序调用。因为 ARM 缺少子程序调用指令,你可以编写以下 ARM 代码。记住,我们处理的是 32 位字推送和拉取,栈必须以四的倍数增加或减少。记住,在 ARM 文献中 r15SP 以及 r13lr 是可以互换的术语:


      sub  r13,r13,#4       @ Pre-decrement the stack pointer (r13 is used as the SP)
      str  r15,[r13]        @ Push the return address in r15 on the stack
      b    Target           @ Jump to the target address
        ...                 @ Return here

ARM 没有子程序返回指令,所以你可以用以下方式实现:


      ldr  r12,[r13],#+4    @ Get saved PC from stack and post-increment the stack pointer
      mov  r15,r12          @ Load PC with return address

以下是一个简单的程序,使用此机制设置调用和返回。注意,我们没有设置初始的栈指针。ARM 的操作系统会做这件事:


.section .text
.global _start
_start: mov  r0,#9          @ Dummy operation
        sub  sp,sp,#4       @ Decrement stack
        str  pc,[sp]        @ Save pc on stack
        b    target         @ Branch to subroutine "target"
        mov  r2,#0xFFFFFFFF @ Return here ... this is a marker
        nop                 @ Dummy operation
        mov  r7,#1          @ Set up exit code
        svc  0              @ Leave program
target: mov  r1,#0xFF       @ Subroutine ... dummy operation
        ldr  r12,[sp],#+4   @ Pull pc off the stack
        mov  r15,r12        @ Return
        .end

图 12.2 展示了运行此代码后 ARM 模拟器的输出。我们包括了反汇编窗口和寄存器窗口。注意 mov r2,#0xFFFFFFFF指令已被转换为mvn r2,#0 操作。回想一下 MVNldr r12,[sp],#+ 已被重命名为 pop {r12}。这相当于弹出栈操作(从栈顶移除一个项目)。

执行代码后的寄存器:


Register group: general
r0             0x9                 9
r1             0xff                255
r2             0xffffffff          4294967295
r12            0x10064             65636
sp             0xbefff380          0xbefff380
lr             0x0                 0
pc             0x1006c             0x1006c <_start+24>

以下是对代码的逐步执行。这是一个子程序调用和返回的示例:


B+ 0x10054 <_start>        mov    r0, #9
   0x10058 <_start+4>      sub    sp, sp, #4
   0x1005c <_start+8>      str    pc, [sp]
   0x10060 <_start+12>     b      0x10074 <target>
   0x10064 <_start+16>     mvn    r2, #0
   0x10068 <_start+20>     nop                    ; (mov r0, r0)
  ⁰x1006c <_start+24>     mov    r7, #1
   0x10070 <_start+28>     svc    0x00000000
   0x10074 <target>        mov    r1, #255        ; 0xff
   0x10078 <target+4>      pop    {r12}           ; (ldr r12, [sp], #4)
   0x1007c <target+8>      mov    pc, r12

在下一节中,我们将探讨 ARM 最强大且最不 RISC 式的操作之一——在内存和多个寄存器之间移动数据块的能力。

块移动指令

在本节中,我们将学习如何移动多个寄存器。基本概念如下:

  • 如何指定一组寄存器

  • 如何寻址内存

  • 如何对寄存器的存储进行排序

  • 不同类型的块移动

一些 CISC 处理器的一个伟大特性是你可以用一条指令将一组寄存器推入栈中。RISC 处理器通常没有这样的指令,因为它与 RISC 理念核心的每周期一次操作的设计约束相冲突。令人惊讶的是,ARM 实现了一个块移动指令,允许你在一次操作(即一条指令)中将一组寄存器复制到或从内存中。以下 ARM 代码演示了如何从内存中加载寄存器 r1r2r3r5


    adr  r0,DataToGo    @ Load r0 with the address of the data area
    ldr  r1,[r0],#4     @ Load r1 with the word pointed at by r0 and update the pointer
    ldr  r2,[r0],#4     @ Load r2 with the word pointed at by r0 and update the pointer
    ldr  r3,[r0],#4     @ and so forth for the remaining registers r3 and r5…
    ldr  r5,[r0],#4

ARM 有一个 块移动到内存 指令 stm 和一个 块移动从内存 指令 ldm,用于将寄存器组从内存中复制到和从内存中复制。块移动指令需要一个两位后缀来描述数据是如何访问的(例如,stmia 或 ldmdb),我们将看到。

从概念上讲,块移动很容易理解,因为它仅仅是 将这些寄存器的内容复制到内存中 的操作,或者相反。在实践中,它更为复杂。ARM 提供了一套完整的选项,用于确定移动的方式 – 例如,寄存器是从高地址到低地址还是从低地址到高地址移动,或者内存指针是在传输前后更新(就像堆栈结构一样)。实际上,块移动就像在其他计算机上找到的推和拉堆栈操作一样。

让我们将寄存器 r1r2r3r5 的内容移动到连续的内存位置,使用 stm 指令:


stmia  r0!,{r1-r3,r5}   @ Note block move syntax. The register list is in braces
                        @ r0! is the destination register with auto indexing
                        @ The register list is {r1-r3,r5} r1,r2,r3,r5

此指令将寄存器 r1 复制到 r3,并将 r5 移动到连续的内存位置,使用 r0 作为指针寄存器并自动索引(由 ! 后缀表示)。ia 后缀表示索引寄存器 r0 在每次传输后增加,数据传输按照 递增 地址顺序进行。我们还将看到,此指令可以写成 stmfd(这是相同的操作,但 ARM 在其文档中为同一事物提供了两种命名约定)。

虽然 ARM 的块移动模式指令有多种变化,但编号最低的寄存器总是存储在最低的地址,其次是下一个编号最低的寄存器在下一个更高的地址,依此类推(例如,前一个示例中的 r1,然后是 r2r3r5)。

考虑以下块移动的示例。因为它比我们遇到的一些指令要复杂一些,我们将演示其执行。我包括了几个不是严格属于演示但包括我在实验中使用的功能。特别是,我在寄存器和内存中使用标记,以便我可以更容易地跟踪调试。例如,在内存块中,我存储了数据字 0xFFFFFFFF0xAAAAAAAA。这些数据除了让我一眼就能看到我在调试内存时数据区域开始和结束的位置外,没有其他功能。同样,我使用类似 0x11111111 的值作为从寄存器移动的字,因为我可以在调试时轻松跟踪它们:


           .text                       @ This is a code section
           .global _start              @ Define entry point for code execution
 _start:    nop                        @ nop = no operation and is a dummy instruction
           ldr    r0,=0xFFFFFFFF       @ Dummy values for testing
           ldr    r1,=0x11111111
           ldr    r2,=0x22222222
           ldr    r3,=0x33333333
           ldr    r4,=0x44444444
           ldr    r5,=0x55555555
           ldr    r0,adr_mem           @ Load pointer r0 with memory
           stmia  r0!,{r1-r3,r5}       @ Do a multiple load to memory
           mov    r10,r0               @ Save r0 in r10 (for debugging)
           ldmdb  r0!,{r6-r9}          @ Now load data from memory
           mov    r11,r0
           mov    r1,#1                @ Terminate command
           svc    0                    @ Call OS to leave
           .word  0xFFFFFFFF           @ A dummy value for testing
           .word  0xAAAAAAAA           @ Another dummy value
adr_mem:   .word  memory               @ The address of the memory for storing data
           .data                       @ Declare a memory segment for the data
memory:    .word  0xBBBBBBBB           @ Yet another memory marker
           .space 32                   @ Reserve space for storage (8 words)
           .word  0xCCCCCCCC           @ Final memory marker
           .end

此代码设置了五个寄存器,其数据在检查内存时很容易看到。程序末尾的两个字标记之间保存了 32 个字节的内存,使用 .space 指令。此块的开始被标记为 memoryr0 指向它。然后这五个寄存器被存储在内存中。执行块存储的指令以浅灰色表示,数据区域以深灰色表示。

我们最初感兴趣的代码是为五个寄存器加载而编写的,分别用0x111111110x55555555预设寄存器r1r5。寄存器r0最初被设置为0xFFFFFFFF,仅作为调试的标记。关键指令是stmia r0!,{r1-r3,r5},其目的是将寄存器r1r2r3r5的内容存储在由r0指向的连续内存位置。

下面的树莓派输出来自gdb调试器。源代码是blockMove1.s。我们省略了一些寄存器值,以便在寄存器未更改或未使用时使列表更易于阅读。同样,重复的命令行也被省略了:


pi@raspberrypi:~/Desktop $ as -g -o blockMove1.o blockMove1.s
pi@raspberrypi:~/Desktop $ ld -o blockMove1 blockMove1.o
pi@raspberrypi:~/Desktop $ gdb blockMove1
(gdb) b 1
Breakpoint 1 at 0x10078: file blockMove1.s, line 6.
(gdb) r
Starting program: /home/pi/Desktop/blockMove1
Breakpoint 1, _start () at blockMove1.s:6
6               ldr    r0,=0xFFFFFFFF         @ Dummy value for testing
(gdb) i r
r0             0x0                 0          # These are the initial registers before we start
r1             0x0                 0
r2             0x0                 0
r3             0x0                 0
r4             0x0                 0
r5             0x0                 0
r6             0x0                 0
r7             0x0                 0
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
sp             0xbefff380          0xbefff380 # The OS sets up the stack pointer
lr             0x0                 0
pc             0x10078             0x10078 <_start+4>  # The OS sets up stack pointer

在查看寄存器之后,我们现在将执行一系列指令。请注意,我们需要一次输入si 1,然后简单地按Return来重复操作:


(gdb) si 1
7              ldr    r1,=0x11111111          # Here we trace seven instructions
8              ldr    r2,=0x22222222
9              ldr    r3,=0x33333333
10             ldr    r4,=0x44444444
11             ldr    r5,=0x55555555
12             ldr    r0,adr_mem              @ Load pointer r0 with memory
13             stmia  r0!,{r1-r3,r5}          @ Multiple load to memory
(gdb) i r
r0             0x200cc             131276

现在,让我们看看我们设置的寄存器。只显示了感兴趣的寄存器。请注意,r0指向0x200CC处的数据。系统软件负责这个地址:


r1             0x11111111          286331153
r2             0x22222222          572662306
r3             0x33333333          858993459
r4             0x44444444          1145324612
r5             0x55555555          1431655765
r6             0x0                 0

到目前为止,我们已经将寄存器r0设置为指针,其值为0x200cc。这个值是由汇编器和加载器确定的。如果您参考源代码,我们使用了ldr r0,adr_mem通过指向实际存储在内存中的数据的指针来加载r0。这是因为软件不允许我们直接加载内存地址。

您可以看到,寄存器已经设置为易于追踪的值。下一步是使用x/16xw gdb命令检查内存,以显示 16 个十六进制数据字:


pc             0x10094             0x10094 <_start+32>
(gdb) x/16xw 0x200cc
0x200cc:    0xbbbbbbbb    0x00000000    0x00000000    0x00000000
0x200dc:    0x00000000    0x00000000    0x00000000    0x00000000
0x200ec:    0x00000000    0xcccccccc    0x00001141    0x61656100
0x200fc:    0x01006962    0x00000007    0x00000108    0x0000001c

我们存储在内存中的两个标记以粗体显示。现在,让我们执行存储的多个寄存器。在此之前,我们将指针复制到r10(这只是为了我的调试目的)以便我们可以看到移动之前它的值。在块移动指令之后,我们显示感兴趣的寄存器:


(gdb) si 1
14               mov    r10,r0
15               ldmdb  r0!,{r6-r9}             @ Now load data from memory
(gdb) i r
r0             0x200dc             131292
r1             0x11111111          286331153
r2             0x22222222          572662306
r3             0x33333333          858993459
r4             0x44444444          1145324612
r5             0x55555555          1431655765
r6             0x0                 0
r10            0x200dc             131292
pc             0x1009c             0x1009c <_start+40>

现在是检验成果的时候了。这是x/16xw显示命令之后的内存。请注意,四个寄存器的内存储存在连续递增的内存位置:


(gdb) x/16xw 0x200cc
0x200cc:    0x11111111    0x22222222    0x33333333    0x55555555
0x200dc:    0x00000000    0x00000000    0x00000000    0x00000000
0x200ec:    0x00000000    0xcccccccc    0x00001141    0x61656100
0x200fc:    0x01006962    0x00000007    0x00000108    0x0000001c

最后,我们将执行最后两个命令并显示寄存器内容:


(gdb) si 1
16               mov    r11,r0
18               mov    r1,#1      @ Terminate command
(gdb) i r
r0             0x200cc             131276
r1             0x11111111          286331153
r2             0x22222222          572662306
r3             0x33333333          858993459
r4             0x44444444          1145324612
r5             0x55555555          1431655765
r6             0x11111111          286331153       Data copied to registers r6 - r9
r7             0x22222222          572662306
r8             0x33333333          858993459
r9             0x55555555          1431655765
r10            0x200dc             131292

您可以看到,从ldmdb r0!,{r6-r9}内存操作中,四个寄存器从内存中复制到寄存器r7r9

考虑ldm的后缀为db。为什么是ldmdb?当我们向内存传输数据时,我们使用了*increment after*后缀,其中指针寄存器用于将数据移动到内存位置,然后移动后增加。当我们检索数据时,我们最初指向最后一个值移动后的位置。因此,为了移除我们存储在内存中的项目,我们必须在每次移动之前递减指针——这就是*decrement before*db)后缀的原因。因此,指令对stmialdmdb分别对应堆栈的压入和弹出操作。

拆解代码

以下是对该程序代码的反汇编。它已被编辑和重新格式化,以便更容易查看。一些指令有两行。一行是 原始 指令,如程序中所示。下一行是汇编器解释的指令。这展示了伪指令如 ldr r1,=0x111111 是如何处理的。

粗体线需要进一步解释,如下所示:


(gdb) disassemble /m
Dump of assembler code for function _start:
5 _start:            nop
   0x00010074 <+0>:  nop ;  (mov r0, r0)
6                    ldr     r0,=0xFFFFFFFF  @ Dummy value for testing
=> 0x00010078 <+4>:  mvn     r0, #0
7                    ldr     r1,=0x11111111
   0x0001007c <+8>:  ldr     r1, [pc, #52]   ; 0x100b8 <adr_mem+4>
8                    ldr     r2,=0x22222222
0x00010080 <+12>: ldr     r2, [pc, #52]   ; 0x100bc <adr_mem+8>
9                    ldr     r3,=0x33333333
   0x00010084 <+16>: ldr     r3, [pc, #52]   ; 0x100c0 <adr_m
10                   ldr     r4,=0x44444444
   0x00010088 <+20>: ldr     r4, [pc, #52]   ; 0x100c4 <adr_mem+16>
11                   ldr     r5,=0x55555555      
   0x0001008c <+24>: ldr     r5, [pc, #52]   ; 0x100c8 <adr_mem+20>
12                   ldr     r0,adr_mem      @ Load pointer r0 with memory
   0x00010090 <+28>: ldr     r0, [pc, #28]   ; 0x100b4 <adr_mem>
13                   stmia   r0!,{r1-r3,r5}  @ Do a multiple load to memory
   0x00010094 <+32>: stmia   r0!, {r1,r2,r3,r5}
14                   mov     r10,r0
   0x00010098 <+36>: mov     r10, r0
15                   ldmdb   r0!,{r6-r9}     @ Now load data from memory
   0x0001009c <+40>: ldmdb   r0!, {r6,r7,r8,r9}
16                   mov     r11,r0
   0x000100a0 <+44>: mov     r11, r0
18                   mov     r1,#1           @ Terminate command
   0x000100a4 <+48>: mov     r1, #1
19                   svc     0               @ Call OS to leave  
   0x000100a8 <+52>: svc 0x00000000
   0x000100ac <+56>: ; <UNDEFINED> instruction: 0xffffffff
   0x000100b0 <+60>: bge 0xfeabab60
   0x000100b4 <+0>:  andeq r0, r2, r12, asr #1

以下命令使用 x/32xw 以十六进制形式显示 32 个连续的字节,以便我们可以观察内存中发生了什么。这就是我们使用标记如 0xAAAAAAAA 使得识别内存中的位置变得容易的地方。


(gdb) x/32xw 0x100a8                         This displays 32 words of memory
0x100a8 <_start+52>:  0xef000000 0xffffffff 0xaaaaaaaa 0x000200cc
0x100b8 <adr_mem+4>:  0x11111111 0x22222222 0x33333333 0x44444444
0x100c8 <adr_mem+20>: 0x55555555 0xbbbbbbbb 0x00000000 0x00000000
0x100d8:              0x00000000 0x00000000 0x00000000 0x00000000
0x100e8:              0x00000000 0x00000000 0xcccccccc 0x00001141
0x100f8:              0x61656100 0x01006962 0x00000007 0x00000108
0x10108:              0x0000001c 0x00000002 0x00040000 0x00000000
0x10118:              0x00010074 0x00000058 0x00000000 0x00000000

第 5 行 包含一个 nop 指令,它什么都不做(除了将 PC 移动到下一个指令)。它可以提供一个占位符供后续代码使用,或者作为调试辅助。在这里,它为第一个指令提供了一个着陆空间。ARM 缺少 nop,汇编器将 nop 翻译为 mov r0,r0。像 nop 一样,这条指令什么也不做!

ldr r0,=0xFFFFFFFF 很有趣。汇编器使用 ARM 的 mvn 在移动之前反转操作数的位。如果操作数是 0,则移动的位将是全部 1s,这正是我们想要的。

指令 7 展示了另一个伪操作:


7                      ldr      r1,=0x11111111
   0x0001007c <+8>:    ldr     r1, [pc, #52]   ; 0x100b8 <adr_mem+4>

指令需要一个 32 位立即数,0x11111111,并且不能以这种方式加载。编译器将指令转换为程序计数器相关的内存加载。地址是当前程序计数器,0x0001007c,加上偏移量 520x44,加上 ARM PC 的 8 字节前缀。在那个目标地址,你会找到存储的 0x11111111 常量。

指令 12 使用了一个类似的伪指令。在这种情况下,它是为了获取内存中的地址,以便用于存储多个寄存器:


12                   ldr     r0,adr_mem      @ Load pointer r0 with memory
   0x00010090 <+28>: ldr     r0, [pc, #28]   @ 0x100b4 <adr_mem>

代码以 svc 结尾,后面跟着注释“未定义。”这是因为反汇编器试图反汇编内存中的数据,但它不适用于有效的指令。

块移动和栈操作

图 12**.2图 12**.5 展示了根据栈类型的不同,块移动指令的四种变化。回想一下,在这篇文章中,我只使用了一种 完全下降 的全栈模式,其中栈指针指向栈顶,并在添加新项目之前递减。这些模式之间的区别在于栈增长的方向(向上或上升和向下或下降),并且取决于栈指针是否指向栈顶的项目或其上的下一个空闲项目。ARM 的文献使用四个术语来描述栈:

  • FD    完全下降      图 12**.2

  • FA     完全上升      图 12**.3

  • ED    空下降   图 12**.4

  • EA    空上升    图 12**.5

块移动通过一条指令加载或存储多个寄存器来提高代码的性能。它们也常用于在调用子程序之前保存寄存器,并在从子程序返回后恢复它们,如下例所示。在以下内容中,SP 是栈指针——即r13(在 ARM 汇编语言中,你可以写r13或 sp)。

当用于加载操作时,后缀是之后增加。当用于存储操作时,后缀是之前减少

图 12.2 – ARM 的四种栈模式之一——满下降(FD,IA 加载和 DB 存储)

图 12.2 – ARM 的四种栈模式之一——满下降(FD,IA 加载和 DB 存储)

在一个满下降栈中,栈指针指向栈顶的项目(满),当向栈中添加一个项目时,栈指针之前减少,当移除一个项目时,栈之后增加

因此,我们得到以下结果:


Push r0 to r3 on the stack    stmfd sp!,{r0-r3}     or    stmdb sp!,{r0-r3}
Pull r0 to r3 off the stack      ldmfd sp!,{r0-r3}     or    ldmia sp!,{r0-r3}

如您所见,我们可以通过它所执行的操作(dbia)或栈的类型(fd)来描述指令。对于汇编语言设计者来说,提供这样的选项相当不寻常,这最初可能会有些令人困惑。

图 12.3 – ARM 的四种栈模式之一——满上升(FA,DA 加载和 IB 存储)

图 12.3 – ARM 的四种栈模式之一——满上升(FA,DA 加载和 IB 存储)

在一个满上升栈中,栈指针指向栈顶的项目(满),当向栈中添加一个项目时,栈指针之前增加。当移除一个项目时,栈之后减少

因此,我们得到以下结果:


Push r0 to r3 on the stack    stmfa sp!,{r0-r3}     or    stmib sp!,{r0-r3}
Pull r0 to r3 off the stack      ldmfa sp!,{r0-r3}     or    ldmda sp!,{r0-r3}

图 12.4 – ARM 的四种栈模式之一——空下降(ED,IB 加载和 DA 存储)

图 12.4 – ARM 的四种栈模式之一——空下降(ED,IB 加载和 DA 存储)

在一个空的下降栈中,栈指针指向栈顶之上的项目(空),当向栈中添加一个项目时,栈指针随后增加。当移除一个项目时,栈在之前减少。因此,我们得到以下结果:


Push r0 to r3 on the stack    stmea sp!,{r0-r3}     or    stmia sp!,{r0-r3}
Pull r0 to r3 off the stack      ldmea sp!,{r0-r3}     or    ldmdb sp!,{r0-r3}

图 12.5 – ARM 的四种栈模式之一——空上升(EA,DB 加载和 IA 存储)

图 12.5 – ARM 的四种栈模式之一——空上升(EA,DB 加载和 IA 存储)

我们使用fd块移动后缀来表示满下降。ARM 允许你为块移动指令使用两种不同的命名约定。你可以写stmialdmdb的配对,或者stmfdldmfd的配对;它们是相同的。是的,这很令人困惑:


                                   @ Call abc and save some registers
       bl     abc                  @ Call subroutine abc, save return address in lr (r14)
       .
abc:   stmfd  sp!,{r0-r3,r8}       @ Subroutine abc. Block move saves registers on the stack
       .
       .                           @ Body of code
       .
       ldmfd  sp!,{r0-r3,r8}       @ Subroutine complete. Now restore the registers
       mov    pc,lr                @ Copy the return address in lr to the PC

由于程序计数器也是一个用户可见的寄存器,我们可以通过将 PC 作为我们保存的寄存器之一来简化代码:


abc:   stmfd  sp!,{r0-r3,r8,lr}    @ Save registers plus address in link register
        :
       ldmfd  sp!,{r0-r3,r8,pc}    @ Restore registers and transfer lr to PC

带有返回地址的链接寄存器被推入栈中,然后在最后,我们拉取保存的寄存器,包括放置在 PC 中的返回地址值,以返回。

块移动提供了一种方便的方法,可以在内存区域之间复制数据。在下一个示例中,我们将从pqr复制 256 个单词到xyz。块移动指令允许我们一次移动八个寄存器,如下面的代码所示:


       adr    r0,pqr               @ r0 points to source (note the pseudo-op adr)
       adr    r1,xyz               @ r1 points to the destination
       mov    r2,#32               @ 32 blocks of eight words to move (256 words total)
Loop:  ldrfd  r0!,{r3-r10}         @ REPEAT Load 8 registers in r3 to r10
       strfd  r1!,{r3-r10          @ Store the registers (moving 8 words at once)
       subs   r2,r2,#1             @ Decrement loop counter
       bne    Loop                 @ Loop back until zero

这结束了关于树莓派和 ARM 汇编语言的章节。在这本书中,你学习了计算机的工作原理及其功能。我们检查了指令集、它们的编码和执行。在最后四章中,我们探讨了具有创新设计的高性能架构。

现在,你应该能够编写自己的程序了。

摘要

计算机中的关键数据结构之一是栈,或后进先出队列。栈是一个只有一端的队列——也就是说,新项从与项离开相同的端进入。这个单一端被称为栈顶TOS)。

栈之所以重要,是因为它使得许多计算过程实现机械化成为可能,从处理算术表达式到翻译语言。在这里,我们关注栈作为一种确保子程序以一致、高效和万无一失的方式被调用和返回的手段。

子程序是一段可以从程序中的任何位置调用(调用)并从调用点返回的代码。这一动作需要管理返回地址,而栈非常适合,因为返回地址的序列是调用地址的逆序列——也就是说,与从栈中推入和拉出项的顺序相同。

我们已经研究了 ARM 的分支和链接指令bl,它可以用来调用子程序而不需要栈的开销。然而,使用带有链接的分支第二次会覆盖链接寄存器中的返回地址,然后你必须使用栈来保存之前的地址。

RISC 计算机在原则上实现简单、单周期操作。ARM 有一组非常非 RISC 的块移动指令,允许你在单个操作中移动整个指令组。你可以在一个操作中将多达 16 个寄存器传输到或从内存。块移动让你可以通过栈将参数传递到和从子程序。

有四种标准的栈实现。栈指针可以指向栈顶的项,或者指向该项之上的空闲空间。同样,栈可以朝向低地址或高地址增长(随着项的添加)。这提供了四种可能的排列。然而,大多数计算机实现的是指向其顶部项且朝向低地址增长的栈。

ARM 文献的一个不寻常的特点是它对栈组织的命名约定有两种。一种约定使用栈的类型(指向顶部或下一个空闲项)和栈的方向,而另一种约定描述了在操作之前或之后栈是增加还是减少——例如,stmia r0!,{r2-r6}stmea r0!,{r2-r6}是相同的操作。

在这本书中,我们介绍了计算机,并演示了如何使用 Python 进行模拟。到第八章结束时,你应该能够设计和模拟一个符合你自定义规范的计算机指令集。

按照一个假设的教学计算机的设计,我们考察了树莓派及其核心的 ARM 微处理器。这为真实计算机的介绍提供了基础。我们描述了 ARM 的指令集架构,解释了其工作原理,并说明了如何在树莓派上编写 ARM 汇编语言程序以及如何调试它们。

在结束这本书之后,你可能想考虑设计你自己的 ARM 模拟器。

最后,我们将提供一些附录,以便你能够找到你最常需要的一些信息片段。

附录 – 关键概念摘要

在这些附录中,我们将简要总结本书中介绍的一些方面。我们将从介绍 IDLE 开始,这是 Python 解释器,它允许你快速开发程序并测试 Python 的功能。

第二个附录简要总结了在 Raspberry Pi 上开发 ARM 汇编语言程序时可能需要的某些 Linux 命令。

第三个附录提供了一个 ARM 汇编程序的运行和调试演示。这个示例的目的是将调试程序所需的所有步骤集中在一个地方。

第四个附录涵盖了可能让学生感到困惑的一些概念,例如计算机对“上”和“下”术语的使用,这些术语有时与“上”和“下”的正常含义不同。例如,向计算机堆栈添加内容会导致计算机堆栈向上增长到较低的地址。

最后一章附录定义了我们讨论计算机语言(如 Python)时使用的一些概念。

使用 IDLE

本书中的 Python 程序是用 Python 编写的,保存为.py文件,然后在集成开发环境中执行。然而,还有另一种执行 Python 的方法,你会在许多文本中看到提到。这是 Python IDLE 环境(包含在 Python 包中),它允许你逐行执行 Python 代码。

IDLE 是一个解释器,它读取输入的 Python 代码行,然后执行它。如果你只想测试几行代码而不想麻烦地创建源程序,这将非常有帮助。

考虑以下示例,其中粗体字体的文本是我的输入:


Python 3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 4
>>> y = 5
>>> print('Sum =', x+y)
Sum = 9
>>>

当你运行编译后的 Python 程序时,输出将在运行窗口中显示。在这里,正如你所看到的,每个输入行在>>>提示符之后都被读取和解释,然后打印结果。

这个窗口实际上是 IDLE 环境的一部分。这意味着如果你的程序崩溃了,你可以在崩溃后检查变量。考虑以下示例,其中我们创建并运行了一个包含错误的程序:


# An error example
x = 5
y = input('Type a number = ')
z = x + y
print('x + y =',z)

如果我们运行这个程序,执行窗口将显示以下消息:


Type a number = 3
Traceback (most recent call last):
  File "E:/ArchitectureWithPython/IDE_Test.py", line 4, in <module>
    z = x + y
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>>

Python 解释器指示存在类型错误,因为我们输入了一个字符串并尝试将其添加到整数中。我们可以在显示窗口中继续操作,查看xy变量,然后按如下方式修改代码。所有键盘输入都是粗体的:


>>> x
5
>>> y
'3'
>>> y = int(y)
>>> z = x + y
>>> z
8
>>>

我们现在已经定位并修复了问题。当然,编辑原始的 Python 程序以纠正源代码是必要的。

由于 IDLE 一次执行一条语句,因此看起来无法执行循环,因为那需要多行代码。有一种方法。IDLE 自动缩进循环中的指令,这允许多个语句。为了完成(关闭)循环,你必须输入两个回车。考虑以下示例:


>>> i = 0
>>> for j in range(0,5):  @ Here we've started a loop
      i = i + j*j         @ Add a statement. Note the automatic indentation
                          @ Now hit the enter key twice.
>>> print(i)               @ We have exited the loop and added a new statement
30                         @ And here is the result
>>>

指令和命令

此附录列出了您在 Raspberry Pi 上运行程序时将使用的一些常用命令:

Linux


cd ..                     @ Change dictionary to parent
mkdir /home/pi/testProg   @ Create new file called testProg in folder pi
ls /home/pi               @ List files in folder pi
as -g -0 file.o file.s    @ Assemble source file file.s to create object file file.o
ld -0 file file.o         @ Link object file file.o
gdb file                  @ Call debugger to debug file
sudo apt-get update       @ Download packages in your configuration source files
sudo apt-get upgrade      @ Updates all installed packages

汇编指令


.text                     @ This is a code section
.global _start            @ _start is a label (first instruction)
.word                     @ Bind 32-bit value to label and store in memory
.byte                     @ Bind 8-bit value to label and store in memory
.equ                      @ .equ x,7 binds or equates 7 to the name x
.asciz                    @ Bind ASCII string to label and store (terminated by 0)
.balign                   @ .balign 4 locates instruction/data is on a word boundary

gdb 调试器


file toDebug              @ Load code file toDebug for debugging
b address                 @ Insert breakpoint at <address> (maybe line number or label)
x/4xw <address>           @ Display memory: four 32-bit words in hexadecimal format
x/7db <address>           @ Display memory: seven bytes in decimal format
r                         @ Run program (to a breakpoint or its termination)
s                         @ Step (execute) an instruction
n                         @ Same as step an instruction
i r                       @ Display registers
i b                       @ Display breakpoints
c                         @ Continue from breakpoint

ARM 汇编语言程序的模板


                     .text          @ Indicate this is code
        .global _start              @ Provide entry point
_start: mov   r0,#0                 @ Start of the code
        mov   r0,#0                 @ Exit parameter (optional)
        mov   r7,#goHome            @ Set up leave command
        svc   #0                    @ Call operating system to exit this code
test:   .word  0xABCD1234           @ Store a word in memory with label 'test'
        .equ   goHome, 1            @ Equate name to value

运行 ARM 程序

在这里,我们整理了所有您需要运行和调试 Raspberry Pi 上程序的信息。我们将从第十一章中的字符串复制示例开始,更详细地讲解,以提供一个程序开发的模板。此程序接受一个 ASCII 字符串并将其反转。在这种情况下,字符串是"Hello!!!"。我们将其设置为 8 个字符长,以便它适合两个连续的单词(8 * 8 位 = 64 位 = 2 个单词)。

我们在程序的.text部分找到了源字符串string1,因为它只被读取,从未被写入。

将接收反转字符串的目标str2位于.data段的读/写内存中。因此,我们必须使用间接指针技术——也就是说,.text 部分在adr_str2处有一个指针,它包含实际字符串str2的地址。

程序包含一些代码未访问的标签(例如preLoopWait)。这些标签的目的是在调试时通过给它们命名来简化断点的使用。

一个最终特性是使用标记器。我们在内存中插入了两个字符串之后的标记器——即0xAAFFFFBB0xCCFFFFCC。这些标记使得在查看内存时更容易定位数据,因为它们很突出。

此程序测试基于指针的寻址、字节加载和存储,以及指针寄存器的自动递增和递减。我们将使用gdb的功能逐步执行此程序的执行:


          .equ    len,8             @ Length of string to reverse (8 bytes/chars)
          .text                     @ Program (code) area
          .global _start            @
_start:   mov     r1,#len           @ Number of characters to move in r1
          adr     r2,string1        @ r2 points at source string1 in this section
          adr     r3,adr_str2       @ r3 points at dest string str2 address in this section
          ldr     r4,[r3]           @ r4 points to dest str2 in data section
preLoop:  add     r5,r4,#len-1      @ r5 points to bottom of dest str2
Loop:     ldrb    r6,[r2],#1        @ Get byte char from source in r6 inc pointer
          strb    r6,[r5],#-1       @ Store char in destination, decrement pointer
          subs    r1,r1,#1          @ Decrement char count
          bne     Loop              @ REPEAT until all done
Wait:     nop                       @ Stop here for testing

Exit:     mov     r0,#0             @ Stop here
          mov     r7,#1             @ Exit parameter required by svc
          svc     0                 @ Call operating system to exit program

string1:  .ascii  "Hello!!!"        @ The source string
marker:   .word   0xAAFFFFBB        @ Marker for testing

adr_str2: .word   str2              @ POINTER to source string2 in data area

          .data                     @ The read/write data area
str2:     .byte   0,0,0,0,0,0,0,0   @ Clear destination string
          .word   0xCCFFFFCC        @ Marker and terminator
          .end

程序被加载到gdb中进行调试,以下是调试步骤。注意,我的输入以粗体显示:

alan@raspberrypi:~/Desktop $ gdb pLoop

第一步是在标签上放置三个断点,这样我们就可以执行代码直到这些点,然后检查寄存器或内存:


(gdb) b _start
Breakpoint 1 at 0x10074: file pLoop.s, line 5.
(gdb) b preLoop
Breakpoint 2 at 0x10084: file pLoop.s, line 9.
(gdb) b Wait
Breakpoint 3 at 0x10098: file pLoop.s, line 14.

我们使用了b <label>三次来设置三个断点。我们可以使用info b命令来检查这些断点,该命令显示断点的状态:


(gdb) info b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x00010074 pLoop.s:5
2       breakpoint     keep y   0x00010084 pLoop.s:9
3       breakpoint     keep y   0x00010098 pLoop.s:14

下一步是运行程序直到第一条指令:


(gdb) r
Starting program: /home/alan/Desktop/pLoop
Breakpoint 1, _start () at pLoop.s:5
5 _start:   mov     r1,#len            @ Number of characters to move in r1
(gdb) c
Continuing.

这里没有太多可看的内容。因此,我们按c继续到下一个断点,然后输入i r来显示寄存器。注意,我们没有显示未访问过的寄存器:


Breakpoint 2, preLoop () at pLoop.s:9
9 preLoop:  add     r5,r4,#len-1       @ r5 points to bottom of dest str2
(gdb) i r
r0             0x0                 0
r1             0x8                 8
r2             0x100a8             65704      Pointer to string1
r3             0x100b4             65716      Pointer to str2 address
r4             0x200b8             131256     Pointer to str2 value
sp             0x7efff360          0x7efff360
lr             0x0                 0
pc             0x10084             0x10084 <preLoop>

让我们来看看代码中的数据部分。寄存器r2指向这个区域,命令表示从0x100A8开始显示以十六进制形式表示的四个内存字:


(gdb) x/4wx 0x100a8
0x100a8 <string1>: 0x6c6c6548 0x2121216f 0xaaffffbb 0x000200b8

三个突出显示的值表示字符串"Hello!!!"和标记0xCCFFFFCC。注意这些值是如何从后往前出现的。这是小端字节序模式的结果。最不重要的字节位于单词的最不重要的末端。从 ASCII 字符的角度来看,这些是lleH !!!o

我们接下来执行一个步骤,并显示数据区域的内存。在这个阶段,代码尚未完全执行,这个区域应该与最初设置的一样:


(gdb) si 1
Loop () at pLoop.s:10
10 Loop:     ldrb    r6,[r2],#1        @ Get byte char from source in r6 inc pointer
(gdb) x/4wx 0x200b8
0x200b8: 0x00000000 0x00000000 0xccffffcc 0x00001141

在这里,你可以看到加载在字节上的零和随后的标记。然后我们再次输入c,继续到Wait断点,此时代码应该已经完成。最后,我们查看寄存器和数据内存:


(gdb) c
Continuing.
Breakpoint 3, Wait () at pLoop.s:14
14 Wait:     nop                       @ Stop here for testing

(gdb) i r
r0             0x0                 0
r1             0x0                 0
r2             0x100b0             65712
r3             0x100b4             65716
r4             0x200b8             131256
r5             0x200b7             131255
r6             0x21                33
sp             0x7efff360          0x7efff360
lr             0x0                 0
pc             0x10098             0x10098 <Wait>
(gdb) x/4wx 0x200b8
0x200b8: 0x6f212121 0x48656c6c 0xccffffcc 0x00001141

注意数据已经改变。正如你所看到的,顺序已经颠倒。再次注意小端字节序对单词内字节顺序的影响。现在数据的顺序是o!!! Hell。最后,我们再次输入c,程序完成:


(gdb) c
Continuing.
[Inferior 1 (process 11670) exited normally]
(gdb)

常见混淆

从 20 世纪 60 年代到今天,计算机的发展迅速且混乱。这种混乱是由于技术发展得太快,以至于系统几个月内就过时了,这意味着大部分设计已经过时,但已经被纳入了现在正被其拖累的系统。同样,出现了许多不同的符号和约定——例如,MOVE A,B是将A移动到B,还是将B移动到A?不同的计算机同时使用了这两种约定。以下是一些有助于解决混淆的提示。

在这本书中,我们将主要采用从右到左的约定进行数据移动。例如,add r1,r2,r2表示将r2r3相加,并将和放入r1。为了突出这一点,我经常将操作的源操作数用粗体字表示。

符号经常被赋予不同的含义。这一点在#@%上尤为明显。

  • if x > y: z = 2。如果x超过y,则使用#重置z。在 ARM 汇编语言中,#用于表示文本值——例如,add r0,r1,#5@将整数5加到r1的内容中,并放入r2

  • @:在 ARM 汇编语言中,at符号用于表示注释。

  • add r1,r2,#%1010表示以二进制形式表示的文本值。Python 使用前缀0b来表示二进制值(例如,0b1010)。

  • 使用0x来表示十六进制值(例如,0xA10C)。

  • 寄存器间接寻址:在汇编语言级别的编程中,一个关键概念是指针——即一个变量,它是内存中某个元素的地址。这种寻址模式被称为寄存器间接、基于指针或甚至索引寻址。

  • 上下:在正常日常使用中,上下表示向天空(上)或向地面(下)的方向。在算术中,它们表示增加一个数字(上)或减少它(下)。在计算机中,当数据项被添加到栈中时,栈向上增长。然而,按照惯例,地址在添加项时向下增长。因此,当向栈中添加项时,栈指针会递减,当从栈中移除项时,栈指针会递增。

  • ldr r0,=0x12345678, 大端序的计算机会在字节内存中以递增的地址顺序存储字节 12,34,56,78,而小端序的计算机则会以 78,56,34,12 的顺序存储字节。ARM 是一个小端序机器,尽管它可以编程为在大端序模式下运行。在实践中,这意味着在调试程序和查看内存转储时必须小心。同样,在执行字值上的字节操作时,也必须小心,以确保选择正确的字节。

词汇

所有专业领域都有自己的词汇,编程也不例外。以下是一些有助于理解文本及其上下文的有用词汇。

  • 1s 和 0s。人类用类似于英语的高级语言(如 Python)编写程序。在高级语言程序可以执行之前,一个叫做 编译器 的软件将其翻译成二进制代码。当你在电脑上运行 Python 程序时,你的源代码会由操作系统与编译器一起自动翻译成机器代码。幸运的是,你不必担心编译过程中在后台发生的所有无形操作。

  • y = "4" + 1。这是一个语法错误,因为我正在添加两个不能相加的不同实体。"4" 是一个字符(你可以打印它),而 1 是一个整数。你可以写 y = "4" + "1"z = 4 + 1。这两个都是语法正确的,y 的值是 "41",而 z 的值是 5

  • 语义错误:语义关注于意义。语法错误意味着句子在语法上是正确的,即使它在语法上是正确的。一个带有语义错误的英语句子示例是,“Twas brillig, and the slithy toves did gyre and gimble in the wabe。”这是语法正确的,但没有意义——也就是说,它是语义错误的。在计算机中,语义错误意味着你的程序没有按照你的意图执行。编译器可以检测语法错误,但通常不能检测语义错误。

  • age = 25,你创建了一个名为 age 的新变量,其值为 25。如果你引用 age,实际的值将被替换。表达式 y = age + 10 将会给 y 赋值为 35。一个变量有四个属性——它的名称(你如何称呼它)、它的地址(它在计算机中的存储位置)、它的值(它实际上是什么)和它的类型(例如,整数、列表、字符或字符串)。

  • c = 2πr,其中 2π 是常数。cr 都是变量。

  • c 及其半径 r。我们给无理数 3.1415926 赋予符号名 π。当程序编译成机器代码时,符号名会被实际值替换。

  • 1234。程序员通常不必担心数据在内存中的实际位置。将程序中使用的逻辑地址转换为内存设备的物理地址是操作系统的领域。

  • c = 2πr,那么 r 是什么?我们(人类)将 r 视为半径值的符号名——比如说,5。但计算机将 r 视为内存地址 1234,必须读取它以提供 r 的实际值。如果我们写 r = r + 1,我们是想表示 r = 5 + 1 = 6 还是表示 r = 1234 + 1 = 1235?区分地址和其内容非常重要。当我们引入指针时,这个因素变得很重要。

  • i 实际上是一个指针;我们只是称它为索引。如果我们改变指针(索引),我们就可以遍历表、数组或矩阵的元素,并遍历元素 x1、x2、x3 和 x4。

第十三章:索引

符号

32 位字面量

处理 310, 311

示例 313

源程序 314-318

A

抽象内存

在 Python 中模拟 42

alu()函数 266

算术和逻辑单元 (ALU) 49, 259, 260

测试 261, 262

算术指令 324

加法 324, 325

位逻辑运算 326, 327

比较操作 324, 325

乘法 325, 326

移位操作 327, 328

减法 324, 325

算术运算 90

算术移位 329

旋转操作 330

ARM 处理器 321, 322

ARM 程序

示例 292-294

运行 374-377

ARM 的架构

算术指令 324

ARM 寄存器集 323, 324

概述 322

ARM 的分支和链接指令 355, 356

汇编器

流程控制操作,测试 163-166

移位操作,测试 167-169

测试 161, 162

汇编指令

处理 125

字典,使用 126-128

标签 128, 129

汇编级程序 58-60

条件指令,执行 61-63

处理文字操作数 63-66

B

二进制指令

构建 129, 130

提取 130-135

参数 131, 132

位处理

在 Python 中 85-87

块移动指令 359-64

代码,反汇编 364-366

布尔 20

带进位设置分支 (bcs) 62, 335

带进位或相同 (bhs) 335

带低 (blo) 分支 335

转换到子程序 (BSR) 355

C 语言

进位位 92

字符 20

CISC 计算机

使用寄存器到内存架构 238

classDecode 函数 169, 170

代码格式 82

注释 22-24

复杂指令集计算机 (CISC) 70

计算机解释器

在 Python 中构建 75

计算机内存 37, 38

条件分支 334, 335

条件执行 336, 337

顺序条件执行 338

条件表达式

在 Python 中使用 30

条件操作 28, 29

条件代码寄存器 (CCR) 62

常数 24

控制单元 (CU) 53

D

数据结构 25

决策 29

减少并分支非零 (DBNE) 163

确定性符号处理机 3

字典机制 111-116

display() 函数 265

动态随机存取存储器 38

E

字节序 312, 313

表达式 23,100

F

字段可编程门阵列 (FPGA) 47

有限状态机 (FSM) 5-8

算法,构建 12

算法问题,解决 9-11

交通灯示例 8,9

浮点数 20

地板整数 21

流程控制指令 333

条件分支 334,335

无条件分支 334

函数 117

在 Python 27,28

G

输入垃圾,输出垃圾 (GIGO) 4

GCC ARM 汇编器

功能 306-310

GCC 汇编器指令 300-303

Geany

参考链接 283

get_lit()函数 265

get_reg()函数 265

H

硬盘驱动器 (HDD) 38

I

IDLE 环境

使用 371,372

if…else 语句 31,32

if 语句

示例 30,31

导入 117,118

缩进

在 Python 110,111

索引寻址 39

间接寻址 39

输入,处理

磁盘输入 123

嵌入式测试程序,使用 123

指令集,处理 123,124

指令

解码 88,89

执行 88,89

指令分析 120

输入,处理 120-123

指令架构级别 (ISA) 45-48

地址 49

位 48

立即访问内存 48

指令 48

文字 49

移动 49

寄存器 49

字 48

指令格式 83

指令寄存器 (IR) 53

指令集 (ISA) 292

整数 20

集成学习和开发环境 (IDLE) 18

可迭代对象 103

K

键 112

L

后进先出队列 (LIFO) 357

列表 25, 26, 105

示例 109, 110

切片 26

列表推导式 103, 104

示例 104

逻辑操作 90

逻辑移位 329

M

机器级指令 66, 67

复杂指令集计算机 (CISC) 70

文字表示方法,方式 71

精简指令集计算机 (RISC) 70

类型与格式 67-69

数学运算符 21

内存地址寄存器 (MAR) 52

内存缓冲寄存器 (MBR) 52

内存数据寄存器 (MDR) 53

memProc() 函数 264

模数运算符 21

N

自然数 20

新的即开即用软件 (NOOBS) 280

栈顶下一个 (NOS) 225

符号表示法 377

at 符号 378

美元符号 378

字节序 378

哈希符号 378

百分比符号 378

寄存器间接寻址 378

上和下 378

O

一地址累加器机器 229-235

操作码 (op-code) 76

运算符优先级

在 Python 中 87

P

流水线 51

基于指针的寻址 39

正整数 20

程序

阅读 16, 17

伪代码 12

伪指令 292

Python 17

抽象内存,模拟 42

位操作 85-87

计算机解释器,构建 75, 76

条件表达式,使用 30

函数 94

函数 27, 28

运算符优先级 87

URL 17

操作 18, 19

Python 代码

用于原始模拟器 77-79

Python 函数 93

分支和流程控制 96, 97

函数和作用域 94, 95

Python 的数据类型 20

布尔值 20

字符 20

浮点数 20

整数 20

字符串 20

Python 栈机 226-229

R

随机存取存储器 (RAM) 38

树莓派基础知识 280-284

树莓派命令 373

树莓派调试器

执行,跟踪 298, 299

内存,显示 298, 299

使用 294-298

树莓派操作系统

ARM 代码,汇编 290

汇编语言,调试 290, 291

汇编语言程序,创建 288, 289

汇编语言程序,编辑 288, 289

基础知识 284, 285

目录导航 286

文件操作 286, 287

内存访问 305

程序和包,安装 287

程序和包,更新 287

实数 20

精简指令集计算机 (RISC) 70

寄存器传输语言 (RTL) 38, 39

用法,示例 40

重复机制 105-107

迭代器 107, 108

从子程序返回 (RTS) 355

旋转操作 330-332

S

顺序条件执行 338

集合 25

位移操作

使用,以合并数据 332, 333

位移类型 328

算术位移 329

逻辑位移 329

旋转操作 331, 332

符号位 92

模拟器 136-140

功能 244-246

示例输出 246-248

固态硬盘 (SSD) 38

平方根 (sqrt(x) 27

栈 356, 357

基于栈的计算器 225, 226

栈操作 366-369

栈指针 (SP) 225, 357

语句 100

静态随机存取存储器 (SRAM) 38

字符串 20

功能 100

处理 100-102

切片 26

文本输入示例 102, 103

子程序调用和返回 358, 359

执行 359

符号处理机 3

T

TC1 汇编语言程序

示例 154-160

TC1 指令集 79-83

解释 83, 84

间接寻址,寄存器 84, 85

TC1 后续脚本 169

classDecode 函数 169, 170

代码列表 172-177

testIndex() 函数 171

testLine 函数 170, 171

TC1 后续脚本 Mark II 177-183

TC1 模拟器程序 140, 141

文件输入 142-154

TC2

样本运行 257- 259

TC2 模拟器

增强 235-238

TC3

注释 273-275

TC3 代码 248-255

TC3 指令集架构 238-244

TC4

示例 263-273

教学计算机 1 (TC1) 47, 79

testIndex() 函数 171, 172

testLine 函数 170, 171

文本用户界面 (TUI) 298

元组 105

二进制补码算术 91

U

超原始单指令计算机 74, 75

无条件分支 334

V

可变长度指令

机器,演示 216-220

变量 21, 24

Visual Studio Code (VS Code) 18

词汇 378

地址 379

编译器 378

常数 379

指针 379

语义错误 379

符号名 379

语法错误 379

值和位置 379

变量 379

约翰·冯·诺依曼架构 49-51

地址路径 51, 52

CPU 的数据路径 55

数据流 57, 58

数据移动指令 56

数据处理指令 56, 57

指令,读取 52-54

Z

零位 92

Packt 标志

www.packtpub.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推进职业生涯。更多信息,请访问我们的网站。

第十四章:为什么订阅?

  • 使用来自超过 4,000 位行业专业人士的实用电子书和视频,节省学习时间,多花时间编程

  • 通过为您量身定制的技能计划提高学习效果

  • 每月免费获得一本电子书或视频

  • 完全可搜索,便于快速访问关键信息

  • 复制粘贴、打印和收藏内容

您知道 Packt 为每本书提供电子书版本,包括 PDF 和 ePub 文件吗?您可以在packtpub.com升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。如需更多信息,请联系我们 customercare@packtpub.com。

www.packtpub.com,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

其他您可能喜欢的书籍

如果您喜欢这本书,您可能对 Packt 出版的其他书籍也感兴趣:

其他您可能喜欢的书籍 - 为初学者编写的计算机编程

为初学者编写的计算机编程

Joakim Wassberg

ISBN: 978-1-83921-686-2

  • 掌握基本编程语言概念,如变量、循环、选择和函数

  • 了解程序是什么以及计算机如何执行它

  • 探索不同的编程语言,了解源代码与可执行代码之间的关系

  • 使用过程式编程、面向对象编程和函数式编程等不同范式解决问题

  • 使用多种编码约定和最佳实践编写高质量的代码

  • 精通如何在程序中跟踪和修复错误

嵌入式系统架构 - 第二版

其他您可能喜欢的书籍 - 嵌入式系统架构 - 第二版

嵌入式系统架构 - 第二版

Daniele Lacamera

ISBN: 978-1-80323-954-5

  • 参与嵌入式产品的设计和定义阶段

  • 掌握为 ARM Cortex-M 微控制器编写代码

  • 建立嵌入式开发实验室并优化工作流程

  • 使用 TLS 确保嵌入式系统安全

  • 揭示通信接口背后的架构

  • 了解物联网中连接和分布式设备的设计和开发模式

  • 掌握多任务并行执行模式和实时操作系统

  • 熟悉可信执行环境(TEE)

Packt 正在寻找像您这样的作者

如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们已经与成千上万的开发者和科技专业人士合作,就像您一样,帮助他们将见解分享给全球科技社区。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交您自己的想法。

分享您的想法

现在,您已经完成了《使用 Python 和 ARM 的实用计算机架构》,我们非常想听听您的想法!如果您在亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面,分享您的反馈或在该购买网站上留下评论。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载这本书的免费 PDF 副本

感谢您购买这本书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?您的电子书购买是否与您选择的设备不兼容?

别担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不仅限于此,您还可以获得独家折扣、时事通讯和每天收件箱中的精彩免费内容。

按照以下简单步骤获取优惠:

  1. 扫描下面的二维码或访问以下链接

下载这本书的免费 PDF 副本

https://packt.link/free-ebook/9781837636679

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱。

posted @ 2025-09-20 21:33  绝不原创的飞龙  阅读(23)  评论(0)    收藏  举报