(一)实现操作系统 | 引导扇区
实现操作系统 | 系统引导
前言
俗话说:基础不牢,地动山摇。在看完计算机组成原理、计算机网络、操作系统、数据结构与算法的一些书籍和网课之后,我发现这些知识不动手去用,只是粗略的过一遍很难有深刻的学习和体会,加上自己的编程水平也需要一个项目来得到锻炼。既然本科阶段拥有大把时间可以去折腾,而且自己感兴趣,那么就去做吧,不给自己设限,走出舒适区才会得到快速成长~
开发环境
Archlinux下 安装nasm和bochs两个包,分别是汇编环境和x86硬件平台的开源模拟器。
sudo pacman -S nasm
sudo pacman -S bochs
vscode 使用 ASM Code Lens
插件,支持汇编语法高亮
写一个boot程序
[org 0x7c00]
; 设置屏幕模式,清除屏幕
mov ax,3 ;设置为80x25的文本模式。
int 0x10
; 初始化段寄存器
mov ax,0
mov ds,ax
mov es,ax
mov ss,ax
mov sp,0x7c00
mov ax,0xb800
mov ds,ax
mov byte[0], 'Y'
; 阻塞
jmp $
; 末尾填充0
times 510 - ($ - $$) db 0
; 最后两个字节必须是0x55,0xaa
; dw 0xaa55
db 0x55,0xaa
编译
nasm -f bin boot.asm -o boot.bin
创建硬盘镜像
bximage
是 Bochs 虚拟机的一个工具,用于创建硬盘镜像文件。
我们要创建一个大小为16MB,每个扇区512字节的硬盘镜像:
bximage -q -hd=16 -func=create -sectsize=512 -imgmode=flat master.img
将 boot.bin 也就是编译出来的文件写入主引导扇区:
dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc
配置 bochs
进入bochs程序生成bochsrc文件,主要是告诉bochs我们的硬盘在哪里。
之前生成硬盘镜像时命令行有下面这句:
ata0-master: type=disk, path="master.img", mode=flat
- 复制到bochsrc文件对应的那一行
- 找到
display_library:
这一行修改为display_library: x, options="gui_debug"
- boot那一行改为disk,因为我们要在硬盘启动而不是软盘
#配置文件示例
# configuration file generated by Bochs
plugin_ctrl: unmapped=true, biosdev=true, speaker=true, extfpuirq=true, parallel=true, serial=true, iodebug=true, pcidev=false, usb_uhci=false
config_interface: textconfig
display_library: x, options="gui_debug"
memory: host=32, guest=32
romimage: file="/usr/share/bochs/BIOS-bochs-latest", address=0x00000000, options=none
vgaromimage: file="/usr/share/bochs/VGABIOS-lgpl-latest"
boot: disk
floppy_bootsig_check: disabled=0
floppya: type=1_44
# no floppyb
ata0: enabled=true, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="master.img", mode=flat
ata0-slave: type=none
ata1: enabled=true, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata1-master: type=none
ata1-slave: type=none
ata2: enabled=false
ata3: enabled=false
optromimage1: file=none
optromimage2: file=none
optromimage3: file=none
optromimage4: file=none
optramimage1: file=none
optramimage2: file=none
optramimage3: file=none
optramimage4: file=none
pci: enabled=1, chipset=i440fx, slot1=none, slot2=none, slot3=none, slot4=none, slot5=none
vga: extension=vbe, update_freq=5, realtime=1, ddc=builtin
cpu: count=1:1:1, ips=4000000, quantum=16, model=bx_generic, reset_on_triple_fault=1, cpuid_limit_winnt=0, ignore_bad_msrs=1, mwait_is_nop=0
cpuid: level=6, stepping=3, model=3, family=6, vendor_string="AuthenticAMD", brand_string="AMD Athlon(tm) processor"
cpuid: mmx=true, apic=xapic, simd=sse2, sse4a=false, misaligned_sse=false, sep=true
cpuid: movbe=false, adx=false, aes=false, sha=false, xsave=false, xsaveopt=false, avx_f16c=false
cpuid: avx_fma=false, bmi=0, xop=false, fma4=false, tbm=false, x86_64=true, 1g_pages=false
cpuid: pcid=false, fsgsbase=false, smep=false, smap=false, mwait=true
print_timestamps: enabled=0
debugger_log: -
magic_break: enabled=0
port_e9_hack: enabled=0
private_colormap: enabled=0
clock: sync=none, time0=local, rtc_sync=0
# no cmosimage
log: -
logprefix: %t%e%d
debug: action=ignore
info: action=report
error: action=report
panic: action=ask
keyboard: type=mf, serial_delay=250, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=false, toggle=ctrl+mbutton
speaker: enabled=true, mode=system
parport1: enabled=true, file=none
parport2: enabled=false
com1: enabled=true, mode=null
com2: enabled=false
com3: enabled=false
com4: enabled=false
主引导扇区
BIOS
全称:Basic Input Output System
BIOS 在加电自检将主引导扇区读 0x7c00 位置,并跳转到这里执行。
int 0x10; BIOS 系统调用,显示器相关的功能
实模式
在 x86 架构的 CPU 中,有两种主要的操作模式:实模式(Real Mode)和保护模式(Protected Mode)。
-
实模式(Real Mode):这是最初的 8086 和 8088 微处理器使用的模式。在这种模式下,CPU 可以直接访问最多 1MB 的物理内存。实模式没有内存保护、虚拟内存或多任务等特性。当你启动计算机时,CPU 会首先进入实模式,然后引导加载器会将其切换到保护模式以便加载和运行现代的操作系统。
-
保护模式(Protected Mode):这是 80286 和后续的 x86 微处理器添加的模式。在这种模式下,CPU 可以访问超过 1MB 的内存,并且提供了内存保护、虚拟内存和多任务等特性。现代的操作系统,如 Windows、Linux 和 macOS,都在保护模式下运行。
在汇编语言引导加载器(bootloader)代码中,代码是在实模式下运行的,因为这是 CPU 在启动时的默认模式。
; 0xb8000 文本显示器的内存区域
mov ax, 0xb800
mov ds, ax
mov byte [0], 'H'
实模式的寻址方式
有效地址 = 段地址 * 16 + 偏移地址
EA (Effective Address)
EA = 0xb800 * 0x10 + 0 = 0xb8000
我们需要段地址的原因:实模式下所有的寄存器几乎都是16位的,实模式下需要访问1M的内存,1M需要20bit的地址,所以还差4bit的地址。剩下的地址就需要段地址来提供了。
16 bit - 1M - 20 bit
20 - 16 = 4
段地址 << 4
保护模式:因为地址变为32 bit,可以访问4G的内存,所以段地址在保护模式下便失效了,而用于另外的功能,以后再讨论。
主引导扇区的结构
一个扇区有512个字节,其中:
- 代码:446B
- 硬盘分区表:64B = 4 * 16B (也就是说硬盘可被分为4个分区)
- 魔数:0xaa55 - 0x55 0xaa
我们使用bochs虚拟机启动,没有硬盘分区表仍然可以启动
主引导扇区主要功能
读取内核加载器,并执行
实模式 print
-
ah: 0x0e
-
al: 字符
-
int 0x10
xchg bx, bx; bochs 魔术断点
mov si, booting
call print
; 阻塞
jmp $
print:
mov ah, 0x0e
.next:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .next
.done:
ret
booting:
db "Booting Onix...", 10, 13, 0; \n\r
硬盘读写
硬盘
- 扇区:是硬盘读写的最小单位,最小一个,最多 256 个扇区
- 机械臂的寻道时间是硬盘性能的主要瓶颈;
- 一般情况下一个磁道有 63 个扇区,主要是由于 BIOS 最大支持这么多;
- 磁道从外侧计数,所以一般情况下 C 盘的读写速度最快;
硬盘读写
硬盘读写有两种模式:
- CHS 模式 / Cylinder / Head / Sector (柱面/磁头/磁道)
- LBA 模式 / Logical Block Address (逻辑块地址)
LBA28,总共能访问 128G 的磁盘空间;
硬盘控制端口
Primary 通道 | Secondary 通道 | in 操作 | out 操作 |
---|---|---|---|
0x1F0 | 0x170 | Data | Data |
0x1F1 | 0x171 | Error | Features |
0x1F2 | 0x172 | Sector count | Sector count |
0x1F3 | 0x173 | LBA low | LBA low |
0x1F4 | 0x174 | LBA mid | LBA mid |
0x1F5 | 0x175 | LBA high | LBA high |
0x1F6 | 0x176 | Device | Device |
0x1F7 | 0x177 | Status | Command |
- 0x1F0:16bit 端口,用于读写数据
- 0x1F1:检测前一个指令的错误
- 0x1F2:读写扇区的数量
- 0x1F3:起始扇区的 0 ~ 7 位
- 0x1F4:起始扇区的 8 ~ 15 位
- 0x1F5:起始扇区的 16 ~ 23 位
- 0x1F6:
- 0 ~ 3:起始扇区的 24 ~ 27 位
- 4: 0 主盘, 1 从片
- 6: 0 CHS, 1 LBA
- 5 ~ 7:固定为1
- 0x1F7: out
- 0xEC: 识别硬盘
- 0x20: 读硬盘
- 0x30: 写硬盘
- 0x1F7: in / 8bit
- 0 ERR
- 3 DRQ 数据准备完毕
- 7 BSY 硬盘繁忙
内核加载器
- 写内核加载器 loader
- 将 loader 写入硬盘
- 在主引导扇区读入
- 检测正确性
- 跳转到 loader 执行
实模式的内存布局
起始地址 | 结束地址 | 大小 | 用途 |
---|---|---|---|
0x000 |
0x3FF |
1KB | 中断向量表 |
0x400 |
0x4FF |
256B | BIOS 数据区 |
0x500 |
0x7BFF |
29.75 KB | 可用区域 |
0x7C00 |
0x7DFF |
512B | MBR 加载区域 |
0x7E00 |
0x9FBFF |
607.6KB | 可用区域 |
0x9FC00 |
0x9FFFF |
1KB | 扩展 BIOS 数据区 |
0xA0000 |
0xAFFFF |
64KB | 用于彩色显示适配器 |
0xB0000 |
0xB7FFF |
32KB | 用于黑白显示适配器 |
0xB8000 |
0xBFFFF |
32KB | 用于文本显示适配器 |
0xC0000 |
0xC7FFF |
32KB | 显示适配器 BIOS |
0xC8000 |
0xEFFFF |
160KB | 映射内存 |
0xF0000 |
0xFFFEF |
64KB-16B | 系统 BIOS |
0xFFFF0 |
0xFFFFF |
16B | 系统 BIOS 入口地址 |
加电硬件会跳转到0xFFFF0也就是BIOS入口地址,进行硬件检测。检测完毕将主引导扇区加载到0x7C00并执行
内存检测
每个机器的内存都不一样,有可能这段内存是空洞,也有可能是不可使用的内存。所以我们需要知道哪块内存可以用,哪块内存不可以用,那么就得知道内存的开始位置、持续长度、类型。
BIOS 0x15 系统调用子功能:0xe820 就提供了这样一个功能
Address Range Descriptor Structure ARDS
字节偏移量 | 属性名称 | 描述 |
---|---|---|
0 | BaseAddrLow | 基地址的低 32 位 |
4 | BaseAddrHigh | 基地址的高 32 位 |
8 | LengthLow | 内存长度的低 32 位,以字节为单位 |
12 | LengthHigh | 内存长度的高 32 位,以字节为单位 |
16 | Type | 本段内存的类型 |
Type 字段
Type 值 | 名称 | 描述 |
---|---|---|
1 | AddressRangeMemory | 这段内存可以被操作系统使用 |
2 | AddressRangeReserved | 内存使用中或者被系统保留,操作系统不可以用此内存 |
3 | 存储ACPI表,可以被操作系统回收。 | |
4 | 操作系统不可使用这段内存。 | |
5 | 已经损坏的内存区域,不可使用。 | |
其他 | 未定义 | 未定义,将来会用到.目前保留. 但是需要操作系统一样将其视为ARR(AddressRangeReserved) |
调用前输入
寄存器或状态位 | 参数用途 |
---|---|
EAX | 子功能号: EAX 寄存器用来指定子功能号,此处输入为 0xE820 |
EBX | 内存信息需要按类型分多次返回,由于每次执行一次中断都只返回一种类型内存的ARDS 结构,所以要记录下一个待返回的内存ARDS,在下一次中断调用时通过此值告诉 BIOS 该返回哪个 ARDS,这就是后续值的作用。第一次调用时一定要置为0,EBX 具体值我们不用关注,字取决于具体 BIOS 的实现,每次中断返回后,BIOS 会更新此值 |
ES: DI | ARDS 缓冲区:BIOS 将获取到的内存信息写入此寄存器指向的内存,每次都以 ARDS 格式返回 |
ECX | ARDS 结构的字节大小:用来指示 BIOS 写入的字节数。调用者和 BIOS 都同时支持的大小是 20 字节,将来也许会扩展此结构 |
EDX | 固定为签名标记 0x534d4150 ,此十六进制数字是字符串 SMAP 的ASCII 码: BIOS 将调用者正在请求的内存信息写入 ES: DI 寄存器所指向的ARDS 缓冲区后,再用此签名校验其中的信息 |
返回值
寄存器或状态位 | 参数用途 |
---|---|
CF 位 | 若CF 位为 0 表示调用未出错,CF 为1,表示调用出错 |
EAX | 字符串 SMAP 的 ASCII 码 0x534d4150 |
ES:DI | ARDS 缓冲区地址,同输入值是一样的,返回时此结构中己经被BIOS 填充了内存信息 |
ECX | BIOS 写入到 ES:DI 所指向的 ARDS 结构中的字节数,BIOS 最小写入 20 字节 |
EBX | 后续值:下一个 ARDS 的位置。每次中断返回后,BIOS 会更新此值, BIOS 通过此值可以找到下一个待返回的 ARDS 结构,咱们不需要改变 EBX 的值,下一次中断调用时还会用到它。在 CF 位为 0 的情况下,若返回后的 EBX 值为 0,表示这是最后一个 ARDS 结构 |
保护模式和全局描述符
实模式和保护模式是CPU的两种主要工作模式,它们主要影响CPU的寻址方式、寄存器大小等关键操作,从而决定了CPU在特定环境下的工作方式。
实模式最初在早期的8088 CPU时期出现,它的内存寻址能力有限,只能管理1MB的内存空间。在实模式下,所有程序都可以直接访问物理内存,包括操作系统的内核空间,这就意味着恶意程序(如病毒)理论上可以完全控制计算机。
为了解决这个问题,以及实现更大、更灵活、更安全的内存访问,保护模式在80286 CPU时期被引入。保护模式提供了16位以上的寻址能力,可以管理的内存空间远超过1MB。更重要的是,保护模式引入了权限管理机制,不同的程序运行在不同的权限级别,低权限的程序不能访问高权限程序的内存空间,从而大大提高了系统的安全性。
而为了兼容性,目前大部分设备启动时先进入实模式,然后切换到保护模式,这里实现的操作系统也将如此。
保护模式
- 信息:IT Information Technology 书籍:《信息论》
- 寄存器 / 有一些寄存器只能被操作系统访问
- 高速缓存
- 内存 / 描述符
- 外部设备 / 硬盘 / in/out
全局描述符
cpu根据描述符判断是否要报错
- 内存的起始位置
- 内存的长度 / 界限 = 长度 - 1
- 内存属性
typedef struct descriptor /* 共 8 个字节 */
{
unsigned short limit_low; // 段界限 0 ~ 15 位
unsigned int base_low : 24; // 基地址 0 ~ 23 位 16M
unsigned char type : 4; // 段类型
unsigned char segment : 1; // 1 表示代码段或数据段,0 表示系统段
unsigned char DPL : 2; // Descriptor Privilege Level 描述符特权等级 0 ~ 3
unsigned char present : 1; // 存在位,1 在内存中,0 在磁盘上
unsigned char limit_high : 4; // 段界限 16 ~ 19;
unsigned char available : 1; // 该安排的都安排了,送给操作系统吧
unsigned char long_mode : 1; // 64 位扩展标志
unsigned char big : 1; // 32 位 还是 16 位;
unsigned char granularity : 1; // 粒度 4KB 或 1B
unsigned char base_high; // 基地址 24 ~ 31 位
} __attribute__((packed)) descriptor;
type segment = 1
| X | C/E | R/W | A |
- A: Accessed 是否被 CPU 访问过
- X: 1/代码 0/数据
- X = 1:代码段
- C: 是否是依从代码段
- R: 是否可读
- X = 0: 数据段
- E: 0 向上扩展 / 1 向下扩展
- W: 是否可写
全局描述符表 GDT Global Descriptor Table
全局描述符表 (GDT) 是一个从 Intel x86-系列处理器 80286 开始用于界定不同内存区域的特征的数据结构。全局描述表位于内存中。全局描述表的条目描述及规定了不同内存分区的各种特征,包括基地址、大小和访问等特权如可执行和可写等。 在 Intel 的术语中,这些内存区域被称为 _段 。
全局描述表用于内存地址的转换。所有程序的内存访问都需要用到GDT中的有关内存区域即x86内存分段的信息。
- 表 - 数组 - 顺序表
- 链表 - 链表
- 哈希表
底层的开发过程中很少用到哈希表,它是较复杂的数据结构,特别容易出错,大部分情况下都使用数组。
descriptor gdt[8192]; //8192个结构体
- 0 必须全为 0 - NULL 描述符
- 8191 描述符
描述符指针 pointer 描述了全局描述符表的基地址和界限。cpu提供一个寄存器 GDT Regiser,并通过这个寄存器获得全局描述符表的起始位置和长度;GDT可能被分为很多块,cpu也提供两个指令用于加载和保存GDT。
- gdtr / 全局描述符表的起始位置和长度
; 告诉cpu全局描述符数组在什么地方
lgdt [gdt_ptr]; 加载 gdt
sgdt [gdt_ptr]; 保存 gdt
typedef struct pointer
{
unsigned short limit; // size - 1
unsigned int base;
} __attribute__((packed)) pointer;
段选择子
段选择子相当于数组的索引(还有一些其他的功能)
- 只需要一个代码段
- 需要一个或多个数据段 / 栈段 / 数据段
- 加载到段寄存器中 / 校验特权级
保护模式中段寄存器用来保存描述符
typedef struct selector
{
unsigned char RPL : 2; // Request PL
unsigned char TI : 1; // 0 全局描述符 1 局部描述符 LDT Local
unsigned short index : 13; // 全局描述符表索引
} __attribute__((packed)) selector;
- cs / ds / es / ss
- fs / gs
A20 线
A20总线,是x86体系的扩充电子线路之一。A20总线是专门用来转换地址总线的第二十一位。
激活A20总线是启动操作系统的步骤之一,通常在启动程式将控制权交给内核之前完成。
打开A20线的方式:0x92 端口第二位置为1
有兴趣可查阅相关资料:
PE Protect Enable
启动保护模式:
cr0 寄存器 0 位 置为 1