计算机是如何启动的?如何制作自己的操作系统

2020-01-22 11:27:54   最后更新: 2020-06-26 16:51:56   访问数量:286




你是否也和我一样,想要知道当我们轻轻按下电源键,电脑哔的一声响,几行字闪过,然后操作系统的启动画面出现,电脑启动可以被使用,这一系列过程中,电脑到底做了什么呢?

 

电源接通,计算机就可以通过下面的流程进行启动了:

  1. 计算机启动之初,最先通电启动的是电源,此时会有一小段时间的不稳定供电,当电源供电稳定后,电源会向外发送一个表示 Power Good 的低电位信号
  2. 80x86 CPU 上级联有一个以晶振为振源的时钟发生器芯片,他就是 8284A,他会接收到电源的 Power Good 信号,于是,他会输出一个 RESET 信号给 CPU
  3. CPU 收到 RESET 信号后,会进行复位操作,将各个寄存器、缓存进行初始化,其中,CS 寄存器会被初始化为 0FFFFh,IP 寄存器则会被初始化为 0000h
  4. CPU RESET 完成后,会自动执行一个远跳转,从而跳转到由 CS:IP 指向的 0xffff0 地址处

 

如果你深入的思考过计算机应该如何被启动,你就会发现这其中存在着一个悖论 -- 如果要启动计算机,那么就要先执行启动程序,可是计算机还没有启动的时候,又怎么去执行启动程序呢?

西方有个谚语:

pull oneself up by one's bootstrap

拽着自己的鞋带把自己拉起来

 

这就是计算机的启动过程被称为 boot 的原因,他就来源于上述谚语中的单词“bootstrap”,那么,计算机设计中是如何解决这个悖论的呢?

早期计算机通过先为内存供电,将启动所需的程序预先写入内存的临时方法来解决这个悖论,但后来,BIOS 的诞生终于圆满的解决了这个问题

上世纪七十年代初,只读存储器 ROM 诞生了,他不再会因为掉电而造成数据的丢失,数据一旦烧录,就无法更改

于是,只要在计算机出厂时,将固定的程序写入 ROM,并且设置电脑开机时率先读取 ROM 的固定位置并执行,就可以解决上述的悖论了

这个在计算机只读存储区 ROM 中存储的就是 BIOS 程序(Basic Input Output System)

 

在硬件设计中,内存地址 FFFF0H 开始的一段空间被从 RAM 映射到了 ROM 上,这样,只要访问基地址 FFFFH 的内存,实际上访问的其实是 ROM 指定偏移的地址,而 ROM 中存储的代码正是 BIOS

通过这样的机制,完整内存地址空间的最后 16 位被 ROM 覆盖,从而让 CPU 将 ROM 当成了一种特殊的内存,而经过上文中 CPU RESET 后的远跳转,BIOS 程序就得以执行了,他主要做了下面几件事:

 

硬件自检

硬件自检也称为 POST(Power On Self Test)

主要功能就是检测启动所需的最基本的硬件工作是否正常,包括:

  1. CPU 内部寄存器测试
  2. BIOS 芯片字节检测
  3. 8237 DMA 控制器测试
  4. 0 ~ 32KB RAM 检测

 

早期的计算机会在这一过程中显示下面的界面:

 

 

但随着计算机工业的发展,硬件问题发生的概率越来越低,与此同时,计算机的启动速度越来越快,整个硬件自检过程也在1秒内就可以完成,这个界面也就不再显示了,除非出现了什么问题,屏幕上才会显示出对应的错误描述信息

 

初始化可编程中断控制器

硬件中断是计算机硬件系统中一个十分重要的功能,是 CPU 与外围设备进行通信的主要手段,主要分成两种:

  1. 可屏蔽中断
  2. 不可屏蔽中断

 

可屏蔽中断是硬件中断的主要来源,在计算机的硬件系统中,CPU 上级联了两个可编程中断控制器 8259A,他们就是用来对可屏蔽中断进行屏蔽或响应的,同时他们还实现了中断优先级的判断等功能

