翻譯:從零開始編寫一個操作系統(tǒng)(第4章)

原文地址 Writing a Simple Operating System — from Scratch
GitHub 上有對應(yīng)的教程:https://github.com/cfenollosa/os-tutorial

32位保護(hù)模式

繼續(xù)16位模式可以加深我們的之前學(xué)到的內(nèi)容,這很好,不過,為了充分發(fā)揮 CPU 的作用,為了更好理解現(xiàn)代計算機(jī)是如何從 CPU 的發(fā)展中受益的(主要是硬件級別的內(nèi)存保護(hù)),我們必須要學(xué)習(xí) 32 位保護(hù)模式。

32位保護(hù)模式主要的不同有:

  • 寄存器拓展到了32位,其他的不變,只要在原先使用寄存器的地方前面加上 e,比如 mov ebx, 0x274fe8fe
  • 為了方便,有額外兩個新的通用段寄存器:fsgs。
  • 32位內(nèi)存地址偏移變的可能了,所以,現(xiàn)在一個偏移可以引用4GB的內(nèi)存(0xffffffff)
  • CPU 對內(nèi)存的段劃分支持的更好,(雖然也稍微有點(diǎn)復(fù)雜了),主要有下面兩個優(yōu)勢:
    • 一個普通的段內(nèi)的代碼在更高優(yōu)先級的段會被禁止執(zhí)行,所以你可以保護(hù)你的內(nèi)核代碼不被用戶應(yīng)用改變。
    • CPU 可以為用戶進(jìn)程實(shí)現(xiàn)虛擬內(nèi)存,這樣,一頁的進(jìn)程內(nèi)存可以被透明的交換到磁盤,當(dāng)需要的時候交換回到內(nèi)存中。這保證了內(nèi)存被有效的使用,因為很少被使用或者執(zhí)行的代碼不需要占用寶貴的內(nèi)存空間
  • 中斷處理更加成熟

將 CPU 從16位模式切換到32位保護(hù)模式最難的地方在于我們必須在內(nèi)存中準(zhǔn)備一個復(fù)雜的數(shù)據(jù)結(jié)構(gòu),叫做 GDT (global descriptor table),這張表定義了內(nèi)存的段和它們的保護(hù)模式屬性。一旦定義了 GDT,可以使用一個特殊的指令加載它,并確保在此之前沒有對 CPU 的任何控制寄存器設(shè)置內(nèi)容。

如果我們不需要在匯編中定義 GDT 的話,會很簡單。不幸的是,如果我們想加載高層語言比如C編譯的內(nèi)核的話,這種底層的切換是無法避免的,通常這種情況下,代碼會被編譯成32位指令而不是更低效的16位指令。

有一個令人震驚的事實(shí):切換到32位保護(hù)模式的時候,我們不能再使用 BIOS 了。如果你覺得使用 BIOS 太底層了,這就像是退了一步,但是前進(jìn)了兩步。

沒有 BIOS

為了更好的使用 CPU,我們必須拋棄所有 BIOS 提供的有用的例程。當(dāng)我們深入32位保護(hù)模式的切換的時候,我們會知道,所有的 BIOS 例程,是基于16位模式開發(fā)運(yùn)行的,這在32位中變得非法了。如果試著使用的話,很可能把機(jī)器弄崩潰了。

所以這意味著,所有32位的 OS 必須自己提供機(jī)器的所有硬件需要的驅(qū)動(比如鍵盤、屏幕、磁盤驅(qū)動,鼠標(biāo),等等)。事實(shí)上,將32位模式短暫的切換到16位然后使用 BIOS 是可能的,不過這種技術(shù)帶來的問題比解決的問題還多,尤其是性能相關(guān)的部分。

切換到32位碰到的第一個問題是如何在屏幕上打印信息,這樣我們就知道正在發(fā)生什么了。之前我們請求 BIOS 在屏幕上打印一個 ASCII 字符,但是它是如何做到將合適的像素展示在計算機(jī)屏幕恰當(dāng)?shù)奈恢蒙系哪兀磕壳?,只要知道顯示設(shè)備可以用很多種方式配置成兩種模式:文本模式和圖像模式。屏幕上展示的內(nèi)容只是某一特定區(qū)域的內(nèi)存內(nèi)容的視覺化展示。所以為了操作屏幕的展示,我們必須在當(dāng)前的模式下管理內(nèi)存的某特定區(qū)域。顯示設(shè)備就是這樣子的一種設(shè)備,和內(nèi)存相互映射的硬件。

