0x110-從頭開始寫操作系統-CPU模擬器

目錄

回顧

系列開篇講了計算機啓動時的情況。計算機啓動時,BIOS 做硬件檢查,然後按順序讀取存儲介質上 512 字節長的 boot sector。如果讀到某個存儲介質的 boot sector 最後 2 個字節是 0xaa55,就加載該介質上的操作系統,將控制權交給該操作系統。

CPU 模擬器

上篇中我們有了一個 512 字節的 boot sector:

e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa

現在,我們要講如何測試這段程序。

有三種選擇:

  • 將這 512 個字節寫入到 USB 等存儲介質,然後重啓電腦,設置 BIOS 到從 USB 啓動
  • 使用 Vmware 或者 Virtualbox 等虛擬機軟件,選擇包含 boot sector code 的文件作爲啓動介質
  • 使用 CPU 模擬器,如 Bochs、QEMU

第一種方式不推薦,光是寫入 raw bytes 到 USB 就是一大堆麻煩事。看這裏這裏還有這裏,另外,還要重啓電腦。

推薦使用第三種方式,同時,推薦使用 QEMU,它比其他產品更高效。

這裏不討論 CPU 模擬器的工作原理,有興趣的小夥伴可以可以 看這裏, 還有這篇 How to Write a Computer Emulator

接下來,我們會使用 QEMU 對所有代碼進行測試。

引導扇區編程(16-bit Real Mode)

在開始之前,我們先看一下什麼是 16-bit Real Mode 以及它的內存管理。

這個系列文章討論的是在 8086 的架構上構建一個操作系統。因爲爲了向前兼容,所有現代 CPU 啓動時都處於16 bit real mode,爲的是模擬 x86 架構中最古老的一代 CPU,也就是 8086。

所以,後面提到的 16 bit 系統和 8086 架構會有交叉使用的地方,統一代指 16 bit real mode 系統。

什麼是 16-bit Real Mode?

16-bit real mode, 也稱爲 16-bit real address mode。是所有 x86 CPU 一種運行模式(另一種常見的是 32-bit Protected Mode)。爲了向前兼容,所有 x86 CPU 啓動時,都處在 16-bit real mode。

16 bit real mode 下,內存管理採用內存分段的方式。

16 bit real mode 下,內存沒有保護機制,所有程序可以隨意訪問任意段內存的內容。

我發現有些東西不能精簡,這是實踐過程中必須弄懂的一些東西。所以我們先展開這個話題,討論一些基礎的東西,爲今後能走的更快更遠。

16-bit 系統中的 16 是什麼意思?

16 位系統,是早期的一種計算機系統架構。那時的 CPU 工藝沒有現在那麼發達,CPU 的能力有限,一次性處理的數據量相比現在也小很多。

CPU 上有兩條總線(BUS),一條地址總線(Address Bus),一條數據總線(Data Bus)。

地址總線負責傳遞內存地址給 CPU,讓 CPU 知道哪裏去儲存或者讀取數據。數據總線負責傳遞地址總線內存地址上的具體數據。

地址總線的寬度決定了系統能夠尋址的總量。

通常情況下,16-bit 系統上,這兩條總線的寬度也是 16 bit。意味着,可以尋址的總量是 64 KB (216 bits),一次傳輸的數據量,是 64 KB (216 bits)。

16-bit 系統架構上,CPU 支持 16 種不同的狀態。每一個狀態可以是開,或者關。意味着,CPU 一個時鐘週期能處理 16 bits 的數據量。再多的數據,就要等到下一個時鐘週期去處理。

同樣的,16-bit 系統的寄存器,也是 16-bit。意味着每個寄存器能存放 65536 (216)個不同的數字。

相應的 32-bit,64-bit 系統,是相同的道理。

8086 架構的內存尋址總量

上面說到了 16-bit 系統的地址總線是也是 16-bit,該系統的尋址總量爲:

216 Bits = 65535 Bits ~= 64 KB

所以,16-bit 系統的尋址總量,大約 64 KB,換句話說,就是 16 位系統只識別 64 KB 內存。

具體看 8086 CPU,寄存器是 16-bit,數據總線也是 16-bit。但是,地址總線被設計成有 20 bit。

因此,8086 CPU 的尋址總量,根據前文所說,就不止 64 KB,而達到了 1 MB(220 bits)。

這裏先提出一個問題,帶着問題讀下去,20 bit 的地址,怎麼放入 16 bit 的寄存器中存儲呢?

8086 架構的內存管理

一切數據都存在與內存當中,所以掌握內存的動向是深入理解底層運作的關鍵。我們一起看一下什麼是內存分段,以及 16-bit real mode 下物理內存地址的計算。

什麼是內存分段?

爲了更加高效利用內存,減少內存碎片,有很多的內存管理技術被髮明出來。內存分段是其中一種。

內存分段的過程,是將一個程序加載到不同的不連續的內存段中。例如,一些簡單的程序,數據將被加載到數據段,代碼被加載到代碼段,其他變量等,加載到堆棧段。複雜的程序,會被加載到更多的段中。

