The Element Of Computer System 笔记(第四周)

第四章 机器语言

背景知识

这里我们集中讨论三个主要的抽象体:处理器(processor)、内存(memory),以及寄存器(registers)

机器

机器语言可以被看作是一种约定的形式,它利用处理器和寄存器来操控内存。
内存 内存(memory)的概念是指“用来存储数据和指令的硬件设备”。从程序员的观点看,所有的内存具有相同的结构:一个连续的固定宽度的单元序列,也称为字(word)或内存单元,每个内存单元都有一个唯一的地址(address)。因此,对于独立的字(word,代表一个数据项或是一个指令),可以通过提供它的地址来描述。之后我们会用符号Memory[address],RAM[address],M[address]来表示内存。
处理器 处理器,通常又称为中央处理器或CPU(Central Processing Unit),是执行一组固定基本操作的设备。这些操作通常包括算数操作和逻辑操作,内存存取操作和控制操作(control operation,也称 branching operations)。这些操作的对象是二进制数值,它们来自寄存器和指定的内存单元。类似的,操作的结果(处理器的输出)既可以存储在寄存器内,也可以存储在指定的内存单元。
寄存器 内存访问是相对较慢的操作,需要很长的指令格式(一个地址可能需要32位)。基于此原因,大多数处理器都配有一些寄存器,每个寄存器只存储一位。它紧挨着处理器,相当于处理器的一个高速本地内存,使得处理器能快速地操控数据和指令。寄存器使得程序员能够尽可能地使用内存访问命令,从而加速程序的执行。

语言

在机器语言中同时会使用二进制码和助记符(symbolic mnemonics)。助记符是一种符号标记,它的名字暗示了其所代表的意思——硬件元素和二进制操作。例如,语言设计者定义操作码1010用add来表示,机器中的寄存器可以使用符号R0、R1、R2等来表示。

这种符号抽象更进一步发展,我们不仅能够阅读符号表示,而且能实际地利用这些符号命令而不是二进制指令来编写程序。接下来,可以使用文本处理程序,将这些符号命令解析为其内含的意域(助记符或操作数),将每个意域翻译成其对应的二进制表示,然后将生成的代码汇编成二进制机器指令。符号表示也称为汇编语言(assembly lanaguage),或简单地说成汇编,而将汇编程序翻译成二进制码的程序则称为汇编编译器(assembler)。

不同的计算机在CPU的操作方式、寄存器的数量和类型,以及汇编语法上各不相同。

命令

算术操作和逻辑操作 计算机需要执行基本的算术操作(比如加法和减法),以及基本的布尔操作(比如按位取反、移位等等)。
ADD R2,R1,R3 // R2 <— R1+R3 其中R1、R2、R3是寄存器
ADD R2,R1,foo // R2 <---- R1+foo 其中foo代表的意思是用户定义标签foo所指向的内存单元的值
AND R1,R1,R2 // R1 <---- 对R1和R2进行按位与(And)操作的结果

内存访问 内存访问命令分为两类。第一类,算术命令和逻辑命令不仅允许操控寄存器,而且还可以操控特定的内存单元。第二类,所有的计算机都会使用load和store命令,用来在寄存器和内存之间传递数据。这些访问命令可能会应用某些类型的寻址方式,并在指令中指定目标内存单元的地址。当然,不同的计算机有不同的寻址方式,下面列出三种绝大多数计算机都支持的寻址方式:

  • 直接寻址(Direct Addressing) 最常用的寻址方式,直接表示一个指定内存单元的地址,或者使用一个符号来代表这个指定的地址,如下所示:
    LOAD R1,67 // R1 <-------Memory[67]
    // 或者假设bar指向内存地址67,那么就有:
    LOAD R1,bar // R1 <-------Memory[67]
  • 立即寻址(Immediate Addressing) 这种寻址方式被用来加载常数-------也就是说,加载那些出现在指令代码里面的数值:我们直接将指令数据域中的内容当做要操作的数据装入寄存器,而不是将该数值当做内存党员的地址,如下所示:
    LOADI R1,67 // R1 <------- 67
  • 间接寻址(Indirect Addressing) 在这种寻址模式中,要访问的内存单元的地址没有直接出现在指令中,而是指令指定的内存单元中的内容代表目标内容单元的地址。这种寻址模式被用来处理指针(pointer)这种语言设施。比如,一条高级语言命令x=foo[j],这里foo是数组变量,x和j是整数变量。那么与这条命令等价的机器语言是?

