從零開始制作 MyOS(一)
從零開始制作 MyOS - 最簡單的操作系統內核
開發環境
- 操作系統:ubuntu22 (windows10 + VMware15pro + ubunut22 + qemu)
- 編譯器:gcc-multilib
- 匯編器:nasm
- 模擬器: QEMU
- 版本控制: git
安裝依賴
ubuntu22 中:
# 安裝必要的工具鏈
sudo apt update
sudo apt install -y build-essential
sudo apt install -y qemu-system-x86
sudo apt install -y nasm # x86架構匯編器
sudo apt install -y gdb
sudo apt install -y git
sudo apt install -y mtools # 用于制作磁盤鏡像
# 安裝交叉編譯器(重要!避免使用宿主系統的libc)
sudo apt install -y gcc-multilib
前置知識
- x86 匯編語言:寄存器,實模式 vs 保護模式,中斷和異常,CPU 特權級
- C 語言編程
- 硬件基礎知識:
- 引導過程:當你按下電源鍵時,發生了什么事
- BIOS/UEFI:它們做了什么
- 內存映射:硬件設備(如 VGA 顯存)在內存中的位置
實模式 VS 保護模式
實模式是處理器的初始狀態,能夠將內存視為連續的,無保護的物理空間,能夠通過簡單的算術運算擴展尋址范圍;而保護模式則是通過硬件強制實施內存訪問策略,將物理內存抽象成虛擬地址空間后的一種內存訪問模式。
實模式下,程序能直接物理地址訪問,而內存訪問的范圍則是在 1MB,也就是 20 位地址線內,不支持多任務,也沒有內存保護。
保護模式下,物理地址被抽象成虛擬內存,程序通過分段和分頁訪問,最高能訪問到 4GB 的內存范圍。
保護模式是現代操作系統的基礎。
計算機啟動過程
當計算機上電后,位于 SPI Flash ROM 中的 BIOS 程序會被運行,該程序的任務是初始化計算的硬件,并且尋找可引導設備,這個可引導設備就是我們要開發的操作系統。
BIOS 在扇區 0 中找到有效地可引導設備后,就會將 CPU 的控制權轉移過去,執行可引導設備程序。
關于 BIOS 程序
- BIOS 引導程序物理存儲地址是在 SPI Flash ROM ,也就是 串行外設接口閃存只讀存儲器 ,這個存儲器是焊接在主板上,容量一般為 16 MB ~ 32 MB,斷電后不丟失數據。
- 在現代計算機中,傳統的 BIOS 被 UEFI,也叫做 統一可擴展固件接口替代,它的存儲位置也是在 SPI Flash 芯片中。
- 由硬件廠商開發,BIOS 廠商根據芯片廠商提供的規范來負責編寫 BIOS 代碼
- BIOS 的任務
- 上電自檢:檢查關鍵硬件,包括 CPU,內存,芯片組等;然后初始化系統管理總線(SMBus),并且驗證硬件完整性和兼容性
- 硬件初始化:設置 CPU 微代碼更新,配置內存控制器和時序參數,初始化 PCIe 設備枚舉,設置 USB,SATA 控制器
- 運行時服務建立:創建中斷向量表,建立 BIOS 數據區,提供系統調用接口(INT,13h 磁盤服務等)
第一步:制作一個最簡單的操作系統內核
啟動電腦時,BIOS 會做自檢,然后找到第一個可以啟動的設備,讀取該設備的第一個扇區(512 字節),如果該扇區最后兩個字節是 0x55 和 0xAA,BIOS 會認為這是一個有效的引導扇區,并將其加載到內存 0x7c00 處執行。
下面我們使用匯編程序編寫一個最簡單的引導程序,也就是一個 boot.asm :(必須使用匯編語言)
; boot.asm - simple BIOS boot sector (512 bytes)
; Assembled with: nasm -f bin -o boot.bin boot.asm
org 0x7C00
bits 16
start:
cli ; disable interrupts while setting up stack
xor ax, ax
mov ss, ax
mov sp, 0x7C00 ; stack grows down from 0x7C00
sti ; enable interrupts
mov si, msg ; pointer to message
.print_char:
lodsb ; al = [si], si++
cmp al, 0
je .hang
mov ah, 0x0E ; BIOS teletype function
mov bh, 0x00 ; page
mov bl, 0x07 ; color/attribute (for teletype this selects fg color)
int 0x10
jmp .print_char
.hang:
cli
hlt
; do not loop: single HLT to hang the CPU. If an interrupt occurs (shouldn't, because
; interrupts are disabled), execution could continue into the padding; we intentionally
; avoid an explicit jump here so the CPU remains halted instead of spinning.
msg db "Hello, OS! Booted from boot.asm", 0
; pad to 510 bytes so that signature is at offset 510-511
times 510 - ($ - $$) db 0
dw 0xAA55
# 編譯匯編文件
nasm -f bin boot.asm -o boot.bin
# 使用 QEMU 運行
qemu-system-x86_64 boot.bin
# vscode 終端 ssh 執行 QEMU 結果
qemu-system-x86_64 -nographic -monitor none -serial mon:stdio -drive file=boot.bin,format=raw,index=0,if=floppy
qemu-system-x86_64 -nographic -serial mon:stdio -drive format=raw,file=boot_serial.bin
運行結果
- 會出現一個 QEMU 的黑屏窗口
- 打印出 “"Hello, OS! Booted from boot.asm"”
代碼詳解
boot.asm 文件都是匯編指令,下面對指令和它背后的意義做一個簡單介紹:
- boot.asm
它是一個最小的 BIOS 引導扇區,做了以下工作:
- 被 BIOS 加載到物理地址 0x0000:0x7C00(也就是線性地址 0x7C00)并從那里執行。
- 在屏幕上打印一行文本(通過 BIOS int 0x10 teletype 服務)。
- 進入 halt 循環停止執行。
- 文件被填充到 512 字節并以 0x55AA 結尾(這是 BIOS 引導簽名)。
代碼設計步驟:
- org 0x7C00 + bits 16:引導扇區在實模式下,并且 BIOS 把第一個扇區加載到 0x0000:0x7C00,因此必須讓匯編器使用那個基址來生成正確的地址。
- 填充到 512 字節并寫入 0x55AA:滿足 BIOS 的最小引導扇區約定。
- 使用 BIOS int 服務(int 0x10)來打印:簡單、兼容且不需要直接操作視頻內存。
- 設置棧:引導階段沒有默認可靠的棧,需要顯式設置以免后續調用/中斷出現問題。
- 禁用/恢復中斷(cli/sti):在設置棧或初始化關鍵結構時防止中斷打斷(可以提高穩定性)。
org 0x7C00
- 告訴匯編器,代碼段在源代碼中被認為是從線性地址 0x7C00 開始的(即 BIOS 把扇區加載到內存 0x0000:0x7C00)
-
bits 16
指示 nasm 生成 16-bit 實模式編碼 -
start
程序入口點標簽(實際 BIOS 會跳轉到 0x7C00,所以這只是代碼中便于引用的標簽) -
xor ax, ax
ax寄存器是xor是一個清零指令,xor ax, ax將 AX 清零(AX = 0),等同于指令mov ax, 0,這里為接下來設 SS = 0 做準備
mov ss, ax
mov指令,匯編語言中的賦值指令,將后者的值賦值給前者- 為了設定棧使用的段,將 SS(棧段寄存器)設為 0x0000(因為 AX 清零)。
- 注意:在實模式下修改 SS 要小心(最好在修改 SP 之前或配套操作)。
mov sp, 0x7C00
- 把棧指針 SP 設為 0x7C00(棧從 0x0000:0x7C00 向下增長)。
- 必須設置棧,否則函數/中斷可能導致不可預期行為。
- 把棧放在 0x7C00 是一種常見簡單做法(和引導扇區加載地址一致),但要確保不覆蓋自身代碼/數據
- 如果后續會加載第二階段,可能選不同位置。
sti
- 允許中斷指令(Set Interrupt Flag)。
- 一般和 cli 配套使用,在想要想讓 BIOS/硬件中斷產生的地方使用
mov si, msg
- 將 msg 值賦值給 si 寄存器,msg 本質是一個地址值,是一個存儲字符串區域的首地址。
- 把 SI 指向數據標簽 msg,用于字符串讀取。
.print_char
- 一個循環標簽
jmp .print_char配合該條指令實現循環功能
lodsb
- 從 [DS:SI] 處加載字節到 AL,然后 SI++(DS 默認是 0x0000,且 org 保證 msg 地址正確)。
- 是一種簡潔的逐字節讀取方式。
cmp al, 0
cmp是一個比較指令;- 比較 al 寄存器的值是否為 0 .
- 目的是檢查是否為字符串結束符(這里用 0 作為結束符)。
- 用于結束循環。
je .hang
- 如果 AL==0,則跳到結束(hang)。
mov ah, 0x0E
- ah 寄存器是
- 設置 BIOS int 0x10 的功能號為 teletype 輸出(TTY 輸出字符到當前光標并前進)。
- 必須設 AH 才能讓 int 0x10 執行正確的子功能。
mov bh, 0x00
- 設置頁面號(page)。BIOS teletype 函數使用 BH 指定頁號(通常 0)。
- 通常設為 0,是標準做法。
mov bl, 0x07
- BL 設置字體屬性/顏色
- 盡管對于 teletype(0x0E)在傳統文本模式 BL 并非總必需,但設會更兼容某些 BIOS。
int 0x10
- int 指令為 BIOS 中斷指令
- 調用 BIOS 視頻中斷,執行上面設置的 teletype 輸出(輸出 AL 中的字符)。
- 必須使用 BIOS 中斷才能在實模式下不直接操作顯存也輸出字符(更簡單)。
-
jmp .print_char
作用:繼續循環輸出下一個字符。
.hang:
cli
hlt
- 進入禁中斷并執行 halt 指令,cpu 進入低功耗等待模式,然后跳回(確保 CPU 不會繼續向下執行垃圾代碼)。
- 需要一個安全的結束點而不返回到隨機內存。hlt 比不停循環省電;
- cli+hlt 防止來自中斷的喚醒(但會阻塞直到外部復位),加上 jump 可在某些環境下避免返回到可能是可執行的區域。
- msg db "Hello, OS! Booted from boot.asm", 0
- 定義以 0 結尾的字符串數據,打印時 lodsb 逐字節讀取直到 0。
times 510 - ($ - $$) db 0
- 把文件填充到偏移 510(即文件前 510 個字節有效,接下來 2 字節用來放簽名)。
- 必須讓整個扇區達到 512 字節,使得簽名位于正確偏移。
dw 0xAA55
- 寫入引導簽名 0x55AA(注意小端序寫入會在磁盤上以 55 AA 的順序保存)。
- BIOS 在嘗試從介質引導時會檢查每個扇區末尾的 0x55AA 來判定該扇區是否為引導扇區;缺失此簽名通常導致 BIOS 忽略該鏡像作為引導設備。
使用串口輸出(boot_serial.asm)
在沒有圖形界面的環境下,推薦用串口輸出調試和顯示信息。下面詳細介紹串口初始化涉及的寄存器和每一步的作用。
串口輸出代碼結構
start:
cli
xor ax, ax
mov ss, ax
mov sp, 0x7C00
sti
call init_serial ; 初始化串口
mov si, msg
.print_loop:
lodsb
cmp al, 0
je .hang
call serial_putchar ; 發送 AL 到串口
jmp .print_loop
.hang:
cli
hlt
; 串口初始化例程 (COM1, 38400 8N1)
init_serial:
; 設置 IER = 0 (禁用中斷)
mov dx, 0x3F8 ; COM1 base port
mov al, 0x00
add dx, 1
out dx, al
sub dx, 1
; 設置 DLAB = 1,準備設置分頻器
mov dx, 0x3F8
add dx, 3
mov al, 0x80 ; LCR: DLAB=1
out dx, al
sub dx, 3
; 設置波特率分頻器 (38400)
mov dx, 0x3F8
mov al, 3 ; divisor low byte
out dx, al
inc dx
mov al, 0 ; divisor high byte
out dx, al
dec dx
; 設置 LCR = 8N1 (8位,無校驗,1停止位)
mov dx, 0x3F8
add dx, 3
mov al, 0x03 ; LCR: DLAB=0, 8N1
out dx, al
sub dx, 3
; 啟用 FIFO
mov dx, 0x3F8
add dx, 2
mov al, 0xC7 ; FCR: 啟用FIFO,清空,14字節閾值
out dx, al
sub dx, 2
; 設置 MCR (RTS/DSR/OUT2)
mov dx, 0x3F8
add dx, 4
mov al, 0x0B ; MCR: IRQs enabled, RTS/DSR set
out dx, al
sub dx, 4
ret
; 串口發送單字符例程 (AL)
serial_putchar:
push dx
push ax
mov dx, 0x3F8
add dx, 5 ; LSR port
.wait_lsr:
in al, dx
test al, 0x20 ; 檢查 THRE (發送寄存器空)
jz .wait_lsr
pop ax ; 恢復要發送的字符到 AL
mov dx, 0x3F8 ; 數據端口
out dx, al
pop dx
ret
msg db "Hello, OS! Booted to serial from boot_serial.asm", 0
串口初始化步驟與寄存器說明
PC 的標準串口 COM1 基地址是 0x3F8,串口芯片(16550A)有多個寄存器,分別控制不同功能:
- 數據端口 (Data Register, 0x3F8)
- 用于收發數據。寫入一個字節即可發送。
- 中斷使能寄存器 (IER, 0x3F9)
- 控制串口中斷。我們設置為 0,禁用所有串口中斷。
- 分頻器鎖存寄存器 (DLL/DLM, 0x3F8/0x3F9, 需 DLAB=1)
- 設置波特率。波特率 = 基準頻率 / 分頻值。常見基準頻率為 115200Hz,分頻值為 3,則波特率為 38400。
- DLL (低字節) 寫入 3,DLM (高字節) 寫入 0。
- 線路控制寄存器 (LCR, 0x3FB)
- 控制數據位、停止位、校驗位和 DLAB 位。
- DLAB=1 時可設置分頻器,DLAB=0 時正常通信。
- 設置為 0x03 表示 8位數據,無校驗,1停止位(8N1)。
- FIFO 控制寄存器 (FCR, 0x3FA)
- 控制 FIFO 緩沖區。0xC7 啟用 FIFO,清空緩沖,設置 14字節閾值。
- 調制解調器控制寄存器 (MCR, 0x3FC)
- 控制 RTS/DSR/OUT2 等信號。0x0B 啟用 IRQs,設置 RTS/DSR。
- 線路狀態寄存器 (LSR, 0x3FD)
- 只讀。用于檢測發送寄存器是否空(THRE 位,0x20)。發送前需輪詢該位。
串口發送字符流程
- 發送字符前,先輪詢 LSR 的 THRE 位,確保發送寄存器空。
- 然后將要發送的字符寫入數據端口 (0x3F8)。
- 這樣可以保證數據不會丟失。
編譯和運行
nasm -f bin boot_serial.asm -o boot_serial.bin
qemu-system-x86_64 -nographic -serial mon:stdio -drive format=raw,file=boot_serial.bin
運行效果
-
如果一切正常,你會在終端看到:
Hello, OS! Booted to serial from boot_serial.asm
-
如果沒有輸出,請檢查 boot_serial.asm 是否正確生成、QEMU 參數是否正確、串口初始化代碼是否有誤。
總結:
- 串口輸出適合無頭環境、遠程調試、嵌入式開發。
- 代碼中每一步都對應串口芯片的硬件寄存器設置,理解這些寄存器有助于后續開發更復雜的 bootloader 和內核調試功能。

浙公網安備 33010602011771號