1. 启动BIOS,准备实模式下的中断向量表和中断服务程序(ISR)
- 按下电源键,处理器(IA-32)进入16位实模式,从
CS:IP=0xFFFF0
处开始运行。 - BIOS程序在主板的一块ROM芯片中,该芯片无需初始化即可直接读取,被接在处理器的
0xFE000-0xFFFFF
地址处。 - BIOS程序的入口地址被设计为
0xFFFF0
,因此上电过后处理器实际上运行的是BIOS程序。 - BIOS程序将中断向量表放在内存的
0x00000
处,长度为1KB(256个中断,每个中断向量为4B),在0x00400处放BIOS数据区,长度为256B,在0x0E05B的处放ISR,大小约8KB。
2. 加载操作系统内核程序并为保护模式做准备
- BIOS触发
int 0x19
中断,在中断向量表中查询到该ISR的地址为0x0E6F2
,处理器跳转到此处执行。 - 该ISR将读取软盘第一个扇区(512B)的数据到内存
0x07C00
处,这个扇区中的程序由bootsect.s文件编译生成,这是Linux 0.11的第一个程序。 - 退出中断后,处理器跳回BIOS程序段,BIOS再将CS=0, IP=0x07C0,这样就可以跳转到
0x07C00
处执行。 - bootsect.s程序第一件事是拷贝
0x07C00
开始的内容到0x90000
处,长度为512B,即: 将bootsect.s自身移动到0x90000处。这一步目的是按照自己的需求安排内存。
boot/bootsect.s
------------------------------------------------------
mov ax,#BOOTSEG
mov ds,ax ; ds=0x07C0
mov ax,#INITSEG
mov es,ax ; es=0x9000
mov cx,#256 ; 循环次数256次
sub si,si ; si=0x0000
sub di,di ; di=0x0000
rep
movw ; 一次mov一个字,ds:si-->es:di
- 拷贝完成后,CS=0x9000,处理器跳转到CS:IP处执行。这里巧妙利用了
jmpi
指令。
boot/bootsect.s
------------------------------------------------------
jmpi go,INITSEG ; jmpi 为段间跳转指令,即跳转到go标号处,然后将cs=INITSEG
go:
mov ax,cs
- 将DS, ES, SS都设置为0x9000,SP设置为0xFF00,此时栈地址为
SS:SP=0x9FF00
。设置栈地址后就可以使用PUSH和POP指令了。
boot/bootsect.s
------------------------------------------------------
go:
mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
- 将软盘中第二个扇区开始的4个扇区(setup.s)拷贝到
0x90200
中,刚好紧挨bootsect的结尾。该拷贝使用int 0x13中断完成。
boot/bootsect.s
------------------------------------------------------
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
- 将软盘中第六个扇区开始的240个扇区拷贝到
0x10000
往后的120KB的空间中,这120KB的内容就是内核程序,也就是system。该拷贝过程与第二扇区的拷贝基本相同。 - 确认根设备号,并跳转到0x90200(setup.s)中执行。跳转仍然使用
jmpi
指令
boot/bootsect.s
------------------------------------------------------
seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev,ax
; 跳转到0x90200,即setup.s
jmpi 0,SETUPSEG
- setup.s开始,此时中断向量表和ISR还在0x0开始地址,所以可以利用BIOS提供的ISR将一些机器系统数据存放到
0x90000-0x901FD
处,共占510B,刚好将bootsect.s程序覆盖,bootsect不再使用。以下省略了大部分代码。
boot/setup.s
------------------------------------------------------
start:
; 保存光标位置
mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
; 保存外接内存大小
mov ah,#0x88
int 0x15
mov [2],ax
...
3. 开始向32位模式转变,为main函数的调用做准备
- 关中断,将system从
0x10000
移动到0x00000
地址处,此时一复制,实模式下的中断向量表和ISR就被覆盖了。下面这段代码实际上是将0x10000-0x90000的内容整体左移了0x10000
boot/setup.s
------------------------------------------------------
cli ! no interrupts allowed !
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw ; 一次传送一个字
jmp do_move
- 建立一个空的中断描述符表(IDT)放在0x0地址处,然后建立一个临时的全局描述符表(GDT)。这两个表可以由用户随意放在内存的合适位置,放置好后使用lidt和lgdt指令将其地址传给IDTR和GDTR寄存器,以后硬件就能够根据IDTR和GDTR的值来找到IDT和GDT。这两个寄存器都是48位的寄存,其中高32位表示表的基地址,低16位表示限长。
boot/setup.s
-------------------------------------------------------------------
end_move:
mov ax,#SETUPSEG ; right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ; load idt with 0,0
lgdt gdt_48 ; load gdt with whatever appropriate
...
gdt:
.word 0,0,0,0 ; dummy
.word 0x07FF ; 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ; base address=0
.word 0x9A00 ; code read/exec
.word 0x00C0 ; granularity=4096, 386
.word 0x07FF ; 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ; base address=0
.word 0x9200 ; data read/write
.word 0x00C0 ; granularity=4096, 386
idt_48:
.word 0 ; idt limit=0
.word 0,0 ; idt base=0L
gdt_48:
.word 0x800 ; gdt limit=2048, 256 GDT entries
.word 512+gdt,0x9 ; gdt base = 0X9xxxx
; 这里 0x9是段地址,512+gdt是偏移量,加起来就是0x90200+gdt刚好是前面gdt标号的地址
- 打开A20,实现32位寻址。A20是通过键盘控制器8042打开的,打开过后可以访问0x100000以上的地址,而不会回滚。
boot/setup.s
-------------------------------------------------------------------
; A20打开后,可以访问0x100000-0x10FFEF的地址,不会回滚
call empty_8042
mov al,#0xD1 ; command write
out #0x64,al ; 访问键盘控制器,打开A20
call empty_8042
mov al,#0xDF ; A20 on
out #0x60,al
call empty_8042
- 对中断控制器8259进行重新编程,目的是空出int 0x00~0x1F做内部使用(P23)。
- 配置CR0,打开32位保护模式,并跳转到内核文件的head.s执行。
boot/setup.s
-------------------------------------------------------------------
! 切换到保护模式,保护模式下,CS不是代码段基地址,而是代码段选择符
mov ax,#0x0001 ; protected mode (PE) bit
lmsw ax ; This is it!
jmpi 0,8 ; jmp offset 0 of segment 8 (cs)
这里执行jmpi后,IP=0,CS=8,由于在保护模式下CS的值表示段选择符,CS=8=0b1000,其中低两位00表示内核特权级,第三位0表示GDT,最高位1表示第1项。
查看GDT的第1项:
0x 00C0 9A00 0000 07FF
0b 0000000011000000 1001101000000000 0000000000000000 0000011111111111
表示:段基址0x00000000,内核特权级,代码段,段限长0x7FF*4KB=8MB(详细解释在P28)
所以这里实际上是跳转到了0x0处执行,也就是head.s
- 设置页基地址
pg_dir
,然后设置DS、ES、FS和GS =0x10,表示保护模式下的段选择符
boot/head.s
-------------------------------------------------------------------
.text
.globl idt,gdt,pg_dir,tmp_floppy_area
pg_dir: # 该标号表示分页机制完成后内核的起始地址
.globl startup_32
startup_32:
movl $0x10,%eax
mov %ax,%ds # 将ds, es, fs, gs设置为保护模式下的段选择符
mov %ax,%es # 0x10=0b10000,和cs=8意义类似
mov %ax,%fs # 最后两位00表示内核特权级,第三位0表示GDT
mov %ax,%gs # 最高两位10表示第2项
...
此处的0x10和之前的CS=8类似,0x10=0b10000,最后两位表示内核特权级,第三位0表示GDT,最高两位10表示表的第二项。
查看GDT第二项:0x00C0 9200 0000 07FF
表示段基址0x0,内核级,数据段,段限长8MB
- 设置SS为段选择符0x10,同时设置ESP=0x1E25C。
boot/head.s
-------------------------------------------------------------------
lss stack_start,%esp
lss指令是80386以后才有的指令,目的是同时设置SS和ESP。这里stack_start为48位的结构体,其中低32位为栈地址,高16位为0x10,以上指令将esp=栈地址(0x1E25C),然后将ss=0x10。0x10的分析方法和上面类似,是同一个段选择符。
- 循环填充IDT,长度为256,填充内容是:
ignore_int的高16位 | 0x8E00 | 0x0008 | ignore_int的低16位
(中断描述符的详细构造在P31),然后将IDTR设置为当前IDT地址。
boot/head.s
-------------------------------------------------------------------
call setup_idt
...
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
/* eax= 0x0008<<16 | ignore_int的低16位
* edx= ignore_int的高16位<<16 | 0x8E00
* 中断描述符见P31,表示中断服务程序偏移地址为ignore_int,段选择符
* 为0x8,P(段存在标志)=1,DPL(特权等级)=00,TYPE(段描述符类型)=0111
*/
lea idt,%edi
mov $256,%ecx
/* 将256个中断描述符全部设置为以上内容,即全部指向ignore_int */
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi) # 表示[edi+4]
addl $8,%edi
dec %ecx
jne rp_sidt
lidt idt_descr # 设置IDTR寄存器(48位)的值
ret
...
ignore_int:
pushl %eax
pushl %ecx
pushl %edx
...
.align 2
.word 0
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long idt
idt: .fill 256,8,0 # idt is uninitialized
# 在内存中创建256个8字节长区域,初始化为0
- 重新构建GDT,将第一项和第二项的限长该为0xFFF,也就是16MB,然后给后面的250多项真正的分配了内存空间。
boot/head.s
-------------------------------------------------------------------
call setup_gdt
...
setup_gdt:
lgdt gdt_descr
ret
...
.align 2
.word 0
gdt_descr:
.word 256*8-1 ; so does gdt (not that that's any
.long gdt ; magic number, but it works for me :^)
...
gdt:
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */
.quad 0x00c0920000000fff /* 16Mb */
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
- 重新设置DS、ES、FS、GS和SS = 0x10。
boot/head.s
-------------------------------------------------------------------
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
- 通过向0x000000写一个数,然后和0x10000比较是否相等来判断A20是否打开。
boot/head.s
-------------------------------------------------------------------
xorl %eax,%eax
1: incl %eax ; check that A20 really IS enabled
movl %eax,0x000000 ; loop forever if it isn't
cmpl %eax,0x100000
je 1b
- 检测x87协处理器是否存在。
boot/head.s
-------------------------------------------------------------------
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
orl $2,%eax # set MP
movl %eax,%cr0
call check_x87
jmp after_page_tables
- 将L6标号和main函数压栈,然后跳转到setup_paging函数。此时栈顶为main函数地址,目的是head程序执行完返回后,以立即执行main函数,main函数不应该跳出,若跳出则接着执行L6
boot/head.s
-------------------------------------------------------------------
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $main
jmp setup_paging
L6:
jmp L6
- 开始建立页目录和页表。将0x0开始的5KB内存清零,
0x0000~0x1000
为页目录,0x1000~0x1FFF
为第1页,0x2000~0x2FFF
为第2页,0x3000~0x3FFF
为第3页,0x4000~0x4FFF
为第4页。将页目录的前4项分别指向4个页表,然后在0x4FFB
中存储0xFFF007
,在0x4FF7
中存储0xFFE007
……直到将所有的页表填满,此时页表建立完毕,将16MB的内存分为了4K页,每页4KB,使用页目录可查询到各页表,使用各页表可以查询到各页。
boot/head.s
-------------------------------------------------------------------
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000
...
.align 2
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl /* 清空0x0开始的5K*4B的空间 */
/* 将后4个页表地址填写到页目录中 */
movl $pg0+7,pg_dir /* set present bit/user r/w */
movl $pg1+7,pg_dir+4 /* --------- " " --------- */
movl $pg2+7,pg_dir+8 /* --------- " " --------- */
movl $pg3+7,pg_dir+12 /* --------- " " --------- */
/*
* 16MB内存被分为4K页,每页为4KB
* 这里填充pg3的最后一个页表项,指向16MB内存的最后一页(0xfff000)
* 为了按4字节对齐,这里只取0xfff007的高12位,最低位的7(0x111)
* 表示用户、读写、存在p,若是0(0x000)表示内核,只读,不存在p
*/
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
/* 开始循环填充,注意这里的pg0存的是0x0,pg0+4存的是0x1000 */
subl $0x1000,%eax
jge 1b
这里页表中存的各页的起始地址,因为要4KB对齐,所以看其中的高12位,低12位表示权限,如这里的0xFFF007
,高12位是最后一页的地址,即0xFFF000
,低12位的0x7=0b111
表示用户、读写、存在p。建立页表的程序被分配在了0x5000以后,因此不会被页表覆盖。
- 将CR3指向页目录表,打开CR0的分页机制开关。
boot/head.s
-------------------------------------------------------------------
/* 将CR3置零,其中的高20位表示页目录基地址,即pg_dir=0x0为也目录的基地址 */
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
/* 将CR0的最高位置1,打开分页机制 */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
- 跳转到main函数执行
boot/head.s
-------------------------------------------------------------------
ret
由于前面已经将main函数的地址压到了栈中,因此只需ret
即可将main地址弹出给EIP,处理器跳到main函数执行。此时仍处于关中断的状态。