- BIOS
- 0xFFFF0
電源正常啟動後,x86 CPU 會先執行 0xFFFF0,也就是 BIOS ROM 的進入點。由於 0xFFFF0 ~ 0xFFFFF 只有少的很可憐的 16 bytes,真正的 BIOS code 勢必要擺到其他位置,此時 0xFFFF0 的作用便是 jmp 到該位置執行 BIOS 程式。
- POST (Power-On Self Test)
BIOS 程式的第一個動作就是執行最基本的 POST 檢查,確保系統在開機當中可以正常運作。通常用 beep 聲來表示檢查結果。各家 BIOS 的 beep 聲都不一樣,以常見的 Award BIOS 為例:
Beep | Error Message |
1 short | POST OK! |
1 long | Memory problem |
1 long, 2 short | Video card error |
1 long, 3 short | No video card or bad video RAM |
Repeating beeps | Memory error |
High Frequency beeps | Overheating CPU |
For more information, visit Phoenix Tech - Award Error Codes.
- Video Card BIOS
POST 之後的首要任務就是啟動顯示卡,畢竟沒有螢幕是很痛苦的一件事情。顯示卡有自己的 BIOS,這時系統 BIOS 會掃描記憶體 0xC000:0000 ~ 0xC780:0000,也就是顯示卡 BIOS 位址,並 jmp 過去執行顯示卡的初始化。一旦成功,開機後的第一個畫面就出現了。
除了顯示卡,其他有自己 BIOS 的裝置也都在這時候啟動,例如 IDE/ATA BIOS 位於 0xC8000。
- Full POST
有了螢幕之後再來一次總檢查:
Video Test: 初始化顯示卡插槽並測試顯示卡和顯示記憶體。
BIOS Identification: 顯示 BIOS 版本、製造商及日期。
Memory Test: 測試並顯示安裝的主記憶體總容量數。
以上是冷開機 (cold-start) 的檢查流程,若是暖開機 (warm-start) 則省略 Memory Test。這時畫面就豐富多了。
除了上述檢查之外,BIOS 還會讀取存 CMOS configuration 資料。這些放在靠小電池維生的 64 bytes CMOS 當中的資料,紀錄著一些使用者可調變的系統資訊,也就是開機時按 Del 鍵進去調整的那堆東西。另外也會做一些額外的檢查,例如動態設定 IDE/ATA 裝置的參數等等,同時顯示在這個畫面當中。
- Summary Screen
一切就緒之後,就像寫論說文「起、承、轉、合」一樣,要來「合」一下。這時 BIOS 會將整個系統資訊都顯示在螢幕上,表示開機動作大抵完成,接下來就準備交棒給下一個程式了。
- Booting
BIOS 所執行的最後一個動作就是交接,將執行權交給下一支程式,boot loader 也好,OS 也好,或者單純的一支小程式。這時會依設定的順序搜尋各開機磁碟裝置 (floppy disk, hard disk, CD-ROM, ...) 的 cylinder 0, head 0, sector 1 (對 hard disk 來說就是 Master Boot Record, MBR),並將該 512 bytes 載入至記憶體 0x0000:7C00 而 jmp 過去完成交接 (注意此時 CS:IP 為 0x0000:7C00 而不是 0x07C0:0000)。對於 MBR 而言,還要多檢查最後兩 bytes 為 0x55AA,才判定為有效的 MBR 並 jmp 過去完成交接;否則繼續搜尋下一個裝置,直到沒有裝置則顯示 "No boot device available" 之類的錯誤訊息。
Image Source: Wikipedia - Cylinder-head-sector
[Top]
- Hello World
- INT 0x10, INT 0x16
利用 BIOS 內建的 INT 0x10,我們可以輕易將字元印在螢幕上;INT 0x16 則可讀取鍵盤的輸入。以下為簡易的 NASM Code,在螢幕中央印出 "Hello Jasonmel!" 之後讀取字元,每讀一個字元之後將它以藍底黃字顯示在螢幕上,除了 Ctrl+C 會重新開始。
執行畫面如下,按下 Ctrl+A 會顯示下面的笑臉。
[Top]
- Boot Loader
- Booting A Program
非常簡單!利用 BIOS INT 0x13 載入該 sector 至記憶體並且 jmp 過去即可。這時要注意避免使用一些已使用的記憶體區段,陳列如下 (部分參考自 HelpPC Reference Library):
0x0000:0 Interrupt Vector Table
0x0040:0 BIOS Data Area
0x0050:0 PrtScr Status / Unused
0x0060:0 Image Load Address
0x07C0:0 Boot code is loaded here at startup (31k mark)
0xA000:0 EGA/VGA RAM for graphics display mode 0Dh & above
0xB000:0 MDA RAM, Hercules graphics display RAM
0xB800:0 CGA display RAM
0xC000:0 EGA/VGA BIOS ROM (thru C7FF)
0xC400:0 Video adapter ROM space
0xC600:0 256 B PGA communication area
0xC800:0 16 KB Hard disk adapter BIOS ROM
0xC800:5 XT Hard disk ROM format, AH=Drive, AL=Interleave
0xD000:0 32 KB Cluster adapter BIOS ROM
0xD800:0 PCjr conventionalsoftware cartridge address
0xE000:0 64 KB Expansion ROM space (hardwired on AT+)
128 KB PS/2 System ROM (thru F000)
0xF000:0 System monitor ROM
PCjr: software cartridge override address
0xF400:0 System expansion ROMs
0xF600:0 IBM ROM BASIC (AT)
0xF800:0 PCjr software cartridge override address
0xFC00:0 BIOS ROM
0xFF00:0 System ROM
0xFFA6:E ROM graphics character table
0xFFFF:0 ROM bootstrap code
0xFFFF:5 8 B ROM date (not applicable for all clones)
0xFFFF:E 1 B ROM machine id
其中比較關鍵的 assembly code 如下。由於讀取可能發生錯誤,因此一般會多加錯誤判斷並多讀幾次比較保險。
- Master Boot Record (MBR)
MBR 位於 hard disk 的 cylinder 0, head 0, sector 1,紀錄著 hard disk 的分割狀態,於開機時被載入至記憶體 0x0000:7C00。開始執行後,由程式依序搜尋 partition 並 check 是否為 active (80),若是,則將控制權繼續交給 active partition。為了避免覆蓋,通常 MBR 會事先將自己搬到 0x0000:0600 的位置,之後才將 partition 的 boot sector 載入至記憶體 0x0000:7C00,再 jmp 過去完成交接。
MBR 格式如下:
+--------+----------------------------------------------------+
| OFFSET | 0 1 2 3 4 5 6 7 8 9 A B C D E F |
+--------+----------------------------------------------------+
| 0x0000 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0010 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0020 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0030 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0040 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0050 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0060 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0x0070 - 0x015F 也都是 CC 故省略
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 0x0160 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0170 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0180 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x0190 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x01a0 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC |
| 0x01b0 | CC CC CC CC CC CC CC CC CC CC CC CC CC CC P1 P1 |
| 0x01c0 | P1 P1 P1 P1 P1 P1 P1 P1 P1 P1 P1 P1 P1 P1 P2 P2 |
| 0x01d0 | P2 P2 P2 P2 P2 P2 P2 P2 P2 P2 P2 P2 P2 P2 P3 P3 |
| 0x01e0 | P3 P3 P3 P3 P3 P3 P3 P3 P3 P3 P3 P3 P3 P3 P4 P4 |
| 0x01f0 | P4 P4 P4 P4 P4 P4 P4 P4 P4 P4 P4 P4 P4 P4 55 AA |
+--------+----------------------------------------------------+
CC: 0x0000 to 0x01BD - Boot loader code (First 446 bytes)
P1: 0x01BE to 0x01CD - Partition entry 1
P2: 0x01CE to 0x01DD - Partition entry 2
P3: 0x01DE to 0x01ED - Partition entry 3
P4: 0x01EE to 0x01FD - Partition entry 4
55AA: 0x01FE to 0x01FF - Boot signature
其中 Partition Entry 格式如下:
+------------+----+-----------+----+-----------+--------------+---------------+
| Byte | 0 | 1 2 3 | 4 | 5 6 7 | 8 9 A B | C D E F |
+------------+----+-----------+----+-----------+--------------+---------------+
| Descrption | BI | Start HCS | FD | End HCS | Start Sector | Num of Sector |
+------------+----+-----------+----+-----------+--------------+---------------+
BI (1): Boot indicator (0x00 off, 0x80 on)
Start HCS (3): Starting head, cylinder and sector
FD (1): Filesystem descriptor
End HCS (3): Ending head, cylinder and sector
Start Sector (4): Starting sector (offset to disk start)
Num of Sector (4): Number of sectors in partition
找 boot sector 比較關鍵的 assembly code 如下,在此不處理延伸分割區的情形。
- Booting DOS
類似 Booting A Program,把 DOS 磁區的第一個 sector 讀入交接即可。接下來該讀入的 DOS boot sector 會將 root directory 讀入 0x0000:0500,確認前兩個 entries 是 IO.SYS 以及 MSDOS.SYS 之後,將 IO.SYS 的前 3 個 sectors 讀入 0x0000:0700,並 jmp 過去將執行權交給 IO.SYS 持續接下來的開機程序。
- Booting FreeBSD
類似 Booting A Program,把 FreeBSD 磁區的第一個 sector 讀入交接即可,也就是三個 stage 中的 stage 1。此 sector 即為 /boot/boot0,載入執行後會將 boot sectors 也就是 slice 1 (FreeBSD 將 partition 稱為 slice) 的前 8192 bytes 讀入,進行 stage 2 booting。此時已有處理 file system 的能力,此時便可讀取檔案系統中的 kernel 進行 stage 3 完成開機。詳細情情可見下列二圖,並可參考來源網站取得進一步的細節。
Image Source: Thinker: FreeBSD 開機流程
- Booting Linux
Linux 的 booting 動作較為複雜,必須先讀懂 ext3 file system 之後將起始檔案讀入後交接。詳細動作參考下圖:
Image Source: Linux Journal - Booting the Kernel
[Top]
- Operating System
- Interrupt
記憶體 0x00000 到 0x00400 存放著系統的 Interrupt Vector Table,每一個 entry 共 4 bytes,紀錄著該 interrupt 的 segment (2 bytes) 及 offset (2 bytes) 以便發生 interrupt 時能跳到對應的位置去執行對應的動作。X86 的 interrupt 可列表如下:
INT (Hex) | IRQ | Common Uses |
00 - 01 | Exception Handlers | 00: Division by Zero; 01: Single Step |
02 | Non-Maskable IRQ | Non-Maskable IRQ (Parity Errors) |
03 - 07 | Exception Handlers | 03: Breakpoint; 04: Overflow; 05: Hardcopy |
08 | Hardware IRQ0 | System Timer |
09 | Hardware IRQ1 | Keyboard |
0A | Hardware IRQ2 | Redirected |
0B | Hardware IRQ3 | Serial Comms. COM2/COM4 |
0C | Hardware IRQ4 | Serial Comms. COM1/COM3 |
0D | Hardware IRQ5 | Reserved/Sound Card |
0E | Hardware IRQ6 | Floppy Disk Controller |
0F | Hardware IRQ7 | Parallel Comms. |
10 - 6F | Software Interrupts | - |
70 | Hardware IRQ8 | Real Time Clock |
71 | Hardware IRQ9 | Redirected IRQ2 |
72 | Hardware IRQ10 | Reserved |
73 | Hardware IRQ11 | Reserved |
74 | Hardware IRQ12 | PS/2 Mouse |
75 | Hardware IRQ13 | Math's Co-Processor |
76 | Hardware IRQ14 | Hard Disk Drive |
77 | Hardware IRQ15 | Reserved |
78 - FF | Software Interrupts | - |
(00 - 1F: BIOS Functions;
20 - 3F: DOS Functions;
60 - 67: User Software Interrupts;
80 - F0: BASIC Interrupts;
F1 - FF: Unused)
當我們要實作自己的 interrupt,只需要更改對應號碼的內容即可,以下為關鍵的 software interrupt assembly code。此時若是 hardware interrupt,則當發生了該 interrupt 時,CPU 會馬上讀取 interrupt table 並跳至對應的位置 (在下面的例子即 myINT: 位址) 的部分去執行對應的動作;若是 software interrupt,則我們可以使用 "INT 數字" 來使用該 interrupt。目前常見 OS 的系統呼叫 (system call),便是用 software interrupt 配合 registers 當參數來完成,例如 DOS 使用 INT 0x21,而 Linux 則使用 INT 0x80。必須注意的是在 interrupt 發生的同時,CPU 會將 flags、cs、ip 依序 push 進 stack 中,而在 iret 時再 pop 出來繼續執行,因此在 interrupt 當中 stack 的任何更動都必須要小心處理。
對於 hardware interrupt,則需要額外多做一些處理。當我們想要 enable/disable 某個 IRQ,必須更改對應 Programmable Interrupt Controller (PIC) 的 Operation Control Word (OCW) 內容。IRQ0 ~ IRQ7 的 disable bit 分別對應到 OCW1 (0x21) 之 bit0 ~ bit7,IRQ8 ~ IRQ15 則對應到 OCW2 (0xA1) 之 bit0 ~ bit7。此外,在 interrupt 結束前需送出 End Of Interrupt (EOI) 給對應的 PIC,對於 PIC1 是 outportb(0x20, 0x20);,對於 PIC2 則是 outportb(0xA0,0x20);。其中的 outportb(0xA0, 0x20) 在 asm 中為以下動作。
mov al, 0x20
out 0xA0, al
如果是以 C 來實作 hardware interrupt,則會類似以下 code。(以 IRQ1 也就是 INT 0x09 為例...)
- Device Driver
所謂的 device driver,說穿了就是 CPU 利用一連串的 I/O 來與 device 溝通,設定 device 的狀態、資料,或是由 device 讀取需要的資訊,可以說是軟硬體之間最底層的橋樑。I/O 方式有兩種:Direct I/O 及 Memory Mapped I/O。
一般而言,記憶體有自己的位址空間,I/O 也有自己的位址空間,而 Direct I/O 就是利用這樣的概念,mov BYTE [0x00], al 和 out 0x00, al 是兩件截然不同的事情。然而當我們為了使用上的方便,而將部份的 I/O 動作映射到記憶體空間時,就形成所謂的 Memory Mapped I/O,此時我們對該映射之記憶體空間做存取動作,就相當於對該 device 做 I/O 的動作。聽起來有點玄,以下便以最常見最基礎的 keyboard driver 和 VGA driver 為例來說明之。
Keyboard Driver (INT 0x09, INT 0x16)
當我們想要從 keyboard 抓取使用者輸入時,通常會利用到 BIOS 的 INT 9 和 INT 16。其中 INT 9 為外部中斷,於使用者按下按鍵的瞬間產生中斷,將輸入的鍵值存入 BIOS 配置的 32 bytes (0x0040:001E ~ 0x0040:003D) 的 buffer 中;而 INT 16 則為內部中斷,若選項為讀取使用者之輸入,則會將 buffer 中的鍵值讀出到 al 暫存器。也就是說整個 keyboard 的運作流程可簡單示意如下圖。
比較需要注意的是,按鍵按下 (key down) 與彈上 (key up) 分別會產生不同的鍵值,這是用來判斷如 Shift、Ctrl、Alt 等特殊按鍵是否按下的重要特性。此時就必須紀錄這些按鍵的狀態,BIOS 為此配置了 0x0040:0017 及 0x0040:0018 紀錄之。
詳細的 INT 9 流程
送 command 0xAD 給 8042 keyboard microcontroller (port 0x64) 關閉鍵盤功能。
等待 8042 keyboard microcontroller (port 0x64) 回傳狀態 0x0A ACK。
由 on-board microcontroller (port 0x60) 回傳作對應的處理:
0xEE – Echo 訊息。
0xFA – Set ACK bit 訊息。
0xFE – Set Resend bit 訊息。
其他– 查表轉換成對應的 ASCII 碼,存入 BIOS buffer。
送 command 0xAE 給 8042 keyboard microcontroller (port 0x64) 開啟鍵盤功能。
詳細的 INT 16 流程
等待直到 buffer 內有值。
取出 buffer 內的值。
以下列出非常陽春只能讀取單一鍵值的 keyboard driver assembly code。若需要更詳細的功能列表,請參考 IBM PC/AT 101 Key Enhanced Keyboard 相關規格說明。
VGA Driver (INT 0x10)
最基本的 VGA card 將畫面資料對應到記憶體位址,因此存取畫面只需存取對應位址即可。其中單色系統由 0xB000:0000 開始,彩色系統由 0xB800:0000 開始,以 word 為單位 (ASCII I.O. byte + attribute H.O. byte),皆由 (0, 0) 對應到 (79, 24),一共 4000 bytes。彩色系統提供 8 個畫面空間可供切換,分別由 0xB000:0000、0xB000:1000、0xB000:2000、...、及 0xB000:7000 開始,而單色系統僅提供一個畫面。以下為顯示一個字元之程式碼,完全沒用到 in/out,非常之歡樂。
雖然 VGA 有 memory mapped I/O 很方便,但遇到顯示狀態或游標狀態的調整,還是得用 direct I/O 來操控相關的 CRT Controller (CRTC) Registers。其中 I/O port 0x03B4和0x03B5 (單色系統)、0x03D4和0x03D5 (彩色系統) 分別對應到 CRTC Address Register 和 CRTC Data Register 的存取。舉讀取游標位置為例,先設定 Address Register 再讀取 Data Register,以下為改變目前游標位置之程式碼。
關於游標位置,BIOS 於 0x0040:0050 ~ 0x0040:005F 亦配置了八組暫存 words,我們也可以善加利用,才不用每次都要大費周章的讀取。
Device Driver 補記
雖然表面上看起來就是一堆 in/out 好像很簡單,然而看了一下 ptt Tech_Job 板的討論,寫 driver 最大的瓶頸不在於照著規格書寫對應的功能,反而是 debug 的過程,常常會出現 bug 出在硬體或 OS 沒按照規格書實作的情形,要是硬體或 OS 廠商死不認帳,還是得自己一步步的找出癥結所在。有經驗的網友 diorite 說:「寫 Driver 的門檻只有兩個 - 不怕累、心要細,但是具備這兩個條件的人... 其實很難找。」又說:「常常都要一個人拿示波器坐在角落加班,沒有迫切金錢需要的人,通常是熬不住的。」話雖如此,就因為人才少,薪水分紅自然就比一般科技人高出許多,可以說是天下沒白吃午餐的最佳實例。
- Context Switch
所謂的 context switch,就是做程式的切換動作。大致上是將切換前的程式執行狀態存下來,並把打算執行程式的狀態回存出來,然後執行該程式。這時候會出現一些問題 (Who, When, Where, Why and How):
Who - 需要存取哪些狀態?
一般而言不外乎暫存器 registers 和旗標 flags。這紀錄著程式執行到哪裡,以及一些不希望下次執行會被更動的值。
When - 何時存取這些狀態?
在 user applications 切換之間,必須經過 OS 的 scheduler 來做排程管理。因此整個切換流程即 app1 -> OS scheduler -> app2,其中的兩個 "->" 便是存取狀態的時機。細部動作為:把 app1 的狀態存起來,讀回 OS 的狀態,scheduler 找出下一個要執行的程式,把 OS 的狀態存起來,讀回 app2 的狀態,執行 app2。
Where - 要將這些狀態存在哪裡?
通常每個程式都會被 allocate 一塊專屬的 stack 空間,這就很好用啦!在程式切換的過程當中,只需要做更改 ss:sp 的動作,就可將不同的程式狀態存在它們各自的 stack 當中,既簡單又方便管理。當然,如果想存在自己另外定的記憶體區塊也沒問題,只是需要多做一些額外的管理就是了。
Why - 為何要存取這些狀態?
在 single task 的 OS 中,一個程式執行完才能執行下一個程式,其實是不需要做這個動作的。然而在 multi-task 的 OS 中,一個程式執行到一半可能會被 interrupt 而轉換到另一個程式,若在切換前沒事先將狀態存下來,之後回到該程式一些狀態可能就不一樣了,這時候該程式應該會很不滿而做出一些詭異或是當機的動作。所以為了不讓程式有意見,還是存一下吧。
How - 要如何存取這些狀態?
一言以敝之:push、pop。
pushf ; push flags as a word
popf ; pop flags as a word
pusha ; push ax, cx, dx, bx, sp, bp, si, di
popa ; pop di, si, bp, sp, bx, dx, cx, ax
另外也要善加利用 call/ret 以及 int/iret 的特性。當我們執行 call addr 的時候,CPU 會將目前的 ip 暫存器 push 到 stack,再將 ip 設為 addr;而當我們執行 ret 的時候,CPU 會 pop 一個 word 到 ip 然後執行。同理 int/iret 也是一樣,只是 push/pop 的是 flags、cs、ip。
總而言之,整個程式切換流程可以大致示意列為以下步驟:
1. app1 running...
2. interrupt
3. context switch from app1 to kernel
4. kernel scheduler pick next application to run
5. context switch form kernel to app1
6. interrupt return
7. app2 running...
- File System
一般而言,資料儲存設備之存取大都是以 sector 為單位,也就是 512 bytes。若任何的檔案大小都小於 512 bytes,我們大可不需要 file system,直接以 CHS 或 LBA 的方式來表示檔案即可。除非你是嵌入式系統的開發者或是史前時代的人類,不然我想一般人應該不會那麼做,一定會用到大於 512 bytes 的檔案,此時就要靠 file system 來管理檔案與儲存設備之間的對應關係。
至於 file system 的設計則端看各位開發者的想像力。以最常見最簡單的 FAT (File Allocation Table) 為例,以數個 sectors 集結為一個 cluster 作為單位,將檔案及目錄以 table 來做一對一的對應。當檔案超過一個 cluster 大小時,則以 chain 的方式串連到另外一個 cluster,以此類推。詳細的 FAT 架構可以參考 Understanding the FAT32 Filesystem,這是目前看過最深入淺出的說明。
基於這樣的概念,結合 FAT 及 GMail 的 label 特性,小弟亦設計了一套非常破爛的 file system,姑且稱作 JasonmelFS,若有興趣可以參考報告用的文件。
- Memory Management
在讀入檔案前,必須先知道該檔案要擺到記憶體哪個位置。位置的選擇,主要考量避免蓋掉其他使用中的檔案,並以發揮記憶體最大使用率,也就是盡可能的塞滿為依歸,同時演算法又不能太過遲鈍,這也正是 memory management 之所以困難的地方。為了避免蓋掉其他檔案,最簡單的做法便是使用 bitmap 的方式,以一對一的方式標記正在使用中的區塊,配置時就要避開這些位置。至於要如何充分利用記憶體以及有效率的演算法,就各憑本事了。
- Protected Mode
under construction...
[Top]
- 開機模擬動畫
- Reference Links