08|重新设置 idt 和 gdt
你好,我是闪客。
上一讲咱们说到,CPU 进入了 32 位保护模式,最后是跳到了内存地址 0 处开始执行代码。
0 位置处存储着操作系统全部核心代码,是由 head.s 和 main.c 以及后面的无数源代码文件编译并链接在一起而成的 system 模块,如图所示:

今天我们一起看看,正式进入 C 语言写的 main.c 之前,head.s究竟写了点啥?
重新设置栈
head.s 文件很短,我们一点点看。
_pg_dir:
_startup_32:
mov eax,0x10
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
lss esp,_stack_start
你有没有注意到开头有个标号 _pg_dir?这个表示页目录,之后在设置分页机制时,页目录会存放在这里,也会覆盖这里的代码,你这里有个印象就行。
再往下连续五个 mov 操作,分别给 ds、es、fs、gs 这几个段寄存器赋值为 0x10,根据段描述符结构解析,表示这几个段寄存器的值为指向全局描述符表中的第二个段描述符,也就是数据段描述符。
最后 lss 指令相当于让 ss:esp 这个栈顶指针,指向了 _stack_start 这个标号的位置。还记得图里的那个原来的栈顶指针在哪里吧?往上翻一下,0x9FF00,现在要变咯。
这个 stack_start 标号定义在了很久之后才会讲到的 sched.c 里,我们这里拿出来分析一波。
long user_stack[4096 >> 2];
struct {
long *a;
short b;
}
stack_start = {&user_stack[4096 >> 2], 0x10};
这是什么意思呢?
首先,stack_start 结构中的高 16 位是 0x10,将会赋值给 ss 栈段寄存器,低 32 位是 user_stack 这个数组的末端地址,将其赋值给 esp 寄存器。
赋值给 ss 的 0x10 仍然按照保护模式下的段选择子去解读,其指向的是全局描述符表中的第二个段描述符(数据段描述符),段基址是 0。
赋值给 esp 寄存器的就是 user_stack 数组的末端地址,那最终的栈顶地址,也指向了这里,后面的压栈操作,就是往这个新的栈顶地址处压咯。
设置 idt 和 gdt
我们继续往下看:
call setup_idt ;设置中断描述符表
call setup_gdt ;设置全局描述符表
mov eax,10h
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
lss esp,_stack_start
这里先设置了 idt 和 gdt,然后又重新执行了一遍刚刚执行过的代码。
为什么要重新设置这些段寄存器呢?因为上面修改了 gdt,所以要重新设置一遍,做个刷新,这样修改才能生效。
那我们接下来就把目光放到设置 idt 上。中断描述符表 idt 我们之前没设置过,所以这里设置具体的值,理所应当。
setup_idt:
lea edx,ignore_int
mov eax,00080000h
mov ax,dx
mov dx,8E00h
lea edi,_idt
mov ecx,256
rp_sidt:
mov [edi],eax
mov [edi+4],edx
add edi,8
dec ecx
jne rp_sidt
lidt fword ptr idt_descr
ret
idt_descr:
dw 256*8-1
dd _idt
_idt:
DQ 256 dup(0)
什么?仔细看完还没全懂,别担心,只要你能知道它的最终效果就行。
中断描述符表 idt 里面存储着一个个中断描述符,每一个中断号就对应着一个中断描述符,而中断描述符里面主要存储着中断程序的地址。这样一个中断号过来后,CPU 就会自动寻找相应的中断程序,然后去执行它。
那这段程序的作用就是,设置了 256 个中断描述符,并且让每一个中断描述符中的中断程序例程都指向一个 ignore_int 的函数地址,这个是个默认的中断处理程序,之后会逐渐被各个具体的中断程序所覆盖。举个例子,比如之后键盘模块会将自己的键盘中断处理程序,覆盖过去。
现在还没发生这种覆盖行为,所以任何中断对应的中断处理程序,都会指向这个默认的函数 ignore_int,也就是说现在这个阶段你按键盘还不好使。
看看新的全局描述表
设置中断描述符表 setup_idt 说完了,那接下来 setup_gdt 就同理了。我们就直接看设置好的新的全局描述符表长什么样吧?
_gdt:
DQ 0000000000000000h ;/* NULL descriptor */
DQ 00c09a0000000fffh ;/* 16Mb */
DQ 00c0920000000fffh ;/* 16Mb */
DQ 0000000000000000h ;/* TEMPORARY - don't use */
DQ 252 dup(0)
其实和我们原先设置好的 gdt 一模一样。
也是有代码段描述符和数据段描述符,然后第四项系统段描述符并没有用到,不用管。最后还留了 252 项的空间,这些空间后面会用来放置任务状态段描述符 TSS 和局部描述符表 LDT,这些都是为多任务准备的,后面再说。
那么目前的 gdt 表项就变成了这样:

为什么原来已经设置过一遍了,这里又要重新设置一遍,你可千万别以为有什么复杂的原因,就是因为原来设置的 gdt 是在 setup 程序中,之后这个地方要被缓冲区覆盖掉,所以这里重新设置在 head 程序中。这样,这块内存区域之后就不会被其他程序用到并且覆盖了。
好了,本文就是两个描述符表位置的变化以及重新设置,再后面一行代码就是又一个令人兴奋的功能了!
jmp after_page_tables
...
after_page_tables:
push 0
push 0
push 0
push L6
push _main
jmp setup_paging
L6:
jmp L6
这个功能就是开启分页机制,并且跳转到 main 函数。跳转到 main 函数,标志我们正式告别让人头大的汇编,进入C语言写的操作系统核心代码,是不是还挺期待的?
总结时刻
这一讲的内容十分简单,就是把 idt 和 gdt 这两个表重新设置了一遍,你可以看下面这张图,一目了然。

如果前面讲的内容你暂时还无法完全理解,那就记住最后这张图就好了。
好,我是闪客。欲知后事如何,且听下回分解。