PA1-3

PA实验报告

PA1

实现调试器

由于这个PA的所有东西几乎都要自己完成,所以首先要实现的就是调试器,如果没有调试器,之后的工作也就没有办法完成。调试器的功能就参考平时用到的调试器和实验手册来,最后完成的有单步执行,打印程序状态,扫描内存,设置监视点和删除监视点。PA的调试器的声明保存在在src/monitor/debug/ui.c文件下的cmd_table中,添加需要的调试器的声明

cmd_table [] = {
 { "help", "Display informations about all supported commands", cmd_help },
 { "c", "Continue the execution of the program", cmd_c },
 { "q", "Exit NEMU", cmd_q },
 { "si", "Let the program execute n steps", cmd_si },
 { "info", "Display the register status and the watchpoint information", cmd_info},
 { "x", "Caculate the value of expression and display the content of the address", cmd_x},
 { "p","Calculate an expression", cmd_p},
 { "w", "Create a watchpoint", cmd_w},
 { "d", "Delete a watchpoint", cmd_d},
}
操作名功能
si单步执行
info读取寄存器状态
x显示地址
p计算表达式
w创建监视点
d删除监视点

具体的实现分别如下:

si/cmd_si

atoi()函数是一个从字符串转数字的函数

static int cmd_si(char *args) {
 /*get the steps number*/
 int steps;
 if (args == NULL){
   steps = 1;
 }
 else{
   steps = atoi(strtok(NULL, " "));
 }

 cpu_exec(steps);
 return 0;
}

info/cmd_info

static int cmd_info(char *args) {
 if (args == NULL) {
   printf("Please input the info r or info w\n");
 }
 else {
   if (strcmp(args, "r") == 0) {
     printf("eax:  0x%-10x    %-10d\n", cpu.eax, cpu.eax);
     printf("edx:  0x%-10x    %-10d\n", cpu.edx, cpu.edx);
     printf("ecx:  0x%-10x    %-10d\n", cpu.ecx, cpu.ecx);
     printf("ebx:  0x%-10x    %-10d\n", cpu.ebx, cpu.ebx);
     printf("ebp:  0x%-10x    %-10d\n", cpu.ebp, cpu.ebp);
     printf("esi:  0x%-10x    %-10d\n", cpu.esi, cpu.esi);
     printf("esp:  0x%-10x    %-10d\n", cpu.esp, cpu.esp);
     printf("eip:  0x%-10x    %-10d\n", cpu.eip, cpu.eip);
   }
   else if (strcmp(args, "w") == 0) {
     display_wp();
   }
   else {
     printf("The info command need a parameter 'r' or 'w'\n");
   }
 }
 return 0;
}

x/cmd_X

trans()函数是一个字符串转到16进制的函数,通过遍历字符串之后每个项-‘0’

static int cmd_x(char *args) {
  if (args == NULL) {
    printf("Input invalid command!\n");
  }
  else {
    int num, addr, i;
    char *exp;
    num = atoi(strtok(NULL, " "));
    exp = strtok(NULL, " ");
    addr = trans(exp);
    for (i = 0; i < num; i++) {
      printf("0x%x\n", vaddr_read(addr, 4));
      addr += 4;
    }
  }
  return 0;
}

p/cmd_p

这个函数主要是调用了后半部分计算表达式的结果,后面细说

static int cmd_p(char *args) {
  if (args == NULL) {
    printf("Input invalid command! Please input the expression.\n");
  }
  else {
    init_regex();
    bool success = true;
    //printf("args = %s\n", args);
    int result = expr(args, &success);
    if (success) {
      printf("result = %d\n", result);
    }
    else {
      printf("Invalid expression!\n");
    }
  }
  return 0;
}

w/cmd_w

在watchpoint.h文件中定义有wp类用来表示监视点,在其.c文件中有关于监视点的函数,这里只是调用了相应的函数

static int cmd_w(char *args) {
  if (args == NULL) {
    printf("Input invalid command! Please input the expression.\n");
  }
  else {
    insert_wp(args);
  }
  return 0;
}

d/cmd_d

static int cmd_d(char *args) {
  if (args == NULL) {
    printf("Input invalid command! Please input the NO.\n");
  }
  else {
    int no = atoi(args);
    delete_wp(no);
  }
  return 0;
}

表达式求值

