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 的空间,当直接存储器不能访问缓冲块时,该区域就可以提供给软驱程序使用