操作系统的状态机模型
操作系统也是程序。操作系统可以启动其他程序,那么又是哪个程序加载了操作系统?这是个鸡生蛋,蛋生鸡的问题。
计算机系统基础,模拟器实验。操作系统上的程序是由加载器加载的,操作系统也是程序,如果操作系统也是个 elf 文件,那也得有个加载器加载。这个加载器本身也是个程序,谁来加载呢?
动手写操作系统
自己写操作系统?
专业和爱好的区别。
软件和硬件的桥梁
最小的 c 程序
#include <sys/syscall.h>
.globl _start
_start:
movq $SYS_write, %rax # write(
movq $1, %rdi # fd=1,
movq $st, %rsi # buf=st,
movq $(ed - st), %rdx # count=ed-st
syscall # );
movq $SYS_exit, %rax # exit(
movq $1, %rdi # status=1
syscall # );
st:
.ascii "\033[01;31mHello, OS World\033[0m\n"
ed:
这个最小程序可以用。现在有个问题,如何让他在没有操作系统的硬件上运行起来。
这就有两个问题
加载到计算机上
硬件上提供一些功能,类似 syscall
这个问题也很简单。为了让计算机能运行一些程序,那么软件的设计者和硬件的设计者坐在一起,达成一些约定,“程序就这样写”,就可以了。
回忆数字电路模拟器,数字电路是有 reset 的,这个 reset 是让整个状态机回到初始状态,这个状态是人设计好的,是确定的。
这就是 Bare-metal 与程序员的约定。硬件和软件的约定。计算机硬件也是状态机,CPU reset 后,处理器(里的寄存器)处于一个确定的状态。这个状态可以去 datasheet 上去查询。
这个状态,厂商制造出来就是这样的。更具体地讲,x86 情况加,cs、ip的指针值,这些东西会写在手册里。CPU 是无情的执行指令的机器(计算机系统基础)。
想要把硬件和软件连起来,就是让 reset 后的 pc 可以读到一条有效的指令。即启动后执行的第一条指令。
看intel的手册,x86 启动后
寄存器的初始状态
EIP = 0x0000fff0
CS = 0xffff0000
实际上这些东西是看得见的。可以用 qemu 来调试,所有的状态都可以看得到。
CPU reset 后 主板厂商的代码就要开始执行了。根据x86手册,pc = ffff0h,一般来讲这里是个跳转指令,这时候状态机已经跑起来了。跑的还不是linux、windows,他们是灵活的,计算机可以启动u盘里的linux,u盘是主板厂商不知道的。这时候跑的是主板厂商的代码,厂商的代码是在 ROM 里写死的代码,不能随便改。这段代码会扫描计算机上所有的设备,可以配置一些东西比如启动项,从U盘启动还是硬盘启动,也可以配置CPU的东西比如关闭超线程、硬件虚拟化、大小核调频率。这个软件就叫 Firmware 固件。
CPU 执行的第一个软件是主板厂商提供的,因为主板厂商知道自己的主板用了什么芯片。
老的计算机 Legacy BIOS (Basic I/O System) ,现在系统更复杂了,是 UEFI (Unified Extensible Firmware Interface),我们甚至可以直接在 UEFI 上写操作系统,也挺好玩。
从学习的角度讲,学 BIOS 稍微简单一点,当然还是比 ARM RISC-V 复杂。CPU reset 程序就跑了,自己可以定义一块 ROM ,没有 firmware 我们写的程序就是 firmware。
Legacy BIOS,一些约定的东西。
Firmware 是计算机运行的第一个软件,必须提供机制,将用户数据载入内存
主要需要做的事情
扫描系统里的硬件
找到一个有操作系统的硬件
加载操作系统
老的 Legacy BIOS 的启动方式:对于一块磁盘,flash、ssd、hhd。操作系统和firmware之间的约定是:启动磁盘的第一个 512 Byte 叫主引导扇区(MBR),这 512 Byte 要由 firmware 搬动到内存的一个确定的物理地址 7c00。这就是firmware和操作系统第一次也是唯一一次握手。
可以直接看看物理磁盘的前 512 Byte,最后一个字节是 aa55 ,表示可以启动。这也是 IBM 时代留下来的约定。当时是按顺序°磁盘的前512,是aa55代表可执行,不是就读下一个。如果全部没找到,那么就启动失败了,没找到可启动的设备(press any key to continue)。
今天的机器,即插即用,usb键盘插上就可以了。
所以 firmware 做的事情(bios的话),内存的 7c00 是磁盘的前512 字节,PC也指向了 7c00
Legacy BIOS 把第一个可引导设备的第一个扇区加载到物理内存的 7c00 位置
此时处理器处于 16-bit 模式
规定 CS:IP = 0x7c00, (R[CS] << 4) | R[IP] == 0x7c00
可能性1:CS = 0x07c0, IP = 0
可能性2:CS = 0, IP = 0x7c00
其他没有约束
这 512 B 的代码就可以做更多事情了,再加载 1M 代码啥的,启动保护模式。最后把操作系统加载上。
所有的这些东西,理论,能不能看看代码?
如何去看这个代码呢?这也是专业人士该考虑的事情。
qume,调试从操作系统上电后的每一步操作。如今的好的工具链。VirtualBox 背后是这个,ffmpeg也是他的作品(格式工厂)
当然也有真机方案:JTAG debugger
这就解决了鸡和蛋的问题。
主板上有个芯片,firmware代码存在里面
CPU reset 后加载 firmware
还有个很重要的用途,这个firmware还要给512Byte代码提供一些支持,比如说打印字符,有异常要有东西显示出来。也即提供一些api,帮助512读磁盘之类的。
以前 IBM PC 成功,因为五专利,每个人都可以造自己的兼容计算机。BIOS也很成功。
后来系统越来越复杂,需要把启动变得更安全。
以前设备接口是确定的,比如鼠标的PS/2接口。后来,有了USB鼠标,甚至USB接个蓝牙适配器,用蓝牙鼠标。这都不是标准设备。再比如USB指纹锁,需要在系统启动时就要能用。不能说操作系统能用再验证,这就晚了。
所以,有了 UEFI。
UEFI 上的操作系统加载,也变复杂了。机器先从UEFI启动,有额外要求
有个 FAT32 (指令格式) 的分区
盘必须按 GPT (GUID Partition Table) 方式格式化
Firmware 加载任意大小的 PE 可执行文件 .efi
没有 legacy boot 512 字节限制
EFI 应用可以返回 firmware
有了前面的系统启动后,操作系统就可以来了。
操作系统的状态机模型
启动加载器(boot loader)会把操作系统代码放到内存里,然后设置pc值,这就跑起来了。这就解释了操作系统也是个C程序。Firmware 和 boot loader 共同完成 “操作系统的加载”
初始化全局变量和栈;分配堆区 (heap)
为 main 函数传递参数
谁给操作系统传递了参数?
如何实现参数传递?
进入C代码之后,完全就是C语言的事情了。
程序就是状态机,操作系统也是状态机。操作系统如果不使用AbstractMachine API的话,就是个普通的 C 程序。剩下的写操作系统,就是要理解api。
单核多线程和多核多线程没有什么本质上的不同。每个线程都有自己的栈。
一个单线程的 C 程序,执行了 mpe_init() 后,变成了什么样子?
多线程的api还是可以容易的去理解的。比较难处理的是 CTE 和 VME。
CPU1 终端,就是把当前执行的所有的线程,保存到 context 里。
VME 虚拟内存。就是一个“需要经过地址翻译”的模式。
读源码
最后更新于