这部分其实与上学期所学的编译原理课程的内容是相似的,不过还是和重新学一样。要完成的任务是表达式匹配,第一步先是定义所支持的运算,并在相应的文件中声明。
###定义支持预算
在nemu中计算的声明在 /sec/monitor/debug/expr.c中,首先先是在一个emun中添加“token type”

enum {
    TK_VAL = 237, TK_OR = 256, TK_AND = 254, TK_EQ = 253, TK_NOT_EQ = 252,
    TK_SM_OR_EQ = 251, TK_BG_OR_EQ = 250, TK_BG = 249, TK_SM = 248,
    TK_LSHIFT = 247, TK_RSHIFT = 246, TK_MUL = 245, TK_DIV = 244,
    TK_NOTYPE = 243, TK_NUM_10 = 242, TK_NUM_16 = 241, TK_$ = 240,
    TK_LBRA = 239, TK_RBRA = 238, TK_POINT = 237, TK_SUB_SELF = 236,
    TK_NEG = 235, TK_$_ADD = 234
    /* TODO: Add more token types */
};

并且还要在下面的rules中通过将每个type对应上相应的正则表达式

rules[] = {
        {"\\|\\|",                 TK_OR},
        {"&&",                 TK_AND},
        {"==",                 TK_EQ},       // equal
        {"!=",                 TK_NOT_EQ},       // not equal
        {"<=",                 TK_SM_OR_EQ},
        {">=",                 TK_BG_OR_EQ},
        {"<<",                 TK_LSHIFT},
        {">>",                 TK_RSHIFT},
        {">",                  TK_BG},
        {"<",                  TK_SM},
        ...(省略)
}

表达式匹配

在定义完这些以后就进入最主要的表达式匹配的过程了。想到表达式匹配,首先能想到的就是将输入的字符串一一识别,但是稍微再想一下就知道这样是得不到结果的,编译原理课上老师说过:“要人先会做,再让机器做”。我们在读一个表达式的时候肯定是读完整个式子才能知道该怎么算,也就是我们要有一个统领全局的函数,它知道表达式的全部信息,对应到计算机的操作那就是递归了,读完全部的内容才开始返回结果,所以表达式的匹配的核心函数是一个递归函数,定义为expr()接下来我就分步总结一下

  1. 首先我们要有一个整体的思想,有两个指示位,中间的字符是我们要处理的内容。最开始做的还是要知道每一个字符的含义,这样我们才能知道该对其用怎样的操作,这个过程比较简单,定义一个make_token()函数对输入的表达式进行遍历就行了,之后将每个字符的类记录下来就可以交给下一步了,不过要注意一些特殊情况
    1. '*'是地址运算还是乘法,这个就要通过上下文和符号所在的位置共同判断
    2. '-'是减法还是负数,与上面的处理类似
  2. 接下来要开始计算了,我们肯定不能见到一个算一个,因为表达式最关键的问题就是优先级。与人一样现寻找表达式中是否含有括号,因为括号的优先级是最高的,定义一个括号查找函数(Check_Parenttheses())来查找是否有括号,如果有则先对括号内的进行递归计算
  3. 到这一步了,依然不能开始计算,因为还是有特殊情况的存在。比如:遇到了字母,遇到了字母并不一定是错误,也有可能是对寄存器的操作,这时候在计算之前需要先进行寄存器的读操作;或者是对地址的操作,也是与我们熟知的数学计算格式不符的,所以需要先处理这些情况。代码太多if,就不贴过来了
  4. 终于,在考虑完特殊情况以后,就可以开始递归运算了,最后返回结果

当然表达式计算的结果不是一帆风顺的,直接报错,比如括号不匹配或者匹配到最后多一个运算符这种情况,在上面的过程中并没有写出来

PA1问题

程序从哪里开始执行

众所周知,程序是从main()函数开始执行的,nemu很明显的有一个main.c文件,打开发现内容十分简单,只调用了两个函数。init_monitor()函数查看内容后,发现它是一个计算机的开机启动和自查过程,包括开启文件系统,检测CPU状态等;ui_mainloop()查看内容后,就知道它就是那个苦苦等待我们输入指令并去执行的,所以我们的程序执行(虽然现在这个系统还没有能执行的程序)应该是打断这个循环并且跑出去执行的。

在cmd_c()函数中,调用cpu_exec()的时候传入了参数-1,意义何在

