跳转至

L0: 为计算机硬件编程

L0: 为计算机硬件编程 (jyywiki.cn)

1. 背景

很多 Tutorial 把硬件的细节 (例如 x86 的 GDT, IDT, TSS, ...) 和操作系统实现捆绑在一起,就显得更难上手。

操作系统和硬件之间的关系被夸大了

  • 硬件的确提供了必要的机制使我们能实现诸如进程/线程切换,但并不意味着除非我们知道有关硬件指令的一切才能编写操作系统;
  • 操作系统在 99.99% 的时候都是普通的 C 程序,只是偶尔会干一些 “出格” 的事情;
  • 当然会在适当的时候引入这些骚 (黑) 操 (科) 作 (技),以及它们是如何借助硬件指令和机制实现;

2. 实验描述

2.1. 阅读 AbstractMachine 文档

阅读 AbstractMachine 的文档

建立了一些基本概念后,就可以试着从样例代码出发开始编程

  • 实验框架中直接包含了 AbstractMachine 的代码并在 Makefile 中完成了配置 (RTFSC),无需额外配置/下载

2.1.实现 AbstractMachine 中 klib 中缺失的函数

AbstractMachine 框架代码中包含一个基础运行库的框架 klib,其中包含了方便你编写 “bare-metal” 程序的系列库函数。

  • 注意多处理器的情况下工作;
  • 编写相当多的 assertions。

2.2. 在 AbstractMachine 中显示一张图片

编写一个直接运行在 AbstractMachine 上 (仅仅启用 IOE 扩展,不使用其他硬件机制如中断/异常/虚拟存储/多处理器) 的程序,显示一张图片;满足:

  1. 显示一张任何图片,但能适配不同的屏幕大小。确保你的图片在 320×200、640×480、800×600 等分辨率下均可以正常显示;
  2. 图片中包含不少于 10 种颜色。
  3. 程序使用的内存 (代码、静态数据总和不超过 4 MiB、堆区 heap 使用不超过 4 MiB);
  4. 按 ESC 键后调用 halt() 退出;除此之外,按任何键程序都不应退出。

你的代码应当有一定的可移植性:

  • 同 minilabs 一样,你的程序可能运行在 32/64-bit 平台,因此你应当使用 intptr_tuintptr_t 来保存指针数值;
  • 兼容大/小端,因此禁止把不同大小的指针类型强制转换;

会在 x86_64-qemu (64-bit) 和 x86-qemu (32-bit) 两个环境下运行,你的程序必须在两个环境下都能正确编译并运行良好。

此外,AbstractMachine 假设 bare-metal 不支持浮点数指令。在 x86_64 的编译选项中,我们添加了 -mno-sse。尽管有浮点数的程序在 ARCH=native 下可以正确编译运行,但可能在其他架构下失效。这么做的目的是使 AbstractMachine 程序能运行在各种 (简化) 的处理器上。

3. 正确性标准

Online Judge 会严格按照上面的要求来评测:

  1. 通过 qemu 的 -m 选项限制内存的大小;使用超过规定的内存可能导致程序 crash;
  2. 在不同的环境下 (x86-qemu 或 x86_64-qemu, native) 运行你的程序,并且可能修改屏幕的分辨率;
  3. 随机发送一定的键盘事件。

Online Judge 会作出如下正确性检查:

  1. 除非按 ESC 键,我们假设程序不结束;如果检测到 halt() 的调用 (包括 assert 失败,我们可能会增加一些额外的合法性检查),将判定为错 (Runtime Error 或 Wrong Answer);
  2. 如果按 ESC 键后程序不结束,将判定为错;
  3. 如果虚拟机或进程发生崩溃、非预期的重启等 (由 undefined behavior 导致,例如访问某非法内存可能导致异常或 CPU Reset),则被判定为错;
  4. 其他违反 specifications 的问题,如在 ioe_init() 之前调用 io_read/io_write,将被判定为错。

此外,我们会链接我们 klib 的参考实现,因此你不必担心你的 klib 有些许问题;但你要保持 klib 库函数的行为与 libc 一致。注意 native 会链接系统本地的 glibc (而非 klib)。

4. 实验指南

4.1. 实现库函数

在编写可移植代码时,应当尽量使用 C 标准提供的机制。

  • 对于printf 的变长参数,可移植的方式是使用 stdarg.h 中的 va_list, va_start, va_arg

4.2. 访问 I/O 设备

TRM(图灵机) 中唯一能够和外界交互的手段是 putch() 在调试终端上打印字符和 halt() 终止程序。。我们的硬件提供了若干 I/O 设备,AbstractMachine 可以通过 IOE 访问它们。在调用 I/O 设备之前,需要调用 ioe_init() 初始化,然后就可以用

void ioe_read (int reg, void *buf);
void ioe_write(int reg, void *buf);

两个函数访问 AbstractMachine 中的 I/O “寄存器” 了。详情请参考 AbstractMachine 文档和框架代码。在 klib-macros.h 中包含了更简化的 I/O 寄存器访问方法 io_readio_write,请大家查看。用这组 API 你就可以省去手工定义变量的麻烦,例如直接

int width = io_read(AM_GPU_CONFIG).width;

到这里,大家可能会产生的一个疑问是:运行在 “裸机” 上的程序可以用哪些标准库?

  • 我们知道,libc 中相当一部分函数都调用操作系统,例如 printf, malloc 等,即便引用了 stdio.h 这样的头文件,它们的实现依然是缺失的;
  • 另一方面,我们引用了一些库的头文件,例如 stdint.h (包含诸如 int32_t 这些类型的定义)、stdarg.h 等,却可以正常工作。这是为什么?

事实上,AbstractMachine 的程序运行在 freestanding 的环境下 (操作系统上的 C 程序运行在 hosted 环境下)。

The __STDC_HOSTED__ macro expands to 1 on hosted implementations, or 0 on freestanding ones. The freestanding headers are: float.h, iso646.h, limits.h, stdalign.h, stdarg.h, stdbool.h, stddef.h, stdint.h, and stdnoreturn.h. You should be familiar with these headers as they contain useful declarations you shouldn't do yourself. GCC also comes with additional freestanding headers for CPUID, SSE and such.

这些头文件中包含了 freestanding 程序也可使用的声明。有兴趣的同学可以发现,可变参数经过预编译后生成了类似 __builtin_va_arg 的 builtin 调用,由编译器翻译成了特

4.3. 绘制一个图片

只要把图片的 RGB 数值写在代码里,不就可以显示图片了吗!没错。甚至我们的 xxd 工具都帮助大家实现了这一点:

// Generated by: xxd -i /bin/ls
unsigned char _bin_ls[] = {
  0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
  ...
  0x00, 0x00, 0x00, 0x00
};
unsigned int _bin_ls_len = 142144;

xxd 可以将任何二进制文件转换成 C/C++ 可以接受的常量数组,而且把数组的长度也固定好。

因此你可以在 “kernel” 的外部准备好任何的图片数据。至于同一个图片如何 “拉伸/缩放” 到不同分辨率,这就是你需要考虑的问题了——不过这也不难,说白了你是希望将一个 n×m 的矩形网格 “投影” 到一个 n′×m′ 的矩形网格上——因此最简单的方法就是在新网格上找到旧网格 “最靠近” 的那个位置对应的像素即可。