我是如何學習寫一個作業系統(二):作業系統的啟動之Bootloader
前言
今天本來的任務看書和把之前寫的FragileOS整理一下,但是到現在還在摸魚,書也只看一點。後來整理了一下寫這個系列的思路,原本的目的是對作業系統原理性的學習和對之前寫的一個玩具型作業系統的回顧,就是想對作業系統的知識的輪廓能有一個瞭解,現在想來想減少對之前寫的系統的回顧,畢竟也只有2000多行,但是還是要有對整個思路的展現。然後增加對Linux 0.12原始碼的一些學習。所以離標題可能比較遠了一點,但是就這樣吧
什麼是作業系統
原本這一節是寫計算機系統和作業系統概述的,但是寫到一半覺得太水就刪了。就總結幾句,後面用到什麼就補什麼。計算機系統的概述應該屬於計算機組成原理的內容,這倆部分也是《作業系統:精髓和設計原理》的第一二章。但是覺得如果對於想學習作業系統內部的程式碼的話,換成彙編的內容會更好。
進入正題,作業系統是什麼
對於計算機來說最根本的執行方式,就是取指執行
對於在螢幕上輸出Hello,World!的過程,首先CPU拿到記憶體中的指令,這些指令是通知CPU把存在某個記憶體中的'H''E''L'等移動到視訊記憶體位置,這樣在螢幕上就可以看到這些字元了。這就是計算機最原始的執行方式
而作業系統就是對硬體層面的抽象,讓我們不用在直面硬體,如果想要再次在螢幕輸出字元,只要直接呼叫作業系統的write(windows下的好像是這個名),C語言中的printf下就是一個系統呼叫
當然作業系統絕對是比想象中的龐大的多,作業系統還對記憶體、終端、磁碟、網路和檔案等等進行管理,光windows 2000應該就有3000多萬行的程式碼了。當然有簡陋的記憶體、程序管理和檔案系統的玩具型核心,只要幾千行程式碼就可以完成了。
作業系統的啟動
對於X86架構的計算機,開機時一共做這幾件事
開機時的CS = 0xFFFF, IP = 0x0000
這時候的CPU處理真實模式,也就是定址的方式是CS:IP (真實模式和保護模式屬於CPU的工作模式,其中比較大的區別就是定址的方式)
定址0xFFFF0
檢查硬體裝置,像鍵盤顯示器之類的
將磁碟0磁軌0扇區讀入0x7c00處
會從這裡讀入512位元組,也就是傳說中的載入程式,這裡放著計算機執行的第一段程式碼
設定cs = 0x7c00 ip = 0x0000
FragileOS/boot
這個是我之前寫的FragileOS的boot,採用的是Intel彙編格式
主要的邏輯就是:
- 先載入到0x7c00位置
- 進行初始化操作
- 呼叫CPU提供的中斷來讀取資料
- 讀取完畢後直接跳到核心的起始位置,也就是引導結束了
(部分程式碼)
org 0x7c00; ;載入到記憶體0x7c00處
LoadAddr EQU 08000h ;核心的記憶體地址
BufferAddr EQU 7E0h ;讀取扇區的時候進行的快取
BaseOfStack EQU 07c00h
entry:
mov ax, 0 ;進行暫存器的初始化操作
mov ss, ax
mov ds, ax
mov ax, BufferAddr
mov es, ax ;ES:BX 資料儲存緩衝區,指示扇區載入後放置的地址
mov ax, 0
mov ss, ax
mov sp, BaseOfStack
mov di, ax
mov si, ax
mov BX, 0 ;ES:BX 資料儲存緩衝區
mov CH, 1 ;CH 用來儲存柱面號
mov DH, 0 ;DH 用來儲存磁頭號
mov CL, 0 ;CL 用來儲存扇區號
read_floppy: ;每次都把扇區寫入快取地址07E00處
cmp byte [load_count], 0 ;比較load_count地址處的值,如果=0就跳轉到begin_load
je begin_load
mov bx, 0
inc CL
mov AH, 0x02 ;AH = 02 表示要做的是讀盤操作
mov AL, 1 ;AL 表示要練習讀取幾個扇區
mov DL, 0 ;驅動器編號,一般我們只有一個軟盤驅動器,所以寫死
int 0x13 ;呼叫BIOS中斷實現磁碟讀取功能
jc fin
Linux 0.12/boot
Linux 0.12的boot自然比上面的複雜的多,Linux採用的boot的彙編是as86格式的,其餘的彙編採用的都是AT&T
Linux 0.12下的boot一共有三個檔案:
- bootsect
- head
- setup
(程式碼太長不全部貼了,有需要的可以私信我)
bootsect
bootsect的主要作用就是把自己移動到0x90000處執行,然後再載入setup模組 (也就是setup.s)到bootsect的後面,再把system模組載入到0x10000處,這個也就是核心的主要部分
bootsect的開頭是一些常量的定義
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
_start先設定好目的地址和源地址
ds:si和es:di
然後執行rep指令
rep指令是重複的意思,它以cx暫存器的值為判斷,如果cx的值為0就停止
movw指令
開始從[si]處移動cx個字到[di]處,這裡也就是一共移動了256個字,512位元組
最後跳轉到0x9000開始執行
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
- 現在的這些程式碼都是在0x90000後的
- 先重新設定段暫存器和棧指標
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
這一部分和我之前的一樣,就是呼叫中斷來讀取磁碟內容,只是Linux讀取的是在第二扇區的setup模組
如果失敗就重新設定驅動器然後跳回重新讀取
成功就跳到ok_load_setup
ok_load_setup是設定根檔案系統裝置的,並且讀入SYSTEM模組 (核心的主要部分)到0x10000處,結尾是跳到SETUP模組
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
小結
一個簡單的boot載入程式,顧名思義就是把做一些引導工作的,進行一些初始化設定再讀入真正的核心部分,進入OS。
其實Linux 0.12一個完整的boot應該還包括setup.s用來完成OS啟動前最後的設定 (進入保護模式等),head.s則是進入之後的設定。但是因為這兩部分包含了一些其它重要概念,所以打算再下一篇寫