本文解析head.s程序,主要分为 部分:
- 1 设置中断描述表(IDT)
- 2 设置全局描述符表(GDT)
- 3 检测A20总线打开
- 4 检测协处理器
- 5 开启分页机制
- 6 调用主函数
- 7 地址再探
在执行main函数之前,先要执行三个由汇编代码生成的程序,即bootsect、setup和head。之后,才执行由main函数开始的用C语言编写的操作系统内核程序。前面我们讲过,第一步,加载bootsect到0x07C00,然后复制到0x90000;第二步,加载setup到0x90200。值得注意的是,这两段程序是分别加载、分别执行的。
head程序与它们的加载方式有所不同。大致的过程是,先将head.s汇编成目标代码,将用C语言编写的内核程序编译成目标代码,然后链接成system模块。也就是说,system模块里面既有内核程序,又有head程序。两者是紧挨着的。要点是,head程序在前,内核程序在后,所以head程序名字为“head”。head程序在内存中占有25 KB + 184 B的空间。前面讲解过,system模块加载到内存后,setup将system模块复制到0x00000位置,由于head程序在system的前面,所以实际上,head程序就在0x00000这个位置。head程序、以main函数开始的内核程序在system模块中的布局示意图如图
head程序除了做一些调用main的准备工作之外,还做了一件对内核程序在内存中的布局及内核程序的正常运行有重大意义的事,就是用程序自身的代码在程序自身所在的内存空间创建了内核分页机制,即在0x000000的位置创建了页目录表、页表、缓冲区、GDT、IDT,并将head程序已经执行过的代码所占内存空间覆盖。这意味着head程序自己将自己废弃,main函数即将开始执行。
设置中断描述表(IDT)
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:
_pg_dir: 标识内核分页机制完成后的内核起始位置,也就是物理内存的起始位置0x000000。head程序马上就要在此处建立页目录表,为分页机制做准备。这一点非常重要,是内核能够掌控用户进程的基础之一。
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
给EAX寄存器赋值,然后DS,ES,FS,GS寄存器的值均变为0x10(二进制下的0001 0000),最后三位与前面讲解的一样,其中最后两位(00)表示内核特权级,从后数第3位(0)表示选择GDT,第4、5两位(10)是GDT的2项,也就是第3项。也就是说,4个寄存器用的是同一个全局描述符,它们的段基址、段限长、特权级都是相同的。特别要注意的是,影响段限长的关键字段的值是0x7FF,段限长就是8 MB。
lss _stack_start,%esp
最后 lss 指令相当于让 ss:esp 这个栈顶指针指向了 _stack_start 这个标号的位置。还记得图里的那个原来的栈顶指针在哪里吧?往上翻一下,0x9FF00,现在要变咯。
这个 stack_start 标号定义在了很久之后才会讲到的 sched.c 里,我们这里拿出来分析一波。
PAGE_SIZE = 4096
long user_stack[PAGE_SIZE >> 2];
struct
{
long *a;
short b;
}
stack_start = {&user_stack[4096 >> 2], 0x10};
首先,stack_start 结构中的高位 8 字节是 0x10,将会赋值给 ss 栈段寄存器,低位 16 字节是 user_stack 这个数组的最后一个元素的地址值,将其赋值给 esp 寄存器。
赋值给 ss 的 0x10(0001 0000)仍然按照保护模式下的段选择子去解读,其指向的是全局描述符表中的第二个段描述符(数据段描述符),段基址是 0。
赋值给 esp 寄存器的就是 user_stack 数组的最后一个元素的内存地址值,那最终的栈顶地址,也指向了这里(user_stack + 0),后面的压栈操作,就是往这个新的栈顶地址处压咯。
注意,栈顶的增长方向是从高地址向低地址的。注意栈段基址和ESP在图中的位置。
!================== step 1, set idt, gdt ====================
call setup_idt
设置中断描述符表,设置全局描述符表
/*
* setup_idt
*
* sets up a idt with 256 entries pointing to
* ignore_int, interrupt gates. It then loads
* idt. Everything that wants to install itself
* in the idt-table may do so themselves. Interrupts
* are enabled elsewhere, when we can be relatively
* sure everything is ok. This routine will be over-
* written by the page tables.
*/
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
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
lidt idt_descr
ret
...
_idt: .fill 256,8,0 # idt is uninitialized
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long _idt
.align 2