TOC
Open TOC
lab 1
Hello OS
环境配置
开机,从 ROM 运行 BIOS 程序,BIOS 程序检查软盘 0 面 0 磁道 1 扇区,如果扇区以 0xaa55 结束,则认定为引导扇区,将其 512 字节的数据加载到内存的 07c00 处,然后设置 PC,跳到内存 07c00 处开始执行代码
汇编代码
一些解释
org 07c00h
让编译器从相对地址 07c00h 处开始编译第一条指令,考虑 BootMessage
代表的字符串地址,是相对寻址
编译
反编译
创建软盘
将 boot.bin 写入软盘
启动虚拟机
配置文件如下
截图
calculator
环境配置
汇编和编译
截图
下面简单讲讲具体的实现
逆向存储各数位,不考虑数的位数,直接加固定位数,高位仍然为 0
化归为加法,以 123 * 123 为例
逆向存储为 321
考虑第一位,321 + 321 + 321 = 963,存储在 mul_cp_dst_buf
中,加到 mul_res_buf
中
考虑第二位,将 321 移位为 0321,0321 + 0321 = 0642,存储在 mul_cp_dst_buf
中,加到 mul_res_buf
中
考虑第三位,将 321 移位为 00321,00321 = 00321,存储在 mul_cp_dst_buf
中,加到 mul_res_buf
中
此时 mul_res_buf
为 963 + 0642 + 00321 = 92151,逆向输出即为 15129
lab 2
RTFM
http://jyywiki.cn/pages/OS/manuals/MSFAT-spec.pdf
下面给出各 region 的大小
- Reserved Region -
BPB_RsvdSecCnt
- FAT Region -
BPB_NumFATs * BPB_FATSz16
- Root Directory Region - 利用
BPB_RootEntCnt
字段
- Data Region - 下标从 2 开始计数
下述方法给出了如下将 data region 的 cluster 转换为 sector
data region 中可能包含文件的数据信息,也可能包含一系列的 entries,包括 short entry 和 long entry
这里只需关注 short entry,且只需关注 short entry 中的 ATTR_DIRECTORY
(目录元信息) 和 ATTR_ARCHIVE
(文件元信息)
DIR_Name[0] = 0xE5
indicates the directory entry is free (available)
short entry 的 DIR_FstClusLO
给出了文件或者目录所在数据的 cluster id,该 cluster id 也用于在 fat region 中寻找下一个 cluster id
例如,某 short entry 描述了某个普通文件的元数据信息,其 cluster id 为 5,而 fat[5] = 6, fat[6] = 0xfff
这说明该普通文件的数据分布在 data region 的 cluster #5 和 cluster #6 中
另外,对应 FAT 12 而言,fat 的一个 entry 占据 12 个比特,所以需要通过一些特殊的方法来进行读取
technology selection
g++ 和 nasm 联合编译
nasm 仅负责填充 syscall id 并进行 write 这个系统调用,对于 x86-64 而言,系统调用指令为 syscall
而非 int 80
调试时可以添加一个桩函数
readline 库,历史记录功能
手写 parser,command 抽象类 execute 执行
mmap 内存映射
将镜像解析为如下数据结构
之后仅读取该数据结构即可
web assembly
https://vgalaxies-blog.vercel.app/posts/wasm/
lab 3
src
https://gitee.com/goodshred/oranges
setup
https://github.com/qemu/vgabios
修改后的配置文件如下
freedos
http://bochs.sourceforge.net/guestos/freedos-img.tar.gz
makefile
book
段式存储
突破 16 位寻址的界限
逻辑地址 -> 线性地址
- 段选择符高 13 位的索引值用来确定当前使用的段描述符在段描述符表中的位置,表示是其中的第几个段表项
- 段选择符存放在段寄存器中
- 段描述符是一种数据结构,实际上就是分段方式下的段表项
- 段描述符表 GDT 实际上就是分段方式下的段表,由若干个段描述符组成
- lgdt 指令加载段描述符表
- LDT,其选择子的 TI 位置 1
实模式 <-> 保护模式
- 16 位代码段中
- 修改寄存器 cr0 的 PE 位
- jmp 跳转
特权级概述
考虑使用门描述符 (调用门) 以及对应的选择子,在不同特权级代码段之间转移
- 段间跳转 - 长跳转,call 压栈 eip 和 cs
- 段内跳转 - 短跳转,call 压栈 eip
对于长跳转,跳转前后 stack 发生变化
TSS 中存放了各个特权级下的 ss 和 esp
- 低特权级 -> 高特权级,使用调用门和 call 指令
- 高特权级 -> 低特权级,使用 ret 指令
页式存储
虚拟存储
线性地址 -> 物理地址
- 修改寄存器 cr0 的 PG 位
- cr3 指定页目录表,包含 PDE,PDE 指向页表,包含 PTE,PTE 指向物理页
- 常用的 PDE 和 PTE 会缓存在 TLB 中
中断和异常
- 实模式下,int 15h 获取内存信息
- 保护模式下,中断向量表被 IDT 替代,存放中断门或陷阱门
- 异常的分类
- 中断的分类
- 外部中断
- 可屏蔽,中断控制器 8259A
- IF 位为 1,且 8259A 的中断屏蔽寄存器 IMR 相应位为 0,才会发生
- 不可屏蔽
- 内部中断,int 中断门,类似 call 调用门
- 中断和异常发生时,stack 也会发生变化,要看是否存在特权级变化
loader & kernel
boot.asm
寻找 LOADER.BIN,LOADER.BIN 为 COM 格式,所以可以直接加载到内存中,并跳转到其开始处
loader.asm
寻找 KERNEL.BIN,注意 KERNEL.BIN 为 FAT12 的格式,需要在其根目录中寻找 kernel 的二进制文件,并加载到内存中
- 跳入保护模式
- 获取内存信息,启动分页机制
- 根据 program header table 的信息加载 kernel
- 跳转到内核
_start
处
kernel.asm
- 切换 stack 和 GDT
- 启动中断和异常机制
- 异常处理例程为
exception_handler
- 中断处理例程为
spurious_irq
keyboard
扫描码
读取扫描码
0x60 为 8042 寄存器端口
console
VGA 寄存器
通过写入显存显示字符,不同的 console 占据不同的显存
tty
tty task 运行在 ring 1,用户进程运行在 ring 3
每个用户进程绑定一个 tty console,通过 write 系统调用进行输出
doc
tty_do_read
keyboard_read
- 解析扫描码
in_process
- 其中处理 ESC 的逻辑
- 对于可输出的字符,存放在 tty 的键盘缓冲区
tty_do_write
lab 4
book
用户栈与内核栈
中断处理程序中应使用内核栈
中断处理机制
以时钟中断为例
doc
syscall
以 print_str
为例,分析流程如下
即在 print_str
调用前关中断,调用后开中断
- 在
syscall.asm
中导出符号 print_str
此处 INT_VECTOR_SYS_CALL
中断向量在 protect.c
中对应的中断处理程序为 sys_call
其中会根据 sys_call_table
调用内核态的 sys_print_str
sys_print_str
最终调用辅助函数 disp_str
即可
scheduler
为了使普通进程 monitor 能够在每个时间片输出 readers 和 writers 的状态,需要在固定的时间片后调度给 monitor 进程
此处时间片的选择应与 readers 和 writers 的时间片保持一致,相关参数如下
monitor 进程不参与下面的讨论
scheduler 依赖的核心数据结构为 ready_queue
,其中存放了可以被调度的 pid
为了避免进程饿死问题,考虑在 PROCESS
结构体中添加字段 has_worked
,当 readers 和 writers 完成了一次读写之后,会 set 该字段为 true
scheduler 会删除 ready_queue
的第一个 pid,由此获得一个 proc,并重新添加该 pid 到 ready_queue
的末尾,若该 proc 的 has_worked
为 false,则调度该 proc,否则重复上述过程
semaphore
其中的 size 代表 wait list 目前的大小
PV 操作如下
其中的 sleep 和 wakeup 会修改对应 semaphore 的 wait list,以及全局的 ready_queue
readers writers
核心算法参考了 Readers–writers problem
所需的 semaphore 如下
前四个为二值信号量,最后一个的 value 可以调整
为了给 monitor 提供相应的信息,对应的 read 和 write 的框架如下
在获取被读写资源资源后,置 proc 的 state 为 DO_READ
或 DO_WRITE
delay
根据往届学长的分析,milli_delay
函数是进程共享的,比如 A 进程和 B 进程都需要延迟 20ms,然后 A 进程和 B 进程轮流被分配时间片,事实上在 A 的时间片里面 B 进程的延迟也在被计时,因为 ticks 对于每个进程是共享变化的
所以需要实现一个非共享的 delay 函数,为此需要:
- 在
PROCESS
结构体中添加字段 sleep_time
- 调用此函数会将对应的 proc 移出
ready_queue
,同时设置 sleep_time
字段
- 在 scheduler 调用前,遍历所有 procs,若 proc 的
sleep_time
非 0,则递减,若减为 0,则重新移入 ready_queue
monitor
依赖下面三个数据结构输出 readers 和 writers 的状态