當(dāng)大部分計算機(jī)啟動時候,雖然它們可能有更先進(jìn)的圖像硬件,但是它們都是先從簡單的視頻圖像數(shù)組(VGA,video graphics array)顏色文本模式,尺寸80*25,開始的。在文本模式,編碼人員不需要為每個字符渲染每一個獨(dú)立的像素點(diǎn),因為一個簡單的字體已經(jīng)在 VGA 顯示設(shè)備內(nèi)部內(nèi)存中定義了。每一個屏幕上字符單元,在內(nèi)存中通過兩字節(jié)表示,第一個字節(jié)被展示字符的 ASCII 編碼,第二個字節(jié)包含字符的一些屬性,比如字符的前景色和背景色,字符是否應(yīng)該閃爍等。

所以,如果我們想在屏幕上展示一個字符,那么我們需要為當(dāng)前的 VGA 模式,在正確的內(nèi)存地址處設(shè)置一個 ASCII 碼值,通常這個地址是 0xb8000。我們稍微改一下原先16位模式的 print_string 例程,我們就可以構(gòu)建一個32位模式的例程,它會將數(shù)據(jù)直接寫到視頻內(nèi)存中,如下所示:

[bits 32]
; Define some constants 
VIDEO_MEMORY equ 0xb8000 
WHITE_ON_BLACK equ 0x0f

; prints a null-terminated string pointed to by EDX

print_string_pm:
  pusha
  mov edx, VIDEO_MEMORY         ; Set edx to the start of vid mem.

print_string_pm_loop:
  mov al, [ebx]                 ; Store the char at EBX in AL 
  mov ah, WHITE_ON_BLACK        ; Store the attributes in AH

  cmp al, 0                     ; if (al == 0), at end of string, so 
  je done                       ; jump to done

  mov [edx], ax                 ; Store char and attributes at current
                                ; character cell.
  add ebx, 1                    ; Increment EBX to the next char in string. 
  add edx, 2                    ; Move to next character cell in vid mem.




jmp print_string_pm_loop        ; loop around to print the next char.

print_string_pm_done :
  popa
  ret                           ; Return from the function

注意,雖然屏幕是通過每一行每一列展示的,視頻相關(guān)內(nèi)存區(qū)域是簡單的序列,比如,第5列第3行的地址可以像下面這樣計算:0xb8000 + 2 * (row * 80 + col)

我們的代碼的缺點(diǎn)是它總是打印字符到屏幕的左上角,所以它會覆蓋之前的信息而不是滾動下去,我們可以花時間添加復(fù)雜的匯編代碼進(jìn)來,不過,我們不要把事情搞的太難了,既然我們已經(jīng)在32位模式了,我們不久就可以用高層語言編寫啟動代碼,然后很多工作就會變得簡單很多。

理解 GDT 表

在我們深入之前,理解 GDT 很重要,因為在32位模式下它十分的基礎(chǔ)?;貞浿暗恼鹿?jié),經(jīng)典的16位模式下基于段的地址運(yùn)行程序員訪問超過16位的地址內(nèi)容?,F(xiàn)在假設(shè)程序員希望將 ax 中的內(nèi)容存到地址 0x4fe56 中。沒有基于段的地址,只能這樣子:

mov [0xffff], ax

這行指令離預(yù)期的地址很遠(yuǎn)。與之對比的,使用段寄存器,這個任務(wù)可以通過下面這樣子完成:

mov bx, 0x4000
mov es, bx
mov [es:0xfe56], ax

雖然,段內(nèi)存和使用偏移到達(dá)內(nèi)存的想法沒有改變,32位實(shí)現(xiàn)的方式完全變了,主要是提供了更多的靈活性。一旦 CPU 切換到32位,它將邏輯地址(比如結(jié)合段寄存器和偏移)轉(zhuǎn)換到物理地址的方式完全不同了:不同于將段寄存器的內(nèi)容乘16,然后加上偏移,現(xiàn)在一個段寄存器變成了 GDT 表中的一個索引,指向一個特別的 段描述符 (SD,segment descriptor)。

一個段描述符是一個8字節(jié)的結(jié)構(gòu),定義了如下的保護(hù)模式段的屬性:

  • 基底層(32位),定義了物理內(nèi)存中段的開始地址
  • 段限制(20位),定義了一個段的大小
  • 各種標(biāo)志,影響了 CPU 是如何解釋段的,比如一個特權(quán)代碼是否能在它內(nèi)部執(zhí)行或者它是只讀的還是只寫的

