记录时使用的讲义为 计算机操作系统实验讲义-2022年修订.pdf
,由于是内部资料故对讲义内容不作上传,仅在该部分作实验内容的简要概括。
由于 os 实验的各项内容对第三方的依赖程度较高不易分离,故不再对每个实验进行文件夹级的区分,转而使用 git tag 对各项实验内容进行分层。
OS 目标架构:i386
讲义推荐环境
- ubuntu 10.04.2-desktop-i386
- i486-linux-gnu (GCC) 4.4.3
- nasm 2.08.02
- bochs 2.6.8
个人环境
- wsl2-archlinux 5.10.102.1-microsoft-standard-WSL2
- i386-elf-gcc (GCC) 12.2.0
- nasm 2.16.01
- qemu-system-i386 8.1.1
- nushell 0.85.0
实验目的:不详
实验内容:搭建环境+学习使用环境+读讲义
内容 1
使用 x86 汇编编写 boot 程序,实现输出
Hello, OS world!
,并在虚拟机中引导启动。
org 0x7c00
mov ax, cs
mov ds, ax
mov es, ax
call DispStr
jmp $
DispStr:
mov ax, BootMessage
mov bp, ax
mov cx, 16
mov ax, 01301h
mov bx, 000ch
mov dl, 0
int 10h
ret
BootMessage:
db "Hello, OS world!"
times 510-($-$$) db 0
dw 0xaa55
nasm -o boot.bin boot.asm
dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
qemu-system-i386 -drive format=raw,file=a.img -display curses
或者可以使用 lab0 中提供的 Makefile 直接执行 make run
。
当使用 qemu 的系统模式在当前终端中显示时(如使用 curses 显示模式),结束 qemu 将成为一件非常困难的事。这里有几种方法帮助你退出正在执行的 qemu-system-?:
- alt+2 切换至 qemu 的 monitor 虚拟终端,并键入 quit 命令结束进程。(详情可见👉)
- 新开终端使用 ps 查询 qemu-system-? 进程的 pid,并使用 kill 杀死进程。
- 在启动 qemu-system-? 时使用 -s 选项启动 gdb 的 tcp 服务,并启动 gdb 连接到 qemu 启动的 remote,连接成功用在 gdb 中使用 monitor quit 命令或 kill 命令退出 qumy-system-? 进程。
内容 2
启动 geekos project0
可以使用 geekos scripts 目录下提供的 startProject 拷贝项目目录。其用法如下:
startProject <project name> <master directory> [<previous project>]
在此处,将每个项目拷贝到 geekos-ws/ 下进行实验。
根据对应情况修改项目文件 build 下的 Makefile 文件:
- 交叉编译
取消 TARGET_CC_PREFIX := i386-elf-
的注释
- 使用较新版本的 GCC
删除 CC_GENERAL_OPTS
变量的 -Werror
并添加 -O0 -fno-stack-protector
- 使用 qemu 代替 bochs
将 fd.img 目标的构建命令修改如下:
fd.img : geekos/fd_boot.bin geekos/setup.bin geekos/kernel.bin
cat geekos/fd_boot.bin geekos/setup.bin geekos/kernel.bin > $@
truncate -s 1474560 $@
完成以上步骤后,修改 lowlevel.asm 下的 Int_With_Err 宏,替换如下:
%macro Int_With_Err 1
align 8
push dword %1 ; push interrupt number
jmp Handle_Interrupt ; jump to common handler
nop
nop ; NOTE: align Int_With_Err to 16 bytes
%endmacro
该修改针对是 geekos 自身的错误断言,x86 是变长指令编码,jmp 短跳转 2 字节,push dword 5 字节,nop 1 字节,故相邻的 Int_No_Err 8 字节对齐后占 16 字节,此处在 Int_With_Err 插入 nop 将其对齐到 16 字节以过 idt.c 下的断言。
make 构建并使用 qemu 启动,注意为 qemu 指定拓展内存过 mem.c 下的断言:
qemu-system-i386 -boot a -fda fd.img -m 10M -display curses -m 10M
使用 make run
运行,完成实验。
实验目的:了解中断控制
实验内容:完成 geekos project0
内容 1
启动线程处理键盘按键消息的回显
阅读 keyboard.h,了解 Keycode 的位结构,依据标志位信息利用小端序联合体将 Keycode 重写为 keycode_des_t 以便访问。
typedef union keycode_des_s {
struct {
char code;
union {
struct {
char special : 1;
char keypad : 1;
char reserved : 2;
char shift : 1;
char alt : 1;
char ctrl : 1;
char release : 1;
} flags;
char raw_flags;
};
};
short raw_data;
} keycode_des_t;
简单编写线程的 worker 回调,其中特判 CTRL+d
处理工作线程的结束,并使用控制字符变美化输出样式:
为了更改输出文本样式,保险起见应该使用 geekos 给出的 Set_Current_Attr 实现,此处图方便直接使用控制字符。
void cb_keyboard_echo(ulong_t arg) {
const int QUIT_KEYCODE = KEY_CTRL_FLAG | 'd';
Print("\e[32mStart keyboard echo service, press `CTRL+d` to quit the "
"server.\e[0m\n");
while (1) {
keycode_des_t key_des = {.raw_data = Wait_For_Key()};
int color_code = key_des.flags.release ? 32 : 31;
const char* hint_text = key_des.flags.release ? "RELEASE" : " PRESS ";
Print("\e[1;%dm[%s]\e[0m ", color_code, hint_text);
if (key_des.flags.ctrl) { Print("CTRL+"); }
if (key_des.flags.alt) { Print("ALT+"); }
if (key_des.flags.shift) { Print("SHIFT+"); }
if (key_des.flags.special && !key_des.flags.keypad) {
Print("%s", SPECIAL_KEY_STRTABLE[key_des.code]);
} else if (key_des.flags.keypad) {
Print("%s", KEYPAD_KEY_STRTABLE[key_des.code & 0x7f]);
} else if (key_des.code > 0x20 && key_des.code < 0x7f) {
Print("%#c", key_des.code);
} else if (key_des.code == ' ') {
Print("SPACE");
} else if (key_des.code == '\t') {
Print("TAB");
} else if (key_des.code == '\r') {
Print("RETURN");
} else if (key_des.code == ASCII_BS) {
Print("BACKSPACE");
} else if (key_des.code == ASCII_ESC) {
Print("ESCAPE");
} else {
Print("%#hhx", key_des.code);
}
Print("\n");
if (key_des.raw_data == QUIT_KEYCODE) { break; }
}
Print("\e[32mKeyboard echo service safely quited .\e[0m\n");
}
其中部分 unprintable char 使用查表法替换按键名称。
最后使用 Start_Kernel_Thread 以一般优先级添加工作线程并立即非阻塞加入:
Start_Kernel_Thread(cb_keyboard_echo, 0, PRIORITY_NORMAL, 1);
内容 2
启动多个输出线程处理键盘按键消息的回显
如下,打印数字及当前线程 pid,待打印的数字在创建线程时作为参数传入。
使用 PAUSE 空循环消耗 CPU 时间实现延时的作用,防止输出刷屏。
void cb_stable_print(ulong_t arg) {
struct Kernel_Thread* self = Get_Current();
while (1) {
Print("msg `%d` from thread [pid=%d]\n", arg, self->pid);
PAUSE(5e8);
}
}
内容 3
实现更有意思的线程函数
实现一个简易命令行,在 dev mode 下激活。
用一个单写多读的全局原子量 ATOMIC_FLAG 平替线程锁的功能,多线程逻辑如下:
- ATOMIC_FLAG 无效时,dev 忙等。
- ATOMIC_FLAG 有效时,keyboard echo 与 stable print 忙等。
- keyboard echo 线程监听到
CTRL+p
后原子写 ATOMIC_FLAG 使有效。 - ATOMIC_FLAG 有效时进入 dev 事务逻辑,处理命令键入与命令执行。
dev 线程事务逻辑如下:
- ATOMIC_FLAG 无效时忙等。
- ATOMIC_FLAG 有效时处理事务,阻塞其它线程。
- 命令键入阶段,循环读定长内的可打印字符;遇到 '\b' 缓冲区退格并 flush 打印;遇到 '\r' 结束命令键入;读入字符数达上限结束命令键入并抛出警告。
- 命令执行阶段,字符串全串匹配分发到各命令处理方法;对于 quit 命令,原子写 ATOMIC_FLAG 使无效;完成命令执行进入下一趟事务处理。
工作回调基本结构如下:
void cb_dev_handler(ulong_t arg) {
while (1) {
if (ATOMIC_FLAG == 0) { continue; }
char cmdbuf[32] = {}, ch = 0;
while (1) {
char* p = cmdbuf;
Print("(dev)> ");
do {
ch = get_printable_char();
if (ch == '\r') { break; }
if (ch == '\b') {
if (p == cmdbuf) { continue; }
*--p = '\0';
Print(" (flush)\n(dev)> %s", cmdbuf);
} else {
Print("%c", ch);
*p++ = ch;
}
} while (p - cmdbuf < 31);
Print("\n");
if (ch != '\r') { Print("(warning) cmd is too long\n"); }
*p = '\0';
if (strcmp(cmdbuf, "version") == 0) {
// 打印版本信息
} else if (strcmp(cmdbuf, "help") == 0) {
// 打印帮助信息
} else if (strcmp(cmdbuf, "info thread") == 0) {
// 打印当前线程信息
} else if (strcmp(cmdbuf, "quit") == 0) {
break;
} else if (strlen(cmdbuf) != 0) {
// 抛出未知命令错误
}
}
ATOMIC_FLAG = 0;
}
}
其中获取单个有效字符:
char get_printable_char() {
while (1) {
keycode_des_t key_des = {.raw_data = Wait_For_Key()};
if ((key_des.raw_flags & 0b11110011) != 0) { continue; }
char code = key_des.code;
if (code > 0x20 && code < 0x7f || code == ' ' || code == '\r'
|| code == '\b') {
return code;
}
}
}
《赠后人书》
我囸,对着指导书抄了一下不停地抄了四个小时好不容易抄完了,测,跑一下直接给我报 GP 异常。
玩尼玛×1
去 GitHub 上找了找别人的 project4,拉取,编译——好,一坨错误,不过不慌,应该是编译器太新导致的。
按照新标准把源码全部修了,编译运行。
好,初始化完直接跑飞了,从 kernel 反向干回了 bios。
玩尼玛×2
再找一个,编译运行。
哟呵,这个竟然没报错。
一看,测,原来根本就没做。
玩尼玛×3
Gitee 上再拉一个。
这玩意报错和运行怎么跟第一个一摸一样,从 GitHub 抄到 Gitee 是吧?
玩尼玛×4
脖子疼……算了再看看,GitHub 上再拉一个。
编译错误依旧,改改,编译运行。
很好,能跑了,但是没完全能跑。
用户进程生成失败你是几个意思?
玩尼玛×5
冷静下来,稍稍反思下。瞅瞅,这几个 repo 都有一两三四五六七拔个 star,总不能全是在耍人吧?
毕竟原装是 bochs 我这用的是自配的 qemu。
突然想到最初配置的时候 qemu 默认的 128M 内存会把 GeekOS 的整型干溢出。
乐,不会真是内存大小的问题吧?
试试 64M,分页输出变少了,但是进程创建依旧失败。
试试 32M,同上。
妈的这输出真 TM 烦,全给你删了。
试试 8M,因为输出全删了,看不出什么东西。
试试 2M,芜湖,寄了,是新的寄!
试试 1M,芜湖,又寄了,是新×2的寄!
反正这玩意肯定是不能要了,Spawn 挂了傻子才返回去继续修 project2。
回到第三个 repo。
试试 8M,芜湖,成了!
傻逼 QEMU,傻逼 GeekOS
试试 12M,芜湖,寄了!
试试 10M,芜湖,寄了!
傻逼 QEMU,傻逼 GeekOS×2
试试 1M,哈哈,根本不够加载内核。
试试 2M,哈哈,根本不够启用分页。
好了,就决定了是 8M 了,虽然 4M 也行。
交了,谁爱做谁做。