南京航空航天大学《计算机组成原理Ⅱ课程设计》
报告
姓名:方建军
班级:1617203
学号:161720326
报告阶段:PA3.1
完成日期:2019.6.8
本次实验,我完成了所有内容
目录
南京航空航天大学《计算机组成原理Ⅱ课程设计》报告
目录
思考题
实验内容
遇到的问题及解决办法
心得体会
其他备注
思考题
1. 什么是操作系统?
操作系统(英语:operating system,缩写作 OS)是管理计算机硬件与软件资源的计算机程序,同时也是计算机系
统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、
操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。
2. 我们在 PA2 中对 cputest 和部分设备测试的程序直接运行在 AM 之上,那么和 Nanos-lite 是运行在 AM 之上
的 有什么差距呢?我们可以把 Nanos-lite 看作一个和 PA2 中这些测试用例同等地位的一个 AM 程序吗?
权限不一样,不可以
3. 我们把 PA2 中运行的这些测试用例的运行方式称作“直接运行在硬件上的用户程序”,经过上个思考题的思考,
请你总结一下操作系统的实质:操作系统就是一个较为大型的( ),它和直接运行在硬件上的程序( )(有/无)实质差
别。
用户程序,无
4. 我们曾经问过这样一个问题:程序设计课上老师说程序运行到 main() 函数的 return 0; 语句,程序就结束
了,是真的吗?现在再回想一下,当时自己的回答是否正确。程序在进入 main 函数前和退出 main 函数后,都
做了什么工作?如果你还不能理解,请结合 _start() 及其中调用过的函数或过程再思考一下。
进入之前先将寄存器等内容入栈,出来时在恢复进入main函数前的状态
5.我们发现,系统调用会根据系统调用号在 IDT 中索引,取得该调用号对应的系统调用服务程序的地址,并跳转过
去。在触发系统调用前,会保护用户相关状态寄存器(EFLAGS, EIP等)到栈中,系统调用完毕后再恢复,这和我们
所学过的什么的过程非常相似?我们可以将系统调用的服务程序理解为一个比较特殊的“函数”吗?请说明理由。这
个“服务程序”和我们的“用户编写的函数”有什么不同?
与函先将数的过程调用类似,可以,因为在调用前都要做准备工作,先将一些寄存器内容入栈,调用完后再恢复
6.我们在 GNU/Linux 下编写了带有 bug 的程序,虽然能通过编译,但在运行时却遇到了“Segmentation Fault”(段
错误),为什么在编译阶段不能发现这些潜在的段错误呢?段错误通常是由哪些程序行为引起的?
段错误一般是当你访问了未申请的内存或非法的内存时产生的。主要还是程序的内存管理有问 题。编译时无法发现
7.我们知道进行函数调用的时候也需要保存调用者的状态:返回地址,以及调用约定(calling convention)中需要
调用者保存的寄存器。而进行异常处理之前却要保存更多的信息。尝试对比它们,并思考两者保存信息不同是什么原
因造成的。
函数调用没有保存cs等,因为异常处理可能涉及到多层函数调用
8. trap.S 中有一行 pushl %esp 的代码,乍看之下其行为十分诡异。你能结合前后的代码理解它的行为吗?Hint:
不用想太多,其实都是你学过的知识。
将陷进帧压栈
9.事件号和系统调用号有什么区别?
事件代表事件编号,而系统调用号代表事件所触发系统调用的编号
10.我们曾经在编写各种 C 语言程序时,遇到过这种情况:
#include
int main(){
int *p = NULL;
printf("I am here!");
*p = 10; // giving value to a NULL address will cause segmentation fault
return 0;
}
程序在运行到 *p = 10; 时出现了段错误,但是 I am here! 这个字符串却并没有被打印到屏幕上,这是为什么?
怎么修改上述代码使得能在 *p = 10; 出现段错误时,仍能打印出 I am here! 这个字符串?如下面的操作所示:
$ vim test.c # type code above into test.c
$ gcc test.c # compile test.c to ./a.out
$ ./a.out # execute ./a.out and a Segmentation fault will be reported
Segmentation fault
$ vim test.c # how to make some modification to let string get printed?
$ gcc test.c # compile test.c again
$ ./a.out # execute ./a.out again and the supposed string was printed
I am here!
Segmentation fault
请你在输入上述程序时,务必不要出现任何手误,确保是原样输入(当然,你无需输入注释内容),否则你将有可能
意外地不会出现此处的段错误。请你结合讲义内容和自己的实验,尝试找出解决方案,并解释其中的原因。
解决方案:将printf放到指针定义前面,
出现原因:C语言在定义指针变量时会先实现内存分配
11.方便起见,我们在更新后的框架代码中替大家实现了几个文件管理函数。那么,请你参考这些 fs_ 开头的函数,
思考一下它们是如何编写出来的
利用一些文件的基本知识
12.网上流传着一些关于仙剑奇侠传的秘技,其中的若干条秘技如下:
1. 很多人到了云姨那里都会去拿三次钱,其实拿一次就会让钱箱爆满!你拿了一次钱就去买剑把钱用到只剩一千
多,然后去道士那里,先不要上楼,去掌柜那里买酒,多买几次你就会发现钱用不完了。
2. 不断使用乾坤一掷(钱必须多于五千文)用到财产低于五千文,钱会暴增到上限,如此一来就有用不完的钱
了。
3. 当李逍遥等级到达 99 级时,用 5~10 只金蚕王,经验点又跑出来了,而且升级所需经验会变回初期 5~10 级内
的经验值,然后去打敌人或用金蚕王升级,可以学到灵儿的法术(从五气朝元开始);升到 199 级后再用 5~10
只金蚕王,经验点再跑出来,所需升级经验也是很低,可以学到月如的法术(从一阳指开始);到 299 级后再
用 10~30 只金蚕王,经验点出来后继续升级,可学到阿奴的法术(从万蚁蚀象开始)。
假设这些上述这些秘技并非游戏制作人员的本意,请尝试解释这些秘技为什么能生效。
相关数据发生了溢出
13.文件读写的具体过程 仙剑奇侠传中有以下行为:
在 navy-apps/apps/pal/src/global/global.c 的 PAL_LoadGame() 中通过 fread() 读取游戏存档
在 navy-apps/apps/pal/src/hal/hal.c 的 redraw() 中通过 NDL_DrawRect() 更新屏幕
请结合代码解释仙剑奇侠传、库函数、libos、Nanos-lite、AM、NEMU 是如何相互协助,来分别完成游戏存档的读
取和屏幕的更新。
游戏存档:库函数读取存档向Nanos-lite申请系统调用,Nanos-lite在转到AM实现,nemu底层实现
屏幕更新同上。
实验内容
1.实现loader
实现简单 loader,触发未实现指令 int:
代码:
ramdisk_read(DEFAULT_ENTRY,0,get_ramdisk_size());
return (uintptr_t)DEFAULT_ENTRY;
代码截图:
思想:直接调用ramdisk_read函数即可
实现引入文件系统后的 loader:
代码:
uintptr_t loader(_Protect *as, const char *filename) {
//ramdisk_read(DEFAULT_ENTRY,0,get_ramdisk_size());
int fd = fs_open(filename, 0, 0);
fs_read(fd, DEFAULT_ENTRY, fs_filesz(fd));
fs_close(fd);
return (uintptr_t)DEFAULT_ENTRY;
}
代码截图:
思想:用fs_open函数打开要加载的文件,用fs_read函数读取即可
2.添加寄存器和 LIDT 指令
根据 i386 手册正确添加 IDTR 和 CS 寄存器:
在reg.h里添加CS和IDTR寄存器即可
截图:
在 restart() 中正确设置寄存器初始值:
设置即可
代码:
static inline void restart() {
/* Set the initial instruction pointer. */
cpu.eip = ENTRY_START;
cpu.eflags = 0x00000002;
cpu.cs = 0x8;
#ifdef DIFF_TEST
init_qemu_reg();
#endif
}
截图:
实现LIDT指令:
代码:
make_EHelper(lidt) {
cpu.idtr.limit = vaddr_read(id_dest->addr, 2);
if (decoding.is_operand_size_16)
cpu.idtr.base = vaddr_read(id_dest->addr + 2, 3);
else
cpu.idtr.base = vaddr_read(id_dest->addr + 2, 4);
print_asm_template1(lidt);
}
代码截图:
思想:根据i386手册实现即可,并添加译码表
3.实现 INT 指令
代码:
raise_intr:
void raise_intr(uint8_t NO, vaddr_t ret_addr) {
/* TODO: Trigger an interrupt/exception with ``NO''.
- That is, use ``NO'' to index the IDT.
*/
union {
GateDesc gd;
struct{uint32_t lo,
hi;
};
}item;
vaddr_t addr;
addr=8*NO+cpu.idtr.base;
item.lo=vaddr_read(addr,4);
item.hi=vaddr_read(addr+4,4);
decoding.is_jmp=1;
decoding.jmp_eip=(item.gd.offset_15_0&0xFFFF)|((item.gd.offset_31_16&0xFFFF)<<16);
rtl_push(&cpu.eflags);
t0 = cpu.cs;
rtl_push(&t0);
rtl_push(&ret_addr);
cpu.IF=0;
}er函数:
helper函数:
make_EHelper(int) {
raise_intr(id_dest->val, decoding.seq_eip);
print_asm("int %s", id_dest->str);
#ifdef DIFF_TEST
diff_test_skip_nemu();
#endif
}
截图:
思想:根据i386手册实现即可,并添加译码表
4.实现其他相关指令和结构体
组织 _RegSet 结构体:
代码:
struct _RegSet {
uintptr_t edi, esi, ebp, esp, ebx, edx, ecx, eax;
int irq;
uintptr_t error_code, eip, cs, eflags;
};
截图:
思想:根据trap frame结构体形成顺序来组织顺序
pusha指令:
代码:
make_EHelper(pusha) {
if (decoding.is_operand_size_16) {
t0 = cpu.esp;
rtl_lr_w(&t1, R_AX);
rtl_push(&t1);
rtl_lr_w(&t1, R_CX);
rtl_push(&t1);
rtl_lr_w(&t1, R_DX);
rtl_push(&t1);
rtl_lr_w(&t1, R_BX);
rtl_push(&t1);
rtl_push(&t0);
rtl_lr_w(&t1, R_BP);
rtl_push(&t1);
rtl_lr_w(&t1, R_SI);
rtl_push(&t1);
rtl_lr_w(&t1, R_DI);
rtl_push(&t1);
}
else {
t0 = cpu.esp;
rtl_lr_l(&t1, R_EAX);
rtl_push(&t1);
rtl_lr_l(&t1, R_ECX);
rtl_push(&t1);
rtl_lr_l(&t1, R_EDX);
rtl_push(&t1);
rtl_lr_l(&t1, R_EBX);
rtl_push(&t1);
rtl_push(&t0);
rtl_lr_l(&t1, R_EBP);
rtl_push(&t1);
rtl_lr_l(&t1, R_ESI);
rtl_push(&t1);
rtl_lr_l(&t1, R_EDI);
rtl_push(&t1);
}
print_asm("pusha");
}
截图:
popa指令: