操作系统主引导扇区
前言
本文参考了以下相关文章以及书籍
《Linux内核完全注释》
引导启动程序(boot)
你需要具备有以下的知识:
- 熟悉AT&T和Intel基本语法
- bochs基本调试指令
总体流程
Linux操作系统启动部分的主要执行流程:一般来说,在接通电源时,主板就已经部分通电了。其处于 Standby 模式,检测是否需要开机。当用户按下电源开关(开机键)后,电源开始进入正常工作模式,它会给主板上电,按照时序供应 5V 和 12V 电源,然后输出 Power_OK 信号,通知主板可以正式工作。最后,主板会向 CPU 的 reset 引脚发送信号,于是CPU将清除所有寄存器中的数据并加载为预设值,根据 Intel SDM vol3 Table 9-1,寄存器值为:
EIP 0000FFF0H
CS Selector F000H
CS base FFFF0000H
CS Limit FFFFH
此时CPU自动进入实模式,并从地址0xFFFF0处开始自动执行代码,这个地址就是ROM-BIOS中的地址。PC机的BIOS将会执行系统的某些硬件检测和诊断功能,并在物理地址0处开始设置和初始化中断向量。此后,它将第一个扇区读入内存绝对地址0x7c00处,并跳转到这个地方开始引导启动机器运行。
当bootsect.s被执行的时候,它会把自己移动到内存0x90000处并把启动盘里后2kB读入内存0x90200处。至于内核其他部分,则会读入0x10000处
Q:此时 CS 为 0xf000 , IP 为 0xfff0 。根据 real mode 下地址的计算方法,此时 CS base 应该等于 0xf000 << 4 = 0xf0000 。但为何 Intel SDM 中说它是 0xffff0000 呢?
A:根据 Intel SDM vol3 9.1.4 First Instruction Executed :
The address FFFFFFF0H is beyond the 1-MByte addressable range of
the processor while in real-address mode. The processor is initialized to
this starting address as follows. The CS register has two parts: the visible
segment selector part and the hidden base address part. In real-address
mode, the base address is normally formed by shifting the 16-bit segment
selector value 4 bits to the left to produce a 20-bit base address. However,
during a hardware reset, the segment selector in the CS register is loaded
with F000H and the base address is loaded with FFFF0000H. The starting
address is thus formed by adding the base address to the value in the EIP
register (that is, FFFF0000 + FFF0H = FFFFFFF0H).
即根据上文的意思,寄存器有一个隐藏的描述符高速缓存器(就算不在32位保护模式,CPU也可以使用它),缓存器当中存储了一个32位的段基地址0xFFFF0000,在 reset 的时候被设置成的。且在16位实模式下仅能取出段基地址的前20位,因此我们需要使用的是这个隐藏的是段基地址前20位+地址寄存器IP去访问内存,而不需要计算得到的 0xF0000 再加上寄存器IP的值,这样提高了访问内存(包括ROM和其他外硬件的映射)的效率。
关于更多描述符高速缓存器的知识可以查阅Intel官方文档或者国内大佬李忠的个人网站
第一条指令
查看这个地址,一般都是跳转指令:
0xfffffff0: ljmp $0xf000,$0xe05b
但有趣的是查看 0xffff0(0xf0000 + 0xffff) 也是同样的内容:
此时CPU指针指向了,0xFFFF0000,即4G内存地址空间最后一个64k的最后16个字节处。这里是系统ROM BIOS存放的地址,并且BIOS会在这里存放一条跳转指令,跳转到BIOS代码中64kb范围内某一条指令开始执行。由于目前大厂商的BIOS容量不定,且又都在1MB/2MB左右,且存储在闪存(Flash Memory)ROM中,因此为了能够执行或访问BIOS中超过64KB范围而又远远不在0-1M空间中的其他BIOS代码或数据,BIOS会首先使用32位访问模式把数据段寄存器的范围设置为4G(而非原来的64K),这样CPU就可以在0-4G范围内执行和操作数据。
此后,BIOS执行了一系列硬件检测和初始化操作后,就会和原来PC机兼容的64KB BIOS代码和数据复制到内存低端1M末端64K处,然后跳转至这个地方并让CPU真正运行在实模式地址下
附上IBM PC compatible PC 规定的内存布局:
+------------------+ <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\
/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000
主引导扇区基本结构
- 代码 446B
- 分区表 64B (4 * 16)
- 魔数 0xaa55
主要功能
读取内核加载器,并跳转执行
硬盘读写
- CHS模式:/ Cylinder / Head / Sector
- LBA模式:/ Logical Block Address
读取硬盘
;---------------------------------
;IO端口 端口用途
;primary通道 secondary通道 读操作时 写操作时
;0x1f0 0x170 data data
;0x1f1 0x171 error features
;0x1f2 0x172 sector countsector count
;0x1f3 0x173 LBA low LBA low
;0x1f4 0x174 LBA mid LBA mid
;0x1f5 0x175 LBA high LBA high
;0x1f6 0x176 device device
;0x1f7 0x177 status command
;--------------------------------
0x1f6 device端口(8)
;0 ~ 3表示起始扇区的24 ~ 27位
;第4位: 0表示主盘,1表示从盘
;5~7位固定为1
;6位1表示采用LBA寻址
0x1f7:
;status寄存器 (读取时)
;0位: 1表示发生错误,错误信息见error寄存器
;3位: 1表示硬盘已经准备好数据,随时可以输出
;6位: 1表示设备就绪,等待命令
;7位: 1表示硬盘正忙,勿扰
command寄存器(写操作时)
- 0xEC: 识别硬盘
- 0x20: 读硬盘
- 0x30: 写硬盘
读取硬盘参考代码
;准备开始读取硬盘
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
;eax = LBA逻辑扇区号
;bx = 将数据写入的内存地址
;cx = 读取扇区数
;-------------------------------------------------------------------------------
;---------------------------------
;IO端口 端口用途
;primary通道 secondary通道 读操作时 写操作时
;0x1f0 0x170 data data
;0x1f1 0x171 error features
;0x1f2 0x172 sector countsector count
;0x1f3 0x173 LBA low LBA low
;0x1f4 0x174 LBA mid LBA mid
;0x1f5 0x175 LBA high LBA high
;0x1f6 0x176 device device
;0x1f7 0x177 status command
;--------------------------------
mov eax, LOADER_START_SECTOR ;loader起始扇区,28位地址,
mov bx, LOADER_BASE_ADDR ;loader写入的地址
mov cx, 1 ;待读入的扇区数
;写入从LOADER_START_SECTOR开始的一个扇区
call rd_disk ;读扇区函数
jmp $
rd_disk:
;备份eax, cx原值
mov esi, eax
mov di, cx
;读写硬盘
;第一步:设置要读取的扇区数
mov dx, 0x1f2 ;虚拟硬盘ata0 master 通过0x1f2端口访问
mov al, cl ;向端口dx输出al寄存器(也就是cl中的内容),表示读取一个扇区
out dx, al
mov eax, esi ;恢复
;第二步:将LBA地址写入端口0x1f3~0x1f4
;LBA地址7~0位(刚好一个al寄存器8位)写入端口0x1f3
mov dx, 0x1f3
out dx, al
;LBA地址15~8位写入端口0x1f4
mov cl, 8
shr eax, cl ;逻辑右移8位
mov dx, 0x1f4
out dx, al
;LBA地址23~16位写入端口0x1f5
shr eax, cl ;再移8位
mov dx, 0x1f5
out dx, al
;LBA地址24~27位写入端口0x1f6(0x1f6 device端口(8位))
;0 ~ 3表示起始扇区的24 ~ 27位
;第4位: 0表示主盘,1表示从盘
;5 7位固定为1
;6位1表示采用LBA寻址,0表示CHS模式
shr eax, cl ;此时eax低八位为初始时的31-24位
and al, 0x0f ;取27~24位
;!!!
or al, 0xe0 ;设置端口0x1f6端口高四位为e 1110
mov dx, 0x1f6
out dx, al
;第三步:向0x1f7端口写入读命令
;0x20 读扇区
;0x30 写扇区
;0xc4 读多个扇区
;0xc4 写多个扇区
mov dx, 0x1f7
mov al, 0x20
out dx, al
;第4步:检测硬盘状态
;status寄存器
;0位: 1表示发生错误,错误信息见error寄存器
;3位: 1表示硬盘已经准备好数据,随时可以输出
;6位: 1表示设备就绪,等待命令
;7位: 1表示硬盘正忙,勿扰
.not_ready:
nop
in al, dx ;读0x1f7端口
and al, 0x88 ;0x88 = 1000 1000b 仅对3 7位进行判断
cmp al, 0x08 ;判断硬盘是否可以传输数据
jnz .not_ready ;若al = 0x08 zf = 1 表示可以传输数据, 若zf = 1即jnz轮询等待
;第5步:从0x1f0端口读数据
mov ax, di ;ax设置读取扇区数
mov dx, 256
mul dx ;ax * dx = 256* 1 = 256字 = 512个字节
;mul 低16位在ax中, cx = 256,每次读取一个字
;结果高位默认在 DX 中存放,低位在 AX 中存放
mov cx, ax
mov dx, 0x1f0
.go_on_read:
in ax, dx
mov [bx], ax ;将读入的一个扇区写入到内存0x900处
add bx, 2
loop .go_on_read ;256次,一次一个字
ret ;返回