通過retf和呼叫門實現特權級轉換
不打算按別人的思路來,因為在我學的過程中上網查,發現網上的部落格都是互相抄的,最終還是抄書的。
Intel 64 和 IA-32架構處理器在進入保護模式之後,就會有一些列保護機制。其中出現了三個特別重要的東西:CPL、DPL、RPL。
CPL表示當前正在執行程式的特權級,它儲存在cs段暫存器裡面;
DPL表示某個段的特權級,儲存在這個段對應的段描述符中;
RPL表示請求訪問特權級,儲存在選擇子中(表示程式希望通過這個級別的特權級區訪問另一個段)。
所謂的特權級一共有4個,ring0,ring1,ring2,ring3,數字越小,特權級越高。對於資料段和程式碼段的訪問都要滿足特權級規則。
對於資料段(資料段都是非一致的),只允許高特權級訪低特權級,或者同級之間訪問,不允許低特權級程式訪問高特權級資料段;
對於一致程式碼段,只允許低特權級訪問高特權級程式碼段,或者同級訪問;
對於非一致程式碼段,只允許同一特權級訪問。
(這裡的一致不一致大致是指這個程式碼段在系統中是否是拿出來共享的,也就是描述符中的一位,不必特別在意,因為大部分段都是非一致的)
我們用Intel手冊中的例子來說:
在這裡面程式碼段C是非一致程式碼段,D是一致程式碼段。程式碼段A能夠訪問到程式碼段C,因為它們處在同一個特權級,使用的選擇子的RPL也是2;
程式碼段B就不能訪問C,因為它的特權級比C要低;但是它卻可以訪問D,因為D是一致程式碼段,只能被低特權級的程式訪問。
大致就是這麼一個過程,我們現在正在執行某一個程式碼段的程式,我們的CPL一般就等於這個程式碼段的DPL,然後我們忽然想要訪問另外一個段,則在保護模式下,我們必須使用一個叫段選擇子的東西去訪問另一個段,而這個選擇子又包含一個RPL,它相當於我們給出一張一卡通,用這張一卡通去訪問目標段,看看這個一卡通能不能滿足目標段的DPL要求。
我們在訪問程式碼段的過程中,我們的特權級也可能發生變化,每當我們的特權級CPL發生變化,處理器就要求我們切換堆疊,所以一個程式按道理需要配上4個備用堆疊,但是實際上如果我們能保證我們不會用到某個特權級,就可以不設對應的堆疊,例如在linux裡面,只用到了ring0和ring3,則就不需要設定特權級為1和2的堆疊,關於怎麼設定堆疊,下面等一下在講。
現在我們先說說特權級切換,我打算先從ring0切換到ring2,在從ring2切換到ring0。但是我們知道,非一致程式碼段是不能從低特權級切換到高特權級的,這裡我們會看到一個叫呼叫門的東西,它可以幫我們從低特權級程式碼段切換到高特權級程式碼段。
首先,從ring0切換到ring2,一開始我以為可以直接jmp Selector:Offset就可以了,但是出錯了。我發現這樣做不行,找Intel手冊,上網查,都找不到辦法,後來還是參考了於淵老師的辦法,我覺得這個方法特變巧妙,就是利用ret/retf指令。我們都知道組合語言裡的call,是函式呼叫,它分為兩種,近的和遠的,所謂近的就是被call的程式在同一個段裡面,所謂遠的麼就是被call的程式再另一個段裡面,對應兩種返回就是ret和retf。在近call中,處理器先把當前(準確的說是下一條)eip推入堆疊,然後把目標指令地址存入eip暫存器,開始執行新的程式,在ret時,就是把棧頂的值(也就是eip)彈出到eip暫存器裡面,如果有引數傳遞,則把esp加上引數佔用地址的大小。如果是遠call和retf的話,則在推入和彈出eip時會多加一個cs,其它一樣。
利用retf的這個特點,我們就可以實現向低特權級的程式碼段的跳轉。因為retf的本質就是把棧頂的兩個地址裡的內容分別彈出到cs:eip裡面,假設我們的目的碼段(低特權級)為SelectorRing2:0則我們可以這麼做:
push SelectorStack2
push StackTop2
push SelectorRing2
push 0
retf
這裡面多推入了堆疊段內容,是因為當特權級發生改變時,需要切換堆疊,而當檢測到特權級變化時,處理器就會從堆疊段裡取堆疊段選擇子和棧頂指標來建立新的堆疊段。
這樣我們就可以進入到[SelectorRing2:0]指向的程式了,注意32位對其和標註,我曾今在這裡沒有對其和標註,而出錯過。
接下來就是在ring2裡面顯示寫什麼東西,再切換回ring0,第特權級程式不能訪問高特權級非一致程式碼段,所以用retf也不行了,這裡就必須使用呼叫門。在Intel手冊裡面有一條非常特別,就是呼叫門只能從低特權級到高特權級,不能反過來。
首先我們建立呼叫門的門描述符
LABEL_CALL_GATE: Gate SelectorRing0, 0, 0, DA_386CGate + DA_DPL2
表示目的碼段的段選擇子為SelectorRing0,偏移為0,傳遞引數為0,特權級為2
然後建立門選擇子
SelectorGate equ LABEL_CALL_GATE - LABEL_GDT + SA_RPL2
這裡也涉及到堆疊的切換,但是卻沒之前那麼簡單了,因為Intel為作業系統想的比較多。在x86裡面有一個叫TSS的東西,Task State Segment,本質上是一個段,是專門用來記錄程式執行狀態的。它能實現的功能是這樣的:一開始我們都會處在ring0的特權級,當我們特權級改變時,如果我們載入了這個TSS,處理器就會到這裡面來找對應的在堆疊,比如我們要跳轉到ring1的程式碼段,則需要一個ring1特權級的堆疊,處理器就會到TSS裡面找到對應的ring1特權級的段選擇子和棧頂指標,來構建新的堆疊,當程式返回的時候,又會恢復為原來的堆疊。
構建TSS段
[section .tss]
align 32
[bits 32]
LABEL_TSS:
dd 0
dd STACK0_TOP
dd SelectorStack0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dw 0
dw $ - LABEL_TSS + 2
db 0x0FF
TSS_LEN equ $ - LABEL_TSS - 1
...
LABEL_DESC_TSS: Descriptor 0, TSS_LEN, DA_386TSS
SelectorTss equ LABEL_DESC_TSS - LABEL_GDT
然後在retf之前,載入TSS
mov ax, SelectorTss
ltr ax
在ring2的程式中,我們就可以使用call這個呼叫門就可以又切換到ring0了
[section .ring2]
align 32
[bits 32]
SEG_RING2:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 10 + 18) * 2
mov ah, 0x0B
mov al, 'R'
mov [gs:edi], ax
add edi, 2
mov al, '2'
mov [gs:edi], ax
call SelectorGate:0
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 10 + 26) * 2
mov ah, 0x0B
mov al, 'R'
mov [gs:edi], ax
add edi, 2
mov al, '2'
mov [gs:edi], ax
jmp $
RING2_LEN equ $ - SEG_RING2 - 1
[section .ring0]
align 32
[bits 32]
SEG_RING0:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 10 + 22) * 2
mov ah, 0x0B
mov al, 'R'
mov [gs:edi], ax
add edi, 2
mov al, '3'
mov [gs:edi], ax
retf
RING0_LEN equ $ - SEG_RING0 - 1
附上使用呼叫門的過程圖