写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
前言
之前我们搭建好了Bochs
学习环境(没搭好的回去弄好再回来看),可惜没有合法的启动盘,那么什么是启动盘,如何正确的启动,下面我们来开始介绍基础部分。
BIOS
BIOS
全称叫Base Input & Output System
,即基本输入输出系统。它的主要工作是检测、初始化硬件。
实模式下的 1MB 内存布局
Intel 8086
有20条地址线,故其可以访问1MB
的内存空间。若按十六进制来表示,是0x00000 - 0xFFFFF
。这lMB
的内存空间被分成多个部分。如下表格所示:
内存地址0x00000 - 0x9FFFF
的空间范围是64KB
,这片地址对应到了动态随机访问内存DRAM
,也就是插在
上的内存条;而0xF0000 - 0xFFFFF
这64KB
内存是ROM
,是只读的,存的就是BIOS
的代码。硬件自己提供了一些初始化的功能调用,BIOS
可以直接调用,并建立了中断向量表,就可以通过int 中断号
来实现相关的硬件调用。而这些中断只有重要的、保证计算机能运行的那些硬件的基本IO
操作,不像高级语言有各种花里胡哨的功能。
我们还要说明一个问题:在CPU
眼里,我们插在主板上的物理内存不是它眼里“全部的内存”。这个是由地址总线宽度决定了可以访问的内存空间大小。打个比方,比如小孩学数苹果数目。结果他只会100以内的,如果苹果数目超了100,就不会数了,不认识了。物理内存也是如此,再多的内存,只要识别能力不够,也是浪费。
BIOS 启动
BIOS
是计算机上第一个运行的软件,所以它不可能自己加载自己,由此可以知道,它是由硬件加载的。BIOS
代码所做的工作也是一成不变的,而且在正常情况下,其本身一般是不需要修改的,存储在ROM
中。ROM
也是块内存,内存就需要被访问。而ROM
被映射在0xF0000 - 0xFFFFF
处,只要访问此处的地址便是访问了BIOS
,这个映射是由硬件完成的。如果不太理解,可以学一下单片机的基础知识和电工学下册。
BIOS
本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是0xFFFF0
。
当我们开始启动虚拟机进入调试状态时,你会看到如下内容:
可以看到,ip
指向的地址指令是jmp far f000:e05b
,这个是跨段跳转,最后执行结果是到了0xFE05B
这个地址,这个是真正BIOS
代码开始的地方。
接下来BIOS
便马不停蹄地检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中0x000-Ox3FF
处建中断向量表IVT
并填写中断例程。然后它的任务完成了,剩下的部分就是交给下一个“负责人”继续处理。
0x7c00 杂谈
BIOS
最后一项工作校验启动盘中位于0盘0道1扇区的内容。在计算机中是习惯以0
作为起始索引的,用“相对”的概念,即偏移量来表示位置显得很直观,所以很多指令中的操作数都是用偏移表示的。0盘0道1扇区本质上就相当于0盘0道0扇区。为什么称为1扇区呢?因为硬盘扇区的表示法有两种,我们描述0盘0道1扇区用的便是其中的一种:CHS
方法,即柱面Cylinder
、磁头Header
、扇区Sector
;另外一种是LBA
方式,这里救不说了。0盘
说的是0
磁头,因为1张盘是有上下两个盘面的,1个盘面上对应一个磁头,所以用磁头Header
来表示盘面。0道
是指0柱面,柱面Cylinder
指的是所有盘面上、编号相同的磁道的集合,形象一点描述就是把很多环叠摞在一起的样子,组合在之后是1个立体的管状。1扇区
是将磁道等距划分成一段段的小区间,由于磁道是圆形的,确切地说是圆环,这些被划分出来的小区间便是扇形,所以称为扇区,而在CHS
方式中扇区的编号是从1开始的。
如果此扇区末尾的两个字节分别是魔数0x55
和OxAA
,BIOS
便认为此扇区中确实存在可执行的程序,此程序便是主引导记录MBR
,它会被加载到物理地址0x7c00
,随后跳转到此地址,继续执行。反之,它就不认。
BIOS
跳转到Ox7c00
是用jmp 0:Ox7c00
实现的,此时段寄存器cs
会被替换成0
。
为什么 MBR 住在这里
因为近啊。就好比你会把经常用的放到身边,用到就会直接拿出来,如果把它放到老远的位置,这个不就费劲了吗。对于BIOS
来说,MBR
就是经常用的东西,放到身边才方便。
为什么是 0x7c00 地址
据说是历史原因,BIOS
规范。它最早出现在IBM
公司出产的个人电PC5150 ROM BIOS
的INT 19H
中断处理程序中。
MBR
不是随便放在哪里都行的,首先不能覆盖己有的数据,其次,不能过早地被其他数据覆盖。通常MBR
的任务是加载某个程序(这个程序一般是内核加载器,很少有直接加载内核的)到指定位置,并将控制权交给它。
按DOS 1.0
要求的最小内存32KB
来说,MBR
希望给人家尽可能多的预留空间,这样也是保全自己的作法,免得过早被覆盖,所以MBR
只能放在32KB
的末尾。其次,MBR
本身也是程序,是程序就要用到栈,栈也是在内存中的,虽然本身只有512
字节,但还要为其所用的栈分配点空间,所以其实际所用的内存空间要大于512
字节,估计1KB
内存够用了。
综上,选择32KB
中的最后1KB
最为合适,那此地址是多少呢?32KB
换算为十六进制为0x8000
,减去1KB
(0x400)的话,等于0x7c00
。
MBR 杂谈
MBR
是独立于操作系统的,能够直接在裸机上运行。它的大小必须是512
字节,保证0x55
和0xAA
这两个
魔数恰好出现在该扇区的最后两个字节处。下面我们来编写一个MBR
程序,并让它跑起来。
我们本教程使用的16位汇编器是as86
,也是Linux 0.11
编写启动代码的其中一个汇编器。它的汇编语法类似Intel
的,而不是麻烦的AT&T
,具体用法请在终端输入man as86
查看。
现在as86
并不自带,我们需要安装,在终端输入以下指令:
sudo apt install bin86
安装成功后,如果输入as86
显示如下信息,表示安装成功:
as: usage: as [-03agjuwO] [-b [bin]] [-lm [list]] [-n name] [-o obj] [-s sym] src
从头啥也不会开始写也不现实,我给出一个以供参考:
.globl begtext,begdata,begbss,endtext,enddata,endbss ;全局标识符,供 ld86 链接使用。
.text
begtext:
.data
begdata:
.bss
begbss:
.text
BOOTSEC=0x7C0
entry start
start:
jmpi go,BOOTSEC ;段间跳转 BOOTSEC 指出跳转地址,标号go是偏移地址
go:
mov ax,cs
mov ds,ax
mov es,ax
mov cx,#20 ;共显示20个字符
mov dx,#0x1004 ;字符显示在屏幕第17行,第5列处
mov bx,#0x000c ;字符显示属性为红色
mov bp,#msg ;指向要显示的字符
mov ax,#0x1301 ;写字符串并移动光标到串结尾处
int 0x10
loop0: jmp loop0 ;死循环
msg:
.ascii "Loading system...!"
.byte 13,10
.org 510 ;表示以后语句从地址 510 偏移开始存放
.word 0xAA55 ;有效引导扇区标志,提BIOS加载引导扇区
.text
endtext:
.data
enddata:
.bss
endbss:
这个代码我命名为boot.s
,然后在该代码所在文件夹下进入终端,输入以下指令:
as86 -0 -a -o boot.o boot.s
ld86 -0 -d -o boot.bin boot.o
编译参数
这样得到的就是我们想要的内容文件boot.bin
。不过我们得把这几个命令行参数介绍一下。
-0
as86
是生成16位汇编代码,如果用了超过8086指令集发出警告。
ld86
是生成16位文件头,这个我们不要,需要删除。
-a
启用与Minix asld
的部分兼容性,看不懂可以不管。
-d
删除文件头。
-o
输出文件名/路径。
写入镜像
得到boot.bin
之后,我们需要将这个数据写入虚拟镜像test.img
当中,我们需要输入以下命令:
dd if=boot.bin of=test.img bs=512 count=1 conv=notrunc
dd
是用于磁盘操作的命令,可以深入磁盘的任何一个扇区。如果要了解详情,请在终端输入man dd
。这里我们仅仅介绍我们使用的参数。
if=
指定要读取的文件。
of=
指定把数据输出到哪个文件。
bs=
指定块的大小,dd
是以块为单位来进行IO
操作的,得指明块是多大字节。
count=
指定拷贝的块数。
conv=
指定如何转换文件。建议在追加数据时,conv
最好用notrunc
方式,也就是不打断文件。
测试
执行完这些操作后,我们双击我们的startLearning.sh
看看结果:
这就说明成功了。
编写 Makefile
以后我们如果频繁更改编译,每次输入这几个指令是不是太麻烦了?我们可以写一个Makefile
文件,每次只需在该目录下输入make
就可以重新编译:
all: boot.bin img
boot.bin: boot.s
as86 -0 -a -o boot.o boot.s
ld86 -0 -d -o boot.bin boot.o
img: boot.bin
rm -f test.img
bximage -hd -mode="flat" -size=60 -q test.img
dd if=boot.bin of=test.img bs=512 count=1 conv=notrunc
clean:
rm -f boot.bin boot.o test.img
磁盘读写
有关磁盘结构,这里就不多说了,我们把重点放到如何用汇编来读写磁盘相关内容。如果想详细了解建议看《操作系统真相还原》的第134页,或者从网络找相关资料。
硬盘控制器属于IO
接口,CPU
和硬盘打交道是通过硬盘控制器实现的,开始硬盘和控制器是分开的,后来被整到一起,这种接口便称为集成设备电路( Integrated Drive Electronics, IDE )。
让硬盘工作,我们需要通过读写硬盘控制器的端口,端口的概念在此重复下,端口就是位于IO
制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。但硬盘十分复杂,目前我们只用到其中的一小部分,具体了解详情请自行搜索AT Attachment with Packet Interface
,一共三卷。
端口可以被分为两组,Command Block registers
和Control Block registers
。Command Block registers
用于向硬盘驱动器写入命令宇或者从硬盘控制器获得硬盘状态,Control Block registers
用于控制硬盘工作
状态。在Control Block registers
组中的寄存器已经精减了,而且咱们基本上用不到,就不赘述了,下面重点介绍Command Block registers
组中的寄存器。
端口是按照通道给出的,也就是说,端口不是直接针对某块硬盘的。一个通道上的主、从两块硬盘都用这些端口号,要想操作某通道上的某块硬盘,需要单独指定。
Data
寄存器在名字上我们就知道它是负责管理数据的,它相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。这个寄存器是16位的,得到了特殊照顾。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。
读硬盘时,端口0x171
和0x1F1
的寄存器名字叫Error
寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在Sector count
寄存器中。在写硬盘时,此寄存器有了别的用途,被称之为Feature
寄存器。有些命令需要指定额外参数,这些参数就写在Feature
寄存器中。寄存器都是8位宽度。
Sector count
寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减一,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是8位寄存器,最大值为255
,若指定为0
,则表示要操作256
个扇区。
硬盘中的扇区在物理上是用柱面-磁头-扇区
来定位的Cylinder Head Sector
,简称为CHS
,但每次我们要事先算出扇区是在哪个盘面,哪个柱面上,这太麻烦了,但这对于磁头来说很直观,它就是根据这些信息来定位扇区的。我们希望磁盘中扇区从
0开始依次递增编号,不用考虑扇区所在的物理结构,这是一种逻辑上为扇区址的方法,全称为逻辑块地址Logical Block Address
。
LBA
有两种,一种是LBA28
,用28
位比特来描述一个扇区的地址,最大支持128 GB
,为了简单,我们可以使用该方式;另外一种是LBA48
,用48
位比特来描述一个扇区的地址,最大支持131072 TB
,目前没有任何存储器超过该大小。
介绍完了LBA
,现在可以说LBA
寄存器了,这里有LBA low
、LBA mid
、LBA high
三个,它们三个都是8位宽度的。LBA low
寄存器用来存储0-7
位,LBA mid
寄存器用来存储8-15
位,LBA high
寄存器存储16-23
位。但这总共才24位,连LBA28
都不够,咱们怎么用呢?Device
寄存器。
Device
寄存器是个杂项,它的宽度是8位。在此寄存器的低4位用来存储LBA
地址的24-27
位。索引4
位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。索引5
位用来设置是否启用LBA
方式,1代表启用LBA
模式,0代表启用CHS
模式。剩余的两位,称为MBS
位,都固定是1
。
在读硬盘时,端口0x1F7
和0x177
的寄存器名称是Status
,它是8位宽度的寄存器,用来给出硬盘的状态信息。索引0位是ERR
位,如果此位为1,表示命令出错了,具体原因可见Error
寄存器。索引3位是Request
位,如果此位为1,表示硬盘己经把数据准备好了,主机现在可以把数据读出来。索引6位是 DRDY
,表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。索引7位是BSY
位,表示硬盘是否繁忙,如果为1表示硬盘正忙着,此寄存器中的其他位都无效。剩余的几位用不到暂且不关注。
在写硬盘时,端口0x1F7
和0x177
的寄存器名称是Command
。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在咱们的系统中,主要使用了三个命令。
- identify: 0xEC ,即硬盘识别。
- read sector: 0x20 ,即读扇区。
- write sector: 0x30 ,即写扇区。
我们来用图简单总结一下:
不管是读硬盘,还是写硬盘,都不是一个指令就完事的。我们先理顺一个步骤:
- 先选择通道,往该通道的
Sector count
寄存器中写入待操作的扇区数。 - 往该通道上的三个
LBA
寄存器写入扇区起始地址的低24
位。 - 往
Device
寄存器中写入LBA
地址的24-27
位,并置第6位置为1,使其为LBA
模式,设置第4位,选择操作的硬盘(master
硬盘或slave
硬盘)。 - 往该通道上的
Command
寄存器写入操作命令。 - 读取该通道上的
Status
寄存器,判断硬盘工作是否完成。 - 如果以上步骤是读硬盘,进入下一个步骤。否则,结束。
- 将硬盘数据读出。
硬盘工作完成后,它己经准备好了数据,咱们该怎么获取呢?一般常用的数据传送方式如下:
- 无条件传送方式
- 查询传送方式
- 中断传送方式
- 直接存储器存取方式
DMA
- IO 处理机传送方式
这些传送方式我就不细说了,这不是我们的重点。感兴趣可以翻阅《操作系统真相还原》的第139页,或者其他资料。第1种方法不能用,因为硬盘需要在某种条件下才能传输。第4种和第5种需要单独的硬件支持。所以我们实现会使用较为简单的第2种和第3种。
在之后的章节,弄好保护模式和分页的基础,我们会使用所有已学知识,学习Linux 1.1
内核源码,并仿照逐步完善写一个十分简单的内核。
实模式杂谈
弄了这么多,我们需要复习一下实模式相关的知识,因为这几篇之后,我们就要搞保护模式,会花费大量的篇幅介绍基础知识。
在实模式下CPU
访问数据将按照基址 + 偏移
来进行。至于分类有寄存器寻址、直接寻址、内存寻址。
在该模式下,用户程序和操作系统可以说是同一特权的程序,因为实模式下没有特权级,它处处和操作系统平起平坐,所以可以执行一些具有破坏性的指令。程序可以随意修改自己的段基址,这样便在内存空间内不受阻拦,可以随意访问任意物理内存,包括访问操作系统所在的内存数据,完全没有保护性可言。用户程序甚至可以覆盖操作系统在内存中的映像,整个计算机世界的和平全靠程序员的心情。
练习与思考
俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做成功,就不要看下一节教程了。
- 独立完成本篇实验,并成功在
Bochs
中打印出红色的Loading system...!
字符串。