一个操作系统的实现(1)
一個操作系統的實現
說明:本文是一個簡單的學習記錄,不是全面給大家提供學習的文章,文章內容均代表作者的個人觀點,難免會有錯誤。轉載請保留作者信息。
?????????????????????????????????????????????????????????????????????????????????????????????????????????2010/11/20?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?sylar_xiong
?????????????????????????????????????????????????????????????????????????????????????????? MSN& Email:cug@live.cn
?
準備:UbuntuOS,? 虛擬機(用于調試OS內核),這個新OS是一個簡單的,常用的OS,以Intel I32為例(他幫我們完成了很多功能,例如中斷/保護模式/特權級)。
?
首先需要掌握幾個基本概念:
1?系統的啟動順序
?????? 系統會首先運行boot(實模式), 然后運行Loader,最后運行kernel(假設為微內核,加載文件系統,MM,shell等模塊)。上個圖:
| 啟動階段 | 完成的工作 | 說明 | 位置 | ? |
| 1 Boot.bin | 加載loader進內存 | 只有512k | 位于硬盤或者軟盤的第一個扇區 | ? |
| 2 Loader.bin | 加載內核,啟動保護和分頁 | ? | ? | ? |
| 3 Kernel.bin | ? | ? | ? | ? |
?
?
2?保護模式
?????? 系統啟動的時候是實模式,在loader階段實現實模式向保護模式的跳轉。
?????? 首先看一下實模式: 8086的地址總線為20位,物理地址=段×16+offset 所以最大可以尋址1M的內存空間(2的20次方)。在實模式下,段0xXXXXh表示0xXXX0h開始的一段內存區域。
?????? 其次看一下地址的概念:我們平時調試程序的時候看到的地址均為logicaladdress,例如地址A等于0x40008000h, OS尋址的時候首先會根據GDT/LDT將A轉化為linear address A’ , 這就是所謂的段地址轉化。 ?接著A’會根據頁表轉化為A’’,這個A’’就是真正的physical address所在了。總結一下也就是:logical address -> linear address -> physical address(實際上我吧第一次跳轉理解為段尋址,第二次跳轉理解為頁尋址)
?????? 保護模式的尋址方式為:
| 描述符1包括: | 段基址 | 段界限 | 屬性 | 一般為全局段 |
| 描述符2包括: | 段基址 | 段界限 | 屬性 | ? |
| LDT1 | ? | ? | ? | 和進程相對應 |
| LDT2 | ? | ? | ? | ? |
| PED | ? | ? | ? | 分頁機制 |
| PTD | ? | ? | ? | ? |
GDT(每個GDT中包含很多描述符和LDT,每個描述符描述了一個段(可以是數據段,代碼段/堆棧段/系統段或者門描述符)的信息,包括段的基地址,界限和段的屬性。其中段的屬性包括段的特權級。GDT中還包含了很多指向LDT的描述符,每個LDT代表一個單獨的進程,我們把一個單獨的進程隨需要的各種數據,例如數據段/代碼段/堆棧段封裝在一個LDT里面)
看一段簡單的實模式跳轉到保護模式的代碼:
[section .gdt]???????????????????????????????????????? ??????// GDT段標識
LABEL_GDT: descriptor????????? 0,???0,?????? 0;????? ?//GDT首地址
LABEL_DESC_CODE32:descriptor 0, 200, 0;
LABEL_VIDEO: descriptor0xb8000h, 0ffffh, DA_DRW;???? //顯存段基址為0xb80000h 段界限為offffh,屬性為該段在內存中存在/為代碼段
?
SelectorCoderequ LABEL_DESC_CODE32 – LABEL_GDT
SelectorViedoequ LABEL_DESC_VIDEO – LABEL_GDT
?
[section .s16]
[BITS 16]????????????????????? //16位代碼
?????? Cli//關中斷
?????? //打開地址線A20
?????? jmp dword SelectorCode32:0? //跳轉到LABEL_SEG_CODE32的0位置處。
?
[section .s32]
[BITS 32]
LABEL_SEG_CODE32:
?????? Movax, SelectorVideo
?????? Movgx, ax????????? //gx為顯存段首地址?????
?
分頁機制補充:通過GDT,LDT得到線性地址后,會利用PED(頁目錄)和PTD(頁表),PED中每個表項指向了一個PTD,而一個PED中每個表項指向了一個世紀的4K物理地址。
同時應該了解到,我們調試程序的時候看到的地址是線性地址,所以運行同一個可執行文件2次的地址和寄存器都是完全一樣的,事實上他們的物理執行地址并不相同。
3?二進制文件 or 可執行文件格式
?????? 純二進制文件:內存映像和二進制文件映像是一樣的。
?????? .bin文件: = loader.bin? =? COM文件:dos運行的文件
?????? ELF文件:OS kernel的文件形式。他的結構如下(例如kernel.elf):
| 文件開始處 | 結構說明 | 備注 |
| ? | Elf header | Elf文件頭,包含文件大小,屬性等 |
| ? | Program header1 | 數據段在文件中的位置和內存中的位置 |
| ? | Program header2 | 代碼段從文件中到內存中的映射關系 |
| ? | ? | 文件執行的時候首先查看elf header,然后根據 Program headerN將每個段加載到內存相應的位置并執行。 |
?
4?特權級
???????我首先假定內核的level=0 系統服務的level=1 應用程序的level=3。程序從一個代碼段轉移到另一個代碼段時(可能的情況是調用系統函數),需要考慮權限問題,利用了CPL,DPL和RPL來判斷是否可以進行代碼轉移。
?????? 利用call調用門實現一個低特權級的進程訪問高特權級的代碼段,利用ret指令實現高特權級到低特權級的跳轉。
?????? Jmp時,不管長跳轉還是短跳轉都一樣的實現。Call時,長跳轉需要比短跳轉額外保存一下當前進程的cs地址到堆棧段中(因為返回時需要知道要返回到哪個代碼段)。例如用戶進程A(ring3)執行時通過調用門訪問系統進程B(ring0)的代碼段(可以理解為一個系統函數調用),執行完后返回,這個過程可以歸納如下:
1)?用戶進程A將參數,返回值,cs地址保存到LDT進程A的堆棧段中。
2)?用戶進程A的程序通過call調用門跳轉到系統進程B中執行。由于現在已經是B中的代碼在執行,所以相應的使用的是B的堆棧段。同時,A的堆棧段的位置ss和esp會保存到TSS(每個進程一個)
3)?B中相應的代碼執行完畢(對應命令ret),執行完畢后通過取出TSS中保存的A的堆棧段中的返回地址來返回A。
?
反過來,由一個系統進程(ring0)進入用戶進程(ring3)的關鍵在于,在ring0向ring3跳轉之前,手動保存ring3進程A的當前cs,eip,ss,esp到A的ss中,然后調用命令retf,這個命令會自動加載A的cs,eip,ss,esp到CPU寄存器,并跳轉開始執行A。這就實現了ring0 toring3。
?
5?中斷和異常
?????? 中斷是一個計算機執行的根本,進程調度,鍵盤輸入等等都是中斷。保護模式下,中斷是通過IDT(中斷描述符表)來實現的,中斷發生時,會在IDT中找到對應的描述符,并轉到相應的中斷處理函數處運行。終端分為軟件中斷(通過int N命令實現)和外部硬件中斷(例如時鐘中斷,鼠標鍵盤中斷等)。
其中外部硬件中斷需要通過和CPU的INTR引腳相連的2個8258A芯片實現。
下面只討論保護模式下的中斷:
?????? 保護模式下,有一個中斷寄存器,指示了IDT的位置和IDT的大小。中斷分為3種類型:中斷門(運行時關閉中斷,只能由內核調用);陷阱門(運行時不關中斷,只能由系統調用);系統門(在用戶態下,可以使用int3、into、bound 及int0x80四條匯編指令進入系統門)
從深層次看一下中斷的過程:
1.CPU檢查是否有中斷/異常信號
CPU在執行完當前程序的每一條指令后,都會去確認在執行剛才的指令過程中中斷控制器(如:8259A)是否發送中斷請求過來,如果有那么CPU就會在相應的時鐘脈沖到來時從總線上讀取中斷請求對應的中斷向量。
對于異常和系統調用那樣的軟中斷,因為中斷向量是直接給出的,所以和通過IRQ(中斷請求)線發送的硬件中斷請求不同,不會再專門去取其對應的中斷向量。
2. 根據中斷向量到IDT表中取得處理這個向量的中斷程序的段選擇符
CPU根據得到的中斷向量到IDT表里找到該向量對應的中斷描述符,中斷描述符里保存著中斷服務程序的段選擇符。
3.根據取得的段選擇符到GDT中找相應的段描述符
CPU使用IDT查到的中斷服務程序的段選擇符從GDT中取得相應的段描述符,段描述符里保存了中斷服務程序的段基址和屬性信息,此時CPU就得到了中斷服務程序的起始地址
?
?
6?最簡單的文件系統FAT12
假設我們有一個硬盤,劃分為了2個分區C和D。C為FAT12,D為NTFS。下面僅僅看看C分區的結構。從小到到依次為:扇區,簇,分區。其中軟盤或硬盤第一個扇區被稱為引導扇區。
| …… | 數據區 | ? |
| …… | ||
| 扇區n | 根目錄 | 存放文件信息(包括文件名,修改時間,文件大小,文件內容對應的數據區索引)和目錄結構 |
| …… | ||
| 扇區19 | ||
| 扇區18 | FAT2 | ? |
| …… | ||
| 扇區10 | FAT1 | 對于文件大小大于512字節的文件來說,將一個文件對應的所有扇區串聯起來。 |
| …… | ||
| 扇區1 | ||
| 扇區0 | 引導扇區 | ? |
一般而言,引導扇區存放boot.bin, boot..bin運行的時候會尋找根目錄中的loader.bin文件,如果找到這個文件名,就會從數據區中將loader.bin取出加載到內存相應位置。
?
?
下面開始正文
一?一個操作系統的啟動過程
???????1從硬盤或者軟盤的引導扇區(最大只能為512b,所以有了Loader.bin)開始啟動,引導扇區(boot.bin)完成的主要任務是從硬盤(或者軟盤,這里假設為FAT12格式的文件系統)中的根目錄找到并加載Loader.bin到內存0x90000h處并將控制權交給boader.bin(即跳轉到0x90000h處執行)。
???????2loader.bin 首先在硬盤(軟盤中)查找并加載kernel.elf到內存(具體的過程就是將kernel.elf中不同的段放到不同的內存位置)
???????3Loader.bin中定義了GDT和一個GDT指向的代碼段(假設為LABEL_PM_START)。然后loader.bin執行一系列匯編打開保護模式并最終通過jmp跳轉到LABEL_PM_START處。
???????4進入保護模式之后,Loader.bin初始化各個寄存器,初始化堆棧,打開分頁機制(步驟為:首先獲取可用內存信息,根據可用內存信息初始化GTD中的頁目錄和頁表,此時的分頁機制還僅僅只是對等映射。)。
???????5loader將kernel.elf加載到物理地址
???????6由于kernel是一個elf文件,所以loader還需要把kernel.elf的不同段復制到指定的地方。此時的物理內存情況為:
| 起始物理地址 | 用途 | 備注 |
| ………… | ………… | ? |
| 101000h | Page tables(PTE) | ? |
| 100000h | PDT | 頁目錄 |
| F0000h | System ROM | ? |
| E0000h | Expansion of sys ROM | ? |
| C0000h | ? | ? |
| A0000h | ? | ? |
| 9fc00h | 系統保留 | ? |
| 90000h | Loader.bin | Loader原文件 |
| 80000h | Kernel.bin | 內核原始文件 |
| 30000h | Kernel | 整理后的內核 |
| 7e00h | Free | ? |
| 7c00h | Boot sector | ? |
| 500h | Free | ? |
| 400h | ROM BIOS參數 | ? |
| 0h | Int vectors | ? |
???????7接著Loader將控制權交給kernel,跳轉到內核的起始地址0x30400h。注意雖然控制權現在已經是內核了,但是esp仍然指向Loader雖在的內存中,同時GDT信息也在loader的內存中,所以我們需要將esp指向kernel中的堆棧,并且將loader中的GDT復制到kernel中。
???????8內核開始運行,內核的運行情況見后文。
運行到這里,看看可能的文件組織結構:
?
Tree:
|--Boot?????????
|--boot.asm????? //boot.bin
|--loader.asm???? // loader.bin
|--include
???????|--load.inc?
???????|--pm.inc?? //保護模式相關
???????|--fat12hdr.inc //文件系統相關
|--include
????????????? |--const.h?? ?
????????????? |--type.h?? //數據類型
????????????? |--protect.h
|--kernel
????????????? |--kernel.asm? //kernel.bin?其中包含內核入口點_start和異常處理入口點。
????????????? |--start.c?????? //內核的C入口cstart() / 初始化IDT
|--lib
????????????? |--string.asm?? //string
注意,在寫makefile的時候,至少要3個標簽,分別為boot loader kernel 和 clean
?
???????9這時,內核已經掌握了控制權,但是由于沒有中斷,內核幾乎不能做什么工作(不能進程調度,不能接受輸入),所以接下來需要完成中斷處理。步驟為:
????????????? 1)設置和CPU直連的2個8259A芯片的寄存器,將硬件初始化(代碼位于starts.c中)。
????????????? 2)手動建立IDT。
????????????? 3)建立異常處理,過程如下。
???????????????????? …………? //kernel.asm
divide_error:? //如果發生異常便會跳到此函數處運行
?????? push 參數(包括錯誤碼和錯誤的ID)
call exception_hander //除零錯處處理函數,顯示錯誤嗎
?
???????????????????? …………..//start.c中運行
???????????????????? idt[INT_VECTOR_DEVIDE].selsect= GDT_idtseclet; //尋址時先在GDT中找到IDT的段地址在加上偏移找到devide_error()的線性地址
???????????????????? idt[INT_VECTOR_DEVIDE].offset= devide_erro r; //錯誤處理函數為devide_error();? //idt中每一個元素代表了一個中斷向量,發生中斷時候,CPU會首先根據中斷寄存器找到idt的起始位置,然后根據中斷向量號和idt數組找到中斷處理程序的位置,并保存相關寄存器后跳轉到中斷處理程序處運行!
?
????????????? 4)建立中斷處理(步驟和建立異常類似,就是中間多了一個設置初始化8259A)。
?
?
未完待續
總結
以上是生活随笔為你收集整理的一个操作系统的实现(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你真的会使用assert吗?
- 下一篇: 在场景中加入第一人称视角运行后一直往下掉