基于JOS 80x86 的堆栈切换简要分析
这个问题一直困扰很久,发现还是有点粗心,源头--堆栈初始化没怎么搞明白.
这里首先强调,一定一定要搞清楚分段和分页保护的机制.
现有分段,后有分页,分页可有可无,看寄存器cr0是否开启PE位(page enable. 在JOS系统的boot.S里就已经开启了)
文章从三个方面对栈进行分析
0. GDT 全局段寻址描述表
1. 栈的初始化.
2.用户栈到内核栈的切换
3.内核栈到用户栈的切换
0. GDT 全局段寻址描述表
你能看见第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阶段,还没有开启之前,就立马设置好了栈.怎么做的呢?
首先,把ax寄存器异或置0.然后把ax寄存器的值赋值给ds es ss寄存器.
初始的时候,数据段,额外段,堆栈段,都指向第0个段.这时候还没有什么分页机制
段寻址 address == segment : offset == (segment << 4 bits ) + offset 就直接得到物理地址了
而这里选择的是第0个段啊!同志啊,...在这个"原始的荒野",你用的地址都是物理地址
接着立马就开启了分页机制,
lgdt指令马上加载我们之前介绍的GDT全局段描述表.
开启分页机制,我们也就进入了保护模式.
接着在bootloader阶段各种段 ds cs都指向$ PROT_MODE_DSEG 0x10指向的内核数据段
重要的事情说三遍,
JOS中堆栈段和数据段指向同一个段,
JOS中堆栈段和数据段指向同一个段,
JOS中堆栈段和数据段指向同一个段,
: )
到后来初始化CPU的时候,也是把ss指向 GD_KD
OK ,到这里栈的初始化就算讲明白了(至少我自我感觉非常良好哈哈哈)
2. 用户栈切换到内核栈.
这里有各种方式可以切换,我们集中分析一种Trap Gate触发的切换就好了(其余的还有Call Gate, Interrupt Gate,Task Gate)可以去看赵炯的0.11 Linux源代码分析那本书,对于80x86的介绍非常的详细,也可以读Intel的手册...
重点放在*(int *)0xDeadBeef = 0就好,其他的可以无视,和我们这一小节的主题无关,我们关注的是栈的切换.
由于这里尝试对一个非法地址写入,那么直接page fault,有米有!
由于触发的异常,那么CPU会帮我们直接把堆栈段进行切换(注意,很多其他寄存器不会自动切换,但是cs ss会!)
口说无凭,我们来测试
下面是刚好在这句坑爹的指令执行之前,各种寄存器的状态
常规寄存器都不需要怎么关注,集中看 cs ss ds es fs gd eflags就好
下面我们看对比图.触发page fault前后的对比.
你会发现 cs ss 变了其他的 ds es fs gs都没变,而且这时候 eflags的IF标识没啦,中断这个时候是被屏蔽的.
结论: 触发异常的时候,CPU是会自动切换 代码段和堆栈段寄存器的,而数据段没有自动切换,以至于我们需要手动的在汇编代码中切换 ds .当ds都完成切换的时候,就完成了所谓的从用户态到内核态切换.
3. 内核态到用户态的切换(这里不讨论用户异常栈的情况).
真正切换的地方在这里.从内核栈切换到普通用户栈.实质上是前面用户态到内核态的一个逆向过程.
寄存器pop的顺序都是完全相反的...
这里把tf指针指向的 struct Trapframe设置成栈顶指针,很巧妙的把各种恢复各种寄存器的值.
直到最后 iret由于返回地址不再内核代码段内,发生堆栈切换.
这是切换前后的寄存器对比图:
切换之后,eflags立马有了 IF,允许了中断调用.