文章目录
计算机启动流程
- 上电启动后,CPU处于16位运行的实模式,分页机制禁止,此时只有1MB内存可用,没有特权级
- CPU跳转到BIOS的入口(地址为0xFFFF0)开始执行
- BIOS进行硬件自检(POST),主板、硬盘、显卡、内存等自检,并保存一些配置数据到特定内存地址处(如硬盘的数量)
- 根据配置的启动顺序(光驱、U盘、硬盘等),加载引导代码运行。例如,从硬盘启动时,将硬盘的第1扇区(主引导纪录)读取到0x7c00处并且跳转到该地址处运行
- 引导代码对操作系统的运行初始环境进行配置,并加载操作系统到内存中
- 跳转到操作系统运行
接管运行控制权
要接管系统的控制权,需要完成两步操作:
- 编写引导程序,控制在512字节内,写入启动磁盘的第0扇区,
- 在第0扇区的最后两个字节(1FE, 1FF)写入0x55, 0xaa。
BIOS在完成自检后,会检查第0扇区的最后两个字节是否是0x55, 0xaa,以此来判断是否包含有效的引导代码。如果是,则自动从引导磁盘的第0扇区加载引导程序到0x7c00处执行。
创建可引导的启动程序
- 在VIsual Studio Code中编写C源文件、汇编文件、链接脚本
- CMake根据配置脚本,调用GCC对源文件进行编译和汇编,调用LD进行链接生成可执行的ELF文件
- CMake还会调用OBJCOPY将ELF文件进行缩小,或者转换成BIN文件。
- 调试前,Visual Studio Code调用一些小工具将ELF、BIN文件写入磁盘映像
- 调试时,QEMU加载磁盘映像文件,然后等待GDB连接。
- GDB连接上QEMU,开始正式的调试过程。
初始化引导程序
引导程序的作用
引导程序的主要作用:boot只完成loader的加载工作,再由loader完成具体的初始化工作和内核加载。
实模式
CPU启动后,自动进入所谓的实模式。可以理解其为最早期的8086芯片的工作模式。这种模式无任何保护机制,只能运行16位代码、不支持虚拟内存、不支持访问1MB以上的内存。后续为通过代码切换到现代的高级保护模式。
内核寄存器
x86包含很多寄存器,以下仅列举与我们编程有关的部分。其中在CPU启动进入实模式后,EAX/EBX/ECX/EDX仅能使用低16位,即AX/BX/CX/DX。
CS/DS/SS/ES/FS/GS为段寄存器,用于指向一段内存区域。访问特定地址时,需要使用段:偏移的形式,生成的地址为段值 << 4 + 偏移,例如访问0x7c00,需要使用0x7c0: 0,或者0: 0x7c00。
在本课程中,没有使用其复杂的分段模式,即采用平坦模式,所有的段寄存器全部指向0。
存储映射
实模式模式只支持访问1MB以内的内存,BIOS会自动将磁盘的第0扇区加载到0x7c00地址处。
使用BIOS中断显示字符
BIOS中断
BIOS提供了一些服务函数,方便开发操作系统使用。
在使用时并不需要知道特定函数的入口地址,因其内部通过向量表的方式去访问,向量表里保存了函数的入口地址。在0地址处,存储着中断向量表,在访问时通过软中断int xx来实现对特定功能的调用。具体每个功能的执行时需要的参数,通过寄存器传递。
显示字符串有很多种方法,这节课时只使用一种简单的方式。
该BIOS中断的作用是:显示字符,同时光标前移,其中AL = 字符、BL = 前景色,BH=页码。
具体来说,BH 为目前的显示页,如果是在图形模式,则 BH 须设为 0,假如是在图形模式下,也可以设定 BL 来表示文字的颜色,文字模式下的 BL 则无功能。至于显示页是什么,不需要了解,我们只需要将它设置成零即可。
使用BIOS中断读取磁盘
存储规划
由于boot的容量限制,所以将大部分初始化和加载内核的功能放在loader中。为简单起见, loader在磁盘上的位置位于紧接boot之后的扇区,即第1扇区开始。长度不限。
boot启动之后,将调用BIOS中断从第1扇区加载loader到0x8000地址处,之后跳转到0x8000地址处运行。
是否必须放在第1扇区,0x8000地址处?不是必须的。可以有其它选择。本课程只是从简单、方便地角度去考虑,并不考虑是否节省内存。
INT13磁盘读取
BIOS提供了磁盘操作的服务中断,其具体使用方法如下:
●AH=02H
●AL=扇区数
●CH=柱面 cx = ch: cl
●CL=扇区
●DH=磁头
●DL=驱动器,00H7FH:软盘;80H0FFH:硬盘
●ES:BX=缓冲区的地址
●出口参数:CF=0——操作成功,AH=00H,AL=传输的扇区数,否则,AH=状态代码,参见功能号01H中的说明。CF标志见EFLAS寄存器中的CF位。
注:磁盘还支持LBA模式下的读取,但是这种读取方式相比直接用BIOS会复杂一些,代码量也多,不易用汇编实现。所以,此处使用BIOS中断读取;在后续的loader实现中,将使用LBA读取,到时可以将其与INT 13读取方式进行比较。
GDB查看内存命令
如果要使用GDB查看内存内容,可以使用x命令,其指令格式如下:
例如 x /100u 0x10000
●n 是一个正整数,表示显示内存的长度。
●f 表示显示的格式
○x 按十六进制格式显示变量。
○d 按十进制格式显示变量。
○u 按十六进制格式显示无符号整型。
○o 按八进制格式显示变量。
○t 按二进制格式显示变量。
○a 按十六进制格式显示变量。
○c 按字符格式显示变量。
○f 按浮点数格式显示变量。
●u:显示的单元大小。默认是4个bytes。b表示单字节,h表示双字节,w表示四字节,g表示八字节。
进入C语言环境并跳到loader
从汇编语言跳到C语言
从汇编跳转到C语言执行,有两种方式:一是用JMP直接跳转过去;二是用CALL指令进行函数调用。
在本课时中,由于是从boot中的汇编跳转到C语言,无需返回,所以直接用JMP跳转。
在使用前,先用.extern boot_entry导入外部boot_entry符号,然后再用jmp boot_entry跳转。
跳转到指定loader运行
boot和loader分属两个工程,共生成两个bin文件。
从boot跳到loader,只知道loader的起始地址为0x8000,所以采用函数指针转换。(void ()(void) 为无参数、无返回值的函数类型。((void ()(void))LOADER_START_ADDR)() 即认为在0x8000地址处存放了这种类型的函数的代码,通过调用函数函数进入到loader中运行。
注:无论是boot还是loader,其工程均已经配置好让start.S中的代码位于生成的bin文件开头。所以无论是boot还是loader,其最开头的指令总是程序的入口指令。