写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

前言

  之前我们搭建好了Bochs学习环境(没搭好的回去弄好再回来看),可惜没有合法的启动盘,那么什么是启动盘,如何正确的启动,下面我们来开始介绍基础部分。

BIOS

  BIOS全称叫Base Input & Output System,即基本输入输出系统。它的主要工作是检测、初始化硬件。

实模式下的 1MB 内存布局

  Intel 8086有20条地址线,故其可以访问1MB的内存空间。若按十六进制来表示,是0x00000 - 0xFFFFF。这lMB的内存空间被分成多个部分。如下表格所示:

羽夏看Linux内核——启动那些事-LMLPHP

  内存地址0x00000 - 0x9FFFF的空间范围是64KB,这片地址对应到了动态随机访问内存DRAM,也就是插在
上的内存条;而0xF0000 - 0xFFFFF64KB内存是ROM,是只读的,存的就是BIOS的代码。硬件自己提供了一些初始化的功能调用,BIOS可以直接调用,并建立了中断向量表,就可以通过int 中断号来实现相关的硬件调用。而这些中断只有重要的、保证计算机能运行的那些硬件的基本IO操作,不像高级语言有各种花里胡哨的功能。
  我们还要说明一个问题:在CPU眼里,我们插在主板上的物理内存不是它眼里“全部的内存”。这个是由地址总线宽度决定了可以访问的内存空间大小。打个比方,比如小孩学数苹果数目。结果他只会100以内的,如果苹果数目超了100,就不会数了,不认识了。物理内存也是如此,再多的内存,只要识别能力不够,也是浪费。

BIOS 启动

  BIOS是计算机上第一个运行的软件,所以它不可能自己加载自己,由此可以知道,它是由硬件加载的。BIOS代码所做的工作也是一成不变的,而且在正常情况下,其本身一般是不需要修改的,存储在ROM中。ROM也是块内存,内存就需要被访问。而ROM被映射在0xF0000 - 0xFFFFF处,只要访问此处的地址便是访问了BIOS,这个映射是由硬件完成的。如果不太理解,可以学一下单片机的基础知识和电工学下册。
  BIOS本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是0xFFFF0

  当我们开始启动虚拟机进入调试状态时,你会看到如下内容:

羽夏看Linux内核——启动那些事-LMLPHP

  可以看到,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开始的
  如果此扇区末尾的两个字节分别是魔数0x55OxAABIOS便认为此扇区中确实存在可执行的程序,此程序便是主引导记录MBR,它会被加载到物理地址0x7c00,随后跳转到此地址,继续执行。反之,它就不认。
  BIOS跳转到Ox7c00是用jmp 0:Ox7c00实现的,此时段寄存器cs会被替换成0

为什么 MBR 住在这里

  因为近啊。就好比你会把经常用的放到身边,用到就会直接拿出来,如果把它放到老远的位置,这个不就费劲了吗。对于BIOS来说,MBR就是经常用的东西,放到身边才方便。

为什么是 0x7c00 地址

  据说是历史原因,BIOS规范。它最早出现在IBM公司出产的个人电PC5150 ROM BIOSINT 19H中断处理程序中。
  MBR不是随便放在哪里都行的,首先不能覆盖己有的数据,其次,不能过早地被其他数据覆盖。通常MBR的任务是加载某个程序(这个程序一般是内核加载器,很少有直接加载内核的)到指定位置,并将控制权交给它。
  按DOS 1.0要求的最小内存32KB来说,MBR希望给人家尽可能多的预留空间,这样也是保全自己的作法,免得过早被覆盖,所以MBR只能放在32KB的末尾。其次,MBR本身也是程序,是程序就要用到栈,栈也是在内存中的,虽然本身只有512字节,但还要为其所用的栈分配点空间,所以其实际所用的内存空间要大于512字节,估计1KB内存够用了。
  综上,选择32KB中的最后1KB最为合适,那此地址是多少呢?32KB换算为十六进制为0x8000,减去1KB(0x400)的话,等于0x7c00

MBR 杂谈

  MBR是独立于操作系统的,能够直接在裸机上运行。它的大小必须是512字节,保证0x550xAA这两个
魔数恰好出现在该扇区的最后两个字节处。下面我们来编写一个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看看结果:

羽夏看Linux内核——启动那些事-LMLPHP

  这就说明成功了。

编写 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,一共三卷。

