运行仙剑奇侠传

原版的仙剑奇侠传是针对Windows平台开发的, 因此它并不能在GNU/Linux中运行(你知道为什么吗?), 也不能在Navy-apps中运行. 网友weimingzhi开发了一款基于SDL库, 跨平台的仙剑奇侠传, 工程叫SDLPAL. 你可以通过git clone命令把SDLPAL克隆到本地, 然后把仙剑奇侠传的数据文件(我们已经把数据文件上传到提交网站上)放在工程目录下, 执行make编译SDLPAL, 编译成功后就可以玩了. 更多的信息请参考SDLPAL工程中的README说明.

我们的框架代码已经把SDLPAL移植到Navy-apps中了. 移植的主要工作就是把仙剑奇侠传调用的所有API重新实现一遍, 因为这些API大多都依赖于操作系统提供的运行时环境, 我们需要根据Nanos-lite和Navy-apps提供的运行时环境重写它们. 重写内容主要包括以下三部分:

  • C标准库
  • 浮点数
  • SDL库

Navy-apps中的Newlib已经提供了C标准库的功能, 我们无需额外移植. 关于浮点数的移植工作, 我们会在PA5中再来讨论, 目前先忽略它. 为了用NDL的API来替代原来SDL的相应功能, 移植工作需要对SDLPAL进行了少量修改, 包括去掉了声音, 修改了和按键相关的处理, 把我们关心的与NDL相关的功能整理到hal/hal.c中, 一些我们不必关心的实现则整理到unused/目录下. 框架代码已经把这些移植工作都做好了, 目前你不需要编写额外的代码来进行移植.

在NEMU中运行仙剑奇侠传

现在是万事俱备, 终于到了激动人心的时刻了! 从课程网站上下载仙剑奇侠传的数据文件, 并放到navy-apps/fsimg/share/games/pal/目录下, 更新ramdisk之后, 在Nanos-lite中加载并运行/bin/pal. 游戏操作请阅读这里.

在我们提供的数据文件中包含一些游戏存档, 可以读取迷宫中的存档. 但与怪物进行战斗需要进行一些浮点数相关的计算, 而NEMU目前没有实现浮点数, 因而不能成功进行战斗. 我们会在PA5中再来解决浮点数的问题, 目前我们先暂时不触发战斗, 可以先通过"新的故事"进行游戏.

pal

我不是南京大学的学生, 如何获取仙剑奇侠传的数据文件?

由于数据文件的版权属于游戏公司, 我们不便公开. 不过作为一款有20年历史的经典游戏, 你应该还是可以通过STFW找到它的.

不要在仙剑奇侠传中按p键

在SDLPAL中, p键是用于截屏的快捷键, 会把截屏结果保存到一个新文件中. 但我们的简易文件系统并不支持新文件的创建, 从而导致panic, 这属于正常现象.

仙剑奇侠传的脚本引擎

navy-apps/apps/pal/src/game/script.c中有一个PAL_InterpretInstruction()的函数, 尝试大致了解这个函数的作用和行为. 然后大胆猜测一下, 仙剑奇侠传的开发者是如何开发这款游戏的? 你对"游戏引擎"是否有新的认识?

不再神秘的秘技

网上流传着一些关于仙剑奇侠传的秘技, 其中的若干条秘技如下:

  1. 很多人到了云姨那里都会去拿三次钱, 其实拿一次就会让钱箱爆满! 你拿了一次钱就去买剑把钱用到只剩一千多, 然后去道士那里, 先不要上楼, 去掌柜那里买酒, 多买几次你就会发现钱用不完了.
  2. 不断使用乾坤一掷(钱必须多于五千文)用到财产低于五千文, 钱会暴增到上限, 如此一来就有用不完的钱了
  3. 当李逍遥等级到达99级时, 用5~10只金蚕王, 经验点又跑出来了, 而且升级所需经验会变回初期5~10级内的经验值, 然后去打敌人或用金蚕王升级, 可以学到灵儿的法术(从五气朝元开始); 升到199级后再用5~10只金蚕王, 经验点再跑出来, 所需升级经验也是很低, 可以学到月如的法术(从一阳指开始); 到299级后再用10~30只金蚕王, 经验点出来后继续升级, 可学到阿奴的法术(从万蚁蚀象开始).

