PA3.1

目录

思考题

1. 什么是操作系统?

操作系统(Operating System,简称OS)是控制和管理计算机软硬件资源,以尽量合理有效的方法组织多个用户共享多种资源的程序集合,任何其他软件都必须在操作系统的支持下才能运行。

2. 我们不一样吗?

nanos-lite实现了更多的功能,但是我认为地位是相同的

3. 操作系统的实质

程序

4. 程序真的结束了吗?

main函数执行之前,主要就是初始化系统相关资源:

  1. 设置栈指针
  2. 初始化static静态和global全局变量,即data段的内容
  3. 将未初始化部分的赋初值
  4. 运行全局构造器
  5. 将main函数的参数,argcargv等传递给main函数,然后才真正运行main函数.

main函数执行之后:

  1. 获取main的返回值
  2. 调用exit退出程序

5. 触发系统调用

int main() {
    write(1,"hello world!\n",12); 
    return 0; 
}

6. 有什么不同?

与函数调用过程十分类似。函数调用时,获取调用函数的起始地址,并跳转过去。在触发函数调用前,会保存相关寄存器到栈中,函数调用完毕后再恢复。

可以。系统调用会根据系统调用号在 IDT 中索引,取得该调用号对应的系统调用服务程序的地址,并跳转过去。在触发系统调用前,会保护用户相关状态寄存器(EFLAGS, EIP等)到栈中,系统调用完毕后再恢复。这个过程与函数调用的过程基本一致,因此可以认为系统调用的服务程序理解为一个比较特殊的“函数”。

7. 段错误

编译的过程是吧高级语言转化为低级语言的过程,期间会检查语法,但是具体程序中指令会跳转到什么地方,编译阶段不负责检查。只有在程序执行中,访问到不访问的地方时,才会触发段错误。

段错误就是指访问的内存超出了系统所给这个程序的内存空间。基本是是错误地使用指针引起的:

8. 对比异常与函数调用

异常:保存寄存器、错误码#irq、 EFLAGS、CS、 EIP,形成了 trap frame(陷阱帧)的数据结构。

函数调用 :调用者保存寄存器和被调用者保存寄存器(不一定保存)

在异常处理的时候已经切换了栈帧,所以要保存更多的信息。

9. 诡异的代码

以指向trapframe 内容的指针esp作为参数,调用trap函数。把eip作为入口参数传进去,然后在执行irq_handle这个函数之前,通过pusha,在栈帧中形成了_RegSer这个结构体,把eip作为一个结构体的起始地址,通过成员irq来分发事件。

10. 注意区分事件号和系统调用号

事件号:指未实现系统调用事件的编号

系统调用号:在识别出系统调用事件后,从寄存器中取出系统调用号和输入参数,根据系统调用号查找系统调用分派表,执行相应的处理函数,并记录返回值。

11. 打印不出来?

printf打印是行缓冲,读取到的字符串会先放到缓冲区里,直到一行结束或者整个程序结束,才输出到屏幕,因为我们打印的字符串一行没有结束,所以就先执行后面的*p=NULL报错了。

因此,我们要让他输出字符串内容,只需要在字符串后面加上\n就表明一行结束,可以输出了。

12. 理解文件管理函数

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

13. 不再神秘的秘技

是游戏的bug,开发时没有注意变量类型等问题,导致在某种状态下出现数据的溢出错误,出现所谓的“秘技”

14. 必答题

存档读取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接口显示到屏幕上面。

15. git loggit branch截图

git log

image-20210614140613226

git branch

image-20210614140510920

操作题

1.实现 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.添加寄存器和 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);
}

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.
   */

  //获取门描述符
  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,

实现完成

4.实现其他相关指令和结构体

1.组织 _RegSet 结构体,需要说明理由

根据讲义可知,现场保存的顺序为:①硬件保存 EFLAGS, CS, EIP ②vecsys() 会压入错误码和异常号 #irqasm_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");}

5.完善事件分发和 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 的值,如果 fd12(分别代表 stdoutstderr),则将 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);}

6.实现堆区管理

在 Nanos-lite 中实现 SYS_brk 系统调用。由于目前 Nanos-lite 还是一个单任务操作系统,空闲的内存都可以让用户程序自由使用,因此我们只需要让 SYS_brk 系统调用总是返回 0 即可,表示堆区大小的调整总是成功。

case SYS_brk:      SYSCALL_ARG1(r)=0;      break;

接下来实现_brk

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;}

7.实现系统调用

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);}

8.成功运行各测试用例

  • hello world

image-20210613180045958

  • text

image-20210614175002125

  • bmptest

image-20210614162136106

  • event

image-20210614163052777

  • 仙剑奇侠传

image-20210614174812365

遇到的问题及解决方法

1.遇到问题:在一开始遇到了fatal error :files.h no such file or dictionary 中断了程序的编译,强制退出了

解决方法:经过同学帮助得知是没有生成编译的文件,应该先在nano_llite 文件下make run 才可以跑通,解决了问题。

实验心得

本次实验难度依然不小,同时也由我自己的原因没有认真阅读讲义,此外,阅读代码花费了大量的时间,完善各个指令和系统调用的函数也花费了不少的时间,出现了很多意想不到的问题导致没有头绪解决,通过询问同学和助教得解决的思路和方法,十分感谢。pa进入了后半段的编写,难度略有增加,还是要静下心来慢慢进行调试。

备注

助教真帅!

明天会更好!

posted @ 2021-08-05 10:34  shangjin2001  阅读(527)  评论(0)    收藏  举报