老李手把手之操作系统(2)

老李手把手之操作系统(2)

本章的目的是创建一个可引导的盘(bootable disk)

以下是本章需要了解的概念,请自行 baidu、google、bing

  • assembler
  • BIOS

操作系统启动过程

在开始本章之前,我必须要简要的说明下计算机的最基本的原理,其实大学里专门开了一门课程《计算机组成原理》,也是 4 大必修课之一,这里面非常详细的讲解了计算机是如何工作的,我在这里简化表达下,其实就是 取指 + 执行,不妨参考下 CPU 工作过程

所谓取指,就是从内存中取出指令,所谓执行就是在 CPU 上运行。问题在于从哪里取指?实际上这个问题在大学计算机核心教程《80x86 汇编语言程序设计》中有详细的讲述。简言之,是从 cpu 的指令寄存器 IP 得到下一条指令的内存地址。除非遇到跳转指令,否则 IP 指向顺序中的下一个指令。

于是 cpu 像一个永动机,不停的取指执行

一切都很美好,但是又有一个问题,在磁盘中的指令集合(代码)是如何被加载到内存中呢~?这个问题如同推动宇宙运转的第一股力量是什么

不过我们知道,磁盘中的代码绝对不是上帝加载到内存中的,一定是通过某种巧妙的方式加载进去的。于是我又继续查询资料。。。

在用户按下计算机电源开关之后,CPU 会自动的将其 CS 寄存器设定为 0xFFFF,将其 IP 寄存器设定为 0x0000。由于 CS:IP 指出了下一条指令的地址,因此 CPU 会跳到 0xFFFF:0x0000 处进行执行

那么,这个 0xFFFF:0x0000 内存地址中是什么指令呢~?

0xFFFF:0x0000 内存地址其实是 0xFFFF0 + 0x0000 = 0xFFFF0,这是因为 8086 有 20 位地址总线决定的特殊方式。从图中可知,0xFFFF0 正好是 BIOS ROM 的地址(CPU 可以直接访问 ROM 的内容),一般这里都存放的是一条跳转指令,将其跳转到系统 BIOS 的入口地址,这样系统就被系统 BIOS 中 ROM 的代码所接管;

我们目前离加载磁盘中的代码并执行,只差一步了,目前系统被 BIOS 所控制(在执行 BIOS 的代码),那什么时候,又是怎样,BIOS 程序将 CPU 控制权交给磁盘的代码呢~?于是继续查资料,BIOS 都做了什么工作:

  1. Pown On 阶段,检查有哪些设备,猜测就是通过访问内存或者设备寄存器来感知设备
  2. POST(power on self test)阶段,自检,比如利用显卡 BIOS 来显示自检的结果(这块非常简单,只要操作0xA0000 ~ 0xBFFFF 的内存即可),比如初始化对磁盘的使用
  3. 加载 bootloader,这个才是最关键的地方,当 BIOS 完成自检之后,它具备了读写磁盘的能力(虽然不知道是怎么做的),那么此时,BIOS 就可以加载用户的代码进入内存,设置 CS:IP,就可以让用户代码开始执行了,也就是说可以把操作 CPU 的控制权交给用户代码了。

问题又来了,BIOS 从磁盘的哪里读数据?要读多少呢?BIOS 怎么知道用户代码的入口在哪里呢?

其实这是一个问题,思考下,CPU 开机会首先执行的是 0xFFFF:0x0000,那么我们可以大胆猜测,BIOS 也应该会从某个固定的磁盘的区域拉数据。继续查资料。。。

When the computer boots, the BIOS doesn't know how to load the OS, so it
delegates that task to the boot sector. Thus, the boot sector must be
placed in a known, standard location. That location is the first sector
of the disk (cylinder 0, head 0, sector 0) and it takes 512 bytes.

这显得非常的合理,我记得我小的时候,如果磁盘的 0 柱面、0 扇区、0 磁头的地方如果有了坏道,系统将无法启动。但是可以通过低格将 (0,0,0) 映射到好的扇区,这样磁盘又可以重装系统并且正常引导。非常有道理的计算机。。。

此时还有一个问题,代码加载到内存的什么地方?如果不确定地方的话,BIOS 没法跳转到指定的位置,请再回忆一下,BIOS 的代码在 ROM 中,是无法修改的,因此,加载到的内存地址一定是确定的,而且,加载代码的容量也是确定的(512B),否则 ROM 的代码没法写。。。查询下资料。。。

在 BIOS 自检等一系列工作完成后,要开始引导了。计算机会将硬盘 0 扇区 512 字节加载到 0x07C00(0x0000:0x7C00)处

果然如我们所料,此时就可以设置 CS:IP = 0x0000:0x7C00,直接跳转过去,这样就完成了控制权的交接。

看来,计算机的启动,确实不是上帝推了一把,而是大家都按照某个约定俗成的规则进行,总结一下:

  1. CPU 稳定后,首先取地址 0xFFFF:0x0000 的内容,作为指令执行
  2. 该地址为系统 BIOS ROM 的编制范围,这个地方的代码是个跳转指令,跳转到 BIOS 的初始化地址继续执行
  3. BIOS 完成自检、设备初始化后,将磁盘的 0 号扇区的内容加载 0x07C00
  4. 跳转到 0x07C00 地址,执行我们的代码了

因此,我们编写的操作系统,其实就是从编写 0 号扇区 512 字节的代码开始,继续往下一步一步的充实,最终能够生成一个基本可用 OS。

可启动的磁盘原理

如上文,我们知道 0 号扇区的 512 字节会被加载到内存的 0x07C00 地址,并执行。为了保证 512 字节是可以被引导的,我们约定,第 511、512 字节必须是0xAA55,否则将拒绝引导该磁盘,转而找下一启动顺位的磁盘。

我们来考虑下这个规定是否合理?通常我们写 bash 脚本的时候也会在第一行加入 #!/bin/bash,sh 脚本会加入 #!/bin/sh,这些都有一个统一的名称叫做 magic code,魔数。通常魔数都是硬编码的,使用计算机的人和编写计算机程序的人都要按照这个硬性规则来。可以说这些硬性规则是计算机发展的基石。再举一个例子,为什么 C 语言的主函数一定要是 main?其实这也是一种约定,否则代码从哪个地方入口呢~?

最简单的引导扇区是这个样子的

e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[ 29 more lines with sixteen zero-bytes each ]
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa

前面三个字节是一个 infinite jump,最后 2 个字节是 0xAA55(这里要注意下大端、小端的问题,x86 是小端模式),不懂的同学看下:字节序列介绍

最简单的一个启动扇区代码

; Infinite loop (e9 fd ff)
loop:
    jmp loop 

; Fill with 510 zeros minus the size of the previous code
times 510-($-$$) db 0
; Magic number
dw 0xaa55

nasm 语法得自己学一学:

  • jmp 是跳转指令
  • dw 是 Define Word 的意思,word = 2 Byte
  • db 是 Define Byte 的意思
  • 分号是注释

自己搜一搜啥都知道了...

编译下这个代码,得到一张 512 Byte 的磁盘

nasm -f bin boot_sect_simple.asm -o boot_sect_simple.bin

使用 qemu 来启动一台虚拟机,且使用上述的虚拟磁盘:

qemu-system-x86_64 boot_sect_simple.bin

效果如下:

posted on 2020-08-07 14:19  silenceli  阅读(263)  评论(0编辑  收藏  举报