BIOS 在完成自检后,就通过初始化指令对两个 8259A 芯片进行初始化工作,将主 8259A 的 8 个中断 IRQ0 ~ IRQ7 映射到中断向量 int 10h ~ int 17h 上(由于从 8259A 是通过主 8259A 的 IR2 引脚进行级联的,因此实际上 IRQ2 中断是由从 8259A 芯片的 IR2 引脚,也就是 IRQ9 重定向实现的)

 

初始化显示设备

完成可编程中断控制器的初始化后,BIOS 开始初始化 CRT 对应的显示内存,并且测试 CRT 视频接口

上述测试正常完成后,BIOS 程序就开始执行内部的显卡标准驱动程序

 

对设备进行测试

完成了上述初始化过程,BIOS 开始检测各个设备,包括两片 8259A 芯片、8253 定时器芯片,以及键盘复位、卡键测试、扩展 IO 测试、扩展 RAM 测试(检测 32KB 以外的整个内存空间)

然后,BIOS 会搜索其他外围设备是否具有 ROM,如果找到,就会自动执行对应设备 ROM 中的程序

 

开启 NMI 并自举

上述初始化工作完成后,BIOS 便开启不可屏蔽中断 NMI,并通过 int 19h 进行自举,触发与外围存储设备的通信,例如软驱、硬盘、光盘等

BIOS 从这些设备中一个一个地去尝试启动,直到找到符合要求的设备

那么,这里说到的“符合要求”是什么呢?怎么确定一个设备是否是启动设备呢?

IBM 制定了一个规范,那就是如果一个存储设备的第 511 字节和第 512 字节分别存储的是 0xAAh 与 0x55h,那么,BIOS 就认为这个设备是启动设备,从而不再去寻找下一个设备,因为磁盘每个扇区正好是 512 字节,这第一个 512 字节也就因此被称为引导扇区,而这一过程中,BIOS 只需要读取每个设备的第一个扇区用来做判断

如果经过上述判断,该设备是一个启动设备,BIOS 就将这第一个扇区载入到内存地址 0x7C00h 的位置,就开始执行这段引导代码了,这也就是操作系统设计时的第一段代码,通过这段代码会加载并跳转到磁盘的另一段代码中,从而开始整个操作系统的引导、初始化与启动工作

 

既然我们已经知道了计算机启动的上述过程,我们能不能编写自己的启动程序呢?答案当然是肯定的

那就让我们来编写操作系统的第一步,从我们自己的启动盘启动,并在屏幕上显示“Hello World my OS!”

 

BIOS 中断基本介绍

既然我们要在屏幕上显示“Hello World my OS!”,那么首先要解决的问题是怎么让 BIOS 能够将内存中的信息显示在屏幕上

BIOS 通过硬件中断的方式内置了很多 IO 功能,具体可以参考: https://en.wikipedia.org/wiki/BIOS_interrupt_call,也可以参考本文的附录

其中 10H 号中断就是显示服务,我们通过 INT 10H 指令就可以触发 10H 号中断

在中断触发后,BIOS 会去读取寄存器 AH 中的值,并根据这个字节的内容,来进行不同的操作,例如,如果 AH 中存储的是 13H,BIOS 就会在屏幕上显示一行字符串

 

利用 BIOS 10H 号中断实现字符串显示

上面已经提到,在 INT 10H 触发时,如果 AH 中存储的是 13H,那么 BIOS 就会在屏幕上显示一行字符串

 

显示方式

寄存器 AL 中的最低两位,决定了具体的显示方式

  • 0 -- 目标字符串仅仅包含字符,显示属性在寄存器 BL 中,不移动光标
  • 1 -- 目标字符串仅仅包含字符,显示属性在寄存器 BL 中,移动光标
  • 2 -- 目标字符串包含字符和属性,不移动光标
  • 3 -- 目标字符串包含字符和属性,移动光标

 

显示属性

如上所述,当 AL 的 1 位为 0 时,BL 表示显示属性:

  • BIT2 ~ BIT0 -- 前景色,RGB 值,000b 为黑色,111b 为白色
  • BIT3 -- 前景色是否加亮,为 1 加亮,为 0 不加亮
  • BIT6 ~ BIT4 -- 背景色,取值见前景色
  • BIT7 -- 是否闪烁,0 不闪烁,1 闪烁

 

其他属性