当数组fooo在高级语言程序里被声明并初始化时,编译器分配一组连续的内存单元来保持这个数组数据,并用符号foo来指代内存单元组的基地址(base address)。

于是当编译器后来遇到表示数组单元的符号(比如foo[j])时,将按以下步骤进行地址解析。首先,应注意第j个数组入口是某个内存单元的物理地址,该地址相对于数组基地址的偏移量为j(为简单起见,溅射每个数组元素占用一个字)。因此,表达式foo[j]相关的地址可以很容易地被计算出来,即只需将foo的值加上j即可。如在C语言程序中,x=foo[j]这样的命令也可以等价地表示成x=*(foo+j),这里“*n”代表“memory[n]的值”。被翻译成机器语言时,这样的命令根据特定汇编语言的语法,会产生如下的代码:

// 将x=foo[j] or x = (foo+j) 翻译成汇编语言
ADD R1,foo,j // R1 <------- foo+j
LOAD
R2,R1 // R2 <-------- Memory[R1]
STR R2 ,x // x <------ R2

Hack机器语言规范详述

概述

Hack是一个基于冯诺依曼架构的16-位计算机,由一个CPU、两个独立的内存模块(instruction memory即指令内存和data memory即数据内存),以及两个内存映射I/O设备(显示器和键盘)组成。

内存地址空间 (Memory Address Space) Hack有两个不同的地址空间:指令地址空间(instruction memory,以下称为指令内存),数据地址空间(data memory,以下称为数据内存)。两个内存区都是16-位宽,有15-位地址空间,这意味着两个内存可设的最大地址都是32K的16-bit word。

CPU仅能执行存储在指令内存中的程序。指令内存是只读设备,程序通过某种外部方法被加载到指令内存中。比如,可以在预先烧写了必要程序的ROM芯片中实现指令内存。要加载新程序,则要替换整个ROM芯片,跟玩游戏时需要在游戏控制台中替换游戏卡一样。为了模拟这个操作,Hack平台的硬件仿真器必须提供一种方法,将某文本文件中用
机器语言编写的程序加载到指令内存中去。

寄存器(Registers) Hack程序员要接触两个称为D和A的16-位寄存器。这些寄存器能够被算术和逻辑指令显示地操控,比如A=D-1或D=!A(这里“!”表示16-位的Not操作)。D仅用来存储数据值,A既可以作为数据寄存器也可以作为地址寄存器。也就是说,根据指令的上下文含义,A中的内容可以被解释为数值或数据存储器中的地址,或者作为指令存储器中的地址。

首先,A存储器能够使“对数据内存(后简称“内存”)的直接存取"变得十分容易。由于Hack的指令是16-位宽,而对地址的描述要用到15位,所以将操作码和地址码都存放在同一条指令中是不可能的。因此,Hack语言的语法规定,内存的存取指令是对隐式的内存地址“M”进行操作,比如D=M+1。为了解析这个地址,规定M总是代表一个内存单元中的数值,该内存单元的地址就是当前A寄存器中的数值。比如说,如果想要执行操作D=Memory[516]-1,就必须使用一条指令来让A寄存器的值置为516,然后使用指令D=M-1来完成操作。

另外,身兼重职的A寄存器也被用来对指令存储器进行直接访问。与内存访问规则一致,Hack的jump指令并不指定某个特定地址。其规定是:任何jump操作总是执行这样的跳转,即跳转到“A寄存器所指定的指令”。如此一来,若要执行操作goto35,就要使用一条指令将A的值置为35,然后第二条指令就直接使用无需描述地址的goto命令。这个操作序列使计算机在下一个时钟周期获取在InstructionMemory[35]位置上的指令。

例子 Hack语言的语法是很容易理解的,这里解释一下@value,这里value可以是数值或是代表数值的符号。这个命令将特定的值存到A寄存器中。比如,如果sum代表内存地址17,那么@17和@sum都将具有一样的功能:A <------ 17。

每个涉及内存地址的操作需要两个Hack命令:一个用来确定将要进行操作的内存单元地址,另一个用来描述要进行的操作。Hack语言正包含了两种指令:一种是地址指令,也称为A-指令;另一种是计算指令,也称为C-指令。每种指令都有二进制表示法和符号表示法,并且都能对计算机产生特定的作用。