一开始我的想法:我一开始就想着这-1肯定是有特殊含义的。查看cpu_exec()函数,看一下n=-1的时候会执行哪些代码,发现里面最大的循环它进不去,只有最后一个
if (nemu_state == NEMU_RUNNING) { nemu_state = NEMU_STOP; }
我就想这个C指令的意思应该是让程序继续进行,那nemu_state应该是STOP也应该不执行这个才对,难道这个state有什么玄机?我就去查还有哪里用到了state,发现在keyboard中有,我就想这个会不会是一个反向定义的锁,在纠结中发现了cpu_exec()函数的一开始有一行指令:
nemu_state = NEMU_RUNNING;
所以,-1的含义只是让程序不执行任何别的东西,只调整这个状态,让NEMU启动…没有玄机
后来老师上课说道:这个-1是一个无符号数,代表最大的整数,这样就能进入中间的大循环,总而让cpu处理之后的指令

框架代码中定义wp_pool等变量的时候使用了关键字static,static在此处的含义是什么,为什么要使用它

根据static的两个性质:可以让这个变量的定义域扩展到整个文件;不予许外部文件读取,有两个作用:

  1. 简化在这个文件的编程
  2. 保护系统安全,因为监视点可以获取系统正在执行程序的安全,如果被外部文件读取,那么系统的安全将受到很大的威胁,信息会被随意窃取

EFLAGS寄存器的CF位是什么意思

是计算中的进位标志符,如果有进位为’1’否则为’0’

ModR/M字节是什么

由于同时出现对内存和寄存器访问这两种情况,所以指令会出现不定长指令的情况,ModR/M字节就表明了是内存还是寄存器的访问,并且还有指令的具体形式

mov指令的具体格式是怎么样的

movx source,destination
Source和destination的值可以是内存地址,存储在内存中的数据值,指令语句中定义的数据值,或者寄存器
#冯诺依曼计算机系统(PA2)

指令的实现

根据实验手册上的指导,首先要 在exus-am/tests/cputest 目录下键入 make ARCH=x86-nemu ALL=dummy run 发现当前系统的指令是不全的,所以需要补充指令。
首先查看反汇编文件
图片
得到我们需要添加哪些指令,之后先是在nemu/src/cpu/exec/all_instr.h中添加执行函数的声明

make_EHelper(call);
make_EHelper(jmp);
make_EHelper(sub);
make_EHelper(xor);
make_EHelper(ret);
make_EHelper(push);
make_EHelper(pop);
make_EHelper(jmp_rm);

在写执行函数之前还需要填写opcode表,这个要按照i386手册来填(毕竟RTFM)不然会出现各种各样的错误

填表的代码太多,而且没啥理解的内容,这里就不粘贴了
现在就需要写执行函数了,我们现在写的代码已经是十分底层的代码了,如果每次写汇编指令都反复调用寄存器未免太多麻烦,所以先写几个RTL函数

标识寄存器

在这之前我们需要定义一些标识寄存器,为了让系统更符合标准系统的样子,不然很多操作是无法完成,下面的函数也是需要用到这些寄存器的,并要对它们操作

			rtlreg_t eflags_init;
      struct
      {
          unsigned int CF:1;  //进位标志
          unsigned int ZF:1;  //零标志
          unsigned int SF:1;  //符号标志
          unsigned int IF:1;  //中断标志
          unsigned int OF:1;  //溢出标志
      };

###RTL函数
RTL(Register Transfer Language)顾名思义是用来使寄存器不那么抽象的函数,相当于是自己实现的一个更方便的接口,不用自己每次判断来修改或者查看寄存器,直接调用函数就行

static inline void rtl_push(const rtlreg_t* src1) {
  // esp <- esp - 4
  // M[esp] <- src1
	cpu.esp -= 4;
	vaddr_write(cpu.esp, 4, *src1);
}

rtl_push():4的原因是计算机系统位数决定的,寄存器自减4相当于把位置空出来,之后再写进去,vaddr_write()函数其实是调用的paddr_write(x,y,z)意思就是把长为y的z写到地址x上

static inline void rtl_pop(rtlreg_t* dest) {
  // dest <- M[esp]
  // esp <- esp + 4
	*dest = vaddr_read(cpu.esp, 4);
	cpu.esp += 4;
}

rtl_pop():与push十分相似,不过是读,并且自增4相当于把当前的给挤出去了

