head.s 的作用是重新设置 IDT 和 GDT,检查 A20 是否已经开启,并设置页目录表及页表,最后跳转到 init/main.c。需要注意的是,该程序使用的是 AT&T 汇编语法格式
在 setup.s 处理结束后,内存分布是这样的:
head.s 的任务完成后,内存会变成这样:
看完程序再回过头来看这张图就会明白了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /* Line 14 */ .text .globl idt,gdt,pg_dir,tmp_floppy_area /* 声明外部或全局变量 */ pg_dir: /* 稍后页目录会从这里开始覆盖前半段程序 */ .globl startup_32 startup_32: movl $0x10,%eax /* 段选择子 0b0000 0000 0001 0000,指向 GDT 第三个描述符 */ mov %ax,%ds /* 描述符在 setup.s 第 213 行,将物理起始地址描述为数据段 */ mov %ax,%es /* 将这个段选择子载入各个段寄存器 */ mov %ax,%fs mov %ax,%gs lss stack_start,%esp /* 加载堆栈段指针,stack_start 符号定义在 kernel/sched.c 中 */ call setup_idt /* 重新设置 IDT */ call setup_gdt /* 重新设置 GDT */ movl $0x10,%eax /* 修改 GDT 后重新加载一遍段寄存器及堆栈段指针 ss:esp */ mov %ax,%ds mov %ax,%es mov %ax,%fs mov %ax,%gs lss stack_start,%esp
|
setup_idt,将 IDT 表 256 个中断描述符都指向 ignore_int 中断服务程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| /* Line 79 */ setup_idt: lea ignore_int,%edx /* edx = 中断服务程序 ignore_int 地址 */ movl $0x00080000,%eax /* 段选择子 0x0008 装入 eax 高 16 位 */ movw %dx,%ax /* ignore_int 地址低 16 位装入 eax 低 16 位 */ movw $0x8E00,%dx /* 此时 edx 为中断门描述符高 4 字节,eax 为低 4 字节 */
lea idt,%edi /* edi = IDT 地址,此时 IDT 未初始化 */ mov $256,%ecx /* 循环 256 次,对 IDT 每个表项都进行相同的赋值 */ rp_sidt: movl %eax,(%edi) /* 中断描述符低 4 字节在 eax 中 */ movl %edx,4(%edi) /* 高 4 字节在 edx 中 */ addl $8,%edi /* 加 8 字节指向 IDT 表下一个表项 */ dec %ecx jne rp_sidt /* 还没有对每一项都初始化,跳回 rp_sidt 继续 */ lidt idt_descr /* 现在 256 项都初始化完成,将 IDT 基址与限长载入 IDTR */ ret
/* Line 224 */ idt_descr: .word 256*8-1 /* IDT 限长 */ .long idt /* IDT 基址 */ /* Line 233 */ .align 8 idt: .fill 256,8,0 /* 预留 256 项,每项 8 bytes,用 0 填充 */
|
回顾一下中断描述符的结构:
ignore_int 中断服务程序的作用只是在屏幕上打印 “Unknown interrupt”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| /* Line 148 */ int_msg: .asciz "Unknown interrupt\n\r" .align 4 ignore_int: pushl %eax /* 保存现场 */ pushl %ecx pushl %edx push %ds /* 虽是 16 位,仍需要 4 字节存储 */ push %es push %fs movl $0x10,%eax /* 段选择子载入段寄存器,使其指向 GDT 中的数据段 */ mov %ax,%ds mov %ax,%es mov %ax,%fs pushl $int_msg /* 字符串指针压参 */ call printk /* 调用 kernel/printk.c 中的 printk 函数 */ popl %eax /* 恢复现场 */ pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret /* 返回 */
|
setup_gdt 作用是将原来的 8M 段界限改成 16M:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| /* Line 106 */ setup_gdt: lgdt gdt_descr /* GDT 的基址与限长(6 bytes)载入 GDTR */ ret
/* Line 229 */ gdt_descr: .word 256*8-1 /* 低 2 字节为限长,256 项,每项 8 字节 */ .long gdt /* 高 4 字节为 GDT 基址 */
/* Line 236 */ gdt: .quad 0x0000000000000000 /* 空描述符 */ .quad 0x00c09a0000000fff /* 代码段描述符 */ .quad 0x00c0920000000fff /* 数据段描述符 */ .quad 0x0000000000000000 /* 系统调用段描述符 - 未使用 */ .fill 252,8,0 /* 预留了 252 项的空间,用于放置新建任务的 LDT 与 TSS 描述符 */
|
.quad 指令直接定义 8 个字节的数据
接着检查 A20 是否开启,先将内存地址 0 处 4 字节赋值 1,再比较内存地址 0x100000(1M) 处的 4 字节与 1 是否相等。如果 A20 没有开启,那么访问 0x100000 就会由于地址回环,回到地址 0,其内容就是 1
1 2 3 4 5 6
| /* Line 33 */ xorl %eax,%eax 1: incl %eax /* eax = 1 */ movl %eax,0x000000 /* 给内存起始地址赋值 1 */ cmpl %eax,0x100000 /* 访问 1M 处内存,与 1 比较 */ je 1b /* 相等说明 A20 没开,往后跳到标号 1,造成死循环 */
|
这里的 1b 表示 向后(backward) 跳到标识符 1,也就是低地址的标识符 1,因为后面的代码里还有一个标识符是 1
接下来的操作是检查数学协处理器是否存在,先假设其存在并执行协处理器指令, 根据结果判断其是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| /* Line 44 */ movl %cr0,%eax /* 取 CR0 */ andl $0x80000011,%eax /* 保存 PG,PE,ET 位 */ orl $2,%eax /* 将 MP 位置位,假设存在数字协处理器 */ movl %eax,%cr0 /* 修改 CR0 */ call check_x87 /* 检测是否真的存在数字协处理器 */ jmp after_page_tables
/* Line 55 */ check_x87: fninit /* 向协处理器发出初始化命令 */ fstsw %ax /* 取协处理器状态字至 ax */ cmpb $0,%al /* 初始化后状态字应为全 0 */ je 1f /* 相等说明存在,向前(高地址)跳到标号 1 */ movl %cr0,%eax /* 否则不存在,修改 CR0 */ xorl $6,%eax /* 0b110,MP 复位,EM 置位 */ movl %eax,%cr0 /* 重新给 CR0 赋值 */ ret .align 4 /* 4 字节对齐,提高取指令、访问数据效率 */ 1: .byte 0xDB,0xE4 /* 80287 协处理器 fsetpm 的机器码,将 287 设置为保护模式,387 忽略 */ ret
|
之后为调用 main 模块做准备,同时设置页目录表和页表,打开分页机制
1 2 3 4 5 6 7 8 9 10
| /* Line 136 */ after_page_tables: pushl $0 /* 为调用 main 传参 */ pushl $0 pushl $0 pushl $L6 /* main 的返回地址 */ pushl $main /* setup_paging 的返回地址 */ jmp setup_paging /* 跳转到 setup_paging */ L6: /* init/main.c 的返回地址,但理论上它不会返回到这里 */ jmp L6 /* 加这么一条以防万一,出错时就知道发生什么问题了 */
|
setup_paging 将内存 0 ~ 0x0FFF(4K) 设置为页目录表(程序开头的 pg_dir 标识符),并填充前四个表项,同时 0x1000 ~ 0x4FFF 每 0x1000(4K) 设置为对应的四张页表,设置之前先需将这 0x5000 的区域清零
页目录表为系统所有进程公用,这四张页表为内核专用,它们一一映射线性地址起始 16 MB 的空间到物理内存上(Linus 的机器内存只有 16MB)
stosl:将 eax 中的值保存到 es:edi 指向的地址中
页目录表与页表表项结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| /* Line 198 */ .align 4 /* 4 字节对齐 */ setup_paging: movl $1024*5,%ecx /* 需将前 0x5000 的区域清零 */ xorl %eax,%eax xorl %edi,%edi cld;rep;stosl /* cld 设置 edi 向高地址自增 */ movl $pg0+7,pg_dir /* pg0 为第一个页表的地址 */ movl $pg1+7,pg_dir+4 /* +7 为设置页目录表项属性中的 U/S, R/W, P 位 */ movl $pg2+7,pg_dir+8 /* 全置 1 表示普通用户可读可写,且存在于内存中 */ movl $pg3+7,pg_dir+12 /* 到这里已经定义了四个页目录项 */ /* 接下来是设置页表项,从后往前赋值 */ movl $pg3+4092,%edi /* edi = 最后一张页表的最后一项地址 */ movl $0xfff007,%eax /* 页表最后一项所对应的物理页是 0xfff000,+7 同上 */ std /* 设置 edi 往低地址自减 */ 1: stosl /* eax 的值赋给 es:edi 指向的地址内容 */ subl $0x1000,%eax /* 设置前一个页表项指向的物理页地址 */ jge 1b /* 如果 eax 不小于 0,跳回 1 处继续赋值 */ xorl %eax,%eax /* eax 清零 */ movl %eax,%cr3 /* 赋给 CR3,设置页目录表基址为 0,准备打开分页机制 */ movl %cr0,%eax /* 获取 CR0 */ orl $0x80000000,%eax /* PG 置 1 */ movl %eax,%cr0 /* 重新给 CR0 赋值,此时已经打开分页机制 */ cld /* 将 DF 复位 */ ret /* 返回到 init/main.c 的 main 函数,还记得之前的压栈吗? */
/* Line 115 */ .org 0x1000 /* .org 设置第一张页表的绝对地址为 0x1000,以下同理 */ pg0:
.org 0x2000 pg1:
.org 0x3000 pg2:
.org 0x4000 pg3:
|
到这里 head.s 的任务就完成了 ,不过还有一个小细节需要注意:
1 2 3 4
| /* Line 127 */ .org 0x5000 tmp_floppy_area: .fill 1024,1,0
|
在地址 0x5000 处还保留了 1K 的空间,当直接存储器不能访问缓冲块时,该区域就可以提供给软驱程序使用