8086 CPU 有 4 個段寄存器,分別是:

  • Code Segment Register (CS) - 用來指向代碼段在內存中的基地址
  • Data Segment Register (DS) - 用來指向數據段在內存中的基地址
  • Extra Segment Register (ES) - 額外的數據寄存器
  • Stack Segment Register (SS) - 用來指向堆棧段在內存中的基地址

內存分段在內存中的示意圖:

圖片來自 geekforgeeks

因爲 8086 是 16 位的,每一個段的長度,最小 16 KB,最大隻能是 64 KB (216 bits)。

16-bit Real Mode 物理內存地址的計算

現在我們來回答前文的問題,20-bit 的地址,怎麼放入 16-bit 的寄存器中存儲呢?

回想一下,大家多少都聽說過 實際內存地址 = 段地址 * 10H (16 進制,相當於二進制左移 4 位) + 偏移地址 的公式。現在我們來講解一下這是怎麼來的。

8086 CPU 是 16 位的,要尋址到 1 MB,就要彌補這 4 bits 的差距。

CPU 內部處理內存地址,實際上是邏輯地址,或者虛擬地址,也是 16 位的。前文問題的答案是,20-bit 的地址,不能放入 16-bit 的寄存器,20-bit 物理地址的尋址,要使用兩個寄存器來完成。

步驟如下:

  • 將段地址存放到某一個段寄存器,比如 CS
  • 將偏移地址存放到 IP (Instruction Pointer)
  • 8086 內部將 CS 的值左移 4 位,最低有效位 (LSB) 補 0
  • 將 CS 左移後的值加上 IP 的值
  • 得到真實物理地址

這就是 20 bit 的地址總線,和 16 bit 的寄存器配合工作的原理。也說明了上述真實物理地址計算公式的來歷。

左移 4 位,是因爲要滿足這 20 bit 地址總線可以訪問 1 MB 內存的設計。

這裏說一下,設計上,幾個寄存器是搭配使用的:

  • CS:IP
  • DS:SI
  • ES:DI
  • SS:SP

更多寄存器配對使用的信息看這裏

再重申一下物理內存地址的計算公式:

8086 架構物理地址 = 段地址 * 10H + 偏移地址

更多關於 16-bit Real Mode 的信息,可以看這兩篇文章,16-bit Real Mode Wiki,以及 OSDev 16-bit Real Mode

第一個引導扇區程序

講了這麼多,終於到了編寫第一個引導扇區程序的時候。有了上面的鋪墊,後面的一些概念理解起來會更簡單。

開始代碼之前,還有一個點需要了解一下。

中斷

計算機有許多最底層的內在機制,如操控屏幕像素,顯示字符。在計算機啓動的最初始的階段,我們沒有 C 語言這樣的高級庫,去和底層交互。目前,如果我們想調用計算機的這些底層能力,只有一種方式,那就是中斷

中斷的作用

顧名思義,中斷髮生的時候,CPU 將暫時停止正在進行的任務,轉而去執行一些指定的優先級更高的操作,然後再返回去繼續原來的任務。

中斷可以由軟件觸發(如下面要講的 0x10 中斷,由彙編指令 int 0x10 觸發),也可以由某些硬件觸發,比如某個硬件要讀取一些網絡數據,這個操作的優先級比 CPU 正在執行的任務更高,那麼這個硬件就觸發一箇中斷,讓 CPU 去讀取數據。

中斷,Interrupt Vector 和 ISR

每一箇中斷,都由一個對應的唯一的數值表示。這個數值,代表該中斷在 interrupt vector 中的索引。

這張 interrupt vector table,包含了指向 interrupt servie routines (ISRs) 的內存地址。

而 ISR 當中,包含的是每個中斷所要執行的的機器指令,例如從磁盤讀取數據等。

舉個例子:

0x10 中斷,調用 ISR 中與屏幕相關的機器指令。0x13 中斷,調用 ISR 中與磁盤讀寫相關的機器指令。

中斷的觸發不完全靠中斷數字的值,還可以依靠通用寄存器中(如 ax 寄存器)的存儲的值。就像是另一個 if 語句。

if interrupt_number == 0x10:
	if ax == 1:
		do_something();
	else if ax == 2:
		do_other_things();

常見中斷

這裏有一些常見中斷,關於中斷的更多信息,可以 參考這篇文章

圖片來自 wiki

Hello World

編程的開始離不開 Hello World 😄

我們將用匯編編寫一個 Hello World 程序,當 QEMU 運行這個程序的時候,BIOS 會在屏幕上顯示 Hello World 字符串。

8086 CPU 有 4 個通用寄存器,ax,bx,cx,dx,他們的長度都是 16 bits (位),2 bytes(字節),1 word (字)。

boot_sect.asm 代碼:

mov ah , 0x0e  ; ax 中寫入 0x0e,代表 scrolling teletype BIOS routine,可以在屏幕輸出字符
mov al,'H'
int 0x10
mov al,'e'
int 0x10
mov al,'l'
int 0x10
mov al,'l'
int 0x10
mov al,'o'
int 0x10
mov al,'W'
int 0x10
mov al,'o'
int 0x10
mov al,'r'
int 0x10
mov al,'l'
int 0x10
mov al,'d'
int 0x10
jmp $  ; 跳轉到當前地址(無限循環)
times 510 - ($ - $$) db 0 ; 512 個字節中剩餘字節全部填充 0,$ 表示當前指令的地址,$$ 表示當前代碼 section 開始的地址,也就是 mov ah , 0x0e 指令的地址
dw 0xaa55 ; 最後一個字節,是 0xaa55,讓 BIOS 知道這是 boot sector

使用 nasm 編譯:

nasm boot_sect.asm -f bin -o boot_sect.bin

使用 QEMU 測試:

qemu boot_sect.bin

使用 od 查看 16 進制內容:

od -t x1 -A n boot_sect.bin

運行如下:

在這裏插入圖片描述

od 查看原始數據:

可以看到 cd 即彙編中斷指令 inteb 即彙編跳轉指令 jmp,最後兩個字節,0xaa55 告訴 BIOS,這是一個正規的 boot sector。

在這裏插入圖片描述

第一個程序測試結束。

總結

  • QEMU 是我們測試代碼的工具
  • 16-bit Real Mode 是 x86 系列 CPU 的一種工作模式,所有 x86 系列 CPU 啓動時,都處於 16-bit Real Mode
  • 16 bit 系統中,只有 64 KB 內存可以被識別,CPU 一次只能處理 16 bit 的數據
  • 8086 架構,因爲有 20 bit 的地址總線,所以尋址總量有 1 MB
  • 8086 架構的內存管理採用內存分段機制,內存分段將程序不同的部分加載到不同的不連續的內存中
  • 16-bit Real Mode 真實物理地址的計算公式爲:物理地址 = 段地址左移 4 位 + 偏移地址
  • 中斷可以讓 CPU 暫時停止當前任務,執行我們指定的任務,然後回去執行原先的任務
  • 中斷在 interrupt vector 中由一個數值做索引,interrupt vector 包含 interrupt service routines 的內存地址
  • 第一個 Hello World 程序使用中斷,在屏幕上顯示 HelloWorld 字符串

下一篇,我們會看到物理地址計算在代碼中的應用,以及一些基礎彙編,如字符串定義,條件控制,堆棧的使用等。


推薦閱讀(參考鏈接):

  • https://unix.stackexchange.com/questions/292933/how-can-i-write-raw-data-to-a-usb-device
  • https://stackoverflow.com/questions/15570526/sending-hex-over-serial-with-python
  • https://stackoverflow.com/questions/48502300/writing-bytes-on-usb-device
  • https://stackoverflow.com/questions/448673/how-do-emulators-work-and-how-are-they-written#:~:text=Basic%20idea%3A,like%20wires%20do%20in%20hardware.
  • https://fms.komkon.org/EMUL8/HOWTO.html
  • https://wiki.osdev.org/Real_Mode#:~:text=Real%20Mode%20is%20a%20simplistic,begin%20execution%20in%20Real%20Mode.
  • https://en.wikipedia.org/wiki/Real_mode
  • https://wiki.osdev.org/Real_Mode#High_Memory_Area
  • https://en.wikipedia.org/wiki/Memory_segmentation
  • https://www.geeksforgeeks.org/memory-segmentation-8086-microprocessor/
  • https://www.geeksforgeeks.org/memory-segmentation-8086-microprocessor/
  • https://medium.com/@esmerycornielle/the-cpu-and-the-memory-2eb300d6c72d
  • https://en.wikipedia.org/wiki/16-bit_computing#:~:text=A%2016%2Dbit%20register%20can%20store%20216%20different%20values.&text=Since%20216%20is%2065%2C536,offsets%2C%20more%20can%20be%20accessed.
  • https://simple.wikipedia.org/wiki/Address_bus#:~:text=An%20address%20bus%20is%20a,bus%20to%20access%20memory%20storage.
  • https://superuser.com/questions/874363/how-many-bytes-a-64-bit-processor-processes
  • https://www.sciencedirect.com/topics/engineering/address-offset
  • https://study.com/academy/lesson/memory-segmentation-definition-purpose.html#:~:text=Segmentation%20describes%20the%20system%20of,segmented%20processes%20to%20be%20loaded.
  • https://en.wikipedia.org/wiki/Intel_8086
  • https://www.geeksforgeeks.org/general-purpose-registers-8086-microprocessor/
  • https://en.wikipedia.org/wiki/BIOS_interrupt_call
  • http://homepage.smc.edu/morgan_david/cs40/segmentation.htm
  • http://www.ti.com/sc/docs/products/micro/msp430/userguid/ag_04.pdf
  • https://www.doc.ic.ac.uk/eedwards/compsys/memory/index.html#::text=Main%2Dmemories%20generally%20store%20and,bit%20word%20%3D%204%20bytes).
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章