<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    過程詳解 | 操作系統16位實模式切換到32位保護模式

    VSole2021-09-22 17:09:11

    這篇文章的內容是我在研究自制操作系統時遇到的一個問題,即如何從16位實模式切換到32位保護模式,網上的資料比較少,講得也不是很詳細,在這跟大家分享一下我的解決方案。

    前置知識:

    計算機是如何啟動的?

    (http://www.ruanyifeng.com/blog/2013/02/booting.html)

    自制操作系統:引導扇區的實現

    (https://www.cnblogs.com/jiqingwu/p/os_boot_sector.html)

    1、實模式和保護模式

    請參考:實模式和保護模式

    (https://blog.csdn.net/qq_37653144/article/details/82818191)

    現代CPU有三種工作模式:實模式、保護模式和虛擬8086模式。CPU在加電后自動進入16位實模式,在主引導記錄MBR加載特定的操作系統后,由操作系通經過一些設置切換到32位保護模式。

    以WIN7為例,首先由MBR載入啟動管理器bootmgr,在bootmgr中進行實模式到保護模式的切換,隨后裝載winload.exe,然后通過winload.exe加載Windows7內核,從而啟動整個Windows7系統。

    2、實模式到保護模式的切換

    從實模式切換到保護模式大致可以分為以下幾個步驟:

    1、屏蔽中斷

    2、初始化全局描述符表(GDT)

    3、將CR0寄存器最低位置1

    4、執行遠跳轉

    5、初始化段寄存器和棧指針

    1、屏蔽中斷

    在16位實模式下的中斷由BIOS處理,進入保護模式后,中斷將交給中斷描述符表IDT里規定的函數處理,在剛進入保護模式時IDTR寄存器的初始值為0,一旦發生中斷(例如BIOS的時鐘中斷)就將導致CPU發生異常,所以需要首先屏蔽中斷。

    屏蔽中斷可以使用cli指令:

    cli
    

    2、初始化GDT

    屏蔽中斷后我們就可以正式開始切換到保護模式的過程了。首先我們要完成GDT的初始化,這也是整個切換過程中最復雜的一步。

    32位保護模式下的地址計算方式和16位實模式不一樣,16位保護模式支持20位的尋址,物理地址等于段地址 * 16 + 偏移地址,其中段地址可以通過段寄存器獲得,段地址和偏移地址都是16位。

    在32位保護模式中,段與段之間是互相隔離的,當訪問的地址超出段的界限時處理器就會阻止這種訪問,因此每個段都需要有起始地址、范圍、訪問權限以及其他屬性四個部分,這四個部分合在一起叫做段描述符(Segment Descriptor),總共需要8個字節來描述。但Intel為了保持向后兼容,將段寄存器仍然規定為16-bit,顯然我們無法用16-bit的段寄存器來直接存儲64-bit的段描述符。

    解決的辦法是將所有64-bit的段描述符放到一個數組中,將16-bit段寄存器的值作為下標來訪問這個數組(以字節為單位),獲取64-bit的段描述符,這個數組就叫做全局描述符表(Global Descriptor Table, GDT)。

    上面這個就是64位段描述符的具體結構,包括段起始地址Base,限長Limit,Access Byte和Flags都是訪問權限,結構如下:

    Access Byte和Flags各項的解釋,我直接復制OS Dev)了,更詳細的解釋可以參考《x86匯編語言:從實模式到保護模式》這本書的11.3節:

    Pr: Present bit. This must be 1 for all valid selectors.

    Privl: Privilege, 2 bits. Contains the ring level, 0 = highest (kernel), 3 = lowest (user applications).

    S: Descriptor type. This bit should be set for code or data segments and should be cleared for system segments (eg. a Task State Segment)

    Ex: Executable bit. If 1 code in this segment can be executed, ie. a code selector. If 0 it is a data selector.

    DC: Direction bit/Conforming bit.

    Direction bit for data selectors: Tells the direction. 0 the segment grows up. 1 the segment grows down, ie. the offset has to be greater than the limit.

    Conforming bit for code selectors:

    • If 1 code in this segment can be executed from an equal or lower privilege level. For example, code in ring 3 can far-jump to conforming code in a ring 2 segment. The privl-bits represent the highest privilege level that is allowed to execute the segment. For example, code in ring 0 cannot far-jump to a conforming code segment with privl==0x2, while code in ring 2 and 3 can. Note that the privilege level remains the same, ie. a far-jump form ring 3 to a privl==2-segment remains in ring 3 after the jump.
    • If 0 code in this segment can only be executed from the ring set in privl.

    RW: Readable bit/Writable bit.

    Readable bit for code selectors: Whether read access for this segment is allowed. Write access is never allowed for code segments.

    Writable bit for data selectors: Whether write access for this segment is allowed. Read access is always allowed for data segments.

    Ac: Accessed bit. Just set to 0. The CPU sets this to 1 when the segment is accessed.

    Gr: Granularity bit,粒度位,如果Gr置0則段界限limit是以字節為單位的,即段的大小為1B到1MB,如果Gr置0則以4KB為單位,段的大小從4KB到4GB。

    Sz: Size bit,置0表示該段的偏移地址或操作數是16位的,比如說代碼段該位置0,則取指令時會使用16位的指令指針寄存器IP來取指令,而棧段在進行棧操作時,會使用SP寄存器;置1表示使用的偏移地址或操作數是32位的,使用EIP和ESP寄存器。

    Flags區域中空余的2位其中一位是給64位CPU使用的,這里我們置為0即可,另一位是留給用戶軟件使用的,CPU并不會使用。

    GDT可以通過LGDT指令加載,加載后GDT的地址和大小被保存到了GDTR寄存器:

     

    LGDT指令的操作數為6字節的數據,2個低位字節為GDT的大小減一,減一的原因是Size的最大值是65535,而GDT最多可以有65536字節(最多8192項)。4個高位字節為GDT的32位地址。

    我們可以在匯編代碼中定義GDT并用LGDT指令加載GDT,然后讓MBR將我們這一段匯編代碼加載到內存中的某個地址,然后跳轉到這個地址執行,這樣就完成了GDT的初始化。

    接下來我們就來看一下在匯編代碼中定義GDT的方法。Intel處理器規定GDT中的第一個描述符必須是空描述符,這是因為寄存器和內存單元的初始值一般都是0,如果程序設計有問題,就會無意中用全0的索引來選擇描述符,gdt_null的作用就有點像C語言中的空指針。用匯編代碼定義如下:

    gdt_start:; 第一個描述符必須是空描述符gdt_null:    dd 0    dd 0gdt_end:
    

    dd 0定義了一個4字節的數據,兩個dd 0正好是8字節,也就是一個描述符的大小。

    接下來我們要定義代碼段和數據段,為了方便,我們可以將代碼段和數據段的起始地址都設為0,大小都設為4GB,這種設置方式也叫作平坦模式。現在的定義如下:

    gdt_start:; 第一個描述符必須是空描述符gdt_null:    dd 0    dd 0; 代碼段描述符gdt_code:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10011010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 數據段描述符gdt_data:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31)gdt_end:
    

    此時代碼段和數據段的訪問權限和其他屬性如下:

    代碼段:Access byte 10011010bPr = 1        ; 標記為有效的段Privl = 0    ; ring3權限S = 1        ; 代碼段或數據段Ex = 1        ; 可執行,即代碼段DC = 0        ; 標記該段只能在Privl權限(即ring3權限)下執行RW = 1        ; 可讀Ac = 0        ; 該段的內容是否被修改,默認為0,由CPU置位Flags 1100bGr = 1        ; 粒度為4KBSz = 1        ; 32位模式
    數據段:Access byte 10010010bPr = 1        ; 標記為有效的段Privl = 0    ; ring3權限S = 1        ; 代碼段或數據段Ex = 0        ; 不可執行,即數據段DC = 0        ; 標記該段只能在Privl權限(即ring3權限)下執行RW = 1        ; 可寫Ac = 0        ; 該段的內容是否被修改,默認為0,由CPU置位Flags 1100bGr = 1        ; 粒度為4KBSz = 1        ; 32位模式
    

    接著我們要定義棧段,由于我們的引導程序是加載到07c00h這個地址的,因此我們可以將07c00h以下的空間都作為棧空間,加入棧段后的定義如下:

    gdt_start:; 第一個描述符必須是空描述符gdt_null:    dd 0    dd 0; 代碼段描述符gdt_code:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10011010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 數據段描述符gdt_data:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 棧段描述符gdt_stack:    dw 0x7c00 ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 01000000b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31)gdt_end:
    棧段:Access byte 10010010bPr = 1        ; 標記為有效的段Privl = 0    ; ring3權限S = 1        ; 代碼段或數據段Ex = 0        ; 不可執行,即數據段DC = 0        ; 標記該段只能在Privl權限(即ring3權限)下執行RW = 1        ; 可寫Ac = 0        ; 該段的內容是否被修改,默認為0,由CPU置位Flags 1100bGr = 0        ; 粒度為1BSz = 1        ; 32位模式
    

    接著我們定義6字節的GDT描述符:

    ; GDT descriptiorgdt_descriptor:dw gdt_end - gdt_start - 1 ; Size of our GDT, always less one of the true sizedd gdt_start ; Start address of our GDT
    

    接著調用lgdt指令將GDT描述符加載到gdtr寄存器,整個GDT的初始化就算是完成了:

    lgdt [gdt_descriptor]
    

    3、將CR0最低位置1

    CR0是系統內的32位控制寄存器之一,可以控制CPU的一些重要特性。其中最低位是保護允許位(Protected Mode Enable, PE),PE位置1后CPU進入保護模式(注意此時還是16位保護模式,不是32位保護模式),置0時則為實模式。現在我們要進入保護模式,即將CR0的最低位置1,匯編代碼如下:

    ; 把 cr0 的最低位置為 1,開啟保護模式mov eax, cr0or eax, 0x1mov cr0, eax
    

    有關CR0其他位的功能以及其他控制寄存器可以查看Control register,這里不再贅述。

    4、執行遠跳轉

    將cr0最低位置1后,CPU就進入了保護模式,此時需要馬上執行一條遠跳轉指令:

    jmp 08h:PModeMain
    

    這條指令有兩個作用,第一個作用是將cs段寄存器的值修改為08h,切換到保護模式后,CPU尋址的方式就從實模式中的段地址 * 16 + 偏移地址改為了通過gdt尋址,所以這里的08h是段選擇子而不是段地址,并且遠跳轉指令會自動將cs的值修改為對應的段選擇子,這里是08h。

    之前提到過,段描述符中的Sz位確定操作數的默認大小,將代碼段寄存器切換到08h后,代碼段描述符的Sz位為1,即32位模式,此時才真正進入了32位保護模式。

    遠跳轉的另一個作用是清空CPU的流水線,流水線的作用在計組中有提到過,為了加速指令的執行,CPU在執行當前指令時會同時加載并解析接下來的一些指令,在進入保護模式之前,已經有許多指令進入了流水線,這些指令都是按16位模式處理的,而進入保護模式后的指令都是32位,所以這里通過一個遠跳轉來讓CPU清空流水線。

    切換到32位模式后,就應該執行32位的指令了,所以從PModeMain開始的指令都采用32位模式編譯,通過[bits 32]這個標記實現:

    [bits 32]PModeMain:
    

    5、初始化段寄存器和棧指針

    上一步中我們將代碼段寄存器cs初始化成了0x08,現在我們還需要初始化其他的段寄存器如數據段寄存器ds,拓展段寄存器es,棧段ss以及fs,gs兩個由操作系統使用的段。

    另外我們還需要初始化棧指針ebp和esp,代碼如下:

    [bits 32]PModeMain:    mov ax, 0x10        ; 將數據段寄存器ds和附加段寄存器es置為0x10    mov ds, ax             mov es, ax    mov fs, ax          ; fs和gs寄存器由操作系統使用,這里統一設成0x10    mov gs, ax    mov ax, 0x18        ; 將棧段寄存器ss置為0x18    mov ss, ax    mov ebp, 0x7c00     ; 現在棧頂指向 0x7c00    mov esp, ebp
    

    此時我們就可以開始執行32位保護模式下的代碼了。

    3、測試:32位保護模式下的打印

    接下來我們要實現的是32位保護模式下的打印功能,測試我們是否已經成功進入32位保護模式了。

    在32位保護模式中我們無法使用實模式下的BIOS中斷指令進行打印,但是天無絕人之路,我們仍然可以通過直接修改顯存來進行打印。

    顯卡包括顯存本身是外圍設備,但CPU很友好的把顯存映射到了0x8B000這個物理地址,因此我們可以直接通過mov指令來訪問顯存。在文本模式下,被打印的字符在顯存中總共占2個字節,第一個字節是字符的ASCII碼,第二個字節是字符的顏色屬性,這里與BIOS中int 10調用的顏色屬性是一樣的:

    打印字符串的代碼我們可以寫成一個函數,具體的內容大家看下面的代碼和注釋:

    ; 打印字符串; @param 被打印的字符串print:    push ebp    mov ebp, esp    mov ebx, [ebp+8]    ; [ebp+8]即傳入的字符串參數    xor ecx, ecx    mov ah, 0x0f        ; ah為打印的顏色屬性,0x0f為白字黑底    mov edx, 0xb8000    ; 顯存的地址loop1_begin:    mov al, [ebx]       ; al為被打印的字符    cmp al, 0           ; 若al為0,結束打印    je loop1_end    mov [edx], ax       ; 向顯存中寫入字符及其顏色屬性(2字節)    inc ebx    add edx, 2    jmp loop1_beginloop1_end:    mov esp, ebp    pop ebp    ret
    

    函數調用:

    PModeTest:    push TEST_STRING             ; 被打印字符的地址    call printPModePause:    hlt    jmp PModePause TEST_STRING db 'We are in protected mode!', 0
    

    效果如下:

    4、完整代碼

    完整的代碼,需要在引導程序中加載這段代碼到正確的位置,這里是09000h,或者直接把org 09000h改成org 07c00h,把這段代碼寫到主引導扇區:

    org 09000h cli                     ; 屏蔽中斷 lgdt [gdt_descriptor]   ; 初始化GDT ; 把 cr0 的最低位置為 1,開啟保護模式mov eax, cr0or eax, 0x1mov cr0, eax jmp 08h:PModeMain [bits 32]PModeMain:    mov ax, 0x10        ; 將數據段寄存器ds和附加段寄存器es置為0x10    mov ds, ax             mov es, ax    mov fs, ax          ; fs和gs寄存器由操作系統使用,這里統一設成0x10    mov gs, ax    mov ax, 0x18        ; 將棧段寄存器ss置為0x18    mov ss, ax    mov ebp, 0x7c00     ; 現在棧頂指向 0x7c00    mov esp, ebp    jmp PModeTest PModeTest:    push TEST_STRING             ; 被打印字符的地址    call printPModePause:    hlt    jmp PModePause ; 打印字符串; @param 被打印的字符串print:    push ebp    mov ebp, esp    mov ebx, [ebp+8]    ; [ebp+8]即傳入的字符串參數    xor ecx, ecx    mov ah, 0x0f        ; ah為打印的顏色屬性,0x0f為白字黑底    mov edx, 0xb8000    ; 顯存的地址loop1_begin:    mov al, [ebx]       ; al為被打印的字符    cmp al, 0           ; 若al為0,結束打印    je loop1_end    mov [edx], ax       ; 向顯存中寫入字符及其顏色屬性(2字節)    inc ebx    add edx, 2    jmp loop1_beginloop1_end:    mov esp, ebp    pop ebp    ret TEST_STRING db 'We are in protected mode!', 0 gdt_start:; 第一個描述符必須是空描述符gdt_null:    dd 0    dd 0; 代碼段描述符gdt_code:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10011010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 數據段描述符gdt_data:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 棧段描述符gdt_stack:    dw 0x7c00 ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 01000000b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31)gdt_end: ; GDT descriptiorgdt_descriptor:dw gdt_end - gdt_start - 1 ; Size of our GDT, always less one of the true sizedd gdt_start ; Start address of our GDT
    

    5、A20地址線

    A20的“A”是“Address”的首字符,A20也就是第21根地址線的意思。這里涉及到一個歷史遺留問題,但如果我們是在Bochs這樣的模擬器上進行調試的話沒有影響,所以我把這個問題放到了最后來講。

    在8086處理器上只有20根地址線,最多支持20位的地址,因此每當物理地址達到最高端0xFFFFF時,再加上1又會繞回最低端0x00000。為了兼容地址回繞這個特性,后來生產的CPU都會默認關閉第21根地址線,需要操作系統在進入保護模式時手動開啟。

    不過某些BIOS,包括模擬器(qemu, Bochs等)會默認幫我們打開A20地址線,所以我們不需要手動開啟了,我們可以用一段程序來測試A20地址線是否打開:

    PModeTest:    mov edi, 0x112345    mov esi, 0x012345    mov [esi], esi    mov [edi], edi    mov eax, [esi]    cmp eax, edi    je A20_DISABLE    push aA20_ENABLE    call print    jmp PModePauseA20_DISABLE:    push aA20_DISABLE    call printPModePause:    hlt    jmp PModePause aA20_ENABLE db 'A20 Enable', 0aA20_DISABLE db 'A20 Disable', 0
    

    測試結果,可以看到在Bochs中A20地址線確實是默認開啟的:

    從80486處理器開始,0x92端口的第2位用來控制A20地址線,將0x92端口的第2位置1即可開啟A20地址線,如果要手動開啟A20地址線的話,可以使用以下代碼:

    in al, 92hor al, 00000010bout 92h, al
    


    保護模式segment
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    在x86系統下,總說一個進程有4GB空間,那么按照這個說法來說,在windows上起一個進程就要占用4GB空間,兩個進程就要占用8GB空間,但是實際上是我們電腦的物理內存往往只有8GB,16GB多一點的可能有32GB,我們卻啟動了幾十個進程,這顯然是矛盾的。
    這篇文章的內容是我在研究自制操作系統時遇到的一個問題,即如何從16位實模式切換到32位保護模式,網上的資料比較少,講得也不是很詳細,在這跟大家分享一下我的解決方案。
    最近,QEMU 的一個bug導致 kexec 之后的內核崩潰,因為 kexec-ed 內核將無法解壓縮壓縮的 initrd。此錯誤僅影響在 QEMU 中使用壓縮內聯的新 Linux 內核。物理機器上的 kexec,以及在 QEMU 中使用壓縮的 initrd 引導 Linux 內核的其他方法,均不受影響。當然,在 QEMU 中,使用未壓縮的initrd來創建新Linux內核是有效的,因此,如果您想在 QEMU中使用基于kexec的引導加載程序,則可能必須使用未壓縮的initrd才能引導目標系統。
    近日,蘋果公司在官網發布公告稱正在評估iPhone上的一項突破性安全措施——隔離模式(Lockdown Mode,又稱鎖定模式),為極少數面臨高級間諜軟件和針對性網絡攻擊風險的用戶提供一種極端的保護模式
    在全國范圍率先推出《設置指南》,是上海網信辦探索加強未成年人網絡保護的一項創新之舉,用于指導上海屬地重點網絡平臺率先高標準、嚴要求地設置“青少年模式”,為未成年人綠色上網提供更好的服務保障。《上海市未成年人網絡保護行動倡議書》發布
    新的“Migo”惡意軟件針對Linux服務器,利用Redis進行加密劫持。使用用戶模式??Rootkit隱藏其活動,使檢測變得困難。保護您的Redi 服務器并保持警惕!
    寫這篇,最開始是無聊。后面想想在windows三環直接操控VmWare里面WIN10的內存,這不就相當于實現了一套簡易版的windows內存解析,對學習和了解內存是非常方便的事情,甚至說對了解整個windows內核都是一個有益的事情。具體原理就直接看代碼吧,里面的注釋寫的比較完善。所以,我走了一個捷徑:關閉win10的分頁文件,重啟!后續完成了上面的NTFS解析后再來測試這一塊。未完成的地方,我都寫了斷點。
    推動移動智能終端未成年人保護相關團體、行業標準上升為國家標準。《答復》顯示,此前,工信部已加強手機等移動智能終端技術規范引導。同時,工信部還引導軟件企業加強未成年網絡保護功能開發。同時,立足電信行業監管,加強計算機、手機終端應用軟件的安全威脅監測和漏洞管理,嚴肅查處違法違規企業,促進軟件產業的健康持續發展。
    0x00 簡要說明 百度百科:Redis(Remote Dictionary Server ),即遠程字典服務,是一個開源的使用ANSI C語言編寫、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。 Redis因配置不當可導致攻擊者直接獲取到服務器的權限。 利用條件:redis以root身份運行,未授權訪問,弱口令或者口令泄露等
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类