static inline void rtl_update_SF(const rtlreg_t* result, int width) {
  // eflags.SF <- is_sign(result[width * 8 - 1 .. 0])
	int sf = 0;
	sf = (*result >> (width * 8 - 1)) & 0x1;
  cpu.eflags.SF = sf;
}

rtl_update_SF():判断符号位的,因为最高位是符号位,通过位操作取出最高位判断是否为1

static inline void rtl_update_ZF(const rtlreg_t* result, int width) {
  // eflags.ZF <- is_zero(result[width * 8 - 1 .. 0])
	int zf = 0;
	if (width == 1) {
		zf = (*result & 0x000000ff) | 0;
	}
	else if (width == 2) {
		zf = (*result & 0x0000ffff) | 0;
	}
	else if (width == 4) {
		zf = (*result & 0xffffffff) | 0;
	}
  cpu.eflags.ZF = (zf == 0) ? 1 : 0;
}

rtl_update_ZF():判断是否为0,利用与或运算

static inline void rtl_update_ZFSF(const rtlreg_t* result, int width) {
	rtl_update_ZF(result, width);
	rtl_update_SF(result, width);
}

执行函数

每一个执行函数不光是返回计算的结果,还要对计算的结果的相应寄存器进行处理

因为Ubuntu打不了汉字,我就把注释加在报告里了

make_EHelper(sub) {
  //计算的过程
  rtl_sub(&t2, &id_dest->val, &id_src->val);
	operand_write(id_dest, &t2);

  //对ZFSF两位的操作
  rtl_update_ZFSF(&t2, id_dest->width);

  rtl_sltu(&t0, &id_dest->val, &t2); //判断大小
  rtl_set_CF(&t0);
  //设置msb位,通过不断地异或找到最大的1
  rtl_xor(&t0, &id_dest->val, &id_src->val); //异或
  rtl_xor(&t1, &id_dest->val, &t2);
  rtl_and(&t0, &t0, &t1); //与
  rtl_msb(&t0, &t0, id_dest->width); //最高值位
  rtl_set_OF(&t0);

  print_asm_template2(sub);
}
make_EHelper(jmp) {
  // the target address is calculated at the decode stage
  decoding.is_jmp = 1;

  print_asm("jmp %x", decoding.jmp_eip);
}

push和pop都是借助了之前的RTL函数

make_EHelper(push) {
	if (id_dest->width == 1) {
		id_dest->val = (int32_t)(int8_t)id_dest->val;
	}
	rtl_push(&id_dest->val);  
  print_asm_template1(push);
}
make_EHelper(pop) {
	rtl_pop(&t0);
	operand_write(id_dest, &t0);
  print_asm_template1(pop);
}

DIFF_TEST

在看代码的时候发现很多部分都使用条件编译写的(ifdef DIFT_TEST/DEBUG),就是模式的切换,检查模式和debug模式两种,现在就是要添加diffset_step()的代码,来帮助我们查看我们的指令实现情况

  if(r.eax != mine.eax || r.ecx != mine.ecx || r.edx != mine.edx ||
		 r.ebx != mine.ebx || r.esp != mine.esp || r.ebp != mine.ebp ||
		 r.esi != mine.esi || r.edi != mine.edi || r.eip != mine.eip) {
    diff = true;
    printf("qemus eax:0x%08x, mine eax:0x%08x @eip:0x%08x\n", r.eax, mine.eax, mine.eip);
    printf("qemus ecx:0x%08x, mine ecx:0x%08x @eip:0x%08x\n", r.ecx, mine.ecx, mine.eip);
    printf("qemus edx:0x%08x, mine edx:0x%08x @eip:0x%08x\n", r.edx, mine.edx, mine.eip);
    printf("qemus ebx:0x%08x, mine ebx:0x%08x @eip:0x%08x\n", r.ebx, mine.ebx, mine.eip);
    printf("qemus esp:0x%08x, mine esp:0x%08x @eip:0x%08x\n", r.esp, mine.esp, mine.eip);
    printf("qemus ebp:0x%08x, mine ebp:0x%08x @eip:0x%08x\n", r.ebp, mine.ebp, mine.eip);
    printf("qemus esi:0x%08x, mine esi:0x%08x @eip:0x%08x\n", r.esi, mine.esi, mine.eip);
    printf("qemus edi:0x%08x, mine edi:0x%08x @eip:0x%08x\n", r.edi, mine.edi, mine.eip);
    printf("qemus eip:0x%08x, mine eip:0x%08x @eip:0x%08x\n", r.eip, mine.eip, mine.eip);
  } 

