模拟一个简易的冯诺依曼式计算机CPU的工作。
CPU字长为16位,共11个寄存器,其中3个系统寄存器,分别为程序计数器,指令寄存器,标志寄存器;8个通用寄存器,即寄存器1、2、3、4(数据寄存器),寄存器5、6、7、8(地址寄存器)。该CPU至多支持32KB内存。内存分两部分,一部分为代码段,从地址0开始。另一部分为数据段,从地址16384开始。
CPU所支持的指令集中,每条指令固定由32位(编号为0到31)二进制数组成,其中第0到7位为操作码,代表CPU要执行哪种操作;第8到15位为操作对象,如寄存器,内存地址等;第16到31位为立即数。该CPU有一个输入端口和一个输出端口。输入端口的数据由标准输入设备(键盘)输入,输出端口的数据输出到标准输出设备(显示器)上。
程序开始时要从指定文件中读入一段用给定指令集写的程序至内存(从地址0开始顺序保存),程序计数器初始值也为0。指令加载完成后程序就开始不断重复取指令、分析指令和执行指令的过程。程序每执行一条指令就要输出CPU当前的状态,如各寄存器的值等。当执行到停机指令时,程序按要求输出后就结束了。
取指令:读取程序计数器PC内的指令地址,根据这个地址将指令从内存中读入,并保存在指令寄存器中,同时程序计数器内容加4,指向下一个条指令。
分析指令:对指令寄存器中的指令进行解码,分析出指令的操作码,所需操作数的存放位置等信息等。
执行指令:完成相关计算并将结果写到相应位置。
*指令输入从文件 “dict.dic” 中获取,非手动输入,只有遇到指令集中的输入操作时才从键盘读入。
*输入输出样例见 冯诺依曼结构作业_提取码BUPT,也可移步 我的Github
指令 | 说明 | |
---|---|---|
停机 指令 |
00000000 00000000 0000000000000000 |
停止程序执行。 |
数据传送 指令 |
00000001 00010000 0000000000000000 |
将一个立即数传送至寄存器1。 |
00000001 00010101 0000000000000000 |
将寄存器5中地址所指向的内存单元(2个字节)的内容传送至寄存器1。 | |
00000001 01010001 0000000000000000 |
将寄存器1的内容传送至寄存器5中地址所指向的内存单元(2个字节)。 | |
算术运算 指令 |
00000010 00010000 0000000000000000 |
将寄存器1内的数与一个立即数相加,结果保存至寄存器1。 |
00000010 00010101 0000000000000000 |
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数相加,结果保存至寄存器1。 | |
00000011 00010000 0000000000000000 |
将寄存器1内的数减去一个立即数,结果保存至寄存器1。 | |
00000011 00010101 0000000000000000 |
将寄存器1内的数减去寄存器5中地址所指向的内存单元(2个字节)里存的数,结果保存至寄存器1。 | |
00000100 00010000 0000000000000000 |
将寄存器1内的数与一个立即数相乘,结果保存至寄存器1。 | |
00000100 00010101 0000000000000000 |
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数相乘,结果保存至寄存器1。 | |
00000101 00010000 0000000000000000 |
将寄存器1内的数除以(C语言的整数除法)一个立即数,结果保存至寄存器1。 | |
00000101 00010101 0000000000000000 |
将寄存器1内的数除以(C语言的整数除法)寄存器5中地址所指向的内存单元(2个字节)里存的数,结果保存至寄存器1。 | |
逻辑运算 指令 |
00000110 00010000 0000000000000000 |
将寄存器1内的数与一个立即数做逻辑与,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) |
00000110 00010101 0000000000000000 |
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑与,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00000111 00010000 0000000000000000 |
将寄存器1内的数与一个立即数做逻辑或,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00000111 00010101 0000000000000000 |
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑或,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00001000 00010000 0000000000000000 |
将寄存器1内的数做逻辑非,结果保存至寄存器1。(如果结果为真则保存1,否则保存0) | |
00001000 00000101 0000000000000000 |
将寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑非,结果仍保存至寄存器5中地址所指向的内存单元。(如果结果为真则保存1,否则保存0) | |
比较 指令 |
00001001 00010000 0000000000000000 |
将寄存器1内的数与一个立即数比较,如两数相等,则标志寄存器被修置为0,如寄存器1大,则标志寄存器被置为1,如寄存器1小,则标志寄存器被置为-1。 |
00001001 00010101 0000000000000000 |
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数比较,如两数相等,则标志寄存器被置为0,如寄存器1大,则标志寄存器被置为1,如寄存器1小,则标志寄存器被置为-1。 | |
跳转 指令 |
00001010 00000000 0000000000000000 |
无条件跳转指令,转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 |
00001010 00000001 0000000000000000 |
如果标志寄存器内的值为0,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 | |
00001010 00000010 0000000000000000 |
如果标志寄存器内的值为1,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 | |
00001010 00000011 0000000000000000 |
如果标志寄存器内的值为-1,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。 | |
输入输出 指令 |
00001011 00010000 0000000000000000 |
从输入端口读入一个整数并保存在寄存器1中。也就是从键盘读一个整数到寄存器1中。 |
00001100 00010000 0000000000000000 |
将寄存器1中的数输出到输出端口。也就是将寄存器1中的数以整数的形式输出到显示器上,同时输出一个换行符。 |
内存和通用寄存器十分相似,都是字节数据的容器,模拟它们是实现后续操作的前提,
容易想到几种方法:
这些都是理论可行的方法,然而实际内存开销较大,并且可能出现不易操作的情况。
例如,对于二维char数组,究竟是否需要多加一列来记录终止符’\0’,以便使用字符串函数;记录数据的过程中,究竟使用字符’0’还是ASCII 0(即’\0’),都需要斟酌。
C++提供了按位存储的容器vector
考虑这样的写法:
struct Byte {
unsigned char bit0 : 1, bit1 : 1, bit2 : 1, bit3 : 1,
bit4 : 1, bit5 : 1, bit6 : 1, bit7 : 1;
};
使用了8个unsigned char来模拟8个位,其后的 “: 1” 说明bit0等各只占1位,而不是1字节。
但即使实现了真正的按位存储,后续操作依然十分繁琐,因为无法使用循环来读写。(你想换成数组?)
为此,union发挥了巨大的作用。考虑如下写法:
union Memory {
unsigned char data;
struct Byte b;
};
union Register {
short data;
struct {
struct Byte b0;
struct Byte b1;
}b;
};
union在此实现了数据的统一。在Memory中,包含两种类型数据,一个是8位的Byte,另一个是1字节的data,任何对data的操作,都会同时作用于Byte,例如当data = 255时,Byte中就有"11111111",直接对data进行读写,就修改了对应的01序列。这为我们提供了极大的便利。
Register中同理,能够用一个2字节的short统辖16位。(其中又使用了一个结构体,是为了防止b0与b1共用同一字节,从而导致高字节无法体现)
当然,其他数据结构同样可行,但这种方法编程复杂度很低,且极大节省内存,模拟的32KB就是真实的32KB。
至于程序计数器、指令寄存器、标志寄存器等,使用基本数据类型即可。
通常而言,操作都是以字节为单位的,有了适当的数据结构,可以方便地进行读写。
我们对元件的操作媒介就是结构体和联合中的data,在寄存器中,它是short(2字节),在内存中,是unsigned char(1字节),如果要修改数据,显然直接修改data即可。
即将执行的指令被程序计数器PC选中,存储于指令寄存器IR中,IR为2字节,在分析指令步骤需要得到指令类型cmd、前后操作对象from和to。指令类型位于高字节,操作对象位于低字节的高4位和低4位。
数据存储的本质还是二进制01序列,不要因为有了data这一便捷的接口就忘记二进制操作。
容易想到如下计算方法:
IR.data = (memory[PC].data << 8) | memory[PC + 1].data;
int cmd = IR.data >> 8;
int from = IR.data & 15, to = (IR.data & 240) >> 4;
IR由指令的前2字节组合而成,按位或换成相加也是可以的。
cmd由IR整体右移8位获取高字节得到。低字节中,from为低4位,和(00001111)B按位与即可;to为高4位,和(11110000)B按位与后右移4位即可。
其余读写也同理,基本都可利用二进制操作。如读写内存(2字节),只需:
//read
short num = (memory[pos].data << 8) | memory[pos + 1].data;
//write
memory[dest].data = immed >> 8;
memory[dest + 1].data = immed & 255;
其中,immed表示立即数,可以知道立即数是补码表示的,那为何读写时不需要考虑补码因素呢?
事实上,依托于先前建立的数据结构,在整个设计过程中,都不需要考虑补码的影响。
试考虑以下原则:
当前获取的立即数,在程序中看起来是十进制数,实际上是一个01序列,因为规定了它的数据类型是short,故它能表示对应范围的十进制数,而在二进制下操作时,与数据类型是无关的。
因此,在读取2字节数据时,通过位运算可以获取完整的2字节序列,我们规定它是short,所以它按照short翻译,也就是按照补码翻译;在写入2字节数据时,如果是向内存中写入,我们拆开原有的2字节,分别通过位运算写入。
如果读写操作是在寄存器之间的,由short到short,就更为简单了,直接对data赋值即可。
指令集虽长,但总共只有12种指令类型,逐一分析就能找到规律。
建立在先前构建的数据结构的基础上,对指令集中各种指令的实现已经十分简单。这里不再逐一给出编写过程,仅讨论一些可能出错的地方。
已通过OJ上所有测试样例
鼓励独立思考,独立解题,请勿直接复制,否则OJ查重无法通过
#include
#include
#include
typedef struct Byte { //stored by bit
unsigned char bit0 : 1, bit1 : 1, bit2 : 1, bit3 : 1,
bit4 : 1, bit5 : 1, bit6 : 1, bit7 : 1;
}Byte;
typedef union Memory {
unsigned char data;
Byte b;
}Memory;
typedef union Register {
short data;
struct {
Byte b0;
Byte b1;
}b;
}Register;
const int START = 16384; //dataSegment starts from 16384
Memory memory[32 * 1024]; //32Kb Memory
unsigned int PC = 0; //ProcessCounter
Register IR; //InstructionRegister, stores only 2 bytes
Register ax[9]; //GeneralRegisters, store only 2 bytes (don't use ax[0])
int FR = 0; //FlagsRegister
void ReadToMemory();
void process();
void move1(int dest, int src);
void move2(int dest, int src);
void cal(int dest, int src, int mode);
void AND(int dest, int src);
void OR(int dest, int src);
void NOT(int dest, int mode);
void cmp(int dest, int src);
void show();
int main() {
//Load all data into memory.
ReadToMemory();
//Analyze and run instructions.
process();
return 0;
}
void move1(int dest, int src) { //immed/memory->ax
short data = (memory[src].data << 8) | memory[src + 1].data;
ax[dest].data = data;
}
void move2(int dest, int src) { //ax->memory
memory[dest].data = ax[src].data >> 8;
memory[dest + 1].data = ax[src].data & 255;
}
void cal(int dest, int src, int mode) { //calculate, mode: 0->add, 1->sub, 2->mul, 3->div
short data = (memory[src].data << 8) | memory[src + 1].data;
if (mode == 0) ax[dest].data += data;
else if (mode == 1) ax[dest].data -= data;
else if (mode == 2) ax[dest].data *= data;
else if (mode == 3) ax[dest].data /= data;
}
void AND(int dest, int src) { //logic and
short data = (memory[src].data << 8) | memory[src + 1].data;
(ax[dest].data & data) ? (ax[dest].data = 1) : (ax[dest].data = 0);
}
void OR(int dest, int src) { //logic or
short data = (memory[src].data << 8) | memory[src + 1].data;
(ax[dest].data | data) ? (ax[dest].data = 1) : (ax[dest].data = 0);
}
void NOT(int dest, int mode) { //logic not
if (mode == 0) ax[dest].data = !ax[dest].data;
else {
if (memory[dest].data || memory[dest + 1].data) memory[dest].data = memory[dest + 1].data = 0;
else memory[dest + 1].data = 1;
}
}
void cmp(int dest, int src) { //compare
short data = (memory[src].data << 8) | memory[src + 1].data;
if (ax[dest].data == data) FR = 0;
else if (ax[dest].data > data) FR = 1;
else FR = -1;
}
void show() { //show you all the information
printf("ip = %hd\nflag = %d\nir = %hd\n", PC, FR, IR.data);
printf("ax1 = %hd ax2 = %hd ax3 = %hd ax4 = %hd\n", ax[1].data, ax[2].data, ax[3].data, ax[4].data);
printf("ax5 = %hd ax6 = %hd ax7 = %hd ax8 = %hd\n", ax[5].data, ax[6].data, ax[7].data, ax[8].data);
}
void ReadToMemory() { //read data and store in memory
FILE* fp = fopen("dict.dic", "r");
unsigned int cnt = 0;
while (1) {
char line[33] = { 0 };
if (fscanf(fp, "%s", line) != EOF) {
if (line[0] < '0' || line[0] > '1') break; //unavailable data
for (int i = 0; i < 4; i++) {
unsigned char _data = 0;
for (int j = 0; j < 8; j++) _data = _data * 2 + line[i * 8 + j] - '0';
memory[cnt++].data = _data;
}
}
else break;
}
fclose(fp);
}
void process() { //main function, to judge and operate
while (1) {
IR.data = (memory[PC].data << 8) | memory[PC + 1].data;
int cmd = IR.data >> 8;
int from = IR.data & 15, to = (IR.data & 240) >> 4;
bool noJump = 1;
if (cmd == 0) { //shut down
PC += 4;
show();
printf("\ncodeSegment :\n");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 8; j++) {
if (j) putchar(' ');
printf("%d", (memory[i * 32 + j * 4].data << 24) | (memory[i * 32 + j * 4 + 1].data << 16) |
(memory[i * 32 + j * 4 + 2].data << 8) | memory[i * 32 + j * 4 + 3].data);
}
putchar('\n');
}
printf("\ndataSegment :\n");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
if (j) putchar(' ');
printf("%hd", (memory[START + i * 32 + j * 2].data << 8) | memory[START + i * 32 + j * 2 + 1].data);
}
putchar('\n');
}
break;
}
if (cmd == 1) { //move
if (from == 0) move1(to, PC + 2); //immed->ax
else if (from >= 5) move1(to, ax[from].data); //memory->ax
else move2(ax[to].data, from); //ax->memory
}
else if (cmd == 2) { //add
from == 0 ? cal(to, PC + 2, 0) : cal(to, ax[from].data, 0);
}
else if (cmd == 3) { //subtract
from == 0 ? cal(to, PC + 2, 1) : cal(to, ax[from].data, 1);
}
else if (cmd == 4) { //multiply
from == 0 ? cal(to, PC + 2, 2) : cal(to, ax[from].data, 2);
}
else if (cmd == 5) { //divide
from == 0 ? cal(to, PC + 2, 3) : cal(to, ax[from].data, 3);
}
else if (cmd == 6) { //logic and
from == 0 ? AND(to, PC + 2) : AND(to, ax[from].data);
}
else if (cmd == 7) { //logic or
from == 0 ? OR(to, PC + 2) : OR(to, ax[from].data);
}
else if (cmd == 8) { //logic not
from == 0 ? NOT(to, 0) : NOT(ax[from].data, 1);
}
else if (cmd == 9) { //compare
from == 0 ? cmp(to, PC + 2) : cmp(to, ax[from].data);
}
else if (cmd == 10) { //jump
short data = (memory[PC + 2].data << 8) | memory[PC + 3].data;
if (from == 0 || from == 1 && FR == 0 || from == 2 && FR == 1 || from == 3 && FR == -1) {
PC += data;
noJump = 0;
show();
}
}
else if (cmd == 11) { //input
printf("in:\n");
scanf("%hd", &ax[to].data);
}
else if (cmd == 12) { //output
printf("out: %hd\n", ax[to].data);
}
if (noJump) {
PC += 4;
show();
}
}
}
北邮19级大作业CPU模拟器指令生成及执行工具
备注1:感谢19级学长制作的工具,为debug提供了很大帮助。生成代码时,请注意按照指令集的规定进行生成,例如操作对象处于高位还是低位,通常都会有影响。同时,其中所给的测试样例具有一些指令集未说明的情况,我写的代码只遵循指令集的要求,虽然OJ上可以通过,但不适应这些情况。
备注2:工具中的执行操作是无效的,因为链接似乎挂掉了。
实际上,这个作业无形中为了解计算机底层原理和汇编语言起到了微妙的入门引领效果。
重新回顾设计过程中涉及的内容。
事实上,如果掌握了足够牢固的基本技能,思路比较开阔,在写这个作业的时候,是完全不需要注意查重的,因为自然不会重复。但在此依旧给出一些防止查重的技巧(不要滥用)。