假设这些上述这些秘技并非游戏制作人员的本意, 请尝试解释这些秘技为什么能生效.

基础设施(3)

如果你的仙剑奇侠传无法正确运行, 借助AM, 你应该可以很快确认是硬件还是软件bug. 如果是硬件bug, 你也许会陷入绝望之中: 基于QEMU的DiffTest速度太慢了! 有什么方法可以加快DiffTest的速度呢?

自由开关DiffTest模式

目前每次DiffTest都是从一开始进行, 但如果这个bug在很久之后才触发, 那么每次都从一开始进行DiffTest是没有必要的. 如果我们怀疑bug在某个函数中触发, 那么我们更希望DUT首先按照正常模式运行到这个函数, 然后开启DiffTest模式, 再进入这个函数. 这样, 我们就节省了前期大量的不必要的比对开销了.

为了实现这个功能, 关键是要在DUT运行中的某一时刻开始进入DiffTest模式. 而进入DiffTest模式的一个重要前提, 就是让DUT和REF的状态保持一致, 否则进行比对的结果就失去了意义. 我们又再次提到了状态的概念, 你应该再熟悉不过了: 计算机的状态就是计算机中的时序逻辑部件的状态. 这样, 我们只要在进入DiffTest模式之前, 把REF的寄存器和内存设置成和DUT一样, 它们就可以从一个相同的状态开始进行对比了.

幸运的是, DiffTest的API已经把大部分的工作都准备好了. 为了控制DUT是否开启DiffTest模式, 我们还需要在简易调试器中添加如下两个命令:

  • detach命令用于退出DiffTest模式, 之后DUT执行的所有指令将不再与REF进行比对. 实现方式非常简单, 只需要让difftest_step(), difftest_skip_dut()difftest_skip_ref()直接返回即可.
  • attach命令用于进入DiffTest模式, 之后DUT执行的所有指令将逐条与REF进行比对. 为此, 你还需要将DUT中物理内存的内容分别设置到REF相应的内存区间中, 并将DUT的寄存器状态也同步到REF中. 特别地, 如果你选择x86, 你需要将DUT中[0, 0x7c00)[0x100000, PMEM_SIZE) 的内存内容分别设置到REF相应的内存区间中, 这是因为x86-QEMU中在0x7c00附近会有GDT相关的代码, 覆盖这段代码会使得QEMU无法在保护模式下运行, 导致后续无法进行DiffTest, 因此我们要绕过相应的内存区间, 具体细节可以参考nemu/tools/qemu-diff/src/isa/x86/init.c中的init_isa()函数.

这样以后, 你就可以通过以下方式来在客户程序运行到某个目标位置的时候开启DiffTest了:

  • 去掉运行NEMU的-b参数(在nexus-am/am/arch/x86-nemu/img/run中), 使得我们可以在客户程序开始运行前键入命令
  • 键入detach命令, 退出DiffTest模式
  • 通过单步执行, 监视点, 断点等方式, 让客户程序通过正常模式运行到目标位置
  • 键入attach命令, 进入DiffTest模式, 注意设置REF的内存需要花费约数十秒的时间
  • 之后就可以在DiffTest模式下继续运行客户程序了