下列寄存器中存储了显示所需的其他信息:

  • ES:BP -- 字符串在内存中的段地址与偏移地址
  • CX -- 字符串长度
  • BH -- 视频区页数
  • DH -- 存储在第几行显示
  • DL -- 存储在第几列显示

 

通过上面这么多的讲解,我们知道,只需要在第一个扇区的 511 字节和 512 字节设置结束标志:0xAA55h,我们就可以将这个磁盘设置为启动盘,而剩下的 510 字节足够保存我们要在屏幕上显示的字符串了

 

汇编器的选择

所以我们需要编写一段汇编代码,主流的汇编器主要有四个:微软家的 MASM、Borland 公司的 TASM、开源的 NASM 以及 GNU 汇编器

MASM 与 TASM 的语法是最为接近的,NASM 语法与他们有一些差别,但只要熟悉三者中一个的语法,通过查阅手册就可以清楚另外两者的代码如何编写了

推荐是在 windows 平台使用微软家的 MASM,在 linux 平台使用 NASM,网上资料非常多,选择跨平台的 TASM 也可以,至于 GNU 汇编器,他的语法与其他三者的差距最大,除非是非常熟悉 GNU 汇编语法,否则不是太推荐使用

本文我们选用开源的 NASM 在 linux 环境下进行编写

 

编写我们自己的启动代码

; author: techlog org 07c00h ; 将下列程序加载到内存地址 7c00h 处 ; 初始化段寄存器 mov ax, cs mov ds, ax mov es, ax call DisplayString jmp $ ; 跳转到当前位置,无限循环 DisplayString: mov ax, BootMessage mov bp, ax ; ES:BP = 串地址 mov cx, [MessageLength] ; CX = 串长度 mov ax, 01301h ; AH = 13h 显示字符串, AL = 01h,显示属性存储在 BL 中 mov bx, 008ch ; BH = 0 从第 0 行开始显示,BL = 8Ch 黑底红字高亮闪烁 mov dl, 0 ; 从第 0 列开始显示 int 10h ; 10h 号中断 ret BootMessage: db "Hello World my OS!" MessageLength: db $-BootMessage times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节 dw 0xaa55 ; 引导扇区标志

 

 

代码说明

上面代码中的注释已经非常清晰,这里做一些额外的说明

 

获取变量地址

在 DisplayString 函数中,我们看到一个赋值语句:

mov ax, BootMessage

 

 

在 MASM 中,我们需要这样写:

mov ax, word ptr BootMessage

 

 

MASM 中,如果要取变量的首地址,需要使用 OFFSET 或 PTR 指令,但在 NASM 中并没有这两个指令,取而代之的是,只要是变量,默认都是返回地址,所以直接使用命令 mov ax, BootMessage 就是讲变量 BootMessage 的首地址放入 ax

而如果你想要将 MessageLength 变量的值放入 cx 中,那么你需要执行:

mov cx, [MessageLength]

 

 

方括号表示取该变量的值

 

$ 与 $$

与 MASM 一样,NASM 中的 $ 标识符也同样代表当前代码的起始地址

除此之外,NASM 增加了 $$ 标识符,代表当前段的起始地址

所以,$$-$,就是本行相对于当前节开始处的距离

 

times

times 是 NASM 中十分实用的一个伪指令,他有两个操作数:

times n m

 

 

表示把 m 重复 n 次

例如 times 3 db 0 指令相当于:

db 0 db 0 db 0

 

 

这有些类似 MASM 中的 DUP 指令(需要先添加 start label 到程序第一行):

db 510-($-start) dup(0)

 

 

编译链接

无论你用哪种汇编器完成代码的编写,都要用相应的汇编器执行编译链接,例如,基于 NASM 编写的上述代码可以在 linux 下执行:

nasm boot.asm -o boot.bin

 

生成二进制文件 boot.bin,如果提示 nasm 命令不存在,使用对应平台下的包管理机制或到官网下载源码编译安装即可

 

写入磁盘

linux 环境

既然我们已经拥有了用于启动的二进制文件,只要将他写入磁盘的第一个扇区并将该磁盘设置为启动盘,开机启动就可以进入这个扇区了

那么,第一步,我们要写入磁盘

在 linux 下,可以通过 dd 命令写入:

