写在前面的话
如果您对该系列感兴趣的话,推荐您先看一下南京大学的计算机组成原理实验(也就是PA)的讲义,然后再来看这篇文章可能有更多地收获。如果您是要完成该作业的学生,我推荐你先看讲义,或者好好听老师的讲课,然后自己独立完成这个作业,但是如果你没有听懂,或者你无论如何也无法理解讲义上面的字,又或者说对讲义上面的某点知识某个问题不了解而又觉得太简单不好意思问老师,那么您可能会从这篇文章里面获得一些你需要的信息。本篇文章将会包括笔者自己做PA的所有经过,希望你并不将该文章当成抄袭的根源,而是成为你思考的源泉。
现在已经到了PA2的阶段,这才真正开始了组成原理的道路,在PA0是搭建环境,PA1复习复习C语言,写写功能函数,而现在才开始真正去做计算机结构内部的东西,难度也上了一个档次。
PA2.2包含要实现的几十条机器指令,我将按照以运行不同文件的顺序来填补他们。由于此次任务较多,我也会写下我遇到的问题和大家共同交流。
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
目录
文章目录
- 写在前面的话
- PA系列传送门
- 目录
- 思考题
- 实验内容
- 实现更多的指令,启用已实现指令,逐个通过测试样例
- 在add.c里面:
- 在add-longlong.c里面
- 在bubble-sort.c里面
- 在fact.c里面
- 在leap-year.c里面
- 在load-store.c里面
- 在matrix-mul.c里面
- 在mov-c.c里面
- 在mul-longlong.c里面
- 在sub-longlong.c里面
- 在bit.c里面
- 在recursion.c里面
- 一键回归测试
- 加入IOE
- 实现输入输出指令,运行Hello World
- 实现 IOE 抽象
- 实现_uptime()
- 实现 _read_key()
- 画矩形函数
- 运行 timetest
- 看看 NEMU 跑多快
- 运行 keytest
- 添加内存映射I/O
- 运行 videotest
- 运行打字小游戏
- 遇到一些的问题以及解决方法
思考题
-
什么是 API?
API是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。
-
AM 属于硬件还是软件?
我认为AM属于软件,因为他并不是真实存在的,而是一个抽象的计算机模型,是理论化的,计算机都是通过这个模型来进行生产出来,变成真实的硬件。我认为他和操作系统比较相近,操作系统是运行在硬件上介于APP层的硬件管理程序,而AM也是运行在NEMU上的介于APP层之间的一个运行程序,相当于实现了和操作系统相同的功能。
-
为什么堆和栈的内容没有放入可执行文件里面
因为堆和栈中数据的变化比较频繁,如果放进可执行文件中读取速度会变慢。所以当程序运行的时候,从Memory里面来申请栈和堆的使用。
-
从你输入完这条命令敲击回车,直到出现 NEMU 的提示符
(nemu)
之间,都做了哪些工作?- 敲回车
ARCH=x86-nemu
:让AM上面的项目编译到x86-nemu
的 AM 中make ALL=dummy run
:用dummy作为测试样例,调用nexus-am/am/arch/x86-nemu/img/run
来启动 NEMU并载入dummy
这个用户程序进入运行- 出现(NEMU)
-
神奇的eflags(2)
+-----+-----+------------------------------------+ | SF | OF | 实例 | +-----+-----+------------------------------------+ | 0 | 0 | 2 - 1 | +-----+-----+------------------------------------+ | 0 | 1 | -1 - 0x80000000 | +-----+-----+------------------------------------+ | 1 | 0 | 1 - 2 | +-----+-----+------------------------------------+ | 1 | 1 | 0x7fffffff - -1 | +-----+-----+------------------------------------+
-
指令中的
above
,below
,greater
,less
这些大小关系词之间有什么关联呢op2>op1
,且是无符号数,触发above
,也就是ja
指令跳转op2<op1
,且是无符号数,触发below
,也就是jb
指令跳转op2>op1
,且是有符号数,触发greater
,也就是jg
指令跳转op2>op1
,且是有符号数,触发less
,也就是jl
指令跳转
-
NEMU的本质
-
label1: x = x - 1; y = y + 1; //x和y是两个加数,先通过一个数相减到零另外一个数相加的方式把x转移到y上面 jne y, label1 //一直到y等于0为止 label2: y = y - 1; a = a + 1; //现在y是x和y的和,只需要把x和y转移到a上面就行了。
-
NEMU还缺少友好的用户交互界面也就是输入输出、图形、视频等。
-
-
事实上,
EMPTY
与EX(?)
和EXW(?, ?)
都是等价的EMPTY
与EX(inv)
和EXW(inv, 0)
都是等价的 -
mov
指令后面的l
,b
,w
这些字符为什么会存在?仔细观察他们是在什么地方出现的,不加这些标识符会有什么后果?每个指令,准确地说,每个指令每次出现都需要用着几个字符标识吗?-
为了说明进行
mov
指令的操作数的宽度。 -
在
exec.h
文件中有如下定义#define suffix_char(width) ((width) == 4 ? 'l' : ((width) == 1 ? 'b' : ((width) == 2 ? 'w' : '?')))
不加这些标识符将会把进行
mov
指令操作数的宽度混淆,无法区别。 -
都需要
-
-
如果让你来设计 CPU 和设备的通信,你会如何实现呢?
从简单来讲,就是采用应答式的通信,CPU通过一系列的发送端口向相应的设备发出指令信号,设备接收之后执行自己的工作,执行结束或者遇到错误发送反馈信号给CPU的接收端口。
-
CPU 需要知道设备是如何工作的吗
不需要,CPU只需要将字节送到端口上,然后剩下的事情交给设备来干,如果有数据返回的话CPU需要知道这个反馈,我倒是认为操作系统需要知道设备是怎么工作的,这样设备出故障了以后才可以采取相关措施。在这期间,设备需要从CPU的输出端口上面读取相应的操作指令来完成它本身的工作。
-
什么是驱动
驱动程序全称设备驱动程序,是添加到操作系统中的特殊程序,其中包含有关硬件设备的信息。此信息能够使计算机与相应的设备进行通信。驱动程序是硬件厂商根据操作系统编写的配置文件,可以说没有驱动程序,计算机中的硬件就无法工作。
操作系统是用户和电脑之间的接口,而驱动程序是硬件可以运行的必要保障,打个比方,操作系统就像是人的生命一样,没有生命,电脑就是一个空的躯体,驱动,就是保证人在有生命的情况下,各个器官都可以正常工作。
-
CPU 在执行这条指令的时候需要知道自己要访存的地址是真正处于内存中还是经过重定向的吗
不需要,只需要把某段地址上的数据设定为指定的值就行了。
-
如果代码中的地址
0x8048000
最终被映射到一个设备寄存器, 去掉volatile
可能会带来什么问题反汇编:
去掉关键字后反汇编:
其实我感觉好像没啥区别,百度了一下,发现volatile去掉以后后面的几步就会省略,然后如果进行这些代码优化的话就会损失数据。
-
这个 hello 程序和我们在程序设计课编程写的第一个 Hello World 程序一样吗?如果不一样, 它们分别运行在计算机哪个层次中?
不一样。计算机中的Hello World 程序运行在硬件层,而我们这个hello程序运行在AM层
-
如何检测多个按键同时被按下
每个键盘对应一个键盘码,当这个键盘被按下去的时候,构成一个通路,发生按键事件,因为每个键盘的按键码是不同的,所以他们之间相互独立,互不影响,当我按下多个键盘的时候,计算机只需要找到对应达成通路的键盘位置来执行相应的操作就行了。
-
分别解释为什么会发生这些错误? 你有办法证明你的想法吗
去掉了很多
static
但是运行nemu
的时候并没有报错…但是去掉inline
以后就会报错。百度了一下inline
关键字的作用,用来把一些频繁使用的函数放到栈里面提高运行效率。所以去掉这个关键字后这个函数就没有定义在栈区,由于其他函数调用这个rtl
操作的时候仍然是在栈区寻找,并没有用到这个在栈区外面的函数,所以就会出现这种错误,显示这个函数定义了但是没有被使用。
而如果把他们两个都去掉的话,就会出现这种错误,生成.o
文件的时候出问题。也就是去掉了staitc
关键字后就会出现多重定义的问题。 -
重新编译后的 NEMU 含有多少个
dummy
变量的实体有29个。可以通过
grep -rn "dummy" |wc -l
指令来查看 -
此时的 NEMU 含有多少个
dummy
变量的实体有58个,多了29个,加上了
debug.h
里面的。 同样通过上面那条指令查看。 -
发现了什么问题
会有一个连接错误,他没初始化之前是若符号,所以不会报错,根据他之前定义的强符号处来处理,而定义了强符号也就是初始化之后呢就会出现两个强符号大家的情况,自然就是连接错误。
-
系统板保留 1K 个 I/O 端口, 那么系统 I/O 地址的范围是多少
1K个接口=2^10,每个端口是8个地址,那么地址就是0000H->8000H。变成16地址编线以后变成8001H-FFFFH。
-
有什么来自 CPU 的信号参与了设备的选通或控制
比如磁盘与内存间的信息交换,希望用硬件在外设与内存间直接进行数据交换,而不通过CPU,这样数据传送的速度的上限就取决于存储器的工作速度.但是,通常系统的地址和数据总线以及一些控制信号线(例如I/O等)是由CPU管理的,所以当CPU发出DMA响应信号之后,DMA控制器接管对总线的控制.在DMA方式时,CPU把这些总线让出来,由他接管,控制传送的字节数,判断DMA是否结束,以及发出DMA结束等信号.这些都是由硬件实现的.在DMA传送结束以后,他结束DMA请求信号,释放总线,使CPU恢复正常工作。
实验内容
实现更多的指令,启用已实现指令,逐个通过测试样例
跟PA2.1一样,还是实现指令,但是这回实现的指令就比之前的多多了,好在熟悉了它的过程,实现起来也没有那么无从下手了。
- Data Movement Instructions:
mov
,push
,pop
,leave
,cltd
(在i386手册中为cdq
),movsx
,movzx
- Binary Arithmetic Instructions:
add
,inc
,sub
,dec
,cmp
,neg
,adc
,sbb
,mul
,imul
,div
,idiv
- Logical Instructions:
not
,and
,or
,xor
,sal(shl)
,shr
,sar
,setcc
,test
- Control Transfer Instructions:
jmp
,jcc
,call
,ret
- Miscellaneous Instructions:
lea
,nop
一共要实现这么多指令,看了看这么多测试用例,先从那个开始实现呢?
在add.c里面:
8d
这个地址没有实现
查表发现是lea
指令
由于这个函数已经写好了是M2G译码函数,我们只需要填表即可
成功实现
在0x83
处出了问题
和2.1实现的sub
一样,要在grp1
里面找,查看反汇编是and
操作没有实现
第5个是and
,填表
在logic.c
里面写执行函数
make_EHelper(and) {
rtl_and(&t1, &id_dest->val, &id_src->val); //目的操作数与源操作数相与
operand_write(id_dest, &t1); //写入目的操作数
rtl_update_ZFSF(&t0, id_dest->width); //更新ZFSF位
t0 = 0;
rtl_set_OF(&t0); //设置OF位为0
rtl_set_CF(&t0); //设置CF位为0
print_asm_template2(and);
}
结果一运行发现and指令读取的立即数不对
最后定位到make_DopHelper(SI)
函数符号扩展的问题在PA2.1没有实现,当初想的太简单了,直接赋值了,没有考虑数据长度的问题。现在修正过来:
之后修改一下执行函数:
成功运行
查看0xff
处,在grp5
里面找,由反汇编得知是push
没有填表
填表
实现
查找0x66
处,查看反汇编,xchg
指令没有实现,查看手册发现是oprand_size
操作
10006a: 66 90 xchg %ax,%ax
但是发现这个操作已经被填表、声明、实现了,那问题在哪里呢?我看到了66后面的90地址,发现我的90地址处没有填入任何东西,先把90地址处填好吧,nop
指令,又是直接执行。
然后执行函数他也已经写好了,吃惊的是只有1个printf
,我看里面也没有写todo
,那就直接用把。
成功实现
查找0x03
处,是add
指令
可以很轻松的填表
操作很简单,但是符号位都要更新。模仿已经实现的adc
指令在arith.c
中实现add
成功实现
查看0x3b
处
填表,从0x38
到0x3d
都要实现
与减法的不同就在于他不改变结果,只改变eflags
,少了操作数的写入操作,在arith.c
里面开始实现
成功实现
查看0x0f
处和反汇编发现是set
操作
10008a: 0f 94 c0 sete %al
表中已经填好了。并且执行函数也在exec.c
里面给出来了。现在在2-byte表里查看0x94
地址处的相关操作
从0x90
开始到0x9f
全部都是set相关操作,全部填表,也是填在里面,手册勘误中
setcc
的长度为1,也就是8位。
大家都一样,译码函数是E,宽度虽然为2,但是手册上面写到first byte is 0FH
,所以我们操作的位数只有一位。
在logic.c
文件中找到了相应的执行函数
make_EHelper(setcc) {
uint8_t subcode = decoding.opcode & 0xf;
rtl_setcc(&t2, subcode);
operand_write(id_dest, &t2);
print_asm("set%s %s", get_cc_name(subcode), id_dest->str);
}
发现rtl_setcc
函数没有被实现。
在cc.c
文件里面找到了rtl_setcc
函数,从注释来看,让我查询每个eflag
来看看是不是满足条件代码。枚举类型不赋初值,第一个默认为0,往后依次加一。这就是讲义中说的哪个需要更新eflags
的操作。根据手册勘误来完成相关的操作!
void rtl_setcc(rtlreg_t* dest, uint8_t subcode) {
bool invert = subcode & 0x1;
enum {
CC_O, CC_NO, CC_B, CC_NB, //值从0到3
CC_E, CC_NE, CC_BE, CC_NBE, //值从4到7
CC_S, CC_NS, CC_P, CC_NP, //值从8到11
CC_L, CC_NL, CC_LE, CC_NLE //值从12到15
};//枚举的值和subcode的字节对应
// TODO: Query EFLAGS to determine whether the condition code is satisfied.
// dest <- ( cc is satisfied ? 1 : 0)
switch (subcode & 0xe) {
case CC_O://0
rtl_get_OF(dest);
break;
case CC_B://2
rtl_get_CF(dest); //小于,通过CF来判断不够减
break;
case CC_E://4
rtl_get_ZF(dest);
break;
case CC_BE: { //6
rtl_get_CF(&t0);
rtl_get_ZF(&t1);
rtl_or(dest, &t0, &t1); //小于等于,CF和ZF至少一个要等于1才行
}break;
case CC_S: //8
rtl_get_SF(dest);
break;
case CC_L: { //12
rtl_get_SF(&t0);
rtl_get_OF(&t1);
rtl_xor(dest, &t1, &t0); //带符号数的小于,SF不能等于OF
}break;
case CC_LE: { //14
rtl_get_ZF(&t0);
rtl_get_SF(&t1);
rtl_get_OF(&t2);
rtl_xor(&t3, &t1, &t2);
rtl_or(dest, &t0, &t3); //带符号数的小于等于,ZF=1或者SF不等于OF
}break;
default: panic("should not reach here");
case CC_P: panic("n86 does not have PF");
}
if (invert) {
rtl_xori(dest, dest, 0x1);
}
}
成功实现
查看2-byte表的0xb6
处,movzx
指令没有实现
将0xb6
和0xb7
处填表,一个宽度为1,一个宽度为2
执行函数相当于是一个0扩展并且不影响符号位,已经写好了。声明一下就可以了。成功实现
查看0x85
处,是test
指令没有实现
填表,0x84
是一个字节,0x85
是变长字节,译码函数G2E
在logic.c
里面写执行函数
成功实现
查看0x74
处,查看反汇编,是jcc
指令没有实现
100034: 74 02 je 100038 <nemu_assert+0xc>
执行函数在control.c
已经实现好了,直接声明+填表就行了。从0x70
开始这一行都填好表,译码函数应该是J
成功实现
查看0x83
处,再查看grp1
,最后看反汇编是add操作,发现自己grp1里面的add没有填表
填表即可
make_group(gp1,
EX(add), EMPTY, EMPTY, EMPTY,
EX(and), EX(sub), EMPTY, EMPTY)
成功实现
还是0x83
处,这回是grp1
的cmp
指令,照样填表,成功实现
make_group(gp1,
EX(add), EMPTY, EMPTY, EMPTY,
EX(and), EX(sub), EMPTY, EX(cmp))
查看0x6a
处,立即数的push操作
填表,译码函数是push_SI
,执行函数还是push
。
运行之后会收到operand_write
报错。检查自己的push
函数,当初2.1写的其实是有问题的。push就应该是简简单单的压栈保存操作,没有写入操作数的过程,就像这里,如果写入操作数的话,函数会找不到操作数的种类而报错。其他的像pop、add指令才是有写入操作数的。将6a和68处全部写好,可以看到,68处是变长,6a处是1个字节
更改函数之后成功实现该指令
最后成功跑通这个文件
在add-longlong.c里面
查看手册,是adc
指令没有实现。
从0x10
到0x15
,根据长度来进行填表,adc
执行函数写好了,直接声明就行了。
成功实现
查看0x09
处,or
指令没有实现
从08开始到0D都要填表,注意操作数长度,常规操作。
根据手册实现执行函数
成功实现,并且完成了add-longlong
文件的运行
在bubble-sort.c里面
在这里就遇到了一个问题,也是一个常见问题,jcc
指令运行的时候eip
的值不对
通过PPT上面讲的各种情况依次判断,首先查看表有没有填错,对照反汇编代码进行比较看看有没有不同的地方,比较发现没有,然后看看其他的指令的eflags
有没有写错,毕竟jcc
是根据eflags
的值来进行跳转的。经过比较发现,先查看jcc
上面一条cmp
指令的SF、ZF、OF位在执行这条指令的前后发生的变化
此时我们就可以判断SF位没有写对,因为我们传入了一个负数,SF位应该是1,但是SF位没变。然后检查rtl_SF
函数,发现2.1写的这个函数有问题,取到的符号位是错的,更改这个函数,通过右移相应位数来找到符号位。
还有一个需要注意的问题就是,写这个rtl
的时候,结果不要用一个变量来存,就比如说我传进rtl_update_SF
这个函数的result
参数是t0
,而在这个函数里面我仍然用t0
储存result
的符号位,那么设置SF位的时候是没问题的,但是如果在cmp
函数里面更新完SF后又要用这个t0
来做其他的操作,那么就会出错了。之前我就犯了这个错误,后来把他改成了没人用的t4
。ZF位也是同样的道理。说到ZF位,其实之前直接用那种[width * 8 - 1]
的放来来取数组相应位置是非常不科学的,之前没出错可能是因为碰巧吧,根本就没有考虑长度的问题。现在修正一下,一起修正的还有rtl_msb
函数,也是用的这种方式,不能直接对数组取这样的位,而是应该通过右移来解决。
解决了SF的问题,现在成功运行
在fact.c里面
查看0x43
处,inc
指令没有实现,填表,都是对寄存器的操作,所以长度为4
这是个巨坑!!!!!这里写的有问题!PA3.1会进行改正
查看操作非常简单,符号位更新ZF、SF和OF。
成功实现
查找0xeb
,jmp
指令没有实现,根据长度填表,
IDEX(J, call), IDEX(J,jmp), EMPTY, IDEXW(J,jmp,1),
指令已经实现,填表后成功运行
查看0x0f
处,imul
指令没有实现
1000b0: 0f af d0 imul %eax,%edx
在2-byte表里面相应位置填
EMPTY, EMPTY, EMPTY, IDEX(E2G, imul2),
查看执行函数可以知道,imul
指令分好几个操作数有不同的执行函数,这里我们实现两个操作数的。声明以后就可以实现了。
查看0x48
处,dec指令没有实现
对32位寄存器操作,宽度为4,开始填表
IDEX(r,dec), IDEX(r,dec), IDEX(r,dec), IDEX(r,dec),
IDEX(r,dec), IDEX(r,dec), IDEX(r,dec), IDEX(r,dec),
在我们这里没有AF,所以实现自减然后更新符号位就行了,模仿之前实现的inc
成功实现并且跑通这个函数
在leap-year.c里面
查找0xf6
处,是grp3
里面的test指令
之前已经实现了test,相应位置填表就行了。
make_group(gp3,
IDEX(test_I,test), EMPTY, EMPTY, EMPTY,
EMPTY, EMPTY, EMPTY, EMPTY)
成功实现
查找0x99
处1000db: 99 cltd
,cltd
指令没有实现,先填表
把cwtl
和cltd
都填了好了,他们两个都没有译码函数,直接执行
EX(cwtl), EX(cltd), EMPTY, EMPTY,
这个指令相对来说很简单,就是简单的将eax
或者ax
寄存器的值符号扩展32位到edx
寄存器,也就是说,如果eax
或ax
寄存器的二进制序列的最高位为0,则cltd
指令就把edx
或者dx
置为0,相反.如果eax
寄存器的二进制序列最高位为1,则cltd
指令将会自从填充edx
寄存器为32个1.判断decoding.is_operand_size_16
的值,根据0和1这两种情况在data-mov.c
里面开始实现
cwtl
指令也是同样的操作,跟ctld
差不多,都是一个if-else
,还省去了右移的操作,直接读出来然后符号扩展就行了。只不过针对的是al
、ax
和eax
这三个寄存器。
成功实现
查看0xf7
处,1000dc: f7 fe idiv %esi
,idiv
操作没有实现。这个也是直接填表然后声明就行了。为了方便,这次把所有grp3里面已经实现的执行函数的表全部填完
make_group(gp3,
IDEX(test_I,test), EMPTY, EMPTY, EMPTY,
EX(mul), EX(imul1),EX(div), EX(idiv)) //imul在这里操作数只有一个
成功实现
并且成功跑通这个函数
在load-store.c里面
查看反汇编和手册,100063: 0f bf 83 c0 01 10 00 movswl 0x1001c0(%ebx),%eax
是movsx
指令没有实现译码函数是
mov_E2G
,执行函数就是已经写好的movsx
,0xbe
和0xbf
这两个地方宽度不同,现在填表
成功实现
查看0xd3
处1000ec: d3 e0 shl %cl,%eax
左移操作没有实现,这次把左移右移和grp2
中的带符号右移sar
全部实现,方便以后。
在grp2
里面填表
这三个操作都不需要更新CF位和OF位,简直省了一半的力气,手册里面把这几个指令的操作写在一起了。而且几乎全部都是符号位的操作,所以我就根据之前已经实现的rtl指令来写,好在这三个指令的rtl操作都已经定义好,直接用就行了。写好一个函数之后,其他两个函数也是照葫芦画瓢。
成功实现
查表得知这次是not
指令填表没有译码函数,直接执行。
make_group(gp3,
IDEX(test_I,test), EMPTY, EX(not), EMPTY,
EX(mul), EX(imul1),EX(div), EX(idiv)) //imul在这里操作数只有一个
这个操作很好实现,说白了就是按位取反,然后不影响任何的标志位。
成功实现并且完美运行load-store.c
在matrix-mul.c里面
长跳转指令没有实现,其实就是没有填表,之前已经实现了短跳转指令,长度为变长。
成功实现并且成功运行程序
在mov-c.c里面
实现指令,没有译码函数,直接执行
在data-mov.c
里面实现这个函数,leave
指令是将栈指针指向帧指针,然后POP备份的原帧指针到%ebp
,所以leave
等价于
movl %ebp %esp
popl %ebp
这样就好实现了,利用rtl指令很快就可以写出来。
成功实现并且运行这个文件
在mul-longlong.c里面
sub指令没有填表,根据长度和译码函数来填表,从28到2d
成功实现并且运行
在sub-longlong.c里面
sbb
指令没有填表,根据长度和译码函数填表
成功实现并运行
在bit.c里面
查看手册,是在grp4
里面的inc
和dec
没有实现。直接填表就行。宽度都是1。
成功实现
查看手册,and
没有填表
填表
成功运行
在recursion.c里面
在grp5
里面有一个call
指令,没有填表
查看反汇编10011a: ff 15 f0 01 10 00 call *0x1001f0
,这个call有点奇怪,要从内存里面读出来地址,然后再跳转。这是转移地址在内存中的call
指令。在control.c
文件里面找到了call_rm
操作需要我写。
看到反汇编中还有100099: ff 25 e8 01 10 00 jmp *0x1001e8
1000c1: ff 05 fc 01 10 00 incl 0x1001fc
等操作,为了方便就把inc
和dec
还有jmp
和jmp_rm
也都填表。
汇编格式为call word/dword ptr 内存单元地址
,其实现的过程大概可以分成这几条
push CS #将call指令的下一条指令的段地址
push IP #及偏移地址入栈
jmp ... #转到子程序去执行
对照着上面的jmp_rm
和call
,其实就是他们两个结合起来的操作。
成功实现并运行
在all-instr.h
中声明所有的函数
一键回归测试
经过上面指令的实现,现在可以通过一键回归测试了
加入IOE
打开#define HAS_IOE
出现窗口
实现输入输出指令,运行Hello World
先在all-instr.h
中声明
在手册中找到in
和out
的位置,从e4->e7
,从ec->ef
根据指令宽度和译码函数填表,这两个指令有专门的译码函数,执行函数是in
和out
。长度只有两种,变长和b
在system.c
里面开始写执行函数,out
要调用 pio_write()
函数,in
要调用 pio_read()
函数并且还要有写入操作数的行为。查看这两个调用的函数,发现 pio_read()
还要有一个data
的返回值,所以需要通过这个返回值来写入操作数。
打开HAS_SERIAL
宏
这时运行出现了问题,遇到了未知的操作
查看手册发现是test指令没有填表
填表
成功实现
实现 IOE 抽象
实现_uptime()
在上课的时候助教讲过,实现这个时间需要通过一个减法,通过0x48
处的端口作为RTC
寄存器获得的当前时间减去NEMU初始化时候的“当前”时间,就会获得这个时间差。
在ioe.c
里面的_uptime()
函数里添加这样的代码:
实现 _read_key()
在ioe.c
里面找到相关代码,返现只有一个返回值是_KEY_NONE
,全局搜索这个返回值,在SPEC.md
中发现按键代码由_KEY_XXX
指定,其中_KEY_NONE = 0
。所以我要根据我按键的不同来返回不同的按键代码。讲义上面讲到初始化时会注册 0x60
处的端口作为数据寄存器, 注册 0x64
处的端口作为状态寄存器,那么我们按键的状态就在0x64
这个端口的寄存器中储存,而通码的值为 断码 + 0x8000
。
画矩形函数
void _draw_rect(const uint32_t *pixels, int x, int y, int w, int h) {
int len = sizeof(uint32_t) * ( (x + w >= _screen.width) ? _screen.width - x : w );
uint32_t *p_fb = &fb[y * _screen.width + x];
for (int j = 0; j < h; j ++) {
if (y + j < _screen.height) {
memcpy(p_fb, pixels, len);
}
else {
break;
}
p_fb += _screen.width;
pixels += w;
}
}
感觉这个函数很复杂,我只能大概讲出来他干了些什么事情…首先要判断这个x+w是不是超出了屏幕的边界,然后呢通过一个大循环来把每个像素的值memcpy
也就是复制到他对应的内存里面,然后显示出来,通过p_fb
和pixels
的不断变化来实现这种动画效果。
运行 timetest
成功在NEMU中运行 timetest
程序,每秒钟输出一句话。
看看 NEMU 跑多快
注释掉DEBUG
和 DIFF_TEST
宏
-
dhrystone (大家的电脑跑多少分呢)
用x86-nemu
跑
-
coremark
用x86-nemu
跑
-
microbench
大规模测试集
ref
小规模测试集test
:make INPUT=TEST run
跑x86-nemu
的时候出了些问题,neg
指令没有实现。求补运算,说实话这个命令挺少见到过的。从手册中可以得知,当操作数为0时,置CF位为0,当操作数不为0时,置CF位为1。现在我需要把目的操作数的所有数据位取反。注意这里的取反不是按位取反,而是x->-x
,
然后发现rol
指令没有实现,在grp2
里面,先填表,发现没有他的执行函数,然后现在来重新在logic.c
里面写执行函数。
左移一次,左移前的最高位送入最低位以及CF,也就是左边移出的位补到右边。与RCL
不同之处在于他没有进位。所以我们需要左移和右移操作,并且最后要写入操作数。一开始我把所有的位数都先左移出来,然后想一下子赋值,结果发现并不能实现,所以就考虑for循环的情况,每次取出来最高位给一个变量赋值,然后这时候目的操作数左移一位,此时这个数的最后面一位变成了0,这个时候我赋值的这个变量的值只有两种情况,对应最高位的两种情况:0或者1,之后通过异或操作,把这个变量与目的操作数异或就达到了目的。
最后跑分:
运行 keytest
运行 keytest
程序
添加内存映射I/O
讲义上面已经写的很详细了,就是加一个if-else
语句就行。
mmio_id <- is_mmio(???);
if (mmio_id != -1) {
调用 mmio_read/write(???);
返回读到的映射内存数据(写映射内存则返回空即可);
}
由于memory.c
里面只包含了nemu.h
这个头文件,所以要新增加头文件mmio.h
运行 videotest
运行 videotest
程序
运行打字小游戏
在typing
目录下运行打字小游戏,如果是虚拟机可能没有那么流畅,如果小于3FPS就说明前面实现的可能有问题,效率比较低。
遇到一些的问题以及解决方法
-
遇到问题:实现and指令的时候nemu打出来的反汇编的立即数是
0xf0
而反汇编文件里面是0xfffffff0
解决方案:在make_DoHelper函数里面没有实现符号扩展,由于默认是0扩展,所以实现失败。
-
遇到问题:实现
push %0x1
指令的时候operand_write函数报错assert(0)
被触发。解决方案:push就应该是简简单单的压栈保存操作,没有写入操作数的过程,就像这里,如果写入操作数的话,函数会找不到操作数的种类而报错。其他的像pop、add指令才是有写入操作数的。去掉push函数里面的写入操作数操作。
-
遇到问题:
diff-test
报错,jcc
指令的eip值不匹配。解决方案:通过PPT上面讲的各种情况依次判断,首先查看表有没有填错,对照反汇编代码进行比较看看有没有不同的地方,比较发现没有,然后看看其他的指令的
eflags
有没有写错,毕竟jcc
是根据eflags
的值来进行跳转的。经过比较发现,jcc
上面一条cmp
指令出现了问题,SF位没有写对,然后检查rtl_SF
函数,发现2.1写的这个函数有问题,取到的符号位是错的,更改这个函数,通过右移相应位数来找到符号位。成功解决。 -
遇到问题:键盘测试总是无法识别相应的键盘,按下去键盘什么都不会输出
解决方案:在
keytest
的程序里面通过printf
来确定位置,最后发现程序并没有执行_read_key
这个函数,打字小游戏都没问题,但是键盘测试一直都接收不到。最后找了很长的时间,疯狂排除错误仍然不知道问题在哪个地方,在可能出现问题的地方到处都调试了然后发现实现的没有问题,一度陷入了深深的悲伤之中。结果一天上课碰巧提起来了这个问题,一跟同学描述我错误的过程,然后同学告诉我应该在make run
之后弹出来的窗口里面打字,而不是在终端里面的nemu
里按键。一下子一语点醒梦中人!我之前所有的键盘的测试,全部都是在nemu
里面输入,而这些输入的东西是被当做跟si
、info
等一样的指令来执行的。而现在我应该在编写好的这个keytest
里面按键来测试程序。回去一试,果然可以成功接收。
PA2.2和PA2.3的部分到这里就结束了,篇幅非常长,感谢您的耐心阅读。