运行结果如下:
图片

输入输出设备

我们要实现的是一台完整的计算机,之前都是CPU内部的事,首先我们需要有设备给CPU输入,当CPU计算完成以后需要输出到设备上,所以我们需要模拟输入输出设备。

串口

设备的初始化通过IOE实现(其中提供了一些可以调用的函数),在src/cpu/exec/system.c
in/out函数
pio_write()是通过内存的复制实现的,pio_read()是内存的读写实现的

make_EHelper(in) {
	t0 = pio_read(id_src->val, id_src->width);
  operand_write(id_dest, &t0);

  print_asm_template2(in);

#ifdef DIFF_TEST
  diff_test_skip_qemu();
#endif
}
make_EHelper(out) {
  pio_write(id_dest->val, id_src->width, id_src->val);

  print_asm_template2(out);

#ifdef DIFF_TEST
  diff_test_skip_qemu();
#endif
}

计时器

在执行程序的时候,有的操作是对时间有要求的,需要通过获取时间来完成某种效果(比如之后的游戏程序),具体的实现为

unsigned long _uptime(){
  return inl(RTC_PORT)-boot_time;
}

键盘的读取

之前在看PA1中的问题的时候,看到过键盘,当有输入的时候打断系统循环,之后读取输入内容,和操作系统课上讲的一样,在读的时候加了一个互斥锁来保持系统的正确性

int _read_key() {
  int ret = _KEY_NONE;
  SDL_LockMutex(key_queue_lock);
  if (key_f != key_r) {
    ret = key_queue[key_f];
    key_f = (key_f + 1) % KEY_QUEUE_LEN;
  }
  SDL_UnlockMutex(key_queue_lock);
  return ret;
}

VGA的实现

在内存中单独开辟一部分空间用来存储颜色信息,当要显示颜色的时候就实现内存的浅拷贝,具体代码如下:

void _draw_rect(const uint32_t *pixels, int x, int y, int w, int h) {
  int cp_bytes = sizeof(uint32_t) * min(w, _screen.width - x);
  for (int j = 0; j < h && y + j < _screen.height; j ++) {
    memcpy(&fb[(y + j) * W + x], pixels, cp_bytes);
    pixels += w;
  }
}

图片

问题

我们知道代码和数据都在可执行文件里面,但却没有提到堆(heap)和栈(stack).为什么堆和栈的内容没有放入可执行文件里面?那程序运行时刻用到的堆和栈又是怎么来的?

我的理解是,堆和栈是系统在执行代码的时候使用的一种数据结构,代码说白了就是调用各种系统提供的函数,系统在实现这些函数的时候自己使用了堆和栈,看起来就好像程序使用了一样

当用户程序陷入死循环时,让用户程序暂停下来,并输出相应的提示信息你觉得应该如何实现?

首先要考虑的是系统改怎么发现程序进入死循环,死循环的定义是程序长时间执行同一段代码,能想到的就是将eip寄存器的值暂时保存如果出现闭环则为死循环,并且打印保存内存对应的指令

在 nemu/include/cpu/rtl.h 中,你会看到由 static inline 开头定义的各种 RTL 指令函数.选择其中一个函数,分别尝试去掉 static,去掉 inline 或去掉两者,然后重新进行编译,你会看到发生错误.请分别解释为什么会发生这些错误?你有办法证明你的想法吗?

  • 去掉static并不会报错
  • 去掉inline会报"defined but not used"
  • 都去掉会报"multiple definition of’…’"
    inline相当于把一个函数固定在一个固定的内存里,即使没有static(不允许外部访问)但是由于内存没有改变所以还是能够访问。但是如果没有inline那就不能被外部访问了,如果都去掉,就会出现在两个内存中同时定义了这个函数,所以出现了重定义。

了解 Makefile 请描述你在 nemu 目录下敲入 make 后,make 程序如何组织.c 和.h 文件,最终生成可执行文件 nemu/build/nemu

  1. 读入Makefile,并且查看是否调用其他的Makefile
  2. 读取include文件的Makefile
  3. 初始化变量
  4. 为目标文件建立依赖关系
  5. 重新生成目标
  6. 执行

