写在最前面:由于视频集数较多、涉及到的内容也很多,所以此专题会分多篇文章进行记录。

揭开钢琴的盖子

钢琴本身就是一个操作系统,其内部是很复杂的。如果只会弹琴,而不去了解其内部结构,就只能弹而不会改。所以我们要揭开钢琴的盖子,去学习其内部复杂的构造。

上图是我们电脑开机后的第一个画面,那么这个画面背后正发生着什么呢

要了解其中发生的事情,需要我们结合计算机的工作原理基本常识来进行。下面我们就来介绍一下计算机的发展流程和工作原理,再循着工作原理从汇编代码的层次来解析一下这个开机画面的底层实现

计算机的发展与工作原理

从白纸到图灵机

从上图可知,计算机就是一个自动化的计算模型,通过控制器实现一个具体的事件,比如图中所示的加法。

但是,上图所示的图灵机只可以进行单个特定的事件。

从图灵机到通用图灵机

通过设置控制器动作来指定操作器的实现,其实就是一个程序。

从通用图灵机到计算机

计算机相比于通用图灵机,一个很重要的突破:存储程序思想。即将指令和数据存储到计算机内部设备,而后控制器读取、解码并执行相应指令。

前进一步却是质的飞跃。冯诺依曼用存储程序思想完美解释了图灵机的两个核心概念:运算规则学会的含义

  • 运算规则:一个指令序列
  • 学会的含义:将指令序列中的指令逐条取出并解释执行

从汇编代码来始界看起面

前面我们已经知道,计算机的操作流程:取指、执行。起始界面也不例外。当然,界面只是计算机启动的一部分,在其背后还有很多无法直观看到的程序操作,下面我们就一一介绍。

计算机启动执行的第一个程序

第一部分程序就是引导扇区程序,占512字节,主要作用就是:实现代码位置的移动、将后续两个模块载入内存将启动界面打到屏幕上

ROM BIOS映射区通电后内存中唯一一个存在代码的地方,也是CPU执行命令的起点。(只有有代码才可以取指执行)

这段区域的代码主要有以下几个功能

  1. 检查RAM、键盘、显示器、软硬磁盘等硬件设施
  2. 将磁盘0磁道、0扇区读入内存0x7c00处。(共512字节,一个扇区,也叫引导扇区)
  3. 将CS=0x07c0,IP=0000。(设置下一步执行的指令位置,也就是引导扇区位置)

在执行完BIOS后会跳转到0x7c00位置,执行此处的指令。

上图就是引导扇区存储的指令,我们从汇编语言的角度来解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

>>>> bootsect.s

mov ax #BOOTSEC mov ds,ax == 最终将#BOOTSEC存放到ds寄存器(数据段寄存器,存储0x7c00
mov ax,#INITSEC mov es,ax == 最终将#INITSEC存放es寄存器(也是段寄存器,存储0x9000)

mov ax,#256 == 将256存储到ax

sub si,si sub di,di == 将si与di寄存器置零。(与DS和ES配合构成两个地址,一个是源地址,一个是目的地址)

rep movw == 移动256字,将BOOTSEC位置数据移动到INITSEC位置(从0x7c000x90000)

!!! 上一步的作用是为了腾出空间让其他程序使用。(后面具体介绍)!!!

jmpi go,INITSEC == 跳转到对应位置去执行go程序(INITSEC为CS go为IP,也就是从执行到go的偏移)

!!! 上代码的作用就是继续执行引导扇区代码,因为已经将其移动,所以要跳到移动后的位置上去!!!

上面详细解释了代码,请仔细观看。其作用就是:移动程序以腾出空间,位置跳转以顺序执行

下面我们继续解释代码,接着上一条代码去讲。具体代码如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> bootsect.s

go:mov ax,cs //将cs存储的数据放到ax,此数据为0x9000
mov ds,ax mov es,ax
mov ss,ax mov sp,#0xff00 // 这两行的作用就是更新段寄存器的数据,包括数据寄存器,栈寄存器等,也定义了栈顶地址。

load_setup: // 下面代码用于将setup模块载入内存
mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200
mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断,用于读取磁盘数据。参数通过上一行代码实现,具体含义见图片介绍。
jnc ok_load_setup
mov ax,#0x0000
mov ax,#0x0000 //复位
int 0x13
j Load_setup

已将对应代码解释清楚,这一段代码的主要作用就是:将setup数据从磁盘载入内存(使用13号中断,读取引导扇区后面的4个扇区的内容)并执行ok_load_setupload_setup.

:setup载入的位置紧靠bootsect,也就是0x90200.通过ES:BX给出此地址。

下面我们继续往下走,看一下ok_load_setup处的代码:(主要介绍一下重点部分)

这一部分代码主要涉及到页面显示后续跳转两部分。

  • 页面显示主要通过int 0x10中断进行,通过对应参数数据来将启动界面打到屏幕上。(msg1存储的就是启动界面的数据,其位置在引导扇区的末尾)补充int 0x10的介绍
  • 后续跳转就是调用 raed_it实现

上面介绍了ok_load_setup部分代码,下面我们来介绍一下read_it代码。注意执行完毕read_it后还要回到此处。

此部分代码将system模块写入内存,引导扇区的代码就算执行完毕,接下来就算执行后续setup和system了.


bootsect程序总结

bootsect.s的主要工作包括以下几个部分

  1. 将磁盘上从第二到第五扇区即构成的setuo模块读到内存0x9200处。
  2. 在显示器上输出操作系统标识
  3. 从磁盘第六个扇区之后读入system模块并将其放到0x10000

setup程序的执行

操作系统第一部分即引导扇区部分代码执行完毕,接下来就要执行setup程序即setup.s。

