写在前面的话
如果您对该系列感兴趣的话,推荐您先看一下南京大学的计算机组成原理实验(也就是PA)的讲义,然后再来看这篇文章可能有更多地收获。如果您是要完成该作业的学生,我推荐你先看讲义,或者好好听老师的讲课,然后自己独立完成这个作业,但是如果你没有听懂,或者你无论如何也无法理解讲义上面的字,又或者说对讲义上面的某点知识某个问题不了解而又觉得太简单不好意思问老师,那么您可能会从这篇文章里面获得一些你需要的信息。本篇文章将会包括笔者自己做PA的所有经过,希望你并不将该文章当成抄袭的根源,而是成为你思考的源泉。
现在已经到了PA2的阶段,这才真正开始了组成原理的道路,在PA0是搭建环境,PA1复习复习C语言,写写功能函数,而现在才开始真正去做计算机结构内部的东西,难度也上了一个档次。
PA系列传送门
PA0:https://blog.csdn.net/qq_41983842/article/details/88921427
PA1.1:https://blog.csdn.net/qq_41983842/article/details/88934779
PA1.2:https://blog.csdn.net/qq_41983842/article/details/89714479
PA1.3:https://blog.csdn.net/qq_41983842/article/details/89714689
PA2.1:https://blog.csdn.net/qq_41983842/article/details/95232055
PA2.2&2.3:https://blog.csdn.net/qq_41983842/article/details/101164495
PA3.1:https://blog.csdn.net/qq_41983842/article/details/103094859
PA3.2:https://blog.csdn.net/qq_41983842/article/details/103843093
PA4:https://blog.csdn.net/qq_41983842/article/details/104667951
文章目录
思考题
-
设某指令执行前
eip
值为x1
,该指令执行后eip
值为x2
,那么x2 - x1
的这个差值都包括了一条指令的哪些组成成分?opcode、操作数的地址、存储地址、指令地址,指令类型不同包括不同的成分。
-
opcode_table
数组中存放了所有指令的信息,请问表中每个表项是什么类型?NEMU
又是如何通过这个表项得知操作数长度、应该使用哪个译码函数、哪个执行函数等信息的?opcode_entry
类型,NEMU
通过set_width
知道操作数长度,通过make_DHelper
函数知道要用哪个译码函数,通过make_EHelper
函数知道要执行函数信息。 -
操作数结构体/共同体中都包括哪些成员,分别存储什么信息?他们是如何实现协同工作的?
typedef struct { uint32_t type; //操作数类型 int width; //操作数宽度 union { uint32_t reg; //寄存器操作数 rtlreg_t addr; //操作数的地址 uint32_t imm; //立即操作数 int32_t simm; //无符号立即操作数 }; rtlreg_t val;//RTL寄存器 char str[OP_STR_SIZE];//操作数指令 } Operand;
-
复现宏定义
-
make_EHelper(mov) //mov 指令的执行函数
#define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)
->
#define concat(x, y) concat_temp(x, y)
->
#define concat_temp(x, y) x ## y
把x和y这两个参数粘在一起变成
xy
,所以在宏定义中将exec_
和mov
粘在一起,执行mov
操作 -
make_EHelper(push) //push 指令的执行函数
#define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)
->
#define concat(x, y) concat_temp(x, y)
->
#define concat_temp(x, y) x ## y
把x和y这两个参数粘在一起变成
xy
,所以在宏定义中将exec_
和push
粘在一起,执行push
操作 -
make_DHelper(I2r) //I2r 类型操作数的译码函数
#define make_DHelper(name) void concat(decode_, name) (vaddr_t *eip)
与上一个一样,把
decode_
和I2r
连接起来,执行immediate to register
译码操作 -
IDEX(I2a, cmp) //cmp 指令的 opcode_table 表项
#define IDEXW(id, ex, w) {concat(decode_, id), concat(exec_, ex), w}
->
id
变量表示译码函数参数名称,传给译码函数,ex
变量表示执行函数参数名称,传给执行函数,w表示width
,处理的数据的宽度。 -
EX(nop) //nop 指令的 opcode_table 表项
#define EX(ex) EXW(ex, 0)
->
ex
变量表示执行函数参数名称,传给执行函数,EX(nop)
=EXW(nop, 0)
-
make_rtl_arith_logic(and) //and 运算的 RTL 指令
#define make_rtl_arith_logic(name) \ static inline void concat(rtl_, name) (rtlreg_t* dest, const rtlreg_t* src1, const rtlreg_t* src2) { \ *dest = concat(c_, name) (*src1, *src2); \ } \ static inline void concat3(rtl_, name, i) (rtlreg_t* dest, const rtlreg_t* src1, int imm) { \ *dest = concat(c_, name) (*src1, imm); \ }
这个我在实现RTL指令的时候详细说明了。
-
-
立即数背后的故事
由于大端小端两种方式数据的字节保存的地址位置相反,需要注意大端小端两种保存数据模式的不同,先判断机子是大端还是小端存储方式,如果是大端,就传到一个将小端存储方式转换成大端存储方式的函数里面就行了。
-
神奇的 eflags
当运算的结果超过字长所能表示的范围时,产生溢出,CF当然不能代替OF,对于运算的数来说,只要符合进位的情况,CF就置1,怎么可能所有的进位情况都会超过字长所表示的范围呢?在运算的过程中,当两个符号相同的数相加,结果的符号与之相反,则OF=1,否则OF=0. 或者当两个符号不同的数相减,结果的符号与减数相同,则OF=1,否则OF=0.
实验内容
实现所有 RTL 指令
一开始打开rtl.h
文件发现里面这么多TODO()
可给我吓坏了,之后好好看了PPT和讲义才找清楚他们都是干什么的,然后才好开始写,一些rtl
指令很简单,完全就是跟着注释走,基本上注释已经把答案写出来了。
在开始写所有的指令之前,有一段代码引起了我的注意
这是干啥的啊,看起来定义了好多的运算,但是定义的又不是那么清楚直接,我想了半天才明白了,重点在他定义的make_rtl_arith_logic
里面,这个函数有一个参数名字叫name
,下面紧接着一个concat
函数,作用应该是将rtl_
和name
连接起来,就构成了rtl_name
这样一个东西,然后呢,在这个函数里面就把这个东西当成c_name
,找到上面的宏定义来实现相应的功能,第35到44行,一共定义了这么多运算,这可就省了好多事了,在接下来完善rtl
指令的时候就可以直接调用这些定义好的运算了。于是就开始实现相关指令
rtl_mv
函数,就是简单赋值:
rtl_not
函数:
rtl_eq0
函数:
rtl_eqi
函数:
rtl_neq0
函数:
rtl_msb
函数:请注意,这个函数的写法是错误的!将在PA2.2中解决
然后这些最简单的操作写完了,就可以写其他的比较复杂的操作了。
rtl_push
函数:
rtl_pop
函数:
rtl_sext
函数,这个函数写的时候是费了我好大的劲,想了很多种情况,然后问了助教之后,不管src
是多少位,通通扩展到32位,这样以来就简单一些,可以通过与一个数相或或者左移右移的方法来实现符号扩展,这里我选择的是左移右移的方式。因为寄存器都是无符号整型,所以左移右移没办法移入1,所以要设置一个带符号整数,通过带符号整数来左移右移,最后赋给dest
。
make_rtl_setget_eflags
,在写这里的时候遇到了一些困难,这个函数没有注释提示,然后我只知道和寄存器有关,然后就无从下手,把这个任务拖了拖,结果后面一看讲义,要我自己实现eflags
结构体,然后就转去先做任务3再回来做这个,其实就是简单的赋值操作。
rtl_update_ZF
函数,简单的判断之后就传到rtl_set_ZF
函数里面设置给ZF
相应的值。请注意,这个函数的写法是错误的!将在PA2.2中发现并解决
rtl_update_SF
函数,找到符号位,传到rtl_set_SF
函数里面设置给SF
相应的值。请注意,这个函数的写法是错误的!将在PA2.2中发现并解决
实现 eflags 寄存器
查询了位域的相关概念,把讲义搬过来
31 23 15 7 0
+-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+
| |O| |I| |S|Z| | |C|
| X | |X| |X| | | X |1| |
| |F| |F| |F|F| | |F|
+-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+
类似于这样子(从百度百科上面抄了个例子)
struct bs {
unsigned a:4;
unsigned :0 ;/*空域*/
char b:4 ;/*从下一单元开始存放*/
unsigned c:4;
}data;
//a 为unsigned,空域为unsigned
那这样以来岂不是就很好实现,对照讲义可以看出来,这些符号位肯定都是无符号整型,这几个符号位各占一位,其他的都是空域。由于下面有初值问题,所以还要定义一个变量存放初值。由于struct
里面是位域,所以只能在struct
外面再搞一个struct
,然后设置一个变量来存放值。
struct {
struct {
uint32_t CF:1;//CF占一位
unsigned : 5;//之后是5位空域
uint32_t ZF:1;//ZF占一位
uint32_t SF:1;//SF占一位
unsigned : 1;//1位空域
uint32_t IF:1;//IF占一位
unsigned : 1;//1位空域
uint32_t OF:1;//OF占一位
unsigned : 20;//20位空域
};
uint32_t value;//赋初值要用
}eflags;
然后要设置eflags
的初值,在手册第10章,一下子就找到了
要设置2为初值,在monitor.c
中的restart
函数里面添加
从任务6回来,发现自己的sub
指令实现的有问题,找了半天的错误,发现逻辑上面没啥错误,然后想到sub
指令跟寄存器结构体有关,不会是我结构体实现的有问题把…就反过来检查自己的结构体,思考了一下好像是有点问题的,寄存器的位域是结构体,但是eflags
寄存器的初值应该是大家共用的而不是单单只有一个才对,并且最开始union
申请空间直接要申请32位的初值空间,这样才可以在这个空间里面定义位域。所以把外面哪个struct
改成了union
实现 6 条基本指令
先把call
指令实现:
/* 0xe8 */ IDEXW(J,call,0), EMPTY, EMPTY, EMPTY,
然后找到decode.c
文件中的make_DHelper(J)
函数,根据i386
手册里面第275页,找到call
指令的相关伪代码并在make_EHelper(call)
函数里面写出来相关操作:
实现完call
指令以后兴致勃勃运行dummy
单步执行一下,结果发现了这个情况
找到Assertion '0' failed
这个问题所在,发现是make_DoHelper(SI)
这个函数没有实现,然后顿时就懵了,不是说好填完表然后写好执行函数就行了嘛,怎么还要写这个译码函数??然后我在讲义里面找到了这样一句话:
make_Dophelper(name)
:名为 decode_op_name
的操作数译码函数的原型说明,那这个函数应该是和带符号立即数的进一步译码有关系,虽然还是不太懂其中的原理,但是根据注释还有前面的make_DoHelper(I)
可以很快写出来相应的功能,照猫画虎嘛。这里实现的也有问题呀!!! 将在PA2.2中发现并解决
之后再尝试运行dummy
,成功实现。
现在来实现push
指令:
接着上一步继续si
告诉我0x55
没有实现,就查表找到0x55
位置,然后查阅i386
手册
从50到57都是跟push
相关的,只要实现32位的就行了,找到make_DHelper(r)
,那么开始填表,虽然要运行我们这个程序的话填0x55
就行了,但是为了以后着想,从0x50
到0x57
都要填。
/* 0x50 */ IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),
/* 0x54 */ IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),
找到data-mov.c
文件中的make_EHelper(push)
函数,开始实现。
make_EHelper(push) {
rtl_push(&t0);//把寄存器压栈
if (id_dest->type == OP_TYPE_REG) { rtl_sr(id_dest->reg, id_dest->width, &t0); }//如果目的操作数是寄存器操作数,写入寄存器
else if (id_dest->type == OP_TYPE_MEM) { rtl_sm(&id_dest->addr, id_dest->width, &t0); }//如果是在内存里面,写入内存
else { assert(0); } //不可能出现其他情况了。
print_asm_template1(push);
}
从第6步回来,怎么想感觉自己都写的没错啊…问题出在哪儿了呢?于是用printf
一行一行打印来确定问题所在之处,最后确定到rtl_sm
这个函数,写入寄存器的操作出了问题,我不是push ebp
么?为啥写入寄存器的值出现问题了啊!返回来从头查看程序代码,发先我的第一行rtl_push(&t0);//把寄存器压栈
这是啥?我为什么要把一个啥也不是的temp
压栈?我说为什么cpu.ebp
一直是0,然后马上更改,给t0
赋上了寄存器的值,然后再入栈,最后再写入操作数,就OK了。这里的push仍然有一步实现有问题!!!将在PA2.2中发现并解决
si
测试实现push
功能
但是由上图可知在0x83
这个地址处他又不知道干啥了,于是查看反汇编发现是sub
指令,接下来就实现sub
指令
在i386
中找到0x83
位置,发现要找grp1
,在grp1
中找到sub
,在第6个位置
回到我们的表里,找到make_group(gp1,
找到第六个EMPTY
,改一下,无译码函数,直接执行,v表示字长不定,所以直接用EX
在arith.c
里面找到执行函数,根据手册上面写的伪代码开始实现,一开始完全照着手册上面来实现,写个if写个else,本来觉得没啥问题,结果一跑就出错了,我想想这逻辑也没问题啊,怎么就跑不对了呢?然后就往下翻,还是在这个文件里面,我看到了有个sbb
执行的执行函数,哎呦,这个跟sub
好像哎,于是我就继续查手册,找到了这个指令的相关内容:
这跟sub
不就差一个CF
的问题么,有了sbb
那么sub
也好实现了,对照着他就可以开始写了:
写完sub
指令发现之前自己的push
第3、4、5行完全可以直接调用operand_write
函数…我竟然没注意到这个函数!
成功实现
继续si
直到报错
发现0x31
这个地址处的指令没有实现,查看反汇编以后是xor
指令,现在就去实现它:
查手册,从0x30
开始到0x35
都是要实现异或的,0x30
、0x31
是E->G
,反过来对应译码函数要找G2E
,0x32
、0x33
正好相反要找E2G
,0x34
要找I2a
,0x35
要找I2r
返回到表中开始填写相关位置
在logic.c
文件中开始写执行函数
还是查手册获得xor
指令怎么写,CF和OF位都是0,目标操作数等于src
和dest
的异或
成功实现
再运行一步又出现问题了!真是问题不断,这次查阅了反汇编,发现是pop
指令没有实现
i386
手册里面,pop
就在push
的旁边,0x58
到0x5F
回到我们的opcode
表,其实跟push
一模一样,译码函数也是r
就连执行函数也和push
的逻辑一模一样,就反过来,一个压栈,一个出栈就行了。
成功实现
继续si
,这回是0xc3
地址处的指令没有实现,还是老惯例,查看反汇编,发现是最后一个ret
指令,现在开始实现吧:
老样子查i386
手册
找到opcode
中0xc3
的位置,因为是空白,所以没有译码函数,直接执行。
/* 0xc0 */ IDEXW(gp2_Ib2E, gp2, 1), IDEX(gp2_Ib2E, gp2), EMPTY, EX(ret),
找到之前写call
指令的那个文件,开始写make_EHelper(ret)
函数
回想理论课中ret
指令做了些什么?将eip
退栈就行了,并且参照call
指令,还要设置跳转标志。
成功实现
对已实现指令增加标志寄存器行为
好像在实现第2个任务的时候,对照着i386
手册就直接把这些指令的符号位设置实现了,当时没看到第四个任务…
call
、push
、ret
、pop
指令不改变任何标志位
sub
、xor
指令改变除了IF
的其他标志位
这些都在任务2实现了
运行第一个客户程序
由于上面几个任务的完成,指令的实现,现在可以成功运行这个程序
不要忘记在all-instr.h
里面声明所有的指令执行函数!
实现 differential testing
在common.h
中把#define DIFF_TEST
的注释取消,讲义用了很长的篇幅来讲解这个东西,但是实现的话却只要一个if
语句
成功发现自己编写失败的指令
哭着回到第二步检查自己的sub
指令的问题…
从eflags
寄存器回来,再次测试
这回sub
没问题了,push
又出现了问题,然后回到第二步,之后就可以成功运行了。
利用 differential testing 检查已实现指令
在经历了任务6以后,成功实现对了所有的指令
PA2.1的内容到此结束,感谢您的耐心阅读,文中加黑加粗的写错的地方将会在后来的PA完成过程中更改。