(一)实现操作系统 | 引导扇区

实现操作系统 | 系统引导

前言

俗话说:基础不牢,地动山摇。在看完计算机组成原理、计算机网络、操作系统、数据结构与算法的一些书籍和网课之后,我发现这些知识不动手去用,只是粗略的过一遍很难有深刻的学习和体会,加上自己的编程水平也需要一个项目来得到锻炼。既然本科阶段拥有大把时间可以去折腾,而且自己感兴趣,那么就去做吧,不给自己设限,走出舒适区才会得到快速成长~

开发环境

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
  1. 复制到bochsrc文件对应的那一行
  2. 找到 display_library: 这一行修改为 display_library: x, options="gui_debug"
  3. 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)。

  1. 实模式(Real Mode):这是最初的 8086 和 8088 微处理器使用的模式。在这种模式下,CPU 可以直接访问最多 1MB 的物理内存。实模式没有内存保护、虚拟内存或多任务等特性。当你启动计算机时,CPU 会首先进入实模式,然后引导加载器会将其切换到保护模式以便加载和运行现代的操作系统。

  2. 保护模式(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

posted @ 2024-03-01 21:36  Merakii  阅读(354)  评论(0)    收藏  举报