BUG和总结

异常控制流(PA3)

这一部分是操作系统的内容,需要实现文件系统,异常处理系统并且实现PA2中功能的加载

加载程序

首先要调整的是ISA总线的编码方式,调整为X86

ISA ?= x86
ifeq($(NAVY_HOME),)
  $(error Must set NAVY_HOME environment variable)
endif

现在是没有文件系统的,所以程序的加载就是磁盘读写,磁盘读写函数原型如下:

ramdisk_read(DEFAULT_ENTRY, 0, get_ramdisk_size());

根据约定,用户程序需要连接到内存的位置是0x4000000(已经设置好),所以loader只用把ramdisk中的从0开始(因为只有一个文件所以偏置为0)所有内容(这个是因为已经帮忙解析好了文件的头ELF信息)都放在这个位置,之后就可以进行make run执行,结果如下
[图片]

中断

首先需要一个CS寄存器,然后初始化;还要在restart()函数中将EFLAGS初始化为0x2

static inline void restart() {
  /* Set the initial instruction pointer. */
  cpu.eip = ENTRY_START;
	cpu.cs = 0x8;
	cpu.eflags.eflags_init = 0x2;

#ifdef DIFF_TEST
  init_qemu_reg();
#endif
}

dummy.c的执行是在中途中断了,我们现在需要实现中断指令,中断发生后为了保证程序还能回来继续运行,所以第一步是保护现场(raise_intr()函数),之后才能去执行后面的指令。在中断后,除了保存现场还要做的就是识别中断了,系统会保存一个中断表(中断描述符表寄存器IDTR),通过查询中断表判断如何处理遇到的中断
IDTR的定义为

struct{
  uint16_t limit;
  uint32_t base;
}idtr;
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, 4) & 0x00ffffff;
	}
	else {
		cpu.idtr.base = vaddr_read(id_dest->addr + 2, 4);	
	}
  print_asm_template1(lidt);
}

保存现场的函数如下

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

  //保存现场
	rtl_push((rtlreg_t *)&cpu.eflags);
	rtl_push((rtlreg_t *)&cpu.cs);
	rtl_push((rtlreg_t *)&ret_addr);
	uint32_t idtr_base = cpu.idtr.base;

  //下一步执行什么
	uint32_t eip_low, eip_high, offset;
	eip_low = vaddr_read(idtr_base + NO * 8, 4) & 0x0000ffff;
	eip_high = vaddr_read(idtr_base + NO * 8 + 4, 4) & 0xffff0000;
	offset = eip_low | eip_high;
	decoding.jmp_eip = offset;
	decoding.is_jmp = true;
}

最后就是我们的中断处理函数

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
}

在运行dummy.c文件发现停在了pusha指令,pusha指令是知道的,作用是将通用寄存器压栈,也就是保存中断前的程序的执行状态,这个指令是需要实现的,结果如下:

make_EHelper(pusha) {
	t0 = cpu.esp;
	rtl_push(&cpu.eax);
  rtl_push(&cpu.ecx);
  rtl_push(&cpu.edx);
  rtl_push(&cpu.ebx);
  rtl_push(&t0);
  rtl_push(&cpu.ebp);
  rtl_push(&cpu.esi);
  rtl_push(&cpu.edi);

	print_asm("pusha");
}

这些数据保存在trapframe结构体中(和操作系统一样),这个结构体如下:

struct _RegSet {
	uintptr_t edi, esi, ebp, esp, ebx, edx, ecx, eax;
  //uintptr_t esi, ebx, eax, eip, edx, error_code, eflags, ecx, cs, esp, edi, ebp;
  int irq;
	uintptr_t error_code, eip, cs, eflags;
};

文件系统

第一步是开启文件记录表,之后我们就可以生成所有程序的文件记录表

文件记录表记录文件名,大小,偏移量(打开的文件才有记录当前读取位置)

我们需要完成文件的打开,关闭,读写,定位的功能

文件打开

文件打开的逻辑很简单,就是遍历所有文件名,如果匹配则打开

int fs_open(const char *pathname, int flags, int mode) {
	int i;
	for (i = 0; i < NR_FILES; i++) {
		if (strcmp(file_table[i].name, pathname) == 0) {
			return i;
		}
	}
	assert(0);
	return -1;
}

文件读写

