基于JOS 80x86 的堆栈切换简明分析

基于JOS 80x86 的堆栈切换简要分析

基于JOS 80x86 的堆栈切换简要分析


这个问题一直困扰很久,发现还是有点粗心,源头--堆栈初始化没怎么搞明白.


这里首先强调,一定一定要搞清楚分段和分页保护的机制.

现有分段,后有分页,分页可有可无,看寄存器cr0是否开启PE位(page enable. 在JOS系统的boot.S里就已经开启了)


文章从三个方面对栈进行分析

0. GDT 全局段寻址描述表

1. 栈的初始化.

2.用户栈到内核栈的切换

3.内核栈到用户栈的切换



0. GDT 全局段寻址描述表

基于JOS 80x86 的堆栈切换简明分析

你能看见第0个段这个时候是不允许访问的,GD_KT右移三位变成 (0x8 >> 3 == 1),第一个段是内核的代码段.可读可执行.第二个是GD_KD 右移三位 (0x10 >> 3 ==  2)第二段.是内核数据段.

第三个GD_UT右移三位(0x18 >> 3 == 3) 第三个段是用户代码段.

第四个GD_UD 右移三位(0x20 >> 3 == 4) 第四个段是用户数据段.

后面的TSS段和我们的主题关系不大,只是任务切换的时候有用.

看过这里之后,嘛嘛再也不用担心别人装逼的时候说"代码和数据是分离的",我听不懂了.

纸老虎!



1.栈的初始化.

首先开机系统开始运行的时候,在boot.S阶段,还没有开启之前,就立马设置好了栈.怎么做的呢?

基于JOS 80x86 的堆栈切换简明分析

首先,把ax寄存器异或置0.然后把ax寄存器的值赋值给ds es ss寄存器.


初始的时候,数据段,额外段,堆栈段,都指向第0个段.这时候还没有什么分页机制

段寻址 address == segment : offset == (segment << 4 bits ) + offset 就直接得到物理地址了

而这里选择的是第0个段啊!同志啊,...在这个"原始的荒野",你用的地址都是物理地址

接着立马就开启了分页机制,

lgdt指令马上加载我们之前介绍的GDT全局段描述表.

开启分页机制,我们也就进入了保护模式.

基于JOS 80x86 的堆栈切换简明分析



接着在bootloader阶段各种段 ds cs都指向$ PROT_MODE_DSEG 0x10指向的内核数据段

基于JOS 80x86 的堆栈切换简明分析


重要的事情说三遍,

JOS中堆栈段和数据段指向同一个段,

JOS中堆栈段和数据段指向同一个段,

JOS中堆栈段和数据段指向同一个段,

: )


到后来初始化CPU的时候,也是把ss指向 GD_KD

基于JOS 80x86 的堆栈切换简明分析

OK ,到这里栈的初始化就算讲明白了(至少我自我感觉非常良好哈哈哈)



2. 用户栈切换到内核栈.

这里有各种方式可以切换,我们集中分析一种Trap Gate触发的切换就好了(其余的还有Call Gate, Interrupt Gate,Task Gate)可以去看赵炯的0.11 Linux源代码分析那本书,对于80x86的介绍非常的详细,也可以读Intel的手册...

重点放在*(int *)0xDeadBeef = 0就好,其他的可以无视,和我们这一小节的主题无关,我们关注的是栈的切换.

基于JOS 80x86 的堆栈切换简明分析

由于这里尝试对一个非法地址写入,那么直接page fault,有米有! 

由于触发的异常,那么CPU会帮我们直接把堆栈段进行切换(注意,很多其他寄存器不会自动切换,但是cs ss会!)

口说无凭,我们来测试

下面是刚好在这句坑爹的指令执行之前,各种寄存器的状态

基于JOS 80x86 的堆栈切换简明分析

常规寄存器都不需要怎么关注,集中看 cs ss ds es fs gd eflags就好


下面我们看对比图.触发page fault前后的对比.


基于JOS 80x86 的堆栈切换简明分析


你会发现 cs ss 变了其他的 ds es fs gs都没变,而且这时候 eflags的IF标识没啦,中断这个时候是被屏蔽的.

结论: 触发异常的时候,CPU是会自动切换 代码段和堆栈段寄存器的,而数据段没有自动切换,以至于我们需要手动的在汇编代码中切换 ds .当ds都完成切换的时候,就完成了所谓的从用户态到内核态切换.


3. 内核态到用户态的切换(这里不讨论用户异常栈的情况).

真正切换的地方在这里.从内核栈切换到普通用户栈.实质上是前面用户态到内核态的一个逆向过程.

寄存器pop的顺序都是完全相反的...

基于JOS 80x86 的堆栈切换简明分析

这里把tf指针指向的 struct Trapframe设置成栈顶指针,很巧妙的把各种恢复各种寄存器的值.

直到最后 iret由于返回地址不再内核代码段内,发生堆栈切换.


这是切换前后的寄存器对比图:


基于JOS 80x86 的堆栈切换简明分析


切换之后,eflags立马有了 IF,允许了中断调用.







基于JOS 80x86 的堆栈切换简明分析