任务描述
本关任务:利用 Python 语言模拟 TOY2 冯·诺依曼体系结构计算机。
相关知识
从实训简介中可以看出,TOY2 计算机和 TOY 计算机类似,其区别在于 TOY2 计算机执行的是十进制的机器指令,而不是汇编指令。TOY2 计算机指令集如表 1 所示。
表 1 TOY2 计算机的指令集
如图 1 所示, TOY2 指令的长度为 6 位,前 2 位为操作码,第 3 位和第 4 ~ 6 位为操作数,全部采用十进制数表示。
图 1 TOY2 的指令格式
冯·诺依曼体系结构计算机的 CPU 工作过程是自动逐条执行指令的过程。当程序需要执行时,CPU 首先需要将程序代码从外存装载到主存,然后在控制单元的控制下,精确地、一步一步地完成指令的执行。现代计算机一直使用冯·诺依曼体系结构, CPU 执行一条指令的过程通常可分为 4 步,经过这样一些步骤完成一条指令的执行所需的时间称为指令周期,其中的每一步称为一个节拍:
- 取指令:指令装载到主存后,CPU 通过程序计数器获得要执行的指令存储地址。根据这个地址,CPU 将指令从主存中读入,并保存在指令寄存器中,同时对程序计数器内容进行“增 1”操作,即指向下一个内存单元。
- 译码:由指令译码器对指令进行解码,分析出指令的操作码,所需操作数和操作数的存放位置等信息。
- 执行:将译码后的操作码分解成一组相关的控制信号序列,以完成指令动作,包括从寄存器读数据、输入到运算器
ALU
进行算术或逻辑运算等。 - 写结果:若指令执行后产生了结果,则将结果写回到指定位置,通常是寄存器。如果必要,将产生的条件反馈给控制单元。
在最后一个节拍完成后,控制单元复位指令周期,从取指令节拍重新开始运行,此时,程序计数器的内容已被自动修改,指向下一条指令所在的主存地址。需要注意的是,运算指令和数据移动指令的执行不会主动修改程序计数器的值,程序计数器将会自动指向程序顺序上的下一条指令;而控制指令,如跳转指令的执行将会主动改变程序计数器的值,使得程序的执行不再是顺序的。
本关卡在对 TOY2 计算机进行建模与模拟时,共有以下几个函数和全局变量:
-
mem
用于模拟主存(共 1000 个存储单元),reg
用于模拟通用寄存器组(共 10 个存储单元),pReg
用于模拟程序计数器,iReg
用于模拟指令寄存器,address
用于模拟程序装载到主存时的起始物理地址。 -
loadProgram
函数,用于加载程序,将程序从外存装入到主存,要求程序第一行指令放在address
对应的主存单元中,然后根据每一行指令前面的逻辑地址,将每条指令放入主存(mem
)对应的单元中。同时,将第 1 条指令的主存单元地址存入pReg
,以便执行程序。 -
cycle
函数,用于模拟 CPU 指令周期,实现上完全按照指令周期的步骤进行,分为取指令、译码、执行和写结果四个阶段。注意cycle
函数的返回值,如果碰到00
指令,返回 0 ,run
函数结束;如果是其他指令,则返回 1 ,使run
函数执行下一条指令。 -
run
函数,其主要功能是从外存加载程序到主存并执行。
编程要求
仔细阅读右侧编辑区给出的程序代码框架,在指定的 Begin-End 区间补充完善loadProgram
函数和cycle
函数,使其能够正确执行使用 TOY2 指令集编写的程序。
测试说明
平台将使用测试集运行你编写的程序代码,若全部的运行结果正确,则通关。
本关卡共有 3 个测试集,测试集的输入数据中,第 1 个是使用 TOY2 计算机指令集编写的程序代码文件,分别命名为 add.toy2、sum100.toy2、div.toy2;第 2 个是给定的主存地址address
;若 .toy2 中的程序代码需要输入数据,则排在第 3 个位置。评测输出为 .toy 中程序代码的输出内容,即11
指令的执行结果。
add.toy2、sum100.toy2、div.toy2 的代码指令如下所示。
#add.toy2
000 031012
001 032013
002 041002
003 111000
004 000000
#sum100.toy2
000 031000
001 032001
002 033001
003 041002
004 042003
005 034101
006 054002
007 094009
008 080003
009 111000
010 000000
#div.toy2
000 100000
001 031034
002 071000
003 021999
004 013999
005 113000
006 000000
测试样例如下所示: 测试输入:add.toy2
2
预期输出:25
测试输入:div.toy2
20
17
预期输出:2
开始你的任务吧,祝你成功!
参考答案
mem = [0]*1000 #主存
reg = [0]*10 #通用寄存器
pReg = 0 #程序计数器
iReg = 0 #指令寄存器
#loadProgram函数:加载程序,将file文件中的程序代码顺序存放到主存,程序第一条指令放入address地址对应的主存单元中,设置程序计数器pReg的值
def loadProgram(file):
global pReg, iReg, reg, mem, address #全局变量声明
########## Begin ##########
pReg = address
fil = open(file, 'r') #打开文件
first = True #用于标识是否为第1条指令
while True: #每循环一次加载一条指令
line = fil.readline() #读1行
if line == '': #若读取完毕,则结束循环
break
flds = line.split() #将1行拆分为若干部分
address2 = int(flds[0]) #第0部分为地址
instruc = int(flds[1]) #第1部分为指令
mem[address2 + address] = instruc #将指令加载到主存单元
fil.close() #关闭文件
########## End ##########
# cycle函数:执行一条 TOY2 指令,包括取指令、指令译码、执行和写结果 4 个步骤
def cycle():
global pReg, iReg, reg, mem, address #全局变量声明
########## Begin ##########
#取指令
iReg = mem[pReg] #根据pReg的值,将指令从mem取到iReg
pReg = pReg + 1 #pReg加1,指向下一条指令
#译码
opcode = iReg//10000 #操作码
op1 = (iReg//1000)%10 #操作数1
op2 = iReg%1000 #操作数2
#执行和写结果
if opcode==0: #停止指令
return False
elif opcode==1: #数据移动指令:寄存器←主存
reg[op1] = mem[op2]
elif opcode==2: #数据移动指令:主存←寄存器
mem[op2] = reg[op1]
elif opcode==3: #数据移动指令:寄存器←数字
reg[op1] = op2
elif opcode==4: #加法指令
reg[op1] = reg[op1]+reg[op2]
elif opcode==5: #减法指令
reg[op1] = reg[op1]-reg[op2]
elif opcode==6: #乘法指令
reg[op1] = reg[op1]*reg[op2]
elif opcode==7: #整除指令
reg[op1] = reg[op1]//reg[op2]
elif opcode==8: #无条件跳转指令
pReg = op2 + address
elif opcode==9: #条件跳转指令
if reg[op1]==0:
pReg = op2 + address
elif opcode==10: #输入指令
reg[op1] = int(input())
elif opcode==11: #输出指令
print(reg[op1])
return True
########## End ##########
# run函数:加载程序并执行程序
def run(file):
global pReg, iReg, reg, mem
loadProgram(file) #加载TOY2程序
while True: #每循环一次,执行一条指令
hasNextInstruc = cycle() #执行一条TOY2指令
if hasNextInstruc==False: #若执行的是停机指令
break #则跳出循环
fil = input() #获取待执行程序所在的文件名
address = int(input()) #获取程序装载到主存的起始物理地址
run(fil) #加载程序并执行