PA实验报告
- PA1
- 实现调试器
- 表达式求值
- PA1问题
- 指令的实现
- DIFF_TEST
- 输入输出设备
- 问题
- 我们知道代码和数据都在可执行文件里面,但却没有提到堆(heap)和栈(stack).为什么堆和栈的内容没有放入可执行文件里面?那程序运行时刻用到的堆和栈又是怎么来的?
- 当用户程序陷入死循环时,让用户程序暂停下来,并输出相应的提示信息你觉得应该如何实现?
- 在 nemu/include/cpu/rtl.h 中,你会看到由 static inline 开头定义的各种 RTL 指令函数.选择其中一个函数,分别尝试去掉 static,去掉 inline 或去掉两者,然后重新进行编译,你会看到发生错误.请分别解释为什么会发生这些错误?你有办法证明你的想法吗?
- 了解 Makefile 请描述你在 nemu 目录下敲入 make 后,make 程序如何组织.c 和.h 文件,最终生成可执行文件 nemu/build/nemu
- BUG和总结
- 异常控制流(PA3)
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()接下来我就分步总结一下
- 首先我们要有一个整体的思想,有两个指示位,中间的字符是我们要处理的内容。最开始做的还是要知道每一个字符的含义,这样我们才能知道该对其用怎样的操作,这个过程比较简单,定义一个make_token()函数对输入的表达式进行遍历就行了,之后将每个字符的类记录下来就可以交给下一步了,不过要注意一些特殊情况
- '*'是地址运算还是乘法,这个就要通过上下文和符号所在的位置共同判断
- '-'是减法还是负数,与上面的处理类似
- 接下来要开始计算了,我们肯定不能见到一个算一个,因为表达式最关键的问题就是优先级。与人一样现寻找表达式中是否含有括号,因为括号的优先级是最高的,定义一个括号查找函数(Check_Parenttheses())来查找是否有括号,如果有则先对括号内的进行递归计算
- 到这一步了,依然不能开始计算,因为还是有特殊情况的存在。比如:遇到了字母,遇到了字母并不一定是错误,也有可能是对寄存器的操作,这时候在计算之前需要先进行寄存器的读操作;或者是对地址的操作,也是与我们熟知的数学计算格式不符的,所以需要先处理这些情况。代码太多if,就不贴过来了
- 终于,在考虑完特殊情况以后,就可以开始递归运算了,最后返回结果
当然表达式计算的结果不是一帆风顺的,直接报错,比如括号不匹配或者匹配到最后多一个运算符这种情况,在上面的过程中并没有写出来
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的两个性质:可以让这个变量的定义域扩展到整个文件;不予许外部文件读取,有两个作用:
- 简化在这个文件的编程
- 保护系统安全,因为监视点可以获取系统正在执行程序的安全,如果被外部文件读取,那么系统的安全将受到很大的威胁,信息会被随意窃取
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
- 读入Makefile,并且查看是否调用其他的Makefile
- 读取include文件的Makefile
- 初始化变量
- 为目标文件建立依赖关系
- 重新生成目标
- 执行
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函数,成功返回文件的更新内容,就可以实现屏幕的更新