我是如何學習寫一個操作系統(二):操作系統的啟動之Bootloader
- 2019 年 10 月 3 日
- 筆記
前言
今天本來的任務看書和把之前寫的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則是進入之後的設置。但是因為這兩部分包含了一些其它重要概念,所以打算再下一篇寫。