【操作系统】模式切换篇
CPU的模式
什么是CPU的模式?這和CPU的發展過程有關,最開始CPU是8位的,后來發展到16位,然后是32位,現在是64位,多少多少位指的是寄存器的位寬。CPU能使用的寄存器寬度以及CPU使用的指令等就構成了CPU的模式,比如16位模式和32位模式,注意除了寄存器,不同模式下CPU對指令的解釋也是不同的,因此16位模式的程序是不能在32位模式下運行的。為了向后兼容,后來的CPU要能運行在之前CPU的模式下,比如32位CPU也能跑16位模式,這樣在之前CPU上編寫的程序也能在新CPU上運行。
說到CPU的模式,我們常常會看到16位實模式,32位保護模式這樣的概念,單從字面上看,讓人像個丈二的和尚。這兩個概念其實包含了兩對概念:
- 16位和32位
- 實模式和保護模式
16位和32位模式很好理解,它描述的是寄存器的寬度和CPU的指令。實模式和保護模式是個啥呢?要理解這兩個概念,我們需要看下這兩個概念的英文原文。
- 實模式原文是real address mode(簡稱為real mode),全稱真實地址模式。
- 保護模式原文是protected virtual address mode,全稱受保護的虛擬地址模式。
首先我們可以看到它們限定的實體都是尋址模式(address mode),也就是CPU訪問內存的方式。CPU訪問內存不都是二維數組的形式嗎?為何還有虛實之分呢?
這里的虛和實指的是段寄存器中的值是(真實的)二維數組下標,還是(虛假的)段描述符偏移。由于虛擬尋址模式下可以控制內存是否允許被訪問,所以它又多了一個限定:受保護的。這也意味著在真實尋址模式下,對任意內存的訪問都是始終被允許的,是不具備段保護功能的。
保護模式下的段尋址
在切換到保護模式之前,我們還得再嘮嘮段尋址,上次嘮了5毛錢的,這次再嘮5毛錢。
首先來回顧一下段尋址的本質,所謂段尋址就是把一維數組抽象成二維數組,用二維數組下標來定位內存字節,CPU會幫我們把二維下標換算成一維下標,這一點依然沒有改變。
實地址模式下段寄存器指哪打哪,CPU沒有拒絕的理由,然而在保護模式下,CPU是可以拒絕非法的段尋址的,因此,保護模式保護的是段,而不是某個或某幾個任意的內存字節。在保護模式下,CPU除了需要知道段的起始地址,還需要知道關于這個段的一些屬性,比如段的類型,安全級別等等。這么多的信息,段寄存器肯定是放不下了,要知道即使是在32位(包括64位)模式下,段寄存器依然只有16位。
為了解決這個問題,我們需要把段的相關信息放到內存中,相比于寄存器,內存就如同大海一樣。這些放在內存中的段信息稱為全局描述符表,英文名Global Descriptor Table,簡稱GDT。全局描述符表中的每一項代表了一個段的信息,稱為段描述符,英文叫Segment Descriptor,簡稱SD。與此同時,我們還要告訴CPU全局描述符表的起始地址和長度,這樣CPU才知道GDT放在那里,這里GDT的起始地址和長度叫做GDT描述符。之后我們使用段尋址時,給到段寄存器的其實是SD相對于GDT起始地址的偏移量。32位保護模式下SD的大小為8字節,如果我們將段寄存器設置為0x8,那么CPU就會使用GDT中的第二個段描述符,如下圖所示。注意GDT的第一個段描述符是null,也就是全零,設置這樣一個非法的段描述符是為了防止從16位實地址模式切換到32位保護模式后忘記設置段寄存器引發的問題。
段描述符中主要包括段的起始地址,段的大小和段的屬性3大部分,SD的結構如下圖所示。
段大小和段起始地址在這8個字節中并不是連續分布的,這樣的設計我也想不通,像極了一開始沒有預留足夠的空間,后來又加上的。
注意Type字段有4個字節,X用來區分代碼段還是數據段,中間兩個比特在代碼段和數段中的解釋是不同的,在代碼段中,C=0表示該段的代碼不能被特權等級(DPL)比它低的段中的代碼調用,也就是說只有特權等級更高或相同的段才能調用這個段中的代碼,這就是保護模式的關鍵所在。
切換到32位保護模式:啟動超級變換形態
我們不能總是讓CPU在16位實地址模式下工作,在真正執行操作系統之前,我們必須將CPU切換到32位保護模式,一是因為效率更高,我們可以利用32位寄存器,二是因為32位保護模式下段尋址方式發生了變化,CPU可以拒絕非法的地址訪問。
進入32位保護模式同時也意味著我們要離開BIOS了,因為BIOS是為16位指令編譯的,沒法在32位模式下使用。因此一旦切換到32位保護模式,包括驅動屏幕,鍵盤,硬盤等都需要我們自己來編寫程序實現了,這絕對是大顯身手的好機會,不管怎樣,我們先切換過去再說。
切換到保護模式需要在內存中準備兩個東西,一個是GDT,一個是GDT描述符。CPU提供了lgdt指令來設置全局段描述符,這只是準備工作,真正讓CPU進入32位模式還需要將cr0寄存器的最后一位設置位1。注意,最然我們一直在說32位保護模式,但是它其實包含了兩個概念,32位模式和保護模式,要分別進行設置。
注意我們還沒介紹中斷向量表(IDT),所以目前我們要關閉中斷,因為32位模式下BIOS提供的那些中斷也不能用了,因為BIOS的程序都是16位指令,無法在32位模式下運行。所以,我們還是先關閉中斷吧。
;; 準備GDT gdt_start: gdt_null:dd 0, 0 ; 第一個段描述符必須是null gdt_code:dw 0xffff ; Limit(0-15位)dw 0x0 ; 基址(0-15位)db 0x0 ; 基址(16-23位)db 10011010b ; 第一個標志+類型標志db 11001111b ; 第二個標志+Limit(16-19位)db 0x0 ; 基址(24-31位) gdt_data:dw 0xffff ; Limit(0-15位)dw 0x0 ; 基址(0-15位)db 0x0 ; 基址(16-23位)db 10010010b ; 第一個標志+類型標志db 11001111b ; 第二個標志+Limit(16-19位) db 0x0 ; 基址(24-31位) gdt_end:;; GDT描述符 gdt_descriptor:dw gdt_end - gdt_start - 1 ; GDT大小,總是真實大小減一dd gdt_start ; GDT起始地址CODE_SEG equ gdt_code - gdt_start ; 代碼段描述符偏移量 DATA_SEG equ gdt_data - gdt_start ; 數據段描述符偏移量;; 切換保護模式 cli ; 關閉中斷,因為我們還沒有設置好保護模式下的中斷向量表 lgdt [gdt_descriptor] ; 設置GDT mov eax, cr0 or eax, 0x1 ; 將cr0寄存器的最后一位設置為1進入32位模式 mov cr0, eax jmp CODE_SEG:init_pm ; 進行一個遠跳轉(機器碼EA),清空指令流水線,; 因為CPU已經處于32位模式了,指令流水線中的16位指令需要清空掉bits 32 ; 編譯為32位指令,也可寫作[bits 32] ;; 初始化保護模式 init_pm:mov ax, DATA_SEG ; 在保護模式,舊的段已經沒有用了,mov ds, ax ; 所以,我們將段寄存器指向GDT中定義得數據段mov es, ax mov fs, ax mov gs, axmov ebp , 0x90000 ; 更新棧得位置mov esp , ebpjmp $ ; for {}上面就是切換到32位保護模式的過程了,關于GDT的設置,目前我們只做了最簡單可行的配置,數據段和代碼段是重疊的,作為示例,先這樣吧。
進入操作系統
我們已經介紹了足夠多的基礎知識了,也知道了操作系統是如何啟動的,我們已經在引導扇區里和BIOS玩的夠久了,是時候真正進入操作系統了。
;; 啟動扇區 org 0x7c00 ; 也可以寫作 [org 0x7c00]OS_ADDR equ 0x7e00 ; 操作系統內存地址 VIDEO_ADDR equ 0xb8000 ; 幀緩存地址 CODE_SEG equ gdt_code - gdt_start ; 代碼段描述符偏移量 DATA_SEG equ gdt_data - gdt_start ; 數據段描述符偏移量;; =================== ;; 1.讀盤 ;; 2.準備GDT ;; 3.切換32位保護模式 ;; 4.跳轉到操作系統 ;; ===================mov [boot_device], dl ; 保存引導盤的驅動器號 ;; 讀盤 mov ah, 0x2 ; 讀磁盤 mov al, 0x10 ; 扇區數,16×512B=8KB mov ch, 0 ; 柱面,從0開始 mov cl, 2 ; 扇區,從1開始 mov dh, 0 ; 磁頭,從0開始 mov dl, [boot_device] ; 驅動器號,0:軟驅A,1:軟驅B,0x80:磁盤C mov bx, OS_ADDR ; 數據地址 int 0x13 ; 磁盤中斷 jc read_disk_err ; 讀盤失敗則跳轉到read_disk_err ;; 清屏 mov ah, 0x6 ; 屏幕初始化或上卷 mov al, 0x0 ; 上卷行數,0表示整個窗口空白 mov bh, 0x0 ; 卷入后空出的行的寫入屬性 mov ch, 0x0 ; 左上角行號 mov cl, 0x0 ; 左上角列號 mov dh, 0x18 ; 右下角行號 mov dl, 0x4f ; 右下角列號 int 0x10 ; 觸發中斷 ;; 切換保護模式 cli ; 關閉中斷,因為我們還沒有設置好保護模式下的中斷向量表 lgdt [gdt_descriptor] ; 設置GDT mov eax, cr0 ; 將寄存器cr0的最 or eax, 0x1 ; 后一位設置為1后, mov cr0, eax ; CPU進入32位模式 jmp CODE_SEG:init_pm ; 執行一個遠跳轉(機器碼EA),清空CPU指令流水線read_disk_err:mov si, err_code_read_disk ; 錯誤碼寫入地址call hex2str ; 將錯誤碼轉換成十六進制字符串mov ah, 0x13 ; 顯示字符串mov al, 0x1 ; 寫入模式mov cx, 0x14 ; 字符串長度mov dh, 0x8 ; 顯示位置的行號mov dl, 0x0 ; 顯示位置的列號mov bp, err_read_disk ; 字符串地址mov bh, 0 ; 頁號mov bl, 0x4 ; 字符顯示屬性int 0x10 ; 打印字符串中斷jmp fin;; 函數 hex2str:pushaadd si, 1shr ax, 4shr al, 4call hex2asciisub si, 1shr ax, 8call hex2asciipoparethex2ascii:pushaadd al, 0x30cmp al, 0x39jle .numadd al, 0x7.num:mov [si], alpoparetfin:hltjmp finbits 32 ; 編譯為32位指令 init_pm:mov ax, DATA_SEG ; 在保護模式,舊的段已經沒有用了,mov ds, ax ; 所以,我們將段寄存器指向GDT中定義得數據段mov es, ax mov fs, ax mov gs, axmov ebp, 0x9fb00 ; 更新棧得位置,指向空閑空間的頂部mov esp, ebpcall OS_ADDR ; 跳轉系統內核,也就是os_main.fin:hltjmp .fin ; for {};; 數據 boot_device: db 0 err_read_disk: db 'read disk failed:' err_code_read_disk: db '??H', 0 msg_load_os: db 'loading os...', 0;; 準備GDT gdt_start: gdt_null:dd 0, 0 ; 第一個段描述符必須為null gdt_code: ; 代碼段dw 0xffff ; Limit(0-15位)dw 0x0 ; 基址(0-15位)db 0x0 ; 基址(16-23位)db 10011010b ; 第一個標志+類型標志db 11001111b ; 第二個標志+Limit(16-19位)db 0x0 ; 基址(24-31位) gdt_data: ; 數據段dw 0xffff ; Limit(0-15位)dw 0x0 ; 基址(0-15位)db 0x0 ; 基址(16-23位)db 10010010b ; 第一個標志+類型標志db 11001111b ; 第二個標志+Limit(16-19位) db 0x0 ; 基址(24-31位) gdt_end:;; GDT描述符 gdt_descriptor:dw gdt_end - gdt_start - 1 ; GDT大小,總是真實大小減一dd gdt_start ; GDT起始地址;; 填充引導扇區 db 510+$$-$ dup 0 ; 填充0直到510字節,$$=0x7c00 dw 0xaa55 ; 啟動扇區標識;; 系統內核 ; bits 32 os_main:mov word [VIDEO_ADDR], 0xf<<8|'A'jmp $db 512+os_main-$ dup 0;; 填充剩余扇區,因為前面我們讀了16個扇區 db 15*512 dup 0如果你在屏幕左上角看到了’A’,那就說明我們成功進入到了操作系統了。啥,這也能成功?在這里我不得不再次提醒各位,內存是一個數組,數組只有下標和值。CPU只會傻傻的一條接著一條執行指令,程序能否正確執行,主要看能不能跳轉到正確的指令,能不能找到正確的數據,而這其中的關鍵在于指令或數據的下標是不是正確,下標,下標,它真的很重要。
大家一定要想明白call OS_ADDR是如何跳轉到os_main的,這里我們取了巧,代碼在磁盤鏡像中的布局和在內存中的布局是一樣的,所以我們不需要使用org額外修正os_main的地址,是不是很神奇呢。
其實我們想做的事情非常簡單,將操作系統的機器指令讀到內存中,放在一個指定的位置,然后在完成一些基本的初始化和設定后跳轉到操作系統的第一條指令所在的地址,至此操作系統登基稱帝,接管一切事物。雖然說起來不難,但是這條登基之路卻是充滿坎坷的。如果把寫操作系統比作登山,那現在我們還只是在山腳下轉了轉,找到了一條上山的小路。
總結
以上是生活随笔為你收集整理的【操作系统】模式切换篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 买房建议啊
- 下一篇: JAVA计算机毕业设计预装箱式净水站可视