上节提到,现在cs:ip指向0地址,此处存储着作为操作系统核心代码的system模块,是由head.s和 main.c以及后面所有源代码文件编译链接而成。head.s(以下简称head)紧挨着main.c,我们先执行head。
重新设置内核栈
_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表示页目录,意为在设置分页机制时,页目录会存放在这里,也会覆盖这里的代码。setup.s(以下简称setup)已经设置了gdt,现在要对段描述符重新设置包括ds/es/fs/gs。都被设置为0x10(00010000),在保护模式下即段选择子为2,指向数据段描述符。根据我们之前gdt表的内容,数据段的基地址是0,于是ds/es/fs/gs的基地址也是0。
lss 指令相当于让 ss:esp 这个栈顶指针(esp是sp的32为扩展),指向了 _stack_start 这个标号的位置(对比lds mem,reg:将段描述符mem的高位存储在 reg 寄存器的高位,而段描述符的低位存储在ds寄存器的低位)。当然之前在bootsec所设置的栈顶0x9ff00位置现在变成了0:stack_start:
// include/linux/mm.h
#define PAGE_SIZE 4096
// kernel/sched.c
long user_stack [ PAGE_SIZE>>2 ] ;
struct {
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
其实从第三节得知我们已经在setup的内存(位于0x90200)设置了idt、gdt。现在则通过call setup_idt和setup_gdt重新设置位于head的内存(位于0x90000)的idt、gdt。为何重复设置?
因为位于setup的内存会在将来设计缓冲区时被覆盖,而且也不能将setup中的idt和gdt直接copy到现在的位置(在执行setup的时候copy无意义,因为如果先执行setup后移动system会覆盖掉copy的idt、gdt;如果先移动system后执行setup则会覆盖掉head内容),于是我们不得不在head重新设置它们。
设置IDT
即便是setup里面的idt也都是空的,现在由head程序正式设置。
setup_idt:
lea edx,ignore_int ;lea将ignore_int偏移地址(16bit)/而mov将第二操作数的内存内容 放入edx
mov eax,00080000h ;将段选择子0x0008置入eax高16位
mov ax,dx ;将ignore_int偏移地址置入eax低16位
mov dx,8E00h ;interrupt gate - dpl=0, present
lea edi,_idt ;lea将_idt所代表偏移地址放入edi
mov ecx,256 ;cx用来计数,256次
rp_sidt:
mov [edi],eax ;[]寄存器间接寻址,表示eax的内容赋予“以edi的内容作为地址指针的”内存。
mov [edi+4],edx
add edi,8
dec ecx
jne rp_sidt
lidt fword ptr idt_descr ;fword ptr是48位指针,用于远程跳转
ret
idt_descr:
dw 256*8-1 ;db字节(1 byte)类型,dw字类型(2 byte),dd双字类型(4 byte)
dd _idt
_idt:
DQ 256 dup(0) ;伪操作,用来定义操作数占用的字节数
ignore_int作为默认中断处理程序函数地址,会放入中断描述符内。中段描述符结构如下:
这段代码意为将eax作为低32bit、edx作为高32bit填充一个中断描述符,并以cx作为计数器一共填充256次(共256项),以此来初始化整个IDT。最后通过lidt加载中断描述符至idtr让cpu识别。
重新设置GDT
setup_gdt:
lgdt gdt_descr
ret
...
.align 2
.word 0
gdt_descr:
.word 256*8-1 ; gdtr内容是gdt的界限, 以及gdt所在的地址
.long _gdt ; 每个gdt项占8byte, 一共256个gdt项, gdt总量2048byte
.align 3
_gdt: .quad 0x0000000000000000; ;.quad为4word/8byte(等同.word 0,0,0,0). NULL desp
.quad 0x00c09a0000000fff ; 代码段, 0fff=>4096, 4096*4096=16Mb
.quad 0x00c0920000000fff ; 数据段, 除了基地址以外,其他同上
.quad 0x0000000000000000 ; TEMPORARY - don't us
.fill 252,8,0 ; space for LDT's and TSS's etc
对照gdt项所设置内容0x00c09a0000000fff的二进制和全局描述符格式:
g(granularity)粒度位如为0,段限长以1字节为单元;为1,段限长以4K字节为单元;dpl描述符特权级0和3级;p段存在位,该位为1指示描述符存在。于是0x00c09a00表示g为1,p为1,那么此代码段限长为4096*4K=16M。
重置了idt/gdt,接着又重新执行了一遍刚刚执行过的代码。为什么要重新设置这些段寄存器呢?因为修改了 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
检验A20地址线是否打开
需要检验A20地址线是否打开,因为这会影响保护模式是否有效。这里通过如果没打开A20则0x100000会回滚到0x000000来判断,并不断循环直到A20开启为止:
xor eax,eax ; 异或,清空eax
1: inc eax ; check that A20 really IS enabled
mov 0x000000,eax ; loop forever if it isn't
cmp 0x100000,eax
je 1b ; zf为0则跳转(通常搭配cmp,如源操作数和目标操作数相等,则跳转)
在检测到保护模式有效后,如果是486之前的cpu,会配备数学协处理器芯片以增强浮点计算能力。大概是先检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在假设协处理器存在的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在。这段代码不贴出来了,详细参见:flash-linux0.11-talk/head.s at main · dibingfa/flash-linux0.11-talk · GitHub
开启分页机制,为进入main函数做准备
设置完协处理器后,将要开启分页机制,这是head的最后阶段也是执行main函数前的最后阶段。
...
jmp after_page_tables
...
after_page_tables:
push 0
push 0
push 0
push L6
push _main
jmp setup_paging
L6:
jmp L6
可以看到将main函数参数、L6以及main函数地址都压栈,然后跳转到设置分页的标号。这些压栈是为了开启分页后执行main函数。
此外,即便main函数退出,程序也不会结束,因为我们看到程序到L6这边,是个死循环。