NJU PA 2022: 激动人心的系统遨游(三)PA3
PA3 - 穿越时空的旅程: 批处理系统
小型操作系统Nanos-lite正式登场!!!虽然这个操作系统是个阉割版的,但是还是有基本的功能。比如要我们自己实现的中断机制,系统调用,AM库的接口以及阉割版文件系统,但是麻雀虽小,五脏俱全啊,原来批处理系统并没有想像中那么复杂,但是还是需要扣很多细节,其中要数上下文(抽象成CTE,等等,又是抽象?)这块内容,贯穿了整个PA的后半程(但是真的很搞!!!)
例如,上下文存储在哪里,保存上下文后pc应该指向哪,或者换句话说程序要运行什么,以及如何识别异常的类型,怎么恢复上下文,这就需要我们好好的阅读指导书了
另外还有程序的ELF,里面包含的信息都非常的有用,包括程序在内存中的地址,各个函数的地址大小还有程序的段空间,这会对调试有很大的帮助
prinf原来里面还有缓冲区啊,第一次听说。printf换行时会刷新该缓冲区
一切皆文件这个思想本质上还是一种抽象,这里很有趣将设备接口抽象成了一个文件进行调用(这里的文件使用open调用,因为此时该文件系统是由中断机制进行调用的,而fopen则是打开指定文件读取里面的内容)
VGA显存的抽象对于我来说是个难点,给个提示,图像是由行和列上的像素组成的,那么像素时如何控制图像大小,位置,以及填充的内容呢,思考一下应该就很清晰了吧
后面移植仙剑的时候像素版调用时的bug,太好玩了,调色版颜色搞错了
有没有吸血鬼的感觉
下面是思考题
1.x86通过软件来保存异常号, 没有类似cause的寄存器.mips32和riscv32也可以这样吗? 为什么?
没思考出来,是否和RISCV32与软件之间的交互有关
2.我们知道进行函数调用的时候也需要保存调用者的状态: 返回地址, 以及calling convention中需要调用者保存的寄存器.而CTE在保存上下文的时候却要保存更多的信息. 尝试对比它们, 并思考两者保存信息不同是什么原因造成的.
CTE要额外保存异常类型和控制状态信息,因为异常触发之后再返回需要复原现场,CTE要保存当时程序运行时的现场,而函数调用则只需要存储变量和返回地址即可
3.你会在__am_irq_handle()中看到有一个上下文结构指针c, c指向的上下文结构究竟在哪里?
在trap.s中
c指向的这个上下文结构又是怎么来的?具体地, 这个上下文结构有很多成员, 每一个成员究竟在哪里赋值的?
在trap.s中定义了mcause,mstatus和mepc的偏移量,对应着Context结构体中定义变量的顺序,随后通过sw 和lw调取cpu中控制寄存器的值
$ISA-nemu.h, trap.S, 上述讲义文字, 以及你刚刚在NEMU中实现的新指令, 这四部分内容又有什么联系?
有的,trap.s中有我们需要实现的新文字,而riscv32-nemu.h则会调用trap.s来进行保存现场的操作
4.从Nanos-lite调用yield()开始, 到从yield()返回的期间, 这一趟旅程具体经历了什么? 软(AM, Nanos-lite)硬(NEMU)件是如何相互协助来完成这趟旅程的?你需要解释这一过程中的每一处细节, 包括涉及的每一行汇编代码/C代码的行为, 尤其是一些比较关键的指令/变量.事实上, 上文的必答题"理解上下文结构体的前世今生"已经涵盖了这趟旅程中的一部分, 你可以把它的回答包含进来.
从yield()调用开始,NEMU开始处理yiedl中的内敛汇编代码,随后在调用到ecall指令之后直接跳转到在mtvem存储的地址,之后调用trap.s,trap.s首先开辟了一块栈空间,随后将当前现场的寄存器值全部通过sw存储在栈中,随后将三个状态寄存器mcause,mstatus和mepc存入通用寄存器中,随后根据AM中Context上下文结构体声明的变量顺序,依次将状态寄存器存入Context中,之后便运行注册的回调函数,其中不同类型的异常会分别产生不同的效果,在此处我们调用的异常类型为yield,随后会调用do_event对异常进行处理,之后再次跳回trap.s进行现场复原,将之前在栈中存储的mstatus和mepc的值重新放回控制寄存器中,通用寄存器同理,然后返回。
5.我们提到了代码和数据都在可执行文件里面, 但却没有提到堆(heap)和栈(stack).为什么堆和栈的内容没有放入可执行文件里面? 那程序运行时刻用到的堆和栈又是怎么来的? AM的代码是否能给你带来一些启发?
堆和栈是在程序运行时开辟的,而不是本身就存储在程序的过程中,程序运行时刻的堆和栈是从内存中开辟一块空间
6.思考一下, GNU/Linux是如何知道"格式错误"的?
看这个文件是否有ELF头
7.你会看到一个segment包含两个大小的属性, 分别是FileSiz和MemSiz, 这是为什么?
FileSize指的是目标文件中的段大小,MemSize指的是内存文件中的段大小
再仔细观察一下, 你会发现FileSiz通常不会大于相应的MemSiz, 这又是为什么?
因为MemSize是内存中段大小,目标文件的段大小不应该超过内存中段大小,否则会产生溢出
8.为什么需要将 [VirtAddr + FileSiz, VirtAddr + MemSiz) 对应的物理区间清零?
初始化未定义的.bss段,具体来说就是在运行时将未定义的.bss数据初始化为0
9.对于批处理系统来说, 系统调用是必须的吗? 如果直接把AM的API暴露给批处理系统中的程序, 会不会有问题呢?
是必须的,因为要同时处理多个程序时,如果没有系统调用,则批处理系统中程序对于内存的操作可能会产生越界或者溢出。(比如一个程序在调用之后,没将环境复原,则下一个程序进行时原本的批处理系统会产生变化)
10.我们知道navy-apps/tests/hello/hello.c只是一个C源文件, 它会被编译链接成一个ELF文件. 那么, hello程序一开始在哪里?它是怎么出现内存中的? 为什么会出现在目前的内存位置?它的第一条指令在哪里? 究竟是怎么执行到它的第一条指令的?hello程序在不断地打印字符串, 每一个字符又是经历了什么才会最终出现在终端上?
11.fixedpt和float类型的数据都是32位, 它们都可以表示2^32个不同的数. 但由于表示方法不一样, fixedpt和float能表示的数集是不一样的.
思考一下, 我们用fixedpt来模拟表示float, 这其中隐含着哪些取舍?
定点数只能模拟能够被2整除的数,最后得到的结果不精确,但是实现方便
浮点数在IEEE754格式下能够更加精确,但是实现复杂
12阅读fixedpt_rconst()的代码, 从表面上看, 它带有非常明显的浮点操作, 但从编译结果来看却没有任何浮点指令. 你知道其中的原因吗?
不太清楚
在navy-apps/apps/pal/repo/src/game/script.c中有一个PAL_InterpretInstruction()的函数, 尝试大致了解这个函数的作用和行为. 然后大胆猜测一下, 仙剑奇侠传的开发者是如何开发这款游戏的? 你对"游戏引擎"是否有新的认识?
仙剑奇侠传的开发者首先将游戏要实现的功能写在一起,这就是基本的游戏引擎(让游戏运作),而它所接受的“燃料“为游戏物体(Object)和游戏脚本(Script)游戏物体决定显示的内容,游戏脚本决定游戏的功能
自古以来, 计算机系统方向的课程就有一个终极拷问:当你在终端键入./hello运行Hello World程序的时候, 计算机究竟做了些什么?你已经实现了批处理系统, 并且成功通过NTerm来运行其它程序. 尽管我们的批处理系统经过了诸多简化,但还是保留了计算机发展史的精髓. 实现了批处理系统之后, 你对上述的终极拷问有什么新的认识?
file system store "hello"--------------------->./hello ---->syscall_execve(hello)------>Interrupt/Expection----->open filepath(../hello)---->run hello------>exit;
运行仙剑奇侠传时会播放启动动画, 动画里仙鹤在群山中飞过. 这一动画是通过navy-apps/apps/pal/repo/src/main.c中的PAL_SplashScreen()函数播放的.阅读这一函数, 可以得知仙鹤的像素信息存放在数据文件mgo.mkf中. 请回答以下问题: 库函数, libos, Nanos-lite, AM, NEMU是如何相互协助,
来帮助仙剑奇侠传的代码从mgo.mkf文件中读出仙鹤的像素信息, 并且更新到屏幕上? 换一种PA的经典问法: 这个过程究竟经历了些什么?
(Hint: 合理使用各种trace工具, 可以帮助你更容易地理解仙剑奇侠传的行为)
犯病时刻:
1._write返回值没设置导致printf只输出头一个文字,耗时两天。
2.sdlpal.cfg配置仙剑奇侠传配置了两天,望周知(RDFS!!!!!!!)
记录:
1.RDFS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!写SDL时 一定要读源码,否则会出大大大大大问题
PA3总结:
经过前两个PA的洗礼(拷打),我觉得做PA3比前面要顺畅多了,但是坑依旧很多,特别是3.2和3.3,一定一定要阅读源码和反复看讲义,RDFS是解决问题的一切真理
这章主要解释了批处理系统是如何让一个程序在上面运行的过程,自己实现了简易的系统调用,文件系统和应用程序运行,进一步加深了我对系统的理解