我们假定本书所用的计算机是基于 IA—32 系列 CPU, 安装了标准单色显示器、 标准键 盘、一个软驱、一块硬盘、16 MB 内存,在内存中开辟了 2 MB 内存作为虚拟盘,并在 BIOS 中设置软驱为启动设备。后续所有的讲解都以此为基础。
目前处于实模式下,内存地址为0x00000~0xFFFFF,共1MB,20位地址线,BIOS所占地址为0xFE000~0xFFFFF,在最末尾。开机加电,CS为0xF000,IP为0xFFF0,所以程序从0xFFFF0处开始执行。BIOS程序在0x00000~0x003FF放置了中断向量表,共4*256=1023个字节,所以共有256个中断向量。在0x00400~0x004FF放置了BIOS数据区,在0x0E05B~0x0FFFE处放置了中断服务程序。如下图所示:
计算机硬件体系结构的设计与BIOS联手操作,会让CPU接收一个int 0x19的中断,CPU指向0x0E6F2,开始执行中断处理程序,将软驱0 号磁头对应盘面的 0 磁道 1 扇区的内容复制至内存0x07C00 ~0x07E00处。
程序从0x07C00处开始执行bootsect.s
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax !起始段寄存器
mov ax,#INITSEG
mov es,ax !目的段寄存器
mov cx,#256 !移动的次数
sub si,si !起始段偏移
sub di,di !目的段偏移
rep
movw
BOOTSEG为0x07C0,INITSEG为0x9000,将ds:si内存地址的内容,移动到es:di内存地址处,一共移动256次,一次是一个字,最后一共移了256*2=512字节。把0x07C00 ~0x07E00移动到0x90000~0x90200。
跳转,并重新设置段寄存器
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
当时 CS 的值为 0x07C0,执行完 这个跳 转后,CS 值变为 0x9000(INITSEG),IP 的值为从 0x9000(INITSEG)到 go: mov ax, cs 这一行对应指令的偏 移。所以程序就转到执行 0x90000 这边的代码了。
上述代码的作用是通过 ax,用 CS 的值 0x9000 来把数据段寄存器(DS)、附加段寄存器 、栈基址寄存器(SS)设置成与代码段寄存器(CS)相同的位置,并将栈顶指针 SP 指 (ES) 向偏移地址为 0xFF00 处。
读入第二扇区开始的4个扇区
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
ok_load_setup:
上面的int 0x19中断是机器自动产生的,现在是我们自己手动配置int 0x13参数,将软盘第二扇区开始的 4 个扇区,即 setup.s 对应的程序加载至内存的0x90200~0x90A00处。
然后又调用int 0x13终端,软盘第六扇区开始的 约 240 个扇区(共120KB)的 system 模块加载至内存的 0x10000~0x2E000中。
获取根设备号
seg cs
mov ax,root_dev !cs:root_dev地址内容付给ax
cmp ax,#0 !此时并不为0,为0x306,所以跳转到root_defined
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
.org 508 !偏移是508
root_dev:
.word ROOT_DEV
boot_flag:
.word 0xAA55 !引导盘最后两个字节必须是0xAA55
跳转到setup.s执行
jmpi 0,SETUPSEG
setup.s首先做的第一件事情就是利用 BIOS 提供的中断服务程序从设备 上提取内核运行所需的机器系统数据,这些机器系统数据被加载到内存的0x90000 ~ 0x901FC (覆盖了0x90000~0x90200)位置。
关中断
cli;
如果没有 cli,又恰好发生中断,如用户不 小心碰了一下键盘,中断就要切进来,就不得不面对实模式的中断机制已经废除、保护模式 的中断机制尚未完成的尴尬局面,结果就是系统崩溃。
移动system模块
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000 !末尾是0x80000~0x8FFFF
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000 !每次共移动32764*2=64KB
rep
movsw !移动一个字
jmp do_move
将ds:si内存地址的内容,移动到es:di内存地址处,一共移动32764*8次,一次是一个字,最后一共移了32764*8*2=512KB。把
0x10000 ~0x8FFFF(共512KB)
移动到
0x00000~0x7FFFF(共512KB)
。在本例中实际上就是0x10000~0x2E000移动到0x00000~0x1E000处。
1)废除 BIOS 的中断向量表,等同于废除了 BIOS 提供的实模式下的中断服务程序。
2)收回刚刚结束使用寿命的程序所占内存空间。
3)让内核代码占据内存物理地址最开始的、天然的、有利的位置。
对中断描 述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置
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位于0x90200~0x90A00的末尾
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 ! 每个是8个字节,一共256个,所以是2048字节,一共可以有256个gdt
.word 512+gdt,0x9 ! 偏移为0x9<<16+0x200+gdt
00 C0 92 00 00 00 07 FF
Segment Limit 0x07FF 2048*4096=8Mb(因为 界限粒度为4K 字节)Segment Base(23-0) 0x000000
Arributes 0xC09A
G=1 表示界限粒度为4K 字节
D=1 表示是32位
P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中
DPL为00,表示内核态
TYPE为A ,表示代码段,执行/读
Segment Base(31-24) 0x00
此部分请参考http://blog.csdn.net/jltxgcy/article/details/865610
所以0x8选择子对应代码段描述符,0x10选择子对应数据段描述符,选择子的格式请参考http://blog.csdn.net/jltxgcy/article/details/8656101
打开A20地址线
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
设置中断,参考http://blog.csdn.net/jltxgcy/article/details/8661959
mov al,#0x11 !往端口20h(主片)写入ICW1
out #0x20,al
.word 0x00eb,0x00eb
out #0xA0,al !往端口A0h(从片)写入ICW1
.word 0x00eb,0x00eb
mov al,#0x20 !往端口21h(主片)写入ICW2
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 !往端口A1h(从片)写入ICW2
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 !往端口21h(主片)写入ICW3
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 !往端口A1h(从片)写入ICW3
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 !往端口21h(主片)写入ICW4
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al !往端口A1h(从片)写入ICW4
.word 0x00eb,0x00eb
mov al,#0xFF !遮蔽主8259所有中断,写入OCW1
out #0x21,al
.word 0x00eb,0x00eb !遮蔽从8259所有终端,写入OCW1
out #0xA1,al
参考此图片:
将 CR0 寄存器第 0 位 (PE)置 1,即设定处理器工作方式为保护模式
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
开始执行head.s
设置数据段高速缓冲寄存器
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
段选择子是0x10,为什么是0x10,请参考段选择子的结构。head.s自己吞噬自己,最后形成的效果图,如下:
所以head占的空间是4KB*4+5KB+184B=25KB+184B
重新设置gdt,idt
call setup_idt
call setup_gdt
...
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt,%edi #idt表的偏移
mov $256,%ecx #总共256个门描述符
rp_sidt:
movl %eax,(%edi) #ds:edi内存中的数据
movl %edx,4(%edi)
addl $8,%edi #每个门描述符是8个字节
dec %ecx
jne rp_sidt #没到256,就继续
lidt idt_descr #设置ldtr
ret
...
setup_gdt:
lgdt gdt_descr #设置gdtr
ret
...
.align 2
.word 0
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long idt
.align 2
.word 0
gdt_descr:
.word 256*8-1 # 每个描述符是8个字节,这表示里面可以存256个描述符
.long gdt # magic number, but it works for me :^)
.align 8
idt: .fill 256,8,0 # 放置在0x054b8~0x5cb8
gdt: .quad 0x0000000000000000 /* 放置在最后0x5cb8~0x64b8
.quad 0x00c09a0000000fff /* 16Mb */ 原来是界限是0xFF,现在是0xFFF,所以为4096*4KB=16MB,为代码段描述符
.quad 0x00c0920000000fff /* 16Mb */ 为数据段描述符
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
设置idt稍微复杂些,请看:
eax中0008放入Selector,5428放入Offset(15-0),edx中0000放入Offset(31-16),Attributes放入8E00,E代表386中断门。
通过选择器重新设置描述符高速缓存寄存器
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
特权级为00,从gdt中取描述符。cs已经设置。
检查A20是否打开
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
A20 如果没打开,则计算机处于 20 位的寻址模式,超过 0xFFFFF 寻址必然“回滚” 。一个特例是 0x100000 会回滚到 0x000000, 也就是说, 地址 0x100000 处存储的值必然和地址0x000000 处存储的值完全相同,如果相同就循环。
检测数据协处理器存在,并设置成保护模式工作状态
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
after_page_tables代码在软盘缓冲区之后,不用担心覆盖,覆盖的是全面已经没有用的16KB(0x0000~0x5000)。
ignore_int也在软盘缓冲区之后,所以不用担心覆盖。
把main函数的EIP压入堆栈,执行setup_paging
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 #ret返回后执行main方法,和call类似
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
.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
/*设置页目录表,分别为1007,2007,3007,4007*/
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 /* --------- " " --------- */
/*设置页表*/
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
CR3 指向页目录表,启动分页机制开关 PG 标志置位
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
返回main,因为刚才压入main的EIP
ret
下一篇文章开始执行main函数了。