我是如何学习写一个操作系统(二):操作系统的启动之Bootloader

前言

今天本来的任务看书和把之前写的FragileOS整理一下,但是到现在还在摸鱼,书也只看一点。后来整理了一下写这个系列的思路,原本的目的是对操作系统原理性的学习和对之前写的一个玩具型操作系统的回顾,就是想对操作系统的知识的轮廓能有一个了解,现在想来想减少对之前写的系统的回顾,毕竟也只有2000多行,但是还是要有对整个思路的展现。然后增加对Linux 0.12源码的一些学习。所以离标题可能比较远了一点,但是就这样吧

什么是操作系统

原本这一节是写计算机系统和操作系统概述的,但是写到一半觉得太水就删了。就总结几句,后面用到什么就补什么。计算机系统的概述应该属于计算机组成原理的内容,这俩部分也是《操作系统:精髓和设计原理》的第一二章。但是觉得如果对于想学习操作系统内部的代码的话,换成汇编的内容会更好。

进入正题,操作系统是什么

对于计算机来说最根本的运行方式,就是取指执行

对于在屏幕上输出Hello,World!的过程,首先CPU拿到内存中的指令,这些指令是通知CPU把存在某个内存中的’H’’E’’L’等移动到显存位置,这样在屏幕上就可以看到这些字符了。这就是计算机最原始的运行方式

而操作系统就是对硬件层面的抽象,让我们不用在直面硬件,如果想要再次在屏幕输出字符,只要直接调用操作系统的write(windows下的好像是这个名),C语言中的printf下就是一个系统调用

当然操作系统绝对是比想象中的庞大的多,操作系统还对内存、终端、磁盘、网络和文件等等进行管理,光windows 2000应该就有3000多万行的代码了。当然有简陋的内存、进程管理和文件系统的玩具型内核,只要几千行代码就可以完成了。

操作系统的启动

对于X86架构的计算机,开机时一共做这几件事

  • 开机时的CS = 0xFFFF, IP = 0x0000

    这时候的CPU处理实模式,也就是寻址的方式是CS:IP (实模式和保护模式属于CPU的工作模式,其中比较大的区别就是寻址的方式)

  • 寻址0xFFFF0

  • 检查硬件设备,像键盘显示器之类的

  • 将磁盘0磁道0扇区读入0x7c00处

    会从这里读入512字节,也就是传说中的引导程序,这里放着计算机执行的第一段代码

  • 设置cs = 0x7c00 ip = 0x0000

FragileOS/boot

这个是我之前写的FragileOS的boot,采用的是Intel汇编格式

主要的逻辑就是:

  • 先加载到0x7c00位置
  • 进行初始化操作
  • 调用CPU提供的中断来读取数据
  • 读取完毕后直接跳到内核的起始位置,也就是引导结束了

(部分代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
org  0x7c00;                                ;加载到内存0x7c00处

LoadAddr EQU 08000h ;内核的内存地址
BufferAddr EQU 7E0h ;读取扇区的时候进行的缓存

BaseOfStack EQU 07c00h

entry:
mov ax, 0 ;进行寄存器的初始化操作
mov ss, ax
mov ds, ax

mov ax, BufferAddr
mov es, ax ;ES:BX 数据存储缓冲区,指示扇区加载后放置的地址

mov ax, 0
mov ss, ax
mov sp, BaseOfStack
mov di, ax
mov si, ax


mov BX, 0 ;ES:BX 数据存储缓冲区
mov CH, 1 ;CH 用来存储柱面号
mov DH, 0 ;DH 用来存储磁头号
mov CL, 0 ;CL 用来存储扇区号

read_floppy: ;每次都把扇区写入缓存地址07E00处
cmp byte [load_count], 0 ;比较load_count地址处的值,如果=0就跳转到begin_load
je begin_load

mov bx, 0
inc CL
mov AH, 0x02 ;AH = 02 表示要做的是读盘操作
mov AL, 1 ;AL 表示要练习读取几个扇区
mov DL, 0 ;驱动器编号,一般我们只有一个软盘驱动器,所以写死

int 0x13 ;调用BIOS中断实现磁盘读取功能
jc fin

Linux 0.12/boot

Linux 0.12的boot自然比上面的复杂的多,Linux采用的boot的汇编是as86格式的,其余的汇编采用的都是AT&T

Linux 0.12下的boot一共有三个文件:

  • bootsect
  • head
  • setup

(代码太长不全部贴了,有需要的可以私信我)

bootsect

bootsect的主要作用就是把自己移动到0x90000处执行,然后再加载setup模块 (也就是setup.s)到bootsect的后面,再把system模块加载到0x10000处,这个也就是内核的主要部分

bootsect的开头是一些常量的定义

1
2
3
4
5
6
SETUPLEN = 4				  ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
  • _start先设置好目的地址和源地址

    ds:si和es:di

  • 然后执行rep指令

    rep指令是重复的意思,它以cx寄存器的值为判断,如果cx的值为0就停止

  • movw指令

    开始从[si]处移动cx个字到[di]处,这里也就是一共移动了256个字,512字节

  • 最后跳转到0x9000开始执行

1
2
3
4
5
6
7
8
9
10
11
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
  • 现在的这些代码都是在0x90000后的
  • 先重新设置段寄存器和栈指针
1
2
3
4
5
6
go:	mov	ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
  • 这一部分和我之前的一样,就是调用中断来读取磁盘内容,只是Linux读取的是在第二扇区的setup模块

  • 如果失败就重新设置驱动器然后跳回重新读取

  • 成功就跳到ok_load_setup

  • ok_load_setup是设置根文件系统设备的,并且读入SYSTEM模块 (内核的主要部分)到0x10000处,结尾是跳到SETUP模块

1
2
3
4
5
6
7
8
9
10
11
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup

小结

一个简单的boot引导程序,顾名思义就是把做一些引导工作的,进行一些初始化设置再读入真正的内核部分,进入OS。

其实Linux 0.12一个完整的boot应该还包括setup.s用来完成OS启动前最后的设置 (进入保护模式等),head.s则是进入之后的设置。但是因为这两部分包含了一些其它重要概念,所以打算再下一篇写。