首先是打开,之后获取文件的大小,通过大小限制读取范围,

		default:
			if(file_table[fd].open_offset >= fs_size || len == 0)
				return 0;
			if(file_table[fd].open_offset + len > fs_size)
				len = fs_size - file_table[fd].open_offset;
			ramdisk_read(buf, file_table[fd].disk_offset + file_table[fd].open_offset, len);
			file_table[fd].open_offset += len;
			break;

写与读都需要获取文件大小(防止超界),之后就是磁盘的写操作,比较简单

			if(file_table[fd].open_offset >= fs_size)
				return 0;	
			if(file_table[fd].open_offset + len > fs_size)
				len = fs_size - file_table[fd].open_offset;
			ramdisk_write(buf, file_table[fd].disk_offset + file_table[fd].open_offset, len);
			file_table[fd].open_offset += len;

文件的定位也比较简单,从三种情况来看,读完,没读,正在读三种情况,open_offset就是操作前文件所在位置,代码过长而又比较简单在这里就不粘贴了

VGA显存抽象成文件

首先是初始化/dev/fs,文件的大小

void init_fs() {
  //长和宽都是定义好的
	file_table[FD_FB].size = _screen.height * _screen.width * 4;
}

实现把缓冲区的内容输出到固定的位置,要通过给的offset获取对应的屏幕位置

ssize_t fs_read(int fd, void *buf, size_t len) {
	ssize_t fs_size = fs_filesz(fd);
	//Log("in the read, fd = %d, file size = %d, len = %d, file open_offset = %d\n", fd, fs_size, len, file_table[fd].open_offset);
	switch(fd) {
		case FD_STDOUT:
		case FD_FB:
			//Log("in the fs_read fd_fb\n");
			break;
		case FD_EVENTS:
			len = events_read((void *)buf, len);
			break;
		case FD_DISPINFO:
			if (file_table[fd].open_offset >= file_table[fd].size)
				return 0;
			if (file_table[fd].open_offset + len > file_table[fd].size)
				len = file_table[fd].size - file_table[fd].open_offset;
			dispinfo_read(buf, file_table[fd].open_offset, len);
			file_table[fd].open_offset += len;	
			break;
		default:
			if(file_table[fd].open_offset >= fs_size || len == 0)
				return 0;
			if(file_table[fd].open_offset + len > fs_size)
				len = fs_size - file_table[fd].open_offset;
			ramdisk_read(buf, file_table[fd].disk_offset + file_table[fd].open_offset, len);
			file_table[fd].open_offset += len;
			break;
	}
	return len;
}

初始化设备

void init_device() {
  _ioe_init();  //初始化Extension

	strcpy(dispinfo ,"WIDTH:400\nHEIGHT:300");
}

在文件系统中添加对/dev/fb;/proc/dispinfo两个文件的支持

switch(fd){
  case FD_STDOUT:
  case FD_FB:
    break;
    ...
}

把设备输入抽象成文件(可视化设备输入)

相当于是获取键盘输入之后输出到屏幕上

size_t events_read(void *buf, size_t len) {
  int key = _read_key();
	bool down = false;
	if (key & 0x8000) {
		key ^= 0x8000;
		down = true;
	}
	if (key == _KEY_NONE) {
		unsigned long t = _uptime();
		sprintf(buf, "t %d\n", t);
	}
	else {
		sprintf(buf, "%s %s\n", down ? "kd" : "ku", keyname[key]);
	}
	return strlen(buf);
}

有了这个函数,还需要添加对它的支持

	case FD_EVENTS:
		len = events_read((void *)buf, len);
		break;

PA3问题

trap.S 有一行 pushl%esp 的代码,能结合前后的代码理解它的行为吗

因为中断的时候是反向压栈的,所以esp会被压倒最下边,这时候push一个esp相当于它还是栈顶指针,不然就失去含义了

文件读写的具体过程 仙剑奇侠传中有以下行为,在 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 是如何相互协助,来分别完成游戏存档的读取和屏幕的更新

读取游戏存档时fread()函数发生中断调用nanos-lite中的fs_read读取文件中的存档,然后fs_read函数执行nemu中的SYS_read,然后就可以成功读取文件内容,进行读档屏幕更新时也是产生中断,然后nanos-lite的IOE函数开始执行,调用am中的IOE函数,成功返回文件的更新内容,就可以实现屏幕的更新

  • 12
    点赞
  • 116
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值