union {
union {
union {
uint32_t _32;
uint16_t _16;
uint8_t _8[2];
};
uint32_t val;
} gpr[8];
struct { // do not change the order of the registers
uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
};
};
in, out
在nemu/src/cpu/exec/system.c中
in: 通过判断目的操作数的宽度来选择pioread[l|w|b]()函数把从id_src中读取的值写入id_dest中的寄存器.
out: 通过判断源操作数的宽度来从id_src的寄存器中读取值然后选择piowrite[l|w|b]()函数把值写入id_src.
# diff_test
qemu eax:0x00000001, nemu eax:0x00000001
qemu ecx:0x00000000, nemu ecx:0x00000000
qemu edx:0x00000000, nemu edx:0x00000000
qemu ebx:0x00000000, nemu ebx:0x00000000
qemu esp:0x00007b30, nemu esp:0x00007b34
qemu ebp:0x00007b48, nemu ebp:0x00007b48
qemu esi:0x0347ae1f, nemu esi:0x0347ae1f
qemu edi:0x33ce1790, nemu edi:0x33ce1790
qemu eip:0x00100e8b, nemu eip:0x00100e73
nemu: ABORT at eip = 0x00100e73
Programming Assignment 实验
https://github.com/GQBBBB/ics2018
[ ] PA5
PA0
搭建环境。 注意:
PA1
RTFSC
实现寄存器结构体 在这一部分,你需要在
nemu/include/cpu/reg.h
中正确实现寄存器才可以跑通NEMU. 手册已经给出了通用寄存器结构.至于为什么这样实现,你还要参考该文件后半部分的
check_reg_index
,define
和nemu/src/cpu/reg.c
.为什么在cmd_c()函数中, 调用cpu_exec()的时候传入了参数-1 我们可以看到
nemu/src/monitor/cpu-exec.c
中的void cpu_exec(uint64_t n)
. 在C99标准中定义了该数据类型typedef unsigned long int uint64_t;
, -1可转化为无符号数2^64 - 1. 这几乎代表执行exec_wrapper()
无数次. 然而exec_wrapper()
函数就是执行并更新%eip
的,cmd_c()
指令的的意图就在于执行所有指令.基础设施
在
nemu/src/monitor/debug/ui.c
中实现各命令:cmd_table
中填写一下要实现的命令,格式可以模仿已存在的.表达式求值
在
nemu/src/monitor/debug/expr.c
中实现表达式求值相关内容.make_token
, 此函数是用于具体识别token, 把识别出来的字符串copy到token.str里(注意末尾加'\0'), 把字符串类型放入token.type中(注意type为int类型,'+-*/'可以直接以符号(char 类型)代替, 例如空格类型为TK_NOTYPE, 需要你在enum中给它一个值,最好>256且不要重复).check_parentheses
检查左右括号是否符合规则:dominant_operator
求主操作符:eval
函数递归求值:实现本节时多打Log, 有助于排错.
监视点
监视点实现可以根据讲义要求实现即可, 主要就是对链表进行操作.
PA2
RTFSC
在开始此节之前建议先行完成基础设施(2) Differential Testing更有助于调试哟~_~
这一节主要分为四个部分: opcode_table数组, 解码函数make_DopHelper, 执行函数make_EHelper和RTL指令. make_DopHelper: 主要以i386手册附录A中的操作数表示记号来命名. make_EHelper: 它们的名字是指令操作本身. 执行函数通过RTL来描述指令真正的执行功能.
我们需要执行
nexus-am/tests/cputest/tests/
下的程序来查看都需要完成那些指令, 有的时候HIT GOOD TRAP并不代表指令完成正确, 程序出错可能是由累积的错误导致的, 总之这一部分非常耗时.opcode_table 根据
nemu/src/cpu/decode/decode.c
的函数的注释和i386手册的Appendix A -- Opcode Map,在opcode_table中进行填写。 填写形式大部分如下所示, opcode_table中的项基本都是IDEXW的变体, 分为id, ex, w三部分.程序会根据id在
nemu/src/cpu/decode/decode.c
中找到译码函数, 译码函数已实现, 只需要填对id即可, 这部分实现要参照decode.c
文件注释和手册. ex就是执行函数的名字, 执行函数分布在nemu/src/cpu/exec/
文件夹下, 需要你自己实现(使用RTL指令), 实现后记得在all-instr.h
中声明一下. 这部分实现要参照手册. w指操作数宽度, 可以看到默认情况下w=0, 当 w=0 时,set_width
会根据decoding.is_operand_size_16
自动判断操作数是两个字节还是四个. Opcode Map中的Ev中的v就是操作数的宽度。在i386的附录A会发现v指的是word or double word,也就是2或4字节。如果传进去的width是0,那么它会判断操作数是2字节还是4字节,如果不是0,那么就直接把width设为操作数宽度。所以,遇到v,那么第三个参数应该是0,如果遇到Eb,b的意思是byte,就是1。 另外, 因为需要在执行函数中使用RTL, 应该在nemu/include/cpu/rtl.h
中根据提示把RTL指令先行完成.程序, 运行时环境与AM
实现
nexus-am/libs/klib/src/string.c
和nexus-am/libs/klib/src/stdio.c
中列出的库函数, 可以参照c库的实现.基础设施(2)
difftest是个很有用的工具, 不过如果不调试的话尽量不要开, 它有可能会导致一些莫名其妙的错误.
一键回归测试结果如下:
输入输出
in, out 在
nemu/src/cpu/exec/system.c
中 in: 通过判断目的操作数的宽度来选择pioread[l|w|b]()函数把从id_src中读取的值写入id_dest中的寄存器. out: 通过判断源操作数的宽度来从id_src的寄存器中读取值然后选择piowrite[l|w|b]()函数把值写入id_src.时钟 根据
nexus-am/am/amdev.h
中定义的数据结构,_DEVREG_TIMER_UPTIME
需要把当前时间减去启动时间, 获取时间需要使用inl(nexus-am/am/arch/x86-nemu/include/x86.h)指令和RTC_PORT(nemu/src/device/timer.c). 我们可以在timer初始化时获取一次时间, 然后在_DEVREG_TIMER_UPTIME处获取一次时间, 用他们的差值来获得启动时间.键盘 按键键盘发送该键的通码(make code); 释放一个键时键盘会发送该键的断码(break code). 通码 = 断码 | 0x8000, 通码和断码的区别在于通码第20位为一, 断码第20位为零. keydown = 1为按下按键, keydown = 0为释放按键. keycode为按键的断码, 没有按键时, keycode为_KEY_NONE. 同样用inl指令从I8042_DATA_PORT(nemu/src/device/keyboard.c)处获取通码/断码.
VGA 用inl指令从SCREEN_PORT(nemu/src/device/vga.c)处获取屏幕大小(高16位为宽, 低16位为高). 内存映射I/O需要通过is_mmio()函数判断一个物理地址是否被映射到I/O空间, 如果是则调用mmio_read()或mmio_write(), 调用时需要提供映射号(is_mmio()返回). 如果不是内存映射I/O的访问, 就访问pmem. 实现_DEVREG_VIDEO_FBCTL:
PA3
自陷指令
int
nemu 程序使用自陷指令
int
进行执行流的切换, 程序执行自陷指令之后, 就会陷入到操作系统预先设置好的跳转目标, 而跳转目标是通过门描述符(GateDesc)来指示的.触发异常后后按要求在nemu中的system.c中实现int的执行函数, 它调用了intr.c中的raise_intr函数, 并传入int指令的dest操作数和当前eip. 按照以下要求完成raise_intr函数:
CTE(ConText Extension)
nexus-am 注意这两个API:
int _cte_init(_Context* (*handler)(_Event ev, _Context *ctx))
用于进行CTE相关的初始化操作. 其中它还接受一个来自操作系统的事件处理回调函数的指针, 当发生事件时, CTE将会把事件和相关的上下文作为参数, 来调用这个回调函数, 交由操作系统进行后续处理. 它具体会做两件事:void _yield()
用于进行自陷操作, 会触发一个编号为_EVENT_YIELD事件. 在x86-nemu中, 我们约定自陷操作通过int $0x81
触发.保存上下文
nexus-am 成功跳转到自陷入口函数vectrap()(trap.S)之后, 需要用
pusha
指令把通用寄存器的值压栈. vectrap()会压入错误码和异常号#irq, 然后跳转到asm_trap(). 这些内容连同之前保存的错误码, #irq, 以及硬件保存的EFLAGS, CS, EIP, 形成了完整的上下文. 接下来代码将会把当前的%esp压栈, 并调用irq_handle()函数(cte.c).irq_handle()会把执行流切换的原因打包成编号为_EVENT_xxx的事件
case 0x81: ev.event = _EVENT_YIELD; break;
, 然后调用在_cte_init()(目前只有vertrap, 其他的还需要自己添加)idt[0x81] = GATE(STS_TG32, KSEL(SEG_KCODE), vectrap, DPL_KERN);
中注册的事件处理回调函数, 将事件交给Nanos-lite来处理. 在Nanos-lite中, 这一回调函数是nanos-lite/src/irq.c中的do_event()函数case _EVENT_YIELD: Log("receive _EVENT_YIELD event"); break;
.系统调用
用户程序通过自陷指令
int $0x80
指令触发系统调用.navy-apps/libs/libos/src/nanos.c
中系统调用接口syscall(), 其中的内联汇编会先把系统调用的参数依次放入%eax, %ebx, %ecx, %edx四个寄存器中, 然后执行自陷指令int $0x80
. x86-nemu的CTE会将这个自陷操作打包成一个系统调用事件_EVENT_SYSCALL, 并交由Nanos-lite继续处理. Nanos-lite收到系统调用事件之后, 就会调出系统调用处理函数do_syscall()(syscall.c)进行处理. do_syscall()首先通过宏GPR1从上下文c中获取用户进程之前设置好的系统调用参数, 通过第一个参数系统调用号进行分发.经过CTE, 执行流会从do_syscall()一路返回到用户程序的上述内联汇编中. 内联汇编最后从%eax寄存器中取出系统调用的返回值, 并返回给_syscall()的调用者, 告知其系统调用执行的情况.
GPR?宏有5个,其中GPR1一定是对应%eax, 按照上文所述的依次放入,对应关系如下:
PS. 到此完成所有步骤后一直出现如下错误:
查看0x00100e73处指令为:
非常纳闷, 竟然在trap.S中出错了, 简直没道理. 发现qemu和nemu的eip不同, 并且push指令明明早已经实现了. 后来把diff_test关掉以后果然HIT GOOD TRAP了......
总结一下自陷指令
int
的流程:标准输出
按照说明实现sys_write即可, 需要注意的是要在
navy-apps/libs/libos/src/nanos.c
的_write()中调用系统调用接口函数.堆区管理
根据讲义使SYS_brk系统调用总是返回0即可, 表示堆区大小的调整总是成功. _sbrk()则通过以下步骤编写:
当没有实现SYS_brk和_sbrk()时, 运行hello程序, 除了第一条使用write输出的Hello World可以完整输出外, 其余使用printf的输出均只输出首字母H. 实现堆区管理后printf函数可以完整输出语句了.
文件系统
实现open, close, read, write, lseek等系统调用, 流程和上面差不多. 需要注意的是write系统调用, 由于在上面实现I/O时简单的实现了一下write(直接由_putc()输出),所以现在需要对它进行改造:
在做到后面时发现这一段话
因此我们可以把上述fs_write代码中的
if (fd == 1 || fd == 2)
替换为if (fd->write)
.当你在VFS中只完成
/dev/fb
和/proc/dispinfo
时, 让Nanos-lite加载/bin/bmptest, 并不能输出看到屏幕上显示ProjectN的Logo:还要继续往下完成
/bin/events
:仙剑运行成功,肉眼可见的刷新,那一群
鸭子从右边飞到左边飞了十几分钟 : ) “仙剑奇侠传”这个界面可能要停留几十分钟才能继续加载,开始新故事后(也就是李逍遥和李大娘的那个场景)会直接退出,目测是触发了SYS_exit系统调用。PA4
多道程序
我们要做的就是恢复该进程上下文之前, 把栈顶指针切到另一个进程栈上,指向另一个进程的上下文结构从而偷梁换柱.
我们需要根据一个叫进程控制块(PCB)的结构(确切的说是其中的_Context *cp)来保存上下文的位置, 从而能找到其他程序上下文.
创建上下文 根据讲义图示设置cp指针, 设置eip为要执行的函数entry, 设置cs为0x8即可.
进程调度 在
nanos-lite/src/proc.c
中完成调度函数schedule(), 在nanos-lite/src/irq.c
中收到_EVENT_YIELD事件后, 调用schedule()并返回其现场. 在nexus-am/am/arch/x86-nemu/src/trap.S
中的asm_trap()添加movl %eax, %esp
, 先将栈顶指针切换到新进程的上下文结构, 然后才恢复上下文, 从而完成上下文切换的本质操作.创建用户进程上下文 与_kcontext()相比, 除了在栈上创建必要的上下文信息之外, _ucontext()还需要在栈上准备一个栈帧, 用于存放main()函数的参数信息(设置为0或NULL). 这个栈帧将来会被
navy-apps/libs/libc/src/platform/crt0.c
中的_start()函数使用.超越容量的界限
在分页机制上运行Nanos-lite 在这里我们需要在
vaddr_read()
和vaddr_write()
中检测CR0的PG是否为1来确认是否需要经过分页地址转换. 需要分页地址转换的调用page_translate()
函数进行转换. 该页表为二级页表, 我们需要先从CR3中获取页目录表基地址, 加上偏移位后获得页表基地址所在页目录项, 页表基地址再加上偏移位之后获得对应页表项, 读取页表项的值加上偏移地址可以得到物理地址.在分页机制上运行用户进程 在context_uload()(nanos-lite/src/loader.c)的开头调用_protect(), 并且把pcb->as的地址传给它.
在
nexus-am/am/arch/x86-nemu/src/vme.c
中实现_map()函数:修改loader()实现: 计算要加载程序有多少页, 然后对于每一页, 申请一页物理页, 建立虚拟地址到物理地址的映射, 最后再把文件内容拷贝至物理页. 记得在加载所有内容至物理页后(for或while循环结束后),更新pcb的cur_brk 和max_brk 为文件末尾地址. 否则未来运行app时会出现以下错误:
实现
nanos-lite/src/mm.c
中的mm_brk()函数, 该函数作用就是通过一个循环将从current->max_brk到new_brk为止的部分,以页为单位分别申请一页空闲页并建立映射.还有一些其他改动按照讲义要求实现即可.
时钟中断
抢占多任务 大部分没什么要说的, 在修改_ucontext()的代码构造上下文的时候, 把eflags寄存器的IF位设置为开中断(1)以保证上下文恢复后能正常响应中断即可.
最后
稍微修改一下events_read(), init_proc()和schedule()函数以支持使用F1, F2, F3切换三个pal.
PA实验必做部分到此结束~