本系列是南京大学蒋炎岩老师的操作系统课程学习笔记
课程主页:老师的wiki
课程视频:B站合集
第一个Lab是实现一个直接运行在硬件上的小游戏,由于Lab包含的项目较复杂,因此通过直接ssh使用vim写代码的方式有点不方便了,所以这里先配置了图形界面,中间也是踩了不少的坑
下载XQuartz
实现图像转发需要在本地机器上先安装一个XServer软件,在Mac上就是XQuartz,安装后可以通过它打开一个xTerm并使用ssh -X登录到远端,然后打开任意图形界面的话,XQuartz就会弹出一个对应的图形界面

本来打算就用这样的方式开发也无妨,大不了卡一点,但是当我使用XQuartz打开VSCode时,键盘映射特别奇怪,没法打出s,而且e变成了backspace,完全没法正常写代码,于是折腾了半天,在VSCode配了一个ssh+X11的环境
VSCode配置Remote SSH
在本机安装Remote - SSH插件后,就可以直接在VSCode通过ssh远程到远端的Linux了,打开文件就跟本地文件一样方便,这样就解决了键位不匹配的问题
这里还需要配置一下ssh的公钥,一是避免每次都要输密码,二是后面进行X11转发必须要配置这个
具体方法就是在远端机器的~/.ssh/目录新建一个文件authorized_keys,并把本机~/.ssh/目录的id_rsa.pub拷贝进去
VSCode配置X11
配置图像转发需要在本机安装Remote X11和Remote X11 (SSH)这两个插件,然后就是照着插件首页的方法配置,中间遇到了两个坑
xeyes没反应,这个是需要将本地的XQuartz打开才能进行转发- 打开了
XQuartz,但是运行xeyes会报错,这个是用VSCodeSSH登录的时候没有加-X选项所以找不到显示器 不过虽然配置了这个,貌似每次过一会就又无法启动GUI程序了,所以最后我还是使用的XQuartz原生的xTerm展示界面,VSCode用来远程写代码和使用终端
熟悉AbstractMachine
环境配好了,终于可以开始写代码了,但是这次程序的运行跟平常不太一样,毕竟要写的是操作系统代码,所以要跑在裸机上。实验提供了AbstractMachine的运行环境,然后我们通过qemu来模拟操作系统的运行。拉完代码后会发现有两个文件夹,abstract-machine和amgame,前者是AbstractMachine的核心代码,后者则是我们实现小游戏的目录
关于AbstractMachine提供的能力,可以在前文的wiki以及仓库中查看,仓库中还有一些示例代码可以参考,这里简单介绍几个常用的
void ioe_read (int reg, void *buf);
void ioe_write(int reg, void *buf);
这两个函数可以读取或写入寄存器
// 读取键盘事件
AM_INPUT_KEYBRD_T event = { .keycode = AM_KEY_NONE };
ioe_read(AM_INPUT_KEYBRD, &event);
// 读取时钟事件
io_read(AM_TIMER_UPTIME).us;
// 写入屏幕像素
static void draw_tile(int x, int y, int w, int h, uint32_t color) {
uint32_t pixels[w * h]; // WARNING: large stack-allocated memory
AM_GPU_FBDRAW_T event = {
.x = x, .y = y, .w = w, .h = h, .sync = 1,
.pixels = pixels,
};
for (int i = 0; i < w * h; i++) {
pixels[i] = color;
}
ioe_write(AM_GPU_FBDRAW, &event);
}
void puts(const char *s);
这个函数则是可以输出字符串到串口
运行默认小游戏
在实现小游戏前,可以先看看实验提供的默认实现是什么样子,从而加深一些理解,改起来也会更轻松
首先要编译amgame中相关的文件,由于MakeFile已经配置好了,因此只需要进入amgame运行make即可。运行之后目录中会生成build文件夹,里面有amgame-x86_64-qemu和amgame-x86_64-qemu.elf两个文件以及一个包含可重定向文件的文件夹

一开始我也是直接尝试运行amgame-x86_64-qemu.elf文件,然后就出现了Segmentation Fault
➜ build git:(L0) ./amgame-x86_64-qemu.elf
[1] 1618650 segmentation fault (core dumped) ./amgame-x86_64-qemu.elf
这个错误的原因老师已经在实验指导书中介绍过了,是因为错误的使用64位汇编去解析32位的指令,所以我们需要在qemu模拟器上运行包含这个文件的镜像文件amgame-x86_64-qemu
qemu-system-x86_64 -S -s -serial none -nographic hello-x86_64-qemu
-S表示在qemu启动后暂停-s表示在qemu启动后等待gdb进行调试。gdb中输入target remote localhost:1234来调试qemu-serial none忽略串口输入/输出-nographics不启动图形界面
实际使用时不带选项启动即可正常运行模拟器。界面显示出来后,我们会看到模拟器中展示的是一些黑白相间的方块,这部分的代码在video.c的splash函数中,可以看到其实是每隔一定距离就给一个方块大小的区域设定了白色的像素
这里总结一下这部分碰到的一些坑
- 使用
gdb调试后,总是显示找不到符号,所以我改了MakeFile,这样编译的时候就会带上符号信息了。然后再在gdb中输入file amgame-x86_64-qemu.elf来加载符号

- 没看到程序中打印的字符串,这个需要在
qemu中点击View菜单,然后选择serial0或者Show Tabs,就能看到了

- 使用Mac远程的时候,接收不到按键信息,我一度以为是
AbstractMachine的问题,而且还尝试进行了调试,甚至还连接了外接键盘,然而都没有效果。最后抱着试一试的心态小心翼翼的给蒋老师发了封邮件,没想到老师秒回,而且也提供了正确的解决方法——只要ssh登录的时候使用-XY选项就可以了,留张截图纪念一下😆

编写小游戏
现在就是将默认代码替换成自己的实现即可,按照实验指导书的思路,我将主循环换成了等待一帧的方式,这里uptime参照了am-kernel中NesLite的实现
然后就是接收键盘事件,处理位置速度的更新,最后刷新屏幕
int main(const char *args) {
ioe_init();
puts("Welcome to the ball game!\n");
puts("Press any key to start.\n");
uint32_t next_frame = 0;
int key;
splash();
while (1) {
while (uptime() < next_frame) ;
while ((key = readkey()) != AM_KEY_NONE) {
kbd_event(key);
}
game_process();
screen_update();
next_frame += 1000 / FPS;
}
return 0;
}
我实现的也是一个很简单的弹球小程序,然后可以通过按键更改两个方向的速度。里面最花费时间的可能也就是画出圆形的像素了,为此需要稍微用一点初中几何的知识,然后还自己写了个简单的sqrt函数。不过做的也不是很细致,偶尔渲染的时候会比较怪,也懒得弄了(
中间也遇到了一些坑:
- 声明像素数组以后,最好全部初始化成0,也就是黑色,不然有可能出现一些奇怪的颜色(比如我设置的是绿色的球,但其他地方出现了蓝色的细线)
- 刷新屏幕时,不能只渲染刚好包含球的那个正方形区域,还需要把原来的像素给清掉,本来我图省事想直接把整个屏幕先都设置成黑色,再去画圆,结果这样会导致像素数组过大,超过模拟器的最大限制。所以后来优化成了包含上一帧的正方形以及目前这一帧的正方形的一个矩形区域
AbstractMachine识别到的键盘输入不是Mac的键盘布局,所以上下左右可能对不上,所以我使用的是WASD来控制

最终效果:

完整代码放在Github仓库