下圖展示了段描述符的實(shí)際結(jié)構(gòu):

段描述符結(jié)構(gòu)

注意,為了避免迷惑,在整個結(jié)構(gòu)中基地址和段的限制大小都被從內(nèi)部分開存放了。所以,比如,段限制部分的低16位是在結(jié)構(gòu)體中的前面2個字節(jié),但是高4位在第7個字節(jié)的開始處。這么做的原因可能是出于開玩笑,也有可能是歷史原因或者受到了 CPU 硬件設(shè)計的影響。

我們不會去詳細(xì)了解所有段描述符的可能的配置,完整的解釋可以在因特爾的開發(fā)者手冊中找到。為了有助于將代碼在32位模式下跑起來,我們會學(xué)習(xí)我們需要的內(nèi)容。

Intel 描述了一個最簡單的段寄存器配置,叫做基本的平坦模型(basic flat model)。在這里,定義了兩個重疊的段,覆蓋了所有可以引用的4GB內(nèi)存,其中一部分是代碼一部分是數(shù)據(jù)。這個模型里面,兩個段重疊說明它沒有試圖保護(hù)其中一個段免受另一個段的影響,也沒有試圖在虛擬內(nèi)存中使用頁技術(shù)。讓事情變得簡單很重要,特別是當(dāng)我們啟動到高層語言的階段時,改變段描述符會更加簡單。

除了代碼段和數(shù)據(jù)段,CPU 需要 GDT 的第一項是一個非法的空描述符(比如,一個8字節(jié)的0)??彰枋龇怯糜谝粋€簡單的機(jī)制,即為了捕獲在訪問地址前忘記設(shè)置特定的段寄存器(這很容易發(fā)生,比如當(dāng)我們的段寄存器有些是 0x0,然后在切換到保護(hù)模式的時候,忘記更新它們?yōu)楹线m的值)。如果地方訪問的時候是一個空描述符,那么 CPU 會引發(fā)一個異常,也就是一個中斷(不要和高層語言比如 Java 中的異常相混淆)。

我們的代碼段將會有下面這些配置:

  • Base:0x0
  • Limit:0xfffff
  • Present: 1,因為段是在內(nèi)存中,用于虛擬內(nèi)存
  • Privilige:0,ring 0 是最高的優(yōu)先級
  • Descriptor type:對于代碼或者數(shù)據(jù)寄存器是1,對于 trap 是0
  • Type:
    • Code:1,因為這是一個代碼段
    • Conforming:0,意思是低優(yōu)先級的段的代碼,無法調(diào)用這個段的代碼,這是內(nèi)存保護(hù)的關(guān)鍵
    • Readable:1,意思是可讀,若為0表示只能用于執(zhí)行??勺x的話,允許我們讀取代碼中定義的常量
    • Accessed:0,這個通常用于調(diào)試或者虛擬內(nèi)存技術(shù),因為當(dāng) CPU 訪問段的時候,它會設(shè)置這個位
  • 其他 flags
    • Granularity:如果設(shè)置為1,它將以4K倍的方式擴(kuò)大我們的限制(即161616),所以我們的 0xfffff 會變成 0xfffff000(也就是左移3個16進(jìn)制位),允許我們的段擴(kuò)大到4GB 內(nèi)存
    • 32-bit default:1,因為我們的段會包含32位的代碼,不然就用0,表示16位代碼。這個實(shí)際上為操作設(shè)置了默認(rèn)的數(shù)據(jù)單元大小,比如 push 0x4 將表示32位的 0x4
    • 64-bit code segment:0,32位下不用
    • AVL:0,當(dāng)需要的時候(比如調(diào)試)可以設(shè)置,不過現(xiàn)在不需要。

因為用的是簡單平坦模式,使用兩個重疊的代碼和數(shù)據(jù)段,數(shù)據(jù)段和代碼段差不多,但是 type 標(biāo)志不一樣:

  • Code:對于 data 是 0
  • Expand down:0,這允許段被擴(kuò)展下來(TODO,解釋這里)
  • Writable:1,表示允許數(shù)據(jù)段被寫,不然的話是只讀的。
  • Accessed:0,經(jīng)常用于調(diào)試和虛擬內(nèi)存技術(shù),因為 CPU 訪問段內(nèi)存時候會設(shè)置這一位。

