原文地址 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。 - 為了方便,有額外兩個新的通用段寄存器:
fs和gs。 - 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)中基地址和段的限制大小都被從內(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、dw 和 dd匯編指令。這些就是我們在設(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)存中獲取他們。但是 jmp 和 call 指令有點(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_SEG 和 DATA_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