第四章,01-机器语言概述
机器语言是符号与物理机器的界面,是硬件与软件的界面。
机器语言向下直接操纵机器,向上提供软件符号接口。
机器(Machines)
机器语言直接打交道的对象是处理器(processor)、寄存器(register)和内存(memory)。
机器语言利用处理器与寄存器操纵内存。
内存
储存数据和指令的硬件设备,硬件结构见第三章,02-16Kb存储器与程序计数器实现。
后面的内容中用符号Memory[addr],RAM[addr]或M[addr]表示内存。
处理器
执行算术操作和逻辑操作、内存存取操作和控制操作。输入输出都是二进制数值,交互对象为寄存器或指定的内存单元。
寄存器
CPU取内存地址j的内容的过程分成三步:
- j从CPU传到RAM
- RAM的直接访问逻辑(direct-access logic)选中地址为j的寄存器
- RAM[j]的内容传回到CPU
内存因为远离CPU,在跑得飞快的CPU面前,数据传输的延时不容忽视。
同时内存通常容量巨大,因此寻址时间较长。
而处理器的电路相对简单,给定输入下很快可以得到输出,速度上就比内存快了不知道多少倍。
若CPU只与内存交互,执行速度就会被拖慢到和内存相同水平。
寄存器是集成在CPU中专门为了伺候CPU的,相当于一组既靠近CPU又提升了运算速度的内存单元,而且数量非常少,通过机器语言指令的几个位就能确定寄存器在哪,指令格式也更短。
数据寄存器(Data Registers)
CPU的短期记忆,如当计算 ( a − b ) − c (a-b)-c (a−b)−c时,将 a − b a-b a−b的结果缓存到数据寄存器中。
寻址寄存器(Addressing Registers)
数据内存中下一个被访问的内存字(word)所在的地址可以通过两种方式给出:
- 作为当前指令的一个部分
- 依赖于前一条指令的执行结果
对于2,将这个地址存储到寻址寄存器中。
程序计数寄存器Program Counter Registers)
存储下一条指令所在的地址,是指令内存的地址。更新方式:
- 当前指令不包含goto指令,PC增1指向下一条指令
- 包含goto n命令,将PC置为n
语言(Languages)
CPU的输入是一组高低不一的电平,使用二进制表示,如果直接使用二进制编码,既难懂又不好维护。
汇编语言(assembly language),或简称汇编,就是二进制指令的助记符,一行汇编代表一串二进制指令,对应ALU的一个操作。
将汇编程序翻译成二进制代码的程序称为汇编编译器(assembler)。
不同的计算机在CPU的操作方式、寄存器的数量和类型,以及汇编语法上各不相同。但也共享一些通用的命令集合,如下所示:
命令(Commands)
算术操作和逻辑操作
计算机执行的基本算术操作和基本布尔操作,用典型的机器语言编写示例:
ADD R2, R1, R3 // R2 <- R1 + R3, 其中R1, R2, R3是寄存器
ADD R2, R1, foo // R2 <- R1 + foo ,其中foor代表用户自定义标签foo所指向的内存单元的值
AND R1, R1, R2 // R1 <- 对R1和R2进行按位与(And)的结果
内存访问
内存访问命令分为两类:
1.算术命令和逻辑命令,如上面的示例,可以操控寄存器和特定的内存单元。
2.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 = f o o [ j ] x=foo[j] x=foo[j]
foo是数组变量,x和j是整数变量。
当数组foo在高级语言程序中被声明并初始化时,编译器分配一组连续的内存单元,并用符号foo来指代该内存单元组的基地址(base address)
当编译器遇到表示数组单元的符号(如foo[j])时,地址解析如下:
第j个元素的物理地址,相对基地址的偏移量为j,即foo+j。
如C语言中,x=foo[j]等价于x=*(foo+j),这时"*n"代表“Memory[n]的值”。
x=foo[j]对应汇编代码如下:
// 将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
控制流程
程序经常会使用分支,如反复(repetition,跳回到循环起始位置, for,while)、有条件的执行(conditional execution,if-else),以及子程序调用(subroutine calling)。
为了支持这些程序结构,汇编语言都可以有条件(conditional)或无条件(unconditional)地跳转到程序指定的地址。
有条件跳转(unconditional jump)
满足特定的布尔条件即跳转。
无条件跳转(unconditional jump)
只指定了跳转的目标地址,执行到这条语句直接跳转,不需要满足特定的布尔条件。
结语
看到这章和第5章有点小失望,因为前3章都是在讲硬件构成,这章插入汇编语言,突然就断了一层,就像刚把砖做好,下一步就告诉我楼已经盖好了。虽然第5章把ALU和内存这些东西拼起来组装成了计算机,但还缺了很多东西,毕竟只靠这些,离开模拟器就不知道怎么进行了。
比如在现实中按步骤组装好硬件,往哪里写汇编?
在后面的章节里,汇编是用已有的高级语言写的,那没有高级语言的时候,第一个汇编器是怎么实现的?
命令最后调动的都是CPU,那么是怎么调动的?谁调动的?不是整个计算机都是通过CPU调动吗?先有鸡还是先有蛋?
好像有个看不见的指挥官,遇到二进制指令,就告诉CPU:这个管脚是这个输入,那个管脚是那个输入,然后CPU的输入就自动变成了这些值?
我往后看,一直到书末尾都没有再提,挺失望的。最后扳回一城的是13章提到所有的仿真器代码都对读者开放,才心里踏实了点。