从硬盘加载bootloader(setup)
从硬盘加载bootloader(setup)
首先明确一下内核启动的过程:
我们要写的代码共三个,分别为boot,bootloader(setup)以及内核代码。我们将这些代码编译完成写入硬盘之后,就要加载到内存中进行执行了。
加载过程是这样的:首先有BIOS将boot(也就是MBR主引导记录)加载到内存中,但是BIOS直接加载的话,在内存当中我们只有一个扇区的大小,也就是512字节,这是不足以我们进行加载内核的准备操作的,所以我们需要更大的内存空间。在前面也给过的内存布局图可以看到内存空闲位置。
那么在里面的两个可用区域就是我们扩展的目标,只要在boot中将bootloader(setup)加载入内存的可用位置,然后跳转到该位置执行,就摆脱了512字节的限制。
在摆脱限制之后,我们就可以做一系列加载内核的准备操作并用加载bootloader(setup)类似的操作将内核加载入内存并执行了,所以bootloader(setup)也可以看做是内核的安装程序。
然后来讲解一下怎么加载bootloader(setup),也就是怎么从硬盘将该内容加载到内存当中。
我们想要将硬盘当中的内容加载到内存中,首先要知道这一部分内容在硬盘的什么位置,这种情况下应当有一种寻址方式,即按照标号的形式标记硬盘当中的每一个扇区,使得我们有方法指定任意的扇区并加载内容。
在磁盘当中寻址有三种方法,即CHS,lba28以及lba48。(讲述不清楚的部分可以去B站或网络自行了解)
CHS即:Cylinder:圆柱,Head:磁头,Sector:扇区
因为在早期的存储方式,即软盘是圆片形状的,在上下两面都可以写数据,因此划分为圆柱标记上下两面;磁头记录的是磁道,由外而内在圆面上划分79个同心圆,从而将软盘划分为80个圆环,称为磁道;然后过圆心画直线将圆等分为18份,那么每个圆环(磁道)也被分为18份,磁道上的每一份称为一个扇区,每个扇区存储的容量一致为512字节。
所以一张盘扇区共有2 * 80 * 18 = 2880个。(后面容量扩展了是怎么实现的我也不知道,可以自己看课了解一下)
如果我们将这2880个扇区以数组的方式理解并编号,即分为1-2880扇区,那么就是lba的编码方式。
lba28是使用28位二进制数表示逻辑块地址,每个对应一个扇区,共允许寻址228个扇区;lba48是使用48位二进制数表示逻辑块地址,允许寻址2⁴⁸个逻辑块。
这里只讲解也只使用lba-28。
但是现在还面临一个问题,早期寄存器都是8位的,也就是说对于28位长的指令,需要四个寄存器才能存储并使用(其实就是硬件接口,在汇编中可以视为寄存器)。
那么使用那些寄存器呢?如图:
假设我们要寻址逻辑块地址0x1A2B3C4,其二进制为0001 1010 0010 1011 0011 1100 0100
分配到寄存器这样:
LBA Low(低八位):1100 0100
LBA Mid(中八位): 1011 0011
LBA High(高八位):1010 0010
最高的四位使用Device/Head寄存器中的0-3bits,即低四位存储;这样的话这个寄存器还剩下四位未使用,用于控制信息,Bits5,7通常保留或用于其他控制目的,即默认设置为1,Bit6表示使用LBA模式(区分CHS),所以也是1,Bit4用于选择主从设备,我们首先肯定是使用主设备的,所以是0,所以Device/Head寄存器中的高四位常为1110。
前面所述仅为寻址格式的部分,但是在实际中读写硬盘还有很多的内容,包括操作几个扇区,有没有准备好,有没有错误等。
前面寄存器的图片中,我们应当知道的是0x1F0是数据传输的位置,因为是16位的寄存器,所以每次可以传输两个字节,读取一个扇区需要循环读取256次。
识别我们要进行的操作是通过0x1F7实现的,
在读的过程中,0x1F7寄存器用于获取硬盘的状态。
写的过程中,根据我们向0x1F7写入的数据识别命令。
命令码:
- 0x20 读
- 0x30 写
- 0x1C 获取硬盘信息
接下来从代码角度分析具体使用方法:
;boot.asm;位于0柱面0磁道1扇区
[ORG 0x7c00][SECTION .data]
BOOT_MAN_ADDR equ 0x500 ;BOOT_MAN_ADDR是变量名,equ是伪指令表示=,0x500是我们要加载setup的地址[SECTION .text]
[BITS 16]
global _start
_start:;设置屏幕为文本模式,清除屏幕mov ax,3int 0x10;将setup读入内存mov edi,BOOT_MAN_ADDR ;用edi存放地址,读到哪里mov ecx,1 ;从那个扇区开始读mov bl,2 ;读几个扇区call read_hd;跳转到setup位置mov si,jmp_to_setupcall printjmp BOOT_MAN_ADDR;主函数逻辑到这里就结束了,后面是实现用到的函数read_hd:; 从磁盘读取的函数; 0x1f2 指定读取或写入的扇区数mov dx,0x1f2mov al,blout dx,al;0x1f3 写入lba28的低八位(这里看不懂的话自己了解下ECX,CX,CL与CH的关系inc dx ;inc是+1指令mov al,clout dx,al;0x1f4,中八位inc dxmov al,chout dx,al;0x1f5 inc dxshr ecx,16 ;右移以操作八位mov al,clout dx,al;0x1f6,低四位是地址,高四位为1110inc dxshr ecx,8 ;将操作数移到cl上,用ch是可行的,我也不理解为什么要弄到cl上,问就是cl更方便,但从代码角度讲确实麻烦了,可能是硬件问题之类的and cl,0b1111 ;高位自动补0mov al,ob1110_0000or al,clout dx,al;0xaf7,状态或命令端口inc dxmov al,0x20 ;读指令out dx,al;设置loop次数,读多少个扇区就loop多少次mov cl,bl
.start_read:push cx ;保存loop次数,防止被修改call .wait_hd_preparecall read_hd_datapop cx ;恢复loop次数loop .start_read.return: ret; 一直等待到硬盘状态为:不繁忙且数据已经准备好
; 也就是第7位是0,第3位是1,第0位为0
.wait_hd_prepare:mov dx,0x1f7.check:in al,dxand al,0b1000_1000cmp al,0b0000_1000jnz .checkret; 读硬盘256次
read_hd_data:mov da,0x1f0mov cx,256.read_word:in ax,dxmov [edi],axadd edi,2loop .read_wordret; print 函数
print:mov ah, 0x0emov bh, 0mov bl, 0x01
.loop:mov al, [si]cmp al, 0jz .doneint 0x10inc sijmp .loop
.done:retjmp_to_setup:db "jump_to_setup...",10,13,0times 510- ($-$$) db 0
db 0x55,0xaa