首页 > 技术文章 > 计算机组成原理Ⅱ课程设计 PA3.1

CodeSuccess 2021-06-06 23:32 原文

计算机组成原理Ⅱ课程设计 PA3.1

目录

思考题

  1. 什么是操作系统?

    我认为操作系统是运行在硬件之上,管理硬件,提供一系列操作硬件的API,供程序使用。因为我自己用Windows比较多,对比Linux,最直观感受是Windows提供的图形界面方便了用户使用,使计算机进入了平常百姓家,而不只仅局限于技术人员。

  2. 我们不⼀样,吗?

    nanos-lite在调用AM的API的基础上,实现了更多的功能和接口,例如文件系统、终端异常处理、加载器等。

    我认为他们是同等地位。

  3. 操作系统的实质

    程序

  4. 程序真的结束了吗?

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

    1. 设置栈指针

    2. 初始化static静态和global全局变量,即data段的内容

    3. 将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容

    4. 运行全局构造器,估计是C++中构造函数之类的吧

    5. 将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数main

    main函数执行之后:

    1. 获取main的返回值
    2. 调用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

  5. 触发系统调用

    编写了一个程序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
    

    结果:成功输出

  6. 有什么不同?

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

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

    “服务程序”的工作都是硬件自动完成的,不需要程序员编写指令来完成相应的内容。在函数运行的过程中遇到异常,才会触发。由于“服务程序”和“用户程序”使用的是不同的堆栈,因此在触发“系统程序”时,涉及到堆栈的切换。

  7. 段错误

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

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

    • 访问系统数据区
    • 内存越界,访问到不属于程序的内存区域(数组越界,变量类型不一致)
    • 多线程程序使用了不安全的函数
    • 多线程读写数据未加锁保护
    • 堆栈溢出

    http://www.360doc.com/content/18/0331/16/21305584_741796830.shtml

  8. 对比异常与函数调用

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

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

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

  9. 诡异的代码

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

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

    事件号:标明我们一个未实现的系统调用事件的编号。

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

  11. 打印不出来?

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

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

    #include <stdio.h>
    int main(){
        int *p = NULL;
        printf("I am here!\n");//这里加上换行
        *p = 10;
        return 0;
    }
    
  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 截图

实验内容

实现 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

  1. 首先用fs_open()根据文件名获取文件位置
  2. 再用fs_filesz()获取文件大小
  3. 用fs_read()读取指定位置指定长度的内容
  4. 最后用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() 会压入错误码和异常号 #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");
}

完善事件分发和 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);
      }
    

    运行成功截图

这里目前只需要实现这三个就可以跑通,然后我就接着往下做了。

最后整体做完,我按照要求来这补了一下系统调用。关键是别忘了把返回值存入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()实现步骤,讲义明确列出来了:

  1. program break 一开始的位置位于 _end
  2. 被调用时,根据记录的 program break 位置和参数 increment,计算出新 program break
  3. 通过 SYS_brk 系统调用来让操作系统设置新 program break
  4. SYS_brk 系统调用成功,该系统调用会返回 0,此时更新之前记录的 program break 的位置,并将旧 program break 的位置作为 _sbrk() 的返回值返回
  5. 若该系统调用失败,_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

遇到的问题及解决办法

  1. 遇到问题:实现堆区管理时,一直是按字符输出,当时搞了好久。

    解决方案:全局make clean,然后重新编译所有文件,过了。后面也遇到了相似问题,只要记得重新make,就解决了。

  2. 遇到问题:_syscall_()对这个函数不太懂,当时思考了很久,也读了很多代码,不知道系统调用的几个函数该怎么填参数。

    解决方案:模仿框架已经写好的_exit(),按顺序把参数填到对应位置上,居然就过了。

实验心得

本次实验感觉难度不小,涉及到了很多计算机底层的知识,阅读代码花了我很长很长时间,虽然pa3.1做完了,代码能跑了,但有些地方的调用关系以及思路还是不太理解,希望在后面的学习中能够逐渐理解整个计算机底层是怎么运行的。

其他备注

助教真帅

推荐阅读