1、计算机启动流程
1)上电启动后,CPU处于16位运行的实模式,分页机制禁止,此时只有1MB内存可用,没有特权级
2)CPU跳转到BIOS的入口(地址为0xFFFF0)开始执行
3)BIOS进行硬件自检(POST),主板、硬盘、显卡、内存等自检,并保存一些配置数据到特定内存地址处(如硬盘的数量)
4)根据配置的启动顺序(光驱、U盘、硬盘等),加载引导代码运行。例如,从硬盘启动时,将硬盘的第1扇区(主引导纪录)读取到0x7c00处并且跳转到该地址处运行
5)引导代码对操作系统的运行初始环境进行配置,并加载操作系统到内存中
6)跳转到操作系统运行
主要工作是完成最后两步,实现自己的引导代码和操作系统。
2、接管控制权
具体而言,要接管系统的控制权,需要完成两步操作:
1)编写引导程序,控制在512字节内,写入启动磁盘的第0扇区,
2)在第0扇区的最后两个字节(1FE, 1FF)写入0x55, 0xaa。
BIOS在完成自检后,会检查第0扇区的最后两个字节是否是0x55, 0xaa,以此来判断是否包含有效的引导代码。如果是,则自动从引导磁盘的第0扇区加载引导程序到0x7c00处执行。具体如下图所示。
本项目只涉及到启动磁盘没有分区的情况,简化项目过程
3、创建可引导的启动程序
后续所有的开发工作都将在此工作模板基础之上完成。整个工程中包含了引导程序、加载程序、操作系统以及三个具体的应用进程。
整体的工作流程如下图所示:
1)在VIsual Studio Code中编写C源文件、汇编文件、链接脚本
2)CMake根据配置脚本,调用GCC对源文件进行编译和汇编,调用LD进行链接生成可执行的ELF文件
3)CMake还会调用OBJCOPY将ELF文件进行缩小,或者转换成BIN文件。
4)调试前,Visual Studio Code调用一些小工具将ELF、BIN文件写入磁盘映像
5)调试时,QEMU加载磁盘映像文件,然后等待GDB连接。
6)GDB连接上QEMU,开始正式的调试过程
4、初始化引导程序
本项目只采用二级引导,即boot只完成loader的加载工作、再由loader完成具体的初始化工作和内核加载
实模式:CPU启动后自动进入实模式,这种模式无任何保护机制,只能运行16位代码,不支持虚拟内存、不支持访问1MB以上的内存,后续可以通过代码切换到高级保护模式
1)内核寄存器:
通用寄存器:CPU进入实模式后,AX/EBX/ECX/EDX仅能使用低16位,即AX/BX/CX/DX。
段寄存器:CS/DS/SS/ES/FS/GS为段寄存器,用于指向一段内存区域。访问特定地址时,需要使用 段:偏移 的形式
在本课程中,没有使用其复杂的分段模式,即采用平坦模式,所有的段寄存器全部指向0。、
2)存储映射
实模式模式只支持访问1MB以内的内存,其中可供我们自由使用的区域如下图灰色部分所示。具体的内存映射如下:
5、使用BIOS中断显示字符
BIOS中断:在0地址处,存储着中断向量表,在访问时通过软中断int xx来实现对特定功能的调用。具体每个功能的执行时需要的参数,通过寄存器传递。
显示字符:利用INT 10,AH=0xE显示字符
该BIOS中断的作用是:显示字符,同时光标前移,其中AL = 字符、BL = 前景色,BH=页码。具体来说:BH 为目前的显示页,如果是在图形模式,则 BH 须设为 0,假如是在图形模式下,也可以设定 BL 来表示文字的颜色,文字模式下的 BL 则无功能
6、使用BIOS中断读取磁盘
为简单起见, loader在磁盘上的位置位于紧接boot之后的扇区,即第1扇区开始。长度不限。(不是必须放在第一扇区0x8000处)
boot启动之后,将调用BIOS中断从第1扇区加载loader到0x8000地址处,之后跳转到0x8000地址处运行。
利用INT13中断读取磁盘
6、进入C语言环境并进入Loader
主要目的是创建loader工程,编写简单的代码,并实现从boot进入到loader中运行。实现两级跳转:从汇编到C,再从boot 到 loader。
1)从汇编语言跳到C语言
从汇编跳转到C语言执行,有两种方式:一是用JMP直接跳转过去;二是用CALL指令进行函数调用。由于是从boot中的汇编跳转到C语言,无需返回,所以直接用JMP跳转。
在使用前,先用.extern boot_entry导入外部boot_entry符号,然后再用jmp boot_entry跳转(具体看代码)
2)跳转到指定loader运行
boot和loader分属两个工程,共生成两个bin文件。从boot跳到loader,只知道loader的起始地址为0x8000,所以采用函数指针转换
7、总结与代码分析
1)代码结构
boot和loader下都有对应的汇编文件。
2)boot中的重要代码
start.S和boot.c
#include "boot.h"
// 16位代码,务必加上
.code16
.text
.global _start
.extern boot_entry
_start:
// 重置数据段寄存器
mov $0, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
// 根据https://wiki.osdev.org/Memory_Map_(x86)
// 使用0x7c00之前的空间作栈,大约有30KB的RAM,足够boot和loader使用
mov $_start, %esp
// 显示boot加载完成提示
mov $0xe, %ah
mov $'L', %al
int $0x10
// 加载loader,只支持磁盘1
// https://wiki.osdev.org/Disk_access_using_the_BIOS_(INT_13h)
read_loader:
mov $0x8000, %bx // 读取到的内存地址
mov $0x2, %cx // ch:磁道号,cl起始扇区号
mov $0x2, %ah // ah: 0x2读磁盘命令
mov $64, %al // al: 读取的扇区数量, 必须小于128,暂设置成32KB
mov $0x0080, %dx // dh: 磁头号,dl驱动器号0x80(磁盘1)
int $0x13
jc read_loader
// 跳转至c部分执行,再由c部分做一些处理
jmp boot_entry
// 原地跳转
jmp .
// 引导结束段
.section boot_end, "ax"
boot_sig: .byte 0x55, 0xaa
__asm__(".code16gcc"); //告诉编译器采用16位模式(实模式)
#include "boot.h"
#define LOADER_START_ADDR 0x8000 // loader加载的地址
/**
* Boot的C入口函数
* 只完成一项功能,即从磁盘找到loader文件然后加载到内容中,并跳转过去
*/
void boot_entry(void) {
((void (*)(void))LOADER_START_ADDR)();
}
3)loader中的重要代码
// 16位代码,务必加上
.code16
.text
.extern loader_entry
.global _start
_start:
// 栈和段等沿用之前的设置,也可以重新设置
// 这里简单起见,就不做任何设置了
// 你可能会想,直接跳到loader_entry,但这样需要先知识loader_entry在哪儿
// boot没有这个能力做到,所以直接用汇编,以便_start处于整个bin文件开头,这样boot直接跳到开头就可以
jmp loader_entry
// 16位代码,必须加上放在开头,以便有些io指令生成为32位
__asm__(".code16gcc");
void loader_entry(void) {
for(;;) {}
}
4)总结
了解计算机启动流程?
启动—跳转到BIOS—自检—加载引导代码—引导代码加载操作系统到内核—跳转到操作系统执行
BIOS自检完成会检查第0扇区最后两个字节是够是x55, 0xaa,来确认是否包含有效的引导代码