dd if=boot.bin of=boot.img bs=512 count=1 conv=notrunc

 

参数 conv=notrunc 意味着a.img不能被截断

 

windows 环境

如果你是在 windows 环境下,你也可以使用 rawrite 或者 UltraISO 软件

如果你安装了 rawrite2 软件,只需要在 cmd 或者是运行窗口中执行:

rawrite2.exe -f boot.bin -d boot.img

 

同时你也可以安装更为强大的 UltraISO 软件,如下图点击启动菜单,加载引导文件选项:

 

 

选择刚刚编译生成的二进制文件,点击工具栏上的保存按钮,就可以生成启动盘 ISO 文件了

 

你可以将刚刚生成 ISO 或者 IMG 文件刻录到 U 盘、光盘或是软盘上,然后放入计算机,重启,在 BIOS 中设置从该设备启动,就可以看到屏幕上显示出了闪闪的“Hello World my OS!”

当然,这显得有些繁琐,我们完全可以换一种方式 -- 使用虚拟机

如果你在 linux 环境下,可以使用轻量化又功能强大的 bochs 虚拟机,或者如果你有图形界面或在 windows、mac 环境下,也可以使用 virtualbox、VMware 等等

这里就不赘述上述某个虚拟机的使用方式了,总之创建一个虚拟环境,然后将我们生成的 img 或是 ISO 文件载入虚拟机的虚拟软驱或光驱,然后点击启动按钮,你就会看到:

 

 

上图就是博主在自己的 windows 环境下使用 virtualbox 虚拟机启动的界面

是不是非常激动人心?是不是从未想过制作一个自己的操作系统是如此简单?我们已经顺利让 BIOS 从我们的初始扇区启动了,并且显示出了激动人心的 Hello World,接下来的事情还有什么难的呢?

操作系统被引导后,究竟做了哪些事情呢?敬请期待,博主的下一篇文章

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 

  1. 10H 号中断 -- 显示服务
  2. 13H 号中断 -- 直接磁盘服务
  3. 14H 号中断 -- 串行端口服务
  4. 15H 号中断 -- 杂项系统服务
  5. 16H 号中断 -- 键盘服务
  6. 17H 号中断 -- 并行端口服务
  7. 1AH 号中断 -- 时钟服务
  8. 00H 号中断 -- “0”作除数
  9. 01H 号中断 -- 单步中断
  10. 02H 号中断 -- 非屏蔽中断(NMI)
  11. 03H 号中断 -- 断点中断
  12. 04H 号中断 -- 算术溢出错误
  13. 05H 号中断 -- 打印屏幕和BOUND越界
  14. 06H 号中断 -- 非法指令错误
  15. 07H 号中断 -- 处理器扩展无效
  16. 08H 号中断 -- 时钟中断
  17. 09H 号中断 -- 键盘输入
  18. 0BH 号中断 -- 通信口(COM2:)
  19. 0CH 号中断 -- 通信口(COM1:)
  20. 0EH 号中断 -- 磁盘驱动器输入/输出
  21. 11H 号中断 -- 读取设备配置
  22. 12H 号中断 -- 读取常规内存大小(返回值AX为内存容量,以K为单位)
  23. 18H 号中断 -- ROM BASIC
  24. 19H 号中断 -- 重启动系统
  25. 1BH 号中断 -- CTRL+BREAK处理程序
  26. 1CH 号中断 -- 用户时钟服务
  27. 1DH 号中断 -- 指向显示器参数表指针
  28. 1EH 号中断 -- 指向磁盘驱动器参数表指针
  29. 1FH 号中断 -- 指向图形字符模式表指针

 

https://en.wikipedia.org/wiki/Booting

https://en.wikipedia.org/wiki/BIOS_interrupt_call

https://wenzhang.baidu.com/page/view?key=61f376962540de51-1426603624

http://wenzhang.baidu.com/page/view?key=64daa4f464f89f13-1426636725

http://www.360doc.com/content/18/0824/12/44974165_780828242.shtml

https://www.cnblogs.com/jiu0821/p/4422464.html

https://techlog.cn/article/list/10183457

《Orange's 一个操作系统的实现》

 






操作系统      os      system      nasm      asm      汇编     


京ICP备15018585号