setup将完成os启动前的设置,具体包括:读取并保存硬件参数参数模式切换

下图给出了setup模块核心代码,不同版本的操作系统代码或许不同,但是核心功能是差不多的。

上图代码可以分为两个部分

  • start:用于硬件信息的读取,主要涉及到int 0x15中断。这一模块的主要功能就是将操作系统建立起来,所以硬件信息特别是内存大小需要提前获取并保存在0x9000处。
  • do_move:进行数据的移动,其中ds:si确定源地址;es:di确定目的地址,即0x0000.也就是将操作系统的system模块代码移动到内存地址为0x0000的位置。

:我们前面介绍bootsect代码时提到,其中有一段代码用于数据的移动,将代码从0x7c00移动到0x9000,就是为了防止此处移动后照成数据的破坏,因为system模块内容很多。

有一个问题:内存地址为0的位置不是存储了中断向量表吗?移动数据到此不会破坏数据吗?

这个答案很简单,因为setup模块执行完毕后就要进入保护模式,在这个模式下中断的调用与原来实模式不同了。不再使用原先的BIOS中断,而是建立新的IDT表并设置新的中断函数。

传送门:操作系统保护模式和实模式的区别

其实二者最本质的区别在于cpu对于指令的解析方式不同。

上图就给出了setup进行模式切换的指令,通过cr0寄存器进行。(涉及到硬件的很多主要控制都是通过设置CR0寄存器完成的,比如后面要学习的分页机制的启动等)

当计算机切换到保护模式,CPU在解析指令时就会使用区别于实模式的电路。(具体内容会在内存管理中介绍)

在保护模式下,cpu的寻址方式发生了变化,不再使用cs直接存储内存地址,而是存储表项下标,而后通过查表获取具体地址。

既然要查表,首先要先有表,这里的表就是gdt表,也叫全局描述符表

关于更多GDT表的知识看一看一下扩展知识篇

上图就给出了gdt表的初始化方式。在给定cs后就要根据其值进行查表,获取一个32位的地址。(此时IP也是32位寄存器)

与bootsect模块一样,setup模块也要进行指令跳转,以执行新的模块部分。

上图给出的 jmpi 0,8 就是一条保护模式下的跳转指令,其目的是跳转到0x0000处执行system模块

system模块执行

system模块是setup模块之后要执行的模块。在具体介绍各个部分之前,先来看一下操作系统的设计。

Image就是操作系统镜像,这个镜像的形成依赖于很多子模块,例如:bootsect、setup、system等,而这些子模块也依赖于更多的子模块,最终形成一个树结构

system模块的第一部分代码:head.s

上图就是head.s的代码,主要关注一个部分:对gdt和idt表进行重新初始化

:在上图中,还要注意一下使用的汇编语言的不同。这里使用了32位汇编。对于汇编语言的具体介绍我们会单独介绍。

上图给出了跳转的流程:与函数调用机制类似,先传入参数和返回地址,之后使用ret进行跳转。

head.s需要在初始化之前进行一些准备工作:设置中断表(不再使用BIOS中断)、设置GDT表(进入system模块需要重新设置)、设置页表(查询得到真正的物理地址)

上图代码所示,IDT表和GDT表的本质就是两个内存空间,所以只需要设计两个包含连续8B内存的数组即可;之后再将这两个表的基址通过lidtlgdt指令分别保存到IDTRGDTR寄存器中。

对于两表的初始化:IDT表的值全0,表示中断不可用,在之后的各模块初始化时会设置对应的中断程序地址到表项里(前面我们使用的BIOS中断不再使用);而GDT表的初始化与前面setup模块设置一致,都是设置一段内存空间并将段基址传入,主要是内核代码段、数据段等。

上图很重要,实际上操作系统启动的所有流程都是为了形成这样一张内存图

上图并不是一个完整的内存视图,而是操作系统内核在内存中的视图,也就是说内存拿出来1M大小的空间用于存储操作系统内核,其他部分为用户态区域

通过观察此内存视图,可知:页表、页目录表、IDT、GDT表都在这个区域内,并且其基址都保存在对应的寄存器中以便于寻址查询。而main.c程序也在这个区域,此程序用来完成操作系统启动的最后一个步骤。同时,一些硬件信息也被存储在了这个区域0x9000位置,这些硬件信息是setup模块获取的,用于后续main.c中进行数据结构的初始化

system模块的第二部分代码main.c

上图给出了main.c的代码,其实就是很多初始化的函数,用于对内存、设备、cpu等进行初始化,之后启动一个shell执行指令

我们以其中一个mem_init()函数为例子:

通过mem_init进行内存的初始化,借助参数进行内存的划分,以供后续使用。其中将管理的起始内存地址设置为4MB,因为0-1MB为系统内核1-4MB为磁盘高速缓存区;结束地址由0x9000处存储的内存信息决定。

其他部分代码也是这样,这些初始化函数共同完成操作系统的初始化工作。

总结

前面我们从底层代码的角度和核心功能出发介绍了操作系统的各个模块:bootsect、setup、system

这三个模块实现的功能各不相同,但是归根结底还是为了实现这两个结果操作系统的写入操作系统的建立

为什么要写入操作系统?

因为计算机的工作原理就是:取指执行,只要内存中有指令才可以进行工作。所以第一步要将操作系统的源码写入内存

为什么要进行初始化?

因为操作系统是一个便于操作硬件的软件,所以要针对不同的硬件获取关键参数初始化不同的数据结构,从而实现对硬件的管理。


写在最后

到这里,对于操作系统学习的第一个部分就结束了。这一节详细结束了操作系统的启动流程,要先理清这个流程,有一个大体的框架,而后再查缺补漏,将一些扩展的知识点进行记录学习。

下一节就要进入到操作系统接口的实现