既然我們已經(jīng)知道兩個段的實(shí)際的配置了,也了解了大部分可能的段描述符設(shè)置,保護(hù)模式為什么在內(nèi)存使用提供了比16位更多的靈活性應(yīng)該也比較清楚了。

使用匯編定義 GDT

現(xiàn)在我們理解了對于我們基本平坦模型,我們要包含怎樣的段描述符在 GDT 中,讓我們看看要如何在匯編中設(shè)置 GDT,這里最需要的是耐心!當(dāng)你覺得這個很無聊的時候,記?。何覀儸F(xiàn)在做的將會在不久幫助我們啟動用高層語言寫的 OS 內(nèi)核。引用一句名言,現(xiàn)在的一小步將是未來的一大步!

我們已經(jīng)知道如何在匯編代碼中定義數(shù)據(jù):使用 db、dwdd匯編指令。這些就是我們在設(shè)置 GDT 項中段描述符字節(jié)的時候?qū)⒁褂玫摹?/p>

事實(shí)上,出于一個簡單的原因(CPU 需要中斷我們的 GDT 表有多長),我們實(shí)際不會直接把 GDT 表的地址給 CPU,而是將一個更加簡單的結(jié)構(gòu)數(shù)據(jù)的地址給 CPU,也就是 GDT 描述符(意思就是一種描述 GDT 的東西)。GDT 描述符是一個6字節(jié)的結(jié)構(gòu)包含下面這些:

  • GDT 的大小(16位)
  • GDT 的地址(32位)

注意,像這種復(fù)雜的數(shù)據(jù)結(jié)構(gòu),在底層語言中我們沒法給出很詳細(xì)的注釋。下面的代碼定義了我們的 GDT 和 GDT 描述符,在代碼中,注意我們是如何使用 db、dw 這些指令的,如何完善結(jié)構(gòu)的每一部分以及標(biāo)志位是如何使用字面二進(jìn)制數(shù)字(后綴為 b)被輕松的定義:

; GDT
gdt_start:

gdt_null:     ; the mandatory null descriptor
dd 0x0        ; ’dd’ means define double word (i.e. 4 bytes)
dd 0x0

gdt_code:     ; the code segment descriptor
  ; base=0x0, limit=0xfffff,
  ; 1st flags: (present)1 (privilege)00 (descriptor type)1 -> 1001b
  ; type flags: (code)1 (conforming)0 (readable)1 (accessed)0 -> 1010b
  ; 2nd flags: (granularity)1 (32-bit default)1 (64-bit seg)0 (AVL)0 -> 1100b

  dw 0xffff   ; Limit (bits 0-15)
  dw 0x0      ; Base (bits 0-15)
  db 0x0      ; Base (bits 16-23)
  db 10011010b; 1st flags, type flags
  db 11001111b; 2nd flags, Limit (bits 16-19)
  db 0x0      ; Base (bits 24-31)

gdt_data:     ; the data segment descriptor
  ; Same as code segment except for the type flags:
  ; type flags: (code)0 (expand down)0 (writable)1 (accessed)0 -> 0010b

  dw 0xffff   ; Limit (bits 0-15)
  dw 0x0      ; Base (bits 0-15)
  db 0x0      ; Base (bits 16-23)
  db 10010010b; 1st flags, type flags
  db 11001111b; 2nd flags, Limit (bits 16-19)
  db 0x0      ; Base (bits 24-31)

gdt_end:      ; The reason for putting a label at the end of the
              ; GDT is so we can have the assembler calculate
              ; the size of the GDT for the GDT decriptor (below)

; GDT descriptior
gdt_descriptor:
  dw gdt_end - gdt_start - 1  ; Size of our GDT, always less one
                              ; of the true size
  dd gdt_start                ; Start address of our GDT

; Define some handy constants for the GDT segment descriptor offsets, which
; are what segment registers must contain when in protected mode. For example,
; when we set DS = 0x10 in PM, the CPU knows that we mean it to use the
; segment described at offset 0x10 (i.e. 16 bytes) in our GDT, which in our
; case is the DATA segment (0x0 -> NULL; 0x08 -> CODE; 0x10 -> DATA)

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

切換32位模式

一旦 GDT 和 GDT 描述符在我們的啟動代碼中準(zhǔn)備好了,我們就已經(jīng)準(zhǔn)備好讓 CPU 從16位切換到32位模式了。