A-指令

A-指令用来为A寄存器设置15-位的值:
A-instruction:@value // 这里value是一个非负的十进制数
// 或者这里代表一个非负十进制数的符号
在这里插入图片描述
这个指令使得计算机将特定的值存储到A寄存器中去。比如说,指令@5,也等价于00101,则使得计算机将用二进制表示的5存储到A寄存器中。

A-指令主要有三种不同的用途。首先,在程序控制下,它提供了唯一一种“将常数输入计算机”的方法;其次,通过将目标数据内存单元的地址放入A寄存器,来为将对该内存单元进行操作的C-指令提供必要的条件;其三,通过将跳转的目的地址放入A寄存器来为执行跳转的C-指令提供条件。
在这里插入图片描述

C-指令

C-指令是Hack平台程序的重点,几乎包含要做的所有事情。指令代码的描述可以是以下三种问题的回答:(a)计算什么;(b)将计算后的值存储到什么地方;(c)下一步做什么。随同A-指令一起,这些描述几乎决定了计算机所有可能的操作。

C-instruction:dest = comp;jump // dest或者jump可以为空
// 如果dest为空,“=”省略
// 如果jump为空,“;”省略
在这里插入图片描述
指令编码最左起第一位为1,则表示该指令是C-指令。接着的两位没有被使用。剩下的位构成了三个域,分别代表指令符号表述的三个部分。符号指令dest=comp;jump的整个语义可以如下表示。Comp域告诉ALU计算什么。Dest域指明计算后的结果(ALU的输出)将被存储到什么地方。Jump域描述了转移条件,即接下来要取出并执行那一条命令。

Computation规范 (计算规范) Hack的设计中,ALU所执行的是一组固定的函数集,该函数集的功能实现了对寄存器D、A、M(M代表Memory[A])的操作。计算函数指令的comp域由1个a-位域(a-bits)和6个c-位域(c-bits)组成。这个7-位模式可以对128个不同的函数进行编码,但其中只有28个函数(如图4.3)在机器语言规范中列出。

前面介绍过C-指令的格式是111a cccc ccdd djjj。若希望让ALU计算D-1,即D寄存器当前值减1。根据图4.3,key通过指令1110 0011 1000 0000(7位操作码被加粗显示)来完成。

Destination规范(目的地规范)C-指令的comp部分计算出来的值能够被存储在不同的地址单元,而具体的位置是由指令的3-位dest域(如图4.4所示)来描述的。第一位和第二位分别决定是否将计算结果存入A或D。第三个d-位域(d-bits)决定是否将值存入M(比如Memory[A])中。

注:图右上角应为当a=1时,书中为印刷错误
在这里插入图片描述
希望计算机将Memory[7]的值加上1,并且将结果也存入D寄存器中。根据图4.3和图4.4,key通过如下的指令来完成:
在这里插入图片描述
在这里插入图片描述
第一条指令使得计算机选中地址为7的寄存器(也就是M)。第二条指令计算M+1的值,并且将结果存入M和D。

jump规范(跳转规范) C-指令的jump域告诉计算机下一步将执行什么命令。有两个可能性:计算机获取并执行程序中紧接着当前指令的下一条指令,这是默认的情况;或者它获取并执行程序中位于其他地址的一条指令。后者,我们假设A寄存器已经装载了跳转的目的地址。

jump操作能够实际执行取决于jump域的三个j-位域(j-bits)和ALU的输出值(根据comp域计算出来的)。第一个j位规定:当ALU输出值小于0时,发生跳转;第二个j位规定:当ALU输出值等于0时,发生跳转;第三个j位规定:当ALU输出值大于0时,发生跳转;这样共有8种可能的组合条件实现跳转。这样共有八中可能的跳转条件的组合。如图4.5所示
在这里插入图片描述
在这里插入图片描述
最后一个指令(0;JMP)执行一个无条件的跳转。因为C-指令语法要求我们必须执行一些计算,所以干脆让ALU去计算0(其实可取任意值),计算机会忽略它。

使用A寄存器的冲突 A寄存器使用上的冲突正像刚才所讲,程序员可以使用A寄存器为“包含M的C-指令”指定数据内存中的地址,也可以为“包含jump的C-指令”指定指令内存中的地址。因此,为了避免A寄存器在使用上的冲突,在编写良好的程序中,在可能引发jump(即在“引用M的C-指令”中也不准引发jump)。

