我们假定本书所用的计算机是基于 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 1bA20 如果没打开,则计算机处于 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_tablesafter_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函数了。