基於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,允許了中斷調用.