不过上面的方法还有漏网之鱼, 具体来说, 我们还需要处理一些特殊的寄存器, 因为它们也属于机器状态的一部分. 以x86为例, 我们还需要处理EFLAGS和IDTR这两个寄存器, 否则, 不一致的EFLAGS会导致接下来的jcc或者setcc指令在QEMU中的执行产生非预期结果, 而不一致的IDTR将会导致在QEMU中执行的系统调用因无法找到正确的目标位置而崩溃. 为了解决EFLAGS的一致性问题, 我们可以修改qemu-diff的代码, 让DIFFTEST_REG_SIZE覆盖EFLAGS, 这样就可以在DiffTest和寄存器访问相关的API中处理EFALGS了, 但不要忘记检查寄存器排列的顺序, 同时在difftest_step()中跳过EFLAGS相关的检查, 因为NEMU中对EFLAGS的实现是不完整的.

而为了解决IDTR的问题, 我们需要使用一种稍微复杂的方法, 这是因为GDB协议中没有定义IDTR寄存器的访问, 这意味着我们无法通过difftest_setregs()来修改QEMU中的IDTR. 既然无法直接修改, 我们就让QEMU执行一段指定的代码来设置IDTR吧. 具体地, 我们只要让QEMU执行一条lidt指令就可以了, 通过这条指令将一个IDT描述符的内容装载到IDTR中. 所以我们首先需要准备一个长度为6字节的IDT描述符, 内容就是当前NEMU中IDTR的内容. 我们需要将这个描述符放置到QEMU中一个安全的地方, 不要被其它内容覆盖的同时, 也不要覆盖其它内容. 一个合适的内存位置就是0x7e00, 我们可以将IDT描述符放在REF的这个内存位置. 然后再准备一条lidt (0x7e00)指令, 把这条指令放在REF的内存位置0x7e40, 然后将REF的eip设置成这个内存位置, 并让REF执行一条指令. 顺利的话, QEMU就设置好IDTR了, 将来QEMU就可以成功执行系统调用了.

实现可自由开关的DiffTest

根据上述内容, 在简易调试器中添加detachattach命令, 实现正常模式和DiffTest模式的自由切换. 如果你不知道怎么做, 请参考我们在PA2中讨论的DiffTest API.

上述文字基本上把涉及的主要内容都介绍过了, 不过留了一些小坑, 关键就看你对状态的理解是否到位了.

快照

更进一步的, 其实连NEMU也没有必要每次都从头开始执行. 我们可以像仙剑奇侠传的存档系统一样, 把NEMU的状态保存到文件中, 以后就可以直接从文件中恢复到这个状态继续执行了. 在虚拟化领域中, 这样的机制有一个专门的名字, 叫快照. 如果你用虚拟机来做PA, 相信你对这个名词应该不会陌生. 在NEMU中实现快照是一件非常简单的事情, 我们只需要在简易调试器中添加如下命令即可:

  • save [path], 将NEMU的当前状态保存到path指示的文件中
  • load [path], 从path指示的文件中恢复NEMU的状态
在NEMU中实现快照

关于NEMU的状态, 我们已经强调过无数次了, 快去实现吧. 另外, 由于我们可能会在不同的目录中执行NEMU, 因此使用快照的时候, 建议你通过绝对路径来指示快照文件.

展示你的批处理系统

在PA3的最后, 你将会向Nanos-lite中添加一些简单的功能, 来展示你的批处理系统.

Navy-apps准备了一个开机菜单程序, 在Nanos-lite中加载/bin/init就可以运行它了. 不过为了运行它, 你还需要在VFS中添加一个特殊文件/dev/tty, 只要让它往串口写入即可. 这个开机菜单程序中准备了若干项选择, 你可以通过键入数字来选择运行相应的程序. 不过你会发现, 键入数字之后就会触发HIT BAD TRAP, 这是因为我们需要实现一个新的系统调用.

这个系统调用就是SYS_execve, 它的作用是结束当前程序的运行, 并启动一个指定的程序. 这个系统调用比较特殊, 如果它执行成功, 就不会返回到当前程序中, 具体信息可以参考man execve. 为了实现这个系统调用, 你只需要在相应的系统调用处理函数中调用naive_uload()就可以了. 目前我们只需要关心filename即可, argvenvp这两个参数可以暂时忽略.