像我之前說的,實(shí)際的切換的代碼是相當(dāng)簡單的,不過理解代碼中的每一步很重要。

第一件我們要做的事是禁止中斷指令:cli (清除中斷,clear interrupt),意味著 CPU 會簡單的忽略將來發(fā)生的任何中斷,直到中斷被重新打開。這非常重要,因為,像基于段的地址訪問一樣,32位下的中斷處理完全不同于16位,導(dǎo)致目前 BIOS 在內(nèi)存中設(shè)置的 IVT 完全失效。即使 CPU 可以將中斷信號映射到正確的 BIOS 例程(比如用戶按了一個鍵盤鍵位,將它的值存到緩沖區(qū)中),因為 BIOS 例程是工作在16位模式的,沒有我們定義在 GDT 中的32位段概念,所以最終一定會把 CPU 弄崩潰(段寄存器存在的值意味著16位的段模式)

下一步是把我們之前幸苦準(zhǔn)備的 GDT 表告訴 CPU。我們使用一個簡單的指令來完成,這個指令需要 GDT 描述符:

lgdt [gdt_descriptor]

一切就緒,開始真正的切換操作,通過設(shè)置 CPU 一個特殊的控制寄存器的第一位:cr0?,F(xiàn)在,我們不能直接設(shè)置寄存器的位,我們必須加載它到一個通用目的寄存器,設(shè)置位,然后存回到 cr0 中。和前面章節(jié)我們使用 and 指令來排除一個值中的無效的位,我們可以使用 or 指令來包含特定的位到一個值中(不會影響其他設(shè)置在控制寄存器中的位,這些位可能被設(shè)置成用于別的重要目的)。

mov eax, cr0  ; To make the switch to protected mode, we set
or eax , 0x1  ; the first bit of CR0 , a control register
mov cr0, eax  ; Update the control register

當(dāng) cr0 被更新之后,CPU 就在32位模式了。

最后一句話不完全正確。因為現(xiàn)代的處理器使用一種被叫做指令流水的技術(shù)。這種技術(shù)允許并行的執(zhí)行不同階段的指令(這里的并行是單個 CPU 中發(fā)生的,而不是多個 CPU),所以會更加快。比如,每一個指令可能先從內(nèi)存中獲得,然后解碼成微指令,再執(zhí)行,然后可能結(jié)果會存回到內(nèi)存中。因為上述這些階段是半獨(dú)立的,它們可以在一個 CPU 周期內(nèi)被同時完成,不過,每個階段屬于不同指令的不同周期(比如說,前一條指令可以在被解碼的同時從內(nèi)存中讀取下一條指令)。

一般為 CPU 編程的時候,我們不需要擔(dān)心 CPU 內(nèi)部的機(jī)制,比如指令流水,不過,切換 CPU 模式情況比較特殊。因為存在風(fēng)險,比如可以某些指令的處理階段被執(zhí)行在錯誤的模式下。所以在讓 CPU 切換模式后,我們需要立刻要求 CPU 完成當(dāng)前流水中的工作。這樣我們就可以確保未來的指令將被執(zhí)行在正確的模式下。

當(dāng) CPU 知道后面將要執(zhí)行的幾條指令,指令流水工作的很好,因為 CPU 可以預(yù)先從內(nèi)存中獲取他們。但是 jmpcall 指令有點(diǎn)不太一樣。因為除非所有的指令都已經(jīng)被執(zhí)行完了,不然 CPU 不知道它們之后將要執(zhí)行的指令,尤其當(dāng)我們要跳轉(zhuǎn)到或者調(diào)用一個很遠(yuǎn)的地方,可能意味著我們會跳轉(zhuǎn)到別的段中。所以,在切換 CPU 模式之后,我們可以立刻調(diào)用跳轉(zhuǎn)指令來跳到很遠(yuǎn)的地方,這會強(qiáng)制要求 CPU 處理完指令流水中剩下的內(nèi)容(即完成所有指令流水中處于不同階段的指令)。

為了跳的遠(yuǎn),不同于一個正常的跳轉(zhuǎn),我們額外提供一個地點(diǎn)段:

jmp <segment>:<address offset>

