目录
- 初始化IDT、IDTR和GDT、GDTR
- 检查协处理器并设置CR0寄存器
- 初始化页表和CR3寄存器,开启分页
初始化IDT、IDTR和GDT、GDTR
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp
call setup_idt
call setup_gdt
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b
个人阅读上面代码的知识点:
- lss _stack_start,%esp 首先lss是远指针加载指令,不了解的查deepseek就懂了。“_stack_start”这个标号找了很久都找不到在哪儿定义的,搜索整个源码都没有。最后还是求助deepseek终于搞懂了(deepseek真自学神器)。要把“_stack_start”中下划线去掉,直接搜索“stack_start”,才知道这个标号是定义在kernel/sched.c中的结构体变量名。之所以要加下划线,是因为早期的编译器编译时会在C中变量名前面加,现在不加了 。由于对kernel代码不懂,所以没过多研究,知道是在初始化栈就行了。
call setup_idt 调用子程序初始化IDT表,通过指令lidt将IDT表的长度和地址加载到IDTR寄存器。
还处在内核初始化阶段,所以只是简单的将IDT表中的描述符初始化为同一个,都指向ignore_int这个中断处理程序。ignore_int子程序就是本文件中,功能就是打印一段字符。- lidt指令加载的IDT表的地址是线性地址,初始化阶段,此时还没开启分页模式,线性地址等于物理地址。开启分页后,IDTR中的线性地址要经过MMU查页表转化成IDT表在内存中的物理地址。
- 需要对IDT表描述符了解,IDT表描述符总共8字节,小端法存入内存,先存低4字节,再高4字节,先低2字节,再高2字节。之所以提小端法是因为我一开始没注意,把描述符的选择子判断错了。
call setup_gdt 调用子程序初始化GDT表,通过指令lgdt将GDT表的长度和地址加载到GDTR寄存器。
GDT表自己构造。- lgdt指令加载的GDT表的地址是线性地址,只不过初始化阶段,此时还没开启分页模式,线性地址等于物理地址。开启分页后,GDTR中的线性地址要经过MMU查页表转化成GDT表在内存中的物理地址。
- 构造GDT表时,GDT表中的描述符大小为8字节,第一个描述符为全0,后面描述符的基地址base=0X00000000,limit=最大值(0X000FFF),把内存管理就设置成了平坦模式, 现代操作系统支持这种模式,这样一个进程的
代码段、数据段、栈段就共享同一个线性地址空间了,方便虚拟内存管理
。
- 保护模式下,指令执行过程中,查询IDT表、GDT表的工作是有硬件来实现的,不是软件模拟的,所以对IDT、GDT中描述符的结构也是硬件规定的。我们在构造这两个表时要按照硬件的规定来。
- movl %eax,0x000000;cmpl %eax,0x100000 这段是负责检查A20地址线是否打开,如果没打开那么0X100000地址就等于0X000000地址,cmpl比较的结果就是相等,那就卡在这个执行死循环。
- je 1b 这个条件跳转指令开始看了我也很蒙,还是靠deepseek,这是以前的写法“b”表示满足条件跳转到后面的标号,“f”表示满足条件跳转到前面的标号。b和f指示跳转的方向的。现在好像不用这种用法了。
检查协处理器并设置CR0寄存器
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
orl $2,%eax # set MP
movl %eax,%cr0
call check_x87
jmp after_page_tables
/*
* We depend on ET to be correct. This checks for 287/387.
*/
check_x87:
fninit
fstsw %ax
cmpb $0,%al
je 1f /* no coprocessor: have to set bits */
movl %cr0,%eax
xorl $6,%eax /* reset MP, set EM */
movl %eax,%cr0
ret
个人阅读上面代码的知识点:
- 了解CR0寄存器,CR0寄存器中,PE位开启保护模式、PG位开启分页机制、WP位写保护位。还有其它涉及协处理器的位,我一知半解就不写了。上面这段代码主要就是检查协处理器,然后进行设置。 指令call check_x87就是调用子程序检查是否存在287/387协处理器。
- fninit fstsw这两个是协处理器指令,自己查deepseek能看懂,不赘述了。check_x87这个子程序功能就是检查协处理器的。
初始化页表和CR3寄存器,开启分页
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
-----------------------------------------------------------------------------------------
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,pg_dir /* set present bit/user r/w */
movl $pg1+7,pg_dir+4 /* --------- " " --------- */
movl $pg2+7,pg_dir+8 /* --------- " " --------- */
movl $pg3+7,pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
个人阅读上面代码的知识点:
- jmp setup_paging 跳转到初始化页表的子程序。该指令前面的push指令是在压栈,pushl $main 目的是从setup_paging子程序返回时,ret指令能返回到main程序。 我还没看过main.c的代码,不赘述了。功能就是结束head.s的初始化任务。开启main.c
- 强化我自己的一个知识点,call和jmp都是跳转指令,区别就是call跳转前要将返回地址压栈,jmp无需压栈直接跳转无法返回。开始我还疑问为什么不用call指令,然后返回到main.c,仔细一想call指令是将下一条指令的地址压栈,只能返回到本源文件call的下一条指令。而我们需要的是结束head.s,返回到另一个源文件main.c。所以通过pushl $main压入返回地址,不用call调用,而用jmp直接跳转到setup_paging初始化页表子程序。
setup_paging子程序初始化页表、CR3寄存器,置CR0寄存器的PG为1开启分页
。了解页表和页表项,即使有些汇编指令不熟,查一下就是看懂这段了。- stosl指令,我忽略了它执行时会自动增加EDI。stos、movs、cmps、scas都是字符串操作指令,可以顺便都了解一下。伟大的deepseek很好用的。
- CR3寄存器是用来存放顶级页表的物理地址的。MMU将线性地址转换位物理地址时需要通过CR3寄存器找到页表进行映射。CR3寄存器的改变,会引起TLB表的改变。CR3改变代表页表的切换,TLB相当于部分页表项的缓存,自然也要切换。
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000
-
刚看见这段代码时,感觉整齐有规律,就是不知道作用是什么。即使知道了.org这个伪指令的功能,也不知道写这段代码的意义。直到最后看到初始化页表的子程序setup_paging。补充点因为这段代码学到的零碎知识点。
- (1).org(Origin)是汇编语言中的一条伪指令(Directive),用于指定程序或数据在内存中的起始地址。 在需要直接控制内存布局的场景(如裸机开发、嵌入式开发)中,.org 是一个简单有效的工具,但在高层编程中通常由链接器管理地址分配。
- (2) 这段代码其实就是在划分页表的页。它们之间的地址差是0X1000(4KB),从0地址开始到0X5000共20KB,分成5个页,每页4KB,第一个页是页目录表,其余4个页都是普通页表。setup_paging子程序的开始将5个页20KB用0填充,接着构造4个普通物理页的页表项填充到0地址的页目录表,最后构造一个普通物理页的页表项填充4个普通物理页。
- (3)你还可以观察到初始化IDT、IDTR和GDT、GDTR的代码,还有检查协处理器并设置CR0寄存器的代码都写在这段代码之前,因为那些初始化、检查的代码执行完就没用了,它们所在的内存空间可以被页表覆盖; 结束head.s跳转到main.c的代码、初始化页表的子程序setup_paging、IDT表GDT表的位置,包括中断处理程序ignore_int的代码位置都在“.org 0X5000”之后,就是防止误操作,初始化页表时覆盖比较重要的代码、表空间。
- (4) 内存中IDT表、GDT表,各有1个,所有应用程序和操作系统共用;页表有多个,每个应程序都有自己的页表,任务切换时,通过修改CR3寄存器切换页表,同时TLB表也要刷新。