添加开机菜单

你需要实现SYS_execve系统调用. 你已经实现过很多系统调用了, 需要注意哪些细节, 这里就不啰嗦了.

展示你的批处理系统

有了开机菜单程序之后, 就可以很容易地实现一个有点样子的批处理系统了. 你只需要修改SYS_exit的实现, 让它调用SYS_execve来再次运行/bin/init, 而不是直接调用_halt()来结束整个系统的运行. 这样以后, 在一个用户程序结束的时候, 操作系统就会自动再次运行开机菜单程序, 来让用户选择一个新的程序来运行.

终极拷问

自古以来, 计算机系统方向的课程就有一个终极拷问:

当你在终端键入./hello运行Hello World程序的时候, 计算机究竟做了些什么?

你已经实现了批处理系统, 开机菜单程序其实就像是运行在终端里面的shell, 而键入数字就相当于在终端里面键入./hello来运行程序. 尽管我们的批处理系统经过了诸多简化, 但还是保留了计算机发展史的精髓. 实现了批处理系统之后, 你对上述的终极拷问有什么新的认识?

运行超级玛丽 (建议二周目思考)

开机菜单中有两个和LiteNES相关的条目, 分别是运行功夫和超级玛丽. 但目前运行它们可能会触发错误, 你知道这是为什么吗?

如果你修复了上述错误, 再选择运行超级玛丽, 你可能会发现最后运行的还是功夫, 你知道这又是为什么吗? 为了让这两个选项运行不同的游戏, 你需要对系统(而不是LiteNES本身)进行哪些改动呢?

体会一下就行了

由于性能原因, LiteNES的运行会非常缓慢. 实验并不要求你流畅地运行LiteNES, 体会一下就行了.

到这里为止, 我们基本上实现了一个"现代风"的批处理系统了. 如果你还记得PA1一开始提到的红白机, 现在我们实现的功能, 就类似红白机开机之后的游戏选择菜单, 甚至比红白机的功能还强大, 毕竟红白机上面并没有运行操作系统. 我们通过在机器中添加CTE来支持用户程序和操作系统之间的执行流切换, 并基于此实现系统调用机制, 向用户程序提供了全新的运行时环境. 系统是用来运行程序的, 我们又通过系统调用给用户程序开放更多的功能, 包括AM中不存在的文件系统. 就靠这几个看似功能简陋的系统调用, 我们就已经能支撑真实游戏仙剑奇侠传的运行了. 真实的游戏和系统固然会更复杂, 但现在的你, 是否觉得对它们多了一些理解呢?

必答题 - 理解计算机系统
  • 理解上下文结构体的前世今生 (见PA3.1阶段)
  • 理解穿越时空的旅程 (见PA3.1阶段)
  • hello程序是什么, 它从而何来, 要到哪里去 (见PA3.2阶段)

  • 仙剑奇侠传究竟如何运行 运行仙剑奇侠传时会播放启动动画, 动画中仙鹤在群山中飞过. 这一动画是通过navy-apps/apps/pal/src/main.c中的PAL_SplashScreen()函数播放的. 阅读这一函数, 可以得知仙鹤的像素信息存放在数据文件mgo.mkf中. 请回答以下问题: 库函数, libos, Nanos-lite, AM, NEMU是如何相互协助, 来帮助仙剑奇侠传的代码从mgo.mkf文件中读出仙鹤的像素信息, 并且更新到屏幕上? 换一种PA的经典问法: 这个过程究竟经历了些什么?

温馨提示

PA3到此结束. 请你编写好实验报告(不要忘记在实验报告中回答必答题), 然后把命名为学号.pdf的实验报告文件放置在工程目录下, 执行make submit对工程进行打包, 最后将压缩包提交到指定网站.

results matching ""

    No results matching ""