對于這條指令,我們要仔細(xì)思考我們想要跳到哪里。假設(shè)我們已經(jīng)在代碼中設(shè)置了一個標(biāo)簽比如說是 start_protected_mode,指的是我們的32位代碼開始的地方。就像我們討論過的,一個近的跳轉(zhuǎn),比如 jmp start_protected_mode 可能還不足以清空當(dāng)前的流水線,而且,我們現(xiàn)在處于一種奇怪的狀態(tài),因為我們的當(dāng)前代碼段(比如 cs)在32位下是非法的。所以我們必須更新 cs 寄存器為 GDT 中的代碼段描述符。段描述符每個8字節(jié)長,并且我們的代碼描述符是 GDT 中的第二項(第一項是空描述符),所以它的偏移是 0x8,這就是我們要設(shè)置 cs 寄存器的值。注意,由于要跳到很遠(yuǎn)的地方執(zhí)行,它會自動引起 CPU 更新 cs 寄存器為目標(biāo)段的值。利用標(biāo)簽,我們可以讓匯編器去計算這些段描述符偏移,然后將他們存為 CODE_SEGDATA_SEG 常量。下面是相應(yīng)代碼:

jmp CODE_SEG:start_protected_mode

[bits 32]
start_protected_mode:

...           ; By now we are assuredly in 32-bit protected mode. 
...

注意,事實(shí)上,從實(shí)際開始跳和跳到的地方之間的距離的角度,我們不需要跳轉(zhuǎn)到很遠(yuǎn),重要的是我們?nèi)绾翁D(zhuǎn)。

也要注意,我們要使用 [bits 32] 指令告訴匯編器,從這里開始,它需要編碼32位模式的指令。注意這并不意味著我們不能在16位模式下使用32位指令,只是說匯編器需要知道32位的情況,因為32位模式的指令和16位下編碼有一點(diǎn)點(diǎn)不同。當(dāng)切換到32位模式,我們使用32位寄存器 eax 來設(shè)置控制位。

現(xiàn)在我們在32位模式下了。進(jìn)入32位模式之后一件重要的要做的事是更新所有的其他段寄存器,(令它們指向我們32位的數(shù)據(jù)段而不是已經(jīng)非法的16位段)并更新棧的位置。

所有這些處理歸結(jié)為如下代碼:

[bits 16]
; Switch to protected mode
switch_to_pm:

cli           ; We must switch of interrupts until we have 
              ; set-up the protected mode interrupt vector
              ; otherwise interrupts will run riot.

lgdt [gdt_descriptor]   ; Load our global descriptor table, which defines
                        ; the protected mode segments (e.g. for code and data)

mov eax , cr0         ; To make the switch to protected mode, we set
or eax, 0x1           ; the first bit of CR0, a control register
mov cr0 , eax

jmp CODE_SEG:init_pm  ; Make a far jump (i.e. to a new segment) to our  32-bit
                      ; code. This also forces the CPU to flush its cache of
                      ; pre-fetched and real-mode decoded instructions, which can 
                      ; cause problems.

[bits 32]
; Initialise registers and the stack once in PM.
init_pm:

  mov ax, DATA_SEG    ; Now in PM , our old segments are meaningless ,
  mov ds, ax          ; so we point our segment registers to the
  mov ss, ax          ; data selector we defined in our GDT
  mov es, ax 
  mov fs, ax 
  mov gs, ax

  mov ebp, 0x90000    ; Update our stack position so it is right 
                      ; at the top of the free space.
  mov esp , ebp

  call BEGIN_PM       ; Finally, call some well-known label

總結(jié)

終于,我們可以把所有的例程包含到啟動代碼中了,并實(shí)現(xiàn)了16位到32位的切換。

; A boot sector that enters 32-bit protected mode. 
[org 0x7c00]

mov bp, 0x9000          ; Set the stack.
mov sp, bp

mov bx, MSG_REAL_MODE 
call print_string

call switch_to_pm       ; Note that we never return from here.

jmp $

%include "../print/print_string.asm" 
%include "gdt.asm"
%include "print_string_pm.asm" 
%include "switch_to_pm.asm"

[bits 32]

; This is where we arrive after switching to and initialising protected mode.
BEGIN_PM:

mov ebx , MSG_PROT_MODE
call print_string_pm      ; Use our 32-bit print routine.

jmp $                     ; Hang.

; Global variables
MSG_REAL_MODE db "Started in 16-bit Real Mode", 0
MSG_PROT_MODE db "Successfully landed in 32-bit Protected Mode", 0

; Bootsector padding 
times 510-($-$$) db 0 
dw 0xaa55
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容