计算机组成原理Ⅱ课程设计 PA3.1
目录
思考题
-
什么是操作系统?
我认为操作系统是运行在硬件之上,管理硬件,提供一系列操作硬件的API,供程序使用。因为我自己用Windows比较多,对比Linux,最直观感受是Windows提供的图形界面方便了用户使用,使计算机进入了平常百姓家,而不只仅局限于技术人员。
-
我们不⼀样,吗?
nanos-lite
在调用AM
的API的基础上,实现了更多的功能和接口,例如文件系统、终端异常处理、加载器等。我认为他们是同等地位。
-
操作系统的实质
程序
无
-
程序真的结束了吗?
main函数执行之前,主要就是初始化系统相关资源:
-
设置栈指针
-
初始化static静态和global全局变量,即data段的内容
-
将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容
-
运行全局构造器,估计是C++中构造函数之类的吧
-
将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数main
main函数执行之后:
- 获取main的返回值
- 调用exit退出程序
int main(int argc, char *argv[], char *envp[]); __attribute__((section(".text.unlikely"))) void _start(int argc, char *argv[], char *envp[]) { int ret = main(argc, argv, envp); exit(ret); asser(t0); }
https://blog.csdn.net/wang_jing_2008/article/details/7946450
-
-
触发系统调用
编写了一个程序
hello.c
const char str[] = "Hello world!\n"; int main() { asm volatile ("movl $4, %eax;" // system call ID, 4 = SYS_write "movl $1, %ebx;" // file descriptor, 1 = stdout "movl $str, %ecx;" // buffer address "movl $13, %edx;" // length "int $0x80"); return 0; }
编译运行
gcc hello.c -o hello -m32
结果:成功输出
-
有什么不同?
与函数调用过程十分类似。函数调用时,获取调用函数的起始地址,并跳转过去。在触发函数调用前,会保存相关寄存器到栈中,函数调用完毕后再恢复。
可以。系统调用会根据系统调用号在 IDT 中索引,取得该调用号对应的系统调用服务程序的地址,并跳转过去。在触发系统调用前,会保护用户相关状态寄存器(EFLAGS, EIP等)到栈中,系统调用完毕后再恢复。这个过程与函数调用的过程基本一致,因此可以认为系统调用的服务程序理解为一个比较特殊的“函数”。
“服务程序”的工作都是硬件自动完成的,不需要程序员编写指令来完成相应的内容。在函数运行的过程中遇到异常,才会触发。由于“服务程序”和“用户程序”使用的是不同的堆栈,因此在触发“系统程序”时,涉及到堆栈的切换。
-
段错误
编译的过程是吧高级语言转化为低级语言的过程,期间会检查语法,但是具体程序中指令会跳转到什么地方,编译阶段不负责检查。只有在程序执行中,访问到不访问的地方时,才会触发段错误。
段错误就是指访问的内存超出了系统所给这个程序的内存空间。基本是是错误地使用指针引起的:
- 访问系统数据区
- 内存越界,访问到不属于程序的内存区域(数组越界,变量类型不一致)
- 多线程程序使用了不安全的函数
- 多线程读写数据未加锁保护
- 堆栈溢出
http://www.360doc.com/content/18/0331/16/21305584_741796830.shtml
-
对比异常与函数调用
异常
保存寄存器、错误码#irq
、 EFLAGS、CS、 EIP,形成了 trap frame(陷阱帧)的数据结构。函数调用
调用者保存寄存器和被调用者保存寄存器(不一定保存)在异常处理的时候已经切换了栈帧,所以要保存更多的信息。
-
诡异的代码
以指向trapframe 内容的指针(esp)作为参数,调用trap函数。把
eip
作为入口参数传进去,然后在执行irq_handle
这个函数之前,通过pusha
,在栈帧中形成了_RegSer
这个结构体,把eip
作为一个结构体的起始地址,通过成员irq
来分发事件。 -
注意区分事件号和系统调用号
事件号:标明我们一个未实现的系统调用事件的编号。
系统调用号:在识别出系统调用事件后,从寄存器中取出系统调用号和输入参数,根据系统调用号查找系统调用分派表,执行相应的处理函数,并记录返回值。
-
打印不出来?
printf打印是行缓冲,读取到的字符串会先放到缓冲区里,直到一行结束或者整个程序结束,才输出到屏幕,因为我们打印的字符串一行没有结束,所以就先执行后面的
*p=NULL
报错了。因此,我们要让他输出字符串内容,只需要在字符串后面加上
\n
就表明一行结束,可以输出了。#include <stdio.h> int main(){ int *p = NULL; printf("I am here!\n");//这里加上换行 *p = 10; return 0; }
-
理解文件管理函数
fs_open
:按照文件名在file_table
里面搜索,若匹配到对应的文件名,则把该文件的读写指针标为0并返回文件的位置(即第几个index);若未匹配到,则报错并返回-1。fs_read
:若文件标号小于2,报错。若fd为FD_EVENTS
,则调用events_read()
读取指定位置指定长度的内容并返回。根据fd计算文件开始的位置,然后计算该文件的剩余字节数remain_bytes
,并根据fd选择读取方式。最后更新读写指针。fs_write
:根据fd计算文件开始位置,并计算该文件的剩余字节数remain_bytes
,根据fd不同,选择不同的写方式,最后更新读写指针。fs_lseek
:根据fd计算文件开始位置,获取文件的读写指针位置和文件大小,然后根据whence
选择不同方式来对new_offset
更新。若更新后,new_offset
小于0或者大于文件大小,则把其置为0或文件大小并返回。fs_close
:返回0 -
不再神秘的秘技
游戏bug,开发时没有注意变量类型等问题,造成在某些特定情况下,会出现溢出现象。
-
必答题
存档读取
:PAL_LoadGame()先打开指定文件然后调用fread()从文件里读取存档相关信息(其中包括调用nanos.c里的_read()以及syscall.c中的sys_raed()),随后关闭文件并把读取到的信息赋值(用fs_write()修改),接着使用AM提供的memcpy()拷贝数据,最后使用nemu的内存映射I/O修改内存。更新屏幕
:redraw()调用ndl.c里面的NDL_DrawRect()来绘制矩形,NDL_Render()把VGA显存抽象成文件,它们都调用了nan0s-lite
中的接口,最后nemu把文件通过I/O接口显示到屏幕上面。 -
git log
和git branch
截图
实验内容
实现 loader
1.实现简单 loader,触发未实现指令 int
根据讲义,我们只需要用到 ramdisk_read 函数,其中第一个参数填入 DEFAULT_ENTRY
,偏移量为 0,长度为 ramdisk 的大小即可。
在loader.c
中,别忘了声明外部函数
extern void ramdisk_read(void *buf, off_t offset, size_t len);
extern size_t get_ramdisk_size();
更新loader()
uintptr_t loader(_Protect *as, const char *filename) {
ramdisk_read(DEFAULT_ENTRY,0,get_ramdisk_size());
return (uintptr_t)DEFAULT_ENTRY;
}
2.实现引入文件系统后的 loader
- 首先用fs_open()根据文件名获取文件位置
- 再用fs_filesz()获取文件大小
- 用fs_read()读取指定位置指定长度的内容
- 最后用fs_close()关闭文件
//读取文件位置
int index=fs_open(filename,0,0);
//读取长度
int length=fs_filesz(index);
//读取内容
fs_read(index,DEFAULT_ENTRY,length);
//关闭文件
fs_close(index);
别忘了引入头文件
#include "../include/fs.h"
添加寄存器和 LIDT 指令
1.根据 i386 ⼿册正确添加 IDTR 和 CS 寄存器
根据手册,可知IDTR
中base32位,limit16位。cs16位
struct {
uint32_t base; //32位base
uint16_t limit; //16位limit
}idtr;
uint16_t cs;
2.在 restart() 中正确设置寄存器初始值
根据讲义可知cs寄存器需要初始化为8
static inline void restart() {
/* Set the initial instruction pointer. */
cpu.eip = ENTRY_START;
cpu.eflags.value=0x2;//eflags赋初始值
cpu.cs=0x8;
#ifdef DIFF_TEST
init_qemu_reg();
#endif
}
3.LIDT 指令细节可在 i386 ⼿册中找到
查表可知,LIDT
在gpr7中
填表
make_group(gp7,
EMPTY, EMPTY, EMPTY, EX(lidt),
EMPTY, EMPTY, EMPTY, EMPTY)
若OperandSize
是16,则limit读取16位,base读取24位
若OperandSize
是32,则limit读取16位,base读取32位
make_EHelper(lidt) {
cpu.idtr.limit=vaddr_read(id_dest->addr,2);//limit16
if (decoding.is_operand_size_16) {
cpu.idtr.base=vaddr_read(id_dest->addr+2,3);//base24
} else
{
cpu.idtr.base=vaddr_read(id_dest->addr+2,4);//base32
}
print_asm_template1(lidt);
}
实现 INT 指令
1.实现写在 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.
*/
//获取门描述符
vaddr_t gate_addr=cpu.idtr.base+8*NO;
//P位校验
if (cpu.idtr.limit<0){
assert(0);
}
//将eflags、cs、返回地址压栈
rtl_push(&cpu.eflags.value);
rtl_push(&cpu.cs);
rtl_push(&ret_addr);
//组合中断处理程序入口点
uint32_t high,low;
low=vaddr_read(gate_addr,4)&0xffff;
high=vaddr_read(gate_addr+4,4)&0xffff0000;
//设置eip跳转
decoding.jmp_eip=high|low;
decoding.is_jmp=true;
}
2. 使⽤ INT 的 helper 函数调⽤ raise_intr()
执行 int
指令后保存的 EIP
指向的是 int
指令的下一条指令,所以第二个参数是decoding.seq_eip
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
}
3.指令细节可在 i386 ⼿册中找到
填表
/* 0xcc */ EX(int3), IDEXW(I,int,1), EMPTY, EMPTY,
成功运行
实现其他相关指令和结构体
1.组织 _RegSet 结构体,需要说明理由
根据讲义可知,现场保存的顺序为:①硬件保存 EFLAGS, CS, EIP ②vecsys()
会压入错误码和异常号 #irq
③ asm_trap()
会把用户进程的通用寄存器保存到堆栈上
则恢复的时候倒序恢复
那么,我们就可知 _RegSet
的组织方式了
struct _RegSet {
uintptr_t edi,esi,ebp,esp,ebx,edx,ecx,eax;
int irq;
uintptr_t error_code,eip,cs,eflags;
};
运行截图在下一问展示
2.pusha
填表
/* 0x60 */ EX(pusha), EMPTY, EMPTY, EMPTY,
3.popa
填表
/* 0x60 */ EX(pusha), EX(popa), EMPTY, EMPTY,
按手册顺序pop即可
make_EHelper(popa) {
rtl_pop(&cpu.edi);
rtl_pop(&cpu.esi);
rtl_pop(&cpu.ebp);
rtl_pop(&t0);
rtl_pop(&cpu.ebx);
rtl_pop(&cpu.edx);
rtl_pop(&cpu.ecx);
rtl_pop(&cpu.eax);
print_asm("popa");
}
3.iret
填表
/* 0xcc */ EX(int3), IDEXW(I,int,1), EMPTY, EX(iret),
根据手册,按顺序eip cs eflags
出栈即可
make_EHelper(iret) {
ret_pop(&decoding.jmp_eip);
decoding.is_jmp=1;
rtl_pop(&cpu.cs);
rtl_pop(&cpu.eflags.value);
print_asm("iret");
}
完善事件分发和 do_syscall
1.完善 do_event,⽬前阶段仅需要识别出系统调⽤事件即可
按照讲义,识别系统调用事件 _EVENT_SYSCALL
,然后调用 do_syscall()
即可
别忘了声明函数do_syscall()
extern _RegSet* do_syscall(_RegSet *r);
static _RegSet* do_event(_Event e, _RegSet* r) {
switch (e.event) {
case _EVENT_SYSCALL:
do_syscall(r);
break;
default: panic("Unhandled event ID = %d", e.event);
}
return NULL;
}
2.添加整个阶段中的所有系统调⽤(none, exit, brk, open, write, read, lseek, close)
实现SYSCALL_ARGx(r)
宏,根据讲义提示,很容易实现
#define SYSCALL_ARG1(r) r->eax
#define SYSCALL_ARG2(r) r->ebx
#define SYSCALL_ARG3(r) r->ecx
#define SYSCALL_ARG4(r) r->edx
完善do_syscall()
,在其中添加如下代码
a[0] = SYSCALL_ARG1(r);
a[1] = SYSCALL_ARG2(r);
a[2] = SYSCALL_ARG3(r);
a[3] = SYSCALL_ARG4(r);
-
none
编写
sys_none()
,该函数什么也不做,返回1,不要忘记设置系统调用的返回值static inline uintptr_t sys_none(_RegSet *r) { //设置系统调用的返回值 SYSCALL_ARG1(r)=1; return 1; }
成功
-
exit
讲义中说:你需要实现
SYS_exit
系统调用,它会接收一个退出状态的参数,用这个参数调用_halt()
即可。这里这个退出状态的参数我不明白怎么找的,反正就是4个参数,挨个试,到最后发现
SYSCALL_ARG2(r)
成功了static inline uintptr_t sys_exit(_RegSet *r) { _halt(SYSCALL_ARG2(r)); return 1; }
-
write
检查
fd
的值,如果fd
是1
或2
(分别代表stdout
和stderr
),则将buf
为首地址的len
字节输出到串口(使用_putc()
即可)。fs_write()
符合上述要求。填写参数的时候,注意buf类型static inline uintptr_t sys_write(uintptr_t fd, uintptr_t buf, uintptr_t len) { return fs_write(fd,(void *)buf,len); }
最后还要设置正确的返回值,否则系统调用的调用者会认为
write
没有成功执行。返回什么,通过man 2 write
可知,要返回写的字符的bytes,正好是fs_write()
的返回值由于
sys_write()
没有参数r
,因此我在do_syscall()
中填写返回case SYS_write: SYSCALL_ARG1(r)=sys_write(a[1],a[2],a[3]); break;
在
navy-apps/libs/libos/src/nanos.c
的_write()
中调用系统调用接口函数通过阅读
_syscall_()
参数,可知要传系统调用类型、参数一参数二、参数三int _write(int fd, void *buf, size_t count){ _syscall_(SYS_write,fd,(uintptr_t)buf,count); }
运行成功截图
这里目前只需要实现这三个就可以跑通,然后我就接着往下做了。
最后整体做完,我按照要求来这补了一下系统调用。关键是别忘了把返回值存入SYSCALL_ARG1(r)
_RegSet* do_syscall(_RegSet *r) {
uintptr_t a[4];
a[0] = SYSCALL_ARG1(r);
a[1] = SYSCALL_ARG2(r);
a[2] = SYSCALL_ARG3(r);
a[3] = SYSCALL_ARG4(r);
switch (a[0]) {
case SYS_none:
sys_none(r);
break;
case SYS_exit:
sys_exit(r);
break;
case SYS_write:
SYSCALL_ARG1(r)=(int)sys_write(a[1],a[2],a[3]);
break;
case SYS_brk:
SYSCALL_ARG1(r)=(int)sys_brk(r);
break;
case SYS_open:
SYSCALL_ARG1(r)=(int)sys_open(a[1],a[2],a[3]);
break;
case SYS_read:
SYSCALL_ARG1(r)=(int)sys_read(a[1],a[2],a[3]);
break;
case SYS_close:
SYSCALL_ARG1(r)=(int)sys_close(a[1]);
break;
case SYS_lseek:
SYSCALL_ARG1(r)=(int)sys_lseek(a[1],a[2],a[3]);
break;
default: panic("Unhandled syscall ID = %d", a[0]);
}
实现堆区管理
在 Nanos-lite 中实现 SYS_brk
系统调用。由于目前 Nanos-lite 还是一个单任务操作系统,空闲的内存都可以让用户程序自由使用,因此我们只需要让 SYS_brk
系统调用总是返回 0
即可,表示堆区大小的调整总是成功。
case SYS_brk:
SYSCALL_ARG1(r)=0;
break;
具体_sbrk()
实现步骤,讲义明确列出来了:
- program break 一开始的位置位于
_end
- 被调用时,根据记录的 program break 位置和参数
increment
,计算出新 program break - 通过
SYS_brk
系统调用来让操作系统设置新 program break - 若
SYS_brk
系统调用成功,该系统调用会返回0
,此时更新之前记录的 program break 的位置,并将旧 program break 的位置作为_sbrk()
的返回值返回 - 若该系统调用失败,
_sbrk()
会返回-1
extern char _end;//声明外部变量
static intptr_t brk=(intptr_t)&_end;//记录开始位置
void *_sbrk(intptr_t increment){
intptr_t pre = brk;
intptr_t now=pre+increment;//记录增加后的位置
intptr_t res = _syscall_(SYS_brk,now,0,0);//系统调用
if (res==0){//若成功,则返回原位置
brk=now;
return (void*)pre;
}//否则返回-1
return (void *)-1;
}
重点注意,改完navy-apps/libs/libos/src/nanos.c
中的代码,要记得重新编译``navy-apps`!!!
实现系统调用
1.sys_open()
调用 fs_open
,根据给定路径、标志和打开模式打开文件,注意pathname类型转换
static inline uintptr_t sys_open(uintptr_t pathname, uintptr_t flags, uintptr_t mode) {
return fs_open((char *)pathname,flags,mode);
}
do_syscall中别忘了写返回值
case SYS_open:
SYSCALL_ARG1(r)=(int)sys_open(a[1],a[2],a[3]);
break;
_open()
模仿已有的例子,别忘了类型转换就可以
int _open(const char *path, int flags, mode_t mode) {
_syscall_(SYS_open,(uintptr_t)path,flags,mode);
}
2.sys_write()
调用 fs_write
,将给定缓冲区的指定长度个字节写入指定文件号的文件中,注意buf类型转换
static inline uintptr_t sys_write(uintptr_t fd, uintptr_t buf, uintptr_t len) {
return fs_write(fd,(void *)buf,len);
}
do_syscall中别忘了写返回值
case SYS_write:
SYSCALL_ARG1(r)=(int)sys_write(a[1],a[2],a[3]);
break;
_write()
模仿已有的例子,别忘了类型转换就可以
int _write(int fd, void *buf, size_t count){
_syscall_(SYS_write,fd,(uintptr_t)buf,count);
}
3.sys_read()
调用 fs_read
,从指定文件号的文件中读取指定长度个字节到给定缓冲区中,注意buf类型转换
static inline uintptr_t sys_write(uintptr_t fd, uintptr_t buf, uintptr_t len) {
return fs_write(fd,(void *)buf,len);
}
do_syscall中别忘了写返回值
case SYS_read:
SYSCALL_ARG1(r)=(int)sys_read(a[1],a[2],a[3]);
break;
_read()
模仿已有的例子,别忘了类型转换就可以
int _read(int fd, void *buf, size_t count) {
_syscall_(SYS_read,fd,(uintptr_t)buf,count);
}
4.sys_lseek()
已经实现
根据fd
确定传入的文件位置,并以此确定文件大小以及读写指针位置。然后根据whence
来确定对指针进行操作,根据offset
对文件进行读写,并判断是否删除完毕或者写入内容超过文件大小。最后返回文件最新的读写位置。
5.sys_close()
调用 fs_close
,关闭指定文件号的文件
static inline uintptr_t sys_close(uintptr_t fd) {
return fs_close(fd);
}
do_syscall中别忘了写返回值
case SYS_close:
SYSCALL_ARG1(r)=(int)sys_close(a[1]);
break;
_close()
三四参数不用写,传入文件位置即可。
int _close(int fd) {
_syscall_(SYS_close,fd,0,0);
}
6.sys_brk()
前面已经实现
成功运⾏各测试⽤例
1.Hello world
2./bin/text
3./bin/bmptest
4./bin/events
5./bin/pal
遇到的问题及解决办法
-
遇到问题:实现堆区管理时,一直是按字符输出,当时搞了好久。
解决方案:全局make clean,然后重新编译所有文件,过了。后面也遇到了相似问题,只要记得重新make,就解决了。
-
遇到问题:
_syscall_()
对这个函数不太懂,当时思考了很久,也读了很多代码,不知道系统调用的几个函数该怎么填参数。解决方案:模仿框架已经写好的
_exit()
,按顺序把参数填到对应位置上,居然就过了。
实验心得
本次实验感觉难度不小,涉及到了很多计算机底层的知识,阅读代码花了我很长很长时间,虽然pa3.1做完了,代码能跑了,但有些地方的调用关系以及思路还是不太理解,希望在后面的学习中能够逐渐理解整个计算机底层是怎么运行的。
其他备注
助教真帅