羽夏看Linux内核——启动那些事-LMLPHP

  端口可以被分为两组,Command Block registersControl Block registersCommand Block registers用于向硬盘驱动器写入命令宇或者从硬盘控制器获得硬盘状态,Control Block registers用于控制硬盘工作
状态。在Control Block registers组中的寄存器已经精减了,而且咱们基本上用不到,就不赘述了,下面重点介绍Command Block registers组中的寄存器。
  端口是按照通道给出的,也就是说,端口不是直接针对某块硬盘的。一个通道上的主、从两块硬盘都用这些端口号,要想操作某通道上的某块硬盘,需要单独指定。
  Data寄存器在名字上我们就知道它是负责管理数据的,它相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。这个寄存器是16位的,得到了特殊照顾。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。
  读硬盘时,端口0x1710x1F1的寄存器名字叫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 lowLBA midLBA 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
  在读硬盘时,端口0x1F70x177的寄存器名称是Status,它是8位宽度的寄存器,用来给出硬盘的状态信息。索引0位是ERR位,如果此位为1,表示命令出错了,具体原因可见Error寄存器。索引3位是Request位,如果此位为1,表示硬盘己经把数据准备好了,主机现在可以把数据读出来。索引6位是 DRDY,表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。索引7位是BSY位,表示硬盘是否繁忙,如果为1表示硬盘正忙着,此寄存器中的其他位都无效。剩余的几位用不到暂且不关注。
  在写硬盘时,端口0x1F70x177的寄存器名称是Command。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在咱们的系统中,主要使用了三个命令。

  1. identify: 0xEC ,即硬盘识别。
  2. read sector: 0x20 ,即读扇区。
  3. write sector: 0x30 ,即写扇区。

  我们来用图简单总结一下:

羽夏看Linux内核——启动那些事-LMLPHP

  不管是读硬盘,还是写硬盘,都不是一个指令就完事的。我们先理顺一个步骤:

  1. 先选择通道,往该通道的Sector count寄存器中写入待操作的扇区数。
  2. 往该通道上的三个LBA寄存器写入扇区起始地址的低24位。
  3. Device寄存器中写入LBA地址的24-27位,并置第6位置为1,使其为LBA模式,设置第4位,选择操作的硬盘(master硬盘或slave硬盘)。
  4. 往该通道上的Command寄存器写入操作命令。
  5. 读取该通道上的Status寄存器,判断硬盘工作是否完成。
  6. 如果以上步骤是读硬盘,进入下一个步骤。否则,结束。
  7. 将硬盘数据读出。

  硬盘工作完成后,它己经准备好了数据,咱们该怎么获取呢?一般常用的数据传送方式如下:

  1. 无条件传送方式
  2. 查询传送方式
  3. 中断传送方式
  4. 直接存储器存取方式DMA
  5. IO 处理机传送方式

  这些传送方式我就不细说了,这不是我们的重点。感兴趣可以翻阅《操作系统真相还原》的第139页,或者其他资料。第1种方法不能用,因为硬盘需要在某种条件下才能传输。第4种和第5种需要单独的硬件支持。所以我们实现会使用较为简单的第2种和第3种。
  在之后的章节,弄好保护模式和分页的基础,我们会使用所有已学知识,学习Linux 1.1内核源码,并仿照逐步完善写一个十分简单的内核。

实模式杂谈

  弄了这么多,我们需要复习一下实模式相关的知识,因为这几篇之后,我们就要搞保护模式,会花费大量的篇幅介绍基础知识。
  在实模式下CPU访问数据将按照基址 + 偏移来进行。至于分类有寄存器寻址、直接寻址、内存寻址。
  在该模式下,用户程序和操作系统可以说是同一特权的程序,因为实模式下没有特权级,它处处和操作系统平起平坐,所以可以执行一些具有破坏性的指令。程序可以随意修改自己的段基址,这样便在内存空间内不受阻拦,可以随意访问任意物理内存,包括访问操作系统所在的内存数据,完全没有保护性可言。用户程序甚至可以覆盖操作系统在内存中的映像,整个计算机世界的和平全靠程序员的心情。

练习与思考

  俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做成功,就不要看下一节教程了。

  1. 独立完成本篇实验,并成功在Bochs中打印出红色的Loading system...!字符串。

下一篇

  羽夏看Linux内核——段相关入门知识

08-07 11:08