前情提要
上一节我们讲了如何启动计算机,这一节我们讲如何加载内核,内核是存在于硬盘上的一段程序,要加载这段程序,那么必然需要从硬盘上读取数据,这里我们就需要使用 ATA PIO 模式
根据ATA规范,所有符合ATA的驱动器必须始终支持PIO模式作为默认的数据传输机制。
现在较为流行的SATA硬盘也是一种符合ATA标准的硬盘,所以当然也需要支持 ATA PIO,而 ATA PIO 较为简单,所以我们就将其当做默认的读取硬盘的模式
在实际应用中,为了获得更好的性能和效率,通常会选择更高级的硬盘访问模式,如 DMA 或 Ultra DMA,以及操作系统提供的直接访问硬盘的接口(如 Windows 的AHCI模式)。这些模式能够更有效地利用系统资源,提供更快速的数据传输速度。甚至是NVME,直接走PCIE通道与CPU直连。但是这些比较复杂,不在本文的考虑范围内。
一、硬盘的主要端口
其中Primary为主通道,Secondary为从通道
其中主通道读时
- 0x1F0 是数据端口
- 0x1F1 是错误端口,可以返回错误信息,每一位都是一个错误信息,包括(0、AMNF未找到地址标记。1、TKZNF未找到零磁道。2、ABRT中止命令。3、MCR变更请求。4、IDNF未找到ID。5、MC 发生了变化。6、UNC不可纠正的数据错误。7、BBK检测到坏块。)
- 0x1F2 是扇区数量端口
- 0x1F3 是LBA低地址
- 0x1F4 是LBA中地址
- 0x1F5 是LBA高地址
- 0x1F6 0-3位,在CHS寻址中表示柱头位,在LBA寻址中,表示LBA地址的24-27位。4位DRV,表示选择主盘或者从盘。5位、永远为1。6位、如果为0则为CHS寻址,如果为1则为LBA寻址。7位、永远为1。
- 0x1F7 是状态寄存器端口 ,0位ERR,如果为1则表示出错了。3位Data ,如果为1表示硬盘已经把数据准备好了。6位DRDY,表示硬盘检测正常,可以执行命令。7位BSY,如果为1表示硬盘正繁忙,此寄存器中的其他位都无效。
主通道写时有一些yu寄存器有了不同的用途
- 0x1F1 是参数端口,用于传递写硬盘时的参数
- 0x1F7 是指令端口,我们主要用到了这么几个指令。0xEC,硬盘识别。0x20,读扇区。0x30,写扇区。
二、加载Loader
哈哈哈哈,上面说的是加载内核,现在又成了加载loader,没办法,加载内核之前就得加载Loader,Loader的作用有
- 加载内核:loader 负责将操作系统内核从存储设备(如硬盘、闪存)中读取到内存中,以便后续执行。
- 确认内核完整性:loader 在加载内核之前通常会对内核进行校验,以确保内核文件的完整性和正确性,避免因为损坏或错误的内核文件导致系统启动失败。
- 设置环境:loader 在加载内核前会设置好适当的执行环境,包括初始化硬件设备、建立内存映射关系等,为内核的正常执行做好准备工作。
- 启动内核:加载完内核后,loader 会将控制权转交给内核的起始地址,启动内核的执行,让操作系统开始运行。
由于MBR是占据了硬盘的第0扇区(以逻辑LBA方式,扇区从0开始编号,若是以物理CHS方式,扇区则从1开始编号),所以我们的loader就放在第1扇区,可以看第二章的内存布局,现在有两块内存可用,0x500~0x7BFF
和0x7E00~9FBFF
,那我们就放在 0x600
的地方吧。下面我们接着改MBR
2.1、修改Mbr使其可以加载Loader
这里我们添加一点宏定义
; os/src/boot/boot.inc
LOADER_BASE_ADDR equ 0x600
LOADER_START_SECTOR equ 0x1
然后改写mbr
; os/src/boot/mbr.s
; 设置开始的地址,并且初始化寄存器
%include "boot.inc"
SECTION MBR vstart=0x7c00 mov ax,cs mov ds,axmov es,axmov ss,axmov fs,axmov sp,0x7c00mov ax,0xb800mov gs,ax; 利用0x06号功能实现清理屏幕
; AL = 0x06 功能号
; AL 上卷的行数(如果为0,表示全部)
; BH 上卷行属性
; (CL,CH) = 窗口左上角的(X,Y)位置,这里是 (0,0)
; (DL,DH) = 窗口右下角的(X,Y)位置,这里是 (80,25)mov ah, 0x06mov al, 0x00mov bh, 0x7mov bl, 0x00mov cx, 0 mov dx, 0x184fint 0x10 ; int 0x10mov byte [gs:0x00],'M' ; 字符为M的ascii值mov byte [gs:0x01],0x0F ; 11100001b 即背景色为黑,字体为白,不闪烁 mov byte [gs:0x02],'B' ;mov byte [gs:0x03],0x0F ; mov byte [gs:0x04],'R' ;mov byte [gs:0x05],0x0F ;mov eax,LOADER_START_SECTOR ; Loader起始扇区 mov bx, LOADER_BASE_ADDR ; Loader起始内存地址mov cx, 1 ; 待写入扇区数call rd_disk_m_16 ; 执行读取硬盘程序jmp LOADER_BASE_ADDR ; 跳转到Loader执行rd_disk_m_16: ; eax=LBA扇区号; ebx=Loader内存; ecx=扇区数量mov esi,eax ; 备份eaxmov di,cx ; 备份cxmov dx,0x1f2 ; 设置要写入端口,即读取端口数mov al,cl ; 设置要读取扇区数out dx,al ; 设置mov eax,esi ; 恢复eaxmov dx,0x1f3 ; 设置要写入端口,即LBA低地址 out dx,al mov cl,8 ; ax右移八位 shr eax,clmov dx,0x1f4 ; 设置要写入端口,即LBA中地址 out dx,alshr eax,cl ; ax右移八位 mov dx,0x1f5 ; 设置要写入端口,即LBA高地址 out dx,alshr eax,cl ; ax右移八位and al,0x0f ; 保留低4位,设置高4位为 0000or al,0xe0 ; 保留低4位,设置高4位为 1110mov dx,0x1f6out dx,almov dx,0x1f7 ;mov al,0x20 ; 读扇区指令 out dx,al.not_ready: ; 未准备好nop ; 不执行任何指令,占用一个机器周期in al,dx ; 查看读取状态and al,0x88 ; 与 10001000 做与运算cmp al,0x08 ; 比较第三位和第七位jnz .not_readymov ax, di ; 要读的扇区数mov dx, 256 ; 乘以256,即要读多少次mul dxmov cx, ax ; 将要读的次数传给cxmov dx, 0x1f0 ; 要读的端口号.go_on_read:in ax,dx ; 向ax中读,一次读两个字节mov [bx],ax ; 将ax中数据给bx地址的内存add bx,2 ; bx中内存地址加2loop .go_on_read ; 循环cx次ret; 将510个字节中剩余的空间填充为0
; $ 是当前地址
; $$ 是本节开头地址,也就是0x7c00
times 510-($-$$) db 0
db 0x55,0xaa
2.2、写一个小Loader
; os/src/boot/loader.s
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
.begin_loader:mov byte [gs:0x00],'L' ; 字符为M的ascii值mov byte [gs:0x01],0x0F ; 11100001b 即背景色为黑,字体为白,不闪烁 mov byte [gs:0x02],'O' ;mov byte [gs:0x03],0x0F ; mov byte [gs:0x04],'A' ;mov byte [gs:0x05],0x0F ;mov byte [gs:0x06],'D' ;mov byte [gs:0x07],0x0F ;mov byte [gs:0x08],'E' ;mov byte [gs:0x09],0x0F ;mov byte [gs:0x0A],'R' ;mov byte [gs:0x0B],0x0F ;; 程序在此处卡住
jmp $
这里loader的作用还是输出一些内容作为指示
2.3、执行
执行前需要把脚本更新一下
# os/run.sh
# 编译mbr
nasm -I src/boot/ -o bin/mbr.bin src/boot/mbr.s
nasm -I src/boot/ -o bin/loader.bin src/boot/loader.s # 复制mbr二进制程序到硬盘
dd if=bin/mbr.bin of=/home/lyj/bochs/bin/hd60M.img bs=512 count=1 seek=0 conv=notrunc
dd if=bin/loader.bin of=/home/lyj/bochs/bin/hd60M.img bs=512 count=2 seek=1 conv=notrunc# 启动仿真
/home/lyj/bochs/bin/bochs -f /home/lyj/bochs/bin/bochsrc.disk
执行!
结束语
第三章也结束了,这一章我们讲了如何加载一个Loader,以及如何读写硬盘,下一章,我们就要开始讲一些有关于保护模式的东西了,先将这个Loader完善一下。