符号

汇编命令可以使用常数或符号来表示内存单元位置(地址)。通过以下三种方式应用到汇编语言中。

  • 预定义符号(predefined symbols):RAM地址的一个特殊子集可以通过如下预定义符号来被所有汇编程序引用:
    虚拟寄存器(Virtual registers) 为了简化汇编程序的编写,用符号R0到R15用来代表0到15号RAM地址。
    预定义指针(predefined pointers) 符号SP、LCL、ARG、THIS和THAT被预定义为表示0到4号RAM地址。注意这四个内存位置都有两种符号表示。

例如,address2可以使用R2或ARG来表示。这种方法会在第7、8章中讨论的虚拟机实现中使用到。
I/O指针 符号SCREEN和KBD被预定义以表示RAM地址16384(0x4000)和24576(0x6000),这两个地址分别是屏幕和键盘内存映像(memory-map)的基地址。这些I/O设备的使用会在后面介绍。

  • 标签符号(Label symbols): 用户自定义符号用来标记goto命令跳转的目的地址。由伪指令“(Xxx)”来声明用户自定义的符号,其意义是:Xxx代表程序中下一条命令的指令内存位置。一个标签只能被定义一次,可以在汇编程序中的任何地方使用,定义之前也可以使用。
  • 变量符号(Variable symbols):在汇编程序中任何用户定义的符号Xxx,如果它不是预定义符号,也不是使用“(Xxx)”的标签符号,那么就被看作是变量,并被汇编程序赋予独立的内存地址(从RAM地址16,即0x0010开始)。
输入/输出处理

Hack平台能够连接两个外部设备:屏幕和键盘。两个设备与计算机平台的交互都是通过内存映像(memory maps)实现的。这意味着在屏幕上描绘像素是通过将二进制写入与屏幕相关的内存段来实现的。同样,键盘的输入是通过读取与键盘相关的内存单元来实现的。物理I/O设备和他们对应的内存映像是通过连续的循环刷新来同步的。

屏幕 Hack计算机包括有一个黑白屏幕,256×512像素。屏幕的内容由RAM基地址为16384(0x4000)的8K内存映射来表示。物理屏幕的一行从屏幕的左上角开始,在RAM中用32个连续的16-位字表示。因此至顶部r行、至左边c列的像素映射到位置为RAM[16394+r*32+c/16]的字的c%16位(从LSB到MSB)。为了读写屏幕上的一个像素,可以对RAM内存映射的相关位进行读写操作(1=黑,0=白)。比如:在这里插入图片描述
键盘 Hack计算机与物理键盘之间通过RAM基地址为24576(0x6000)的单字内存映像进行交互。只要再键盘上敲击一个键,其对应的16-位ASCII码值就出现在RAM[24576];没有击键时,该内存单元的值就为0。除了常用的ASCII码以外,Hack键盘还可以识别图4.6中列出的一些键。

语法规范和文件格式

二进制文件 二进制文件由文本行组成。每行都是一连串的16个“0”和“1”ASCII字符,其对应一条单独的机器语言指令。文件中的所有行组成了机器语言程序。当机器语言程序被加载到计算机指令内存中,约定文件中第n行所表示的二进制码被存储到指令内存地址为n的单元中(程序行数和内存地址计数都是从0开始计)。按照惯例,机器语言程序被存进扩展名为“hack”的文本文件中,比如prog.hack。
汇编语言文件 习惯上,汇编语言程序存储在以“asm”为扩展名的文本文件里,比如prog.asm。汇编文件由文本行组成,每一行代表一条指令或者一个符号定义:
在这里插入图片描述

  • 指令(Instruction):一条A-指令或者C-指令
  • (Symbol):这条微指令会让编译器把Symbol标签分配给程序中下一条命令被存储的内存单元地址。因为它不生成任何机器代码,所以称为“伪命令(pseudo-command)”。
  • 常数(constants)和符号(symbols) 常数必须是非负的并且总是用十进制表示。用户自定义的符号可以是任何字母、数字、下划线(_)、点(.)、美元符号($)、冒号(:)构成的字符串,但它不能以数字开头,
  • 、注释: 以双斜线(//)开头,并到本行结束符之前(不换行)的文本被认为是注释,不被程序执行。
  • 空格 空格字符以及空行也被程序忽略。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值