《30天自制操作系統(tǒng)》是一本兼具趣味性、實(shí)用性與學(xué)習(xí)性的操作系統(tǒng)圖書。作者從計算機(jī)的構(gòu)造、匯編語言、C語言開始解說,讓讀者在實(shí)踐中掌握算法。在這本書的指導(dǎo)下,從零編寫所有代碼,30天后就可以制作出一個具有窗口系統(tǒng)的32位多任務(wù)操作系。
《30天自制操作系統(tǒng)》適合操作系統(tǒng)愛好者和程序設(shè)計人員閱讀。
只需30天 從零開始編寫一個五臟俱全的圖形操作系統(tǒng) 39.1KB迷你系統(tǒng) 實(shí)現(xiàn)多任務(wù)、漢字顯示、文件壓縮,還能聽歌看圖玩游戲 日本編程天才 揭開CPU、內(nèi)存、磁盤以及操作系統(tǒng)底層工作模式的神秘面紗
“好想編寫一個操作系統(tǒng)呀!”筆者的朋友曾說這是所有程序員都曾經(jīng)懷揣的一個夢想。說“所有的程序員”可能有點(diǎn)夸張了,不過作為程序員的夢想,它至少也應(yīng)該能排進(jìn)前十名吧。
也許很多人覺得編寫操作系統(tǒng)是個天方夜譚,這一定是操作系統(tǒng)業(yè)界的一個陰謀(笑)。他們故意讓大家相信編寫操作系統(tǒng)是一件非常困難的事情,這樣就可以高價兜售自己開發(fā)的操作系統(tǒng),而且操作系統(tǒng)的作者還會被頂禮膜拜。那么實(shí)際情況又怎么樣呢?和別的程序相比,其實(shí)編寫操作系統(tǒng)并沒有那么難,至少筆者的感覺是這樣。
在各位讀者之中,也許有人曾經(jīng)挑戰(zhàn)過操作系統(tǒng)的編寫,但因?yàn)樘y而放棄了。擁有這樣經(jīng)歷的人也許不會認(rèn)同筆者的觀點(diǎn)。其實(shí)你錯了,你的失敗并不是因?yàn)榫帉懖僮飨到y(tǒng)太難,而是因?yàn)闆]有人告訴你那其實(shí)是一件很簡單的事而已。
不僅是編寫操作系統(tǒng),任何事都是一樣的。如果講解的人認(rèn)為它很難,那就不可能把它講述得通俗易懂,即便是同樣的內(nèi)容,也會講得無比復(fù)雜。這樣的講解,肯定是很難懂的。
那么,你想不想和筆者一起再挑戰(zhàn)一次呢?如果你曾經(jīng)夢想過編寫自己的操作系統(tǒng),一定會覺得樂在其中的。
可能有人會說,這本書足足有700多頁,怎么會“有趣”和“簡單”呢?唔,這么一說筆者也覺得挺心虛的,不過其實(shí)也只是長了那么一點(diǎn)點(diǎn)啦。平均下來的話,每天只有大約23頁的內(nèi)容,你看,也沒有那么長吧?
這本書的文風(fēng)非常輕松,也許你不知不覺中就會讀得很快。但是這樣的話可能印象不會很深,最好還是能靜下心來慢慢地讀。書中所展示的程序代碼和文字的說明同樣重要,因此也希望大家仔細(xì)閱讀。只要注意這些,理解本書的內(nèi)容就應(yīng)該沒有問題了。
在本書中,我們使用C語言和匯編語言來編寫操作系統(tǒng),不過不必?fù)?dān)心,你可以在閱讀本書的同時來逐步學(xué)習(xí)關(guān)于這些編程語言的知識。本書在這方面寫得非常仔細(xì),如果能有人通過本書終于把C語言中的指針給搞懂了,那筆者的目的也就達(dá)到了。即便是從這樣的水平開始,30天后你也能夠編寫出一個很棒的操作系統(tǒng),請大家拭目以待吧!
川合秀實(shí)(Hidemi Kawai),生于1975年,是一位以“輕量化”編程思想見長的“非主流”開發(fā)者。2000年因自行開發(fā)的OSASK項(xiàng)目而名聲大噪。OSASK是一個開源的32位微型操作系統(tǒng),它并非以Linux等內(nèi)核為基礎(chǔ),而是完全從零開始開發(fā),在一張軟盤的容量下實(shí)現(xiàn)了GUI、多任務(wù)、多語言等高級特性,啟動時間只需1秒。本書的內(nèi)容可以看成是作者以O(shè)SASK為藍(lán)本,教會讀者從零開始開發(fā)一個操作系統(tǒng),同時可以讓初學(xué)者在編寫操作系統(tǒng)的過程中,了解操作系統(tǒng)背后更多的知識。
第0天 著手開發(fā)之前
1 前言
2 何謂操作系統(tǒng)
3 開發(fā)操作系統(tǒng)的各種方法
4 無知則無畏
5 如何開發(fā)操作系統(tǒng)
6 操作系統(tǒng)開發(fā)中的困難
7 學(xué)習(xí)本書時的注意事項(xiàng)(重要。
8 各章內(nèi)容摘要
第1天 從計算機(jī)結(jié)構(gòu)到匯編程序入門
1 先動手操作
2 究竟做了些什么
3 初次體驗(yàn)匯編程序
4 加工潤色
第2天 匯編語言學(xué)習(xí)與Makefile入門
1 介紹文本編輯器
2 繼續(xù)開發(fā)
3 先制作啟動區(qū)
4 Makefile入門
第3天 進(jìn)入32位模式并導(dǎo)入C語言
1 制作真正的IPL
2 試錯
3 讀到18扇區(qū)
4 讀入10個柱面
5 著手開發(fā)操作系統(tǒng)
6 從啟動區(qū)執(zhí)行操作系統(tǒng)
7 確認(rèn)操作系統(tǒng)的執(zhí)行情況
8 32位模式前期準(zhǔn)備
9 開始導(dǎo)入C語言
10 實(shí)現(xiàn)HLT(harib00j)
第4天 C語言與畫面顯示的練習(xí)
1 用C語言實(shí)現(xiàn)內(nèi)存寫入(harib01a)
2 條紋圖案(harib01b)
3 挑戰(zhàn)指針(harib01c)
4 指針的應(yīng)用(1)(harib01d)
5 指針的應(yīng)用(2)(harib01e)
6 色號設(shè)定(harib01f)
7 繪制矩形(harib01g)
8 今天的成果(harib01h)
第5天 結(jié)構(gòu)體、文字顯示與GDT/IDT初始化
1 接收啟動信息(harib02a)
2 試用結(jié)構(gòu)體(harib02b)
3 試用箭頭記號(harib02c)
4 顯示字符(harib02d)
5 增加字體(harib02e)
6 顯示字符串(harib02f)
7 顯示變量值(harib02g)
8 顯示鼠標(biāo)指針(harib02h)
9 GDT與IDT的初始化(harib02i)
第6天 分割編譯與中斷處理
1 分割源文件(harib03a)
2 整理Makefile(harib03b)
3 整理頭文件(harib03c)
4 意猶未盡
5 初始化PIC(harib03d)
6 中斷處理程序的制作(harib03e)
第7天 FIFO與鼠標(biāo)控制
1 獲取按鍵編碼(hiarib04a)
2 加快中斷處理(hiarib04b)
3 制作FIFO緩沖區(qū)(hiarib04c)
4 改善FIFO緩沖區(qū)(hiarib04d)
5 整理FIFO緩沖區(qū)(hiarib04e)
6 總算講到鼠標(biāo)了(harib04f)
7 從鼠標(biāo)接受數(shù)據(jù)(harib04g)
第8天 鼠標(biāo)控制與32位模式切換
1 鼠標(biāo)解讀(1)(harib05a)
2 稍事整理(harib05b)
3 鼠標(biāo)解讀(2)(harib05c)
4 移動鼠標(biāo)指針(harib05d)
5 通往32位模式之路
第9天 內(nèi)存管理
1 整理源文件(harib06a)
2 內(nèi)存容量檢查(1)(harib06b)
3 內(nèi)存容量檢查(2)(harib06c)
4 挑戰(zhàn)內(nèi)存管理(harib06d)
第10天 疊加處理
1 內(nèi)存管理(續(xù))(harib07a)
2 疊加處理(harib07b)
3 提高疊加處理速度(1)(harib07c)
4 提高疊加處理速度(2)(harib07d)
第11天 制作窗口
1 鼠標(biāo)顯示問題(harib08a)
2 實(shí)現(xiàn)畫面外的支持(harib08b)
3 shtctl的指定省略(harib08c)
4 顯示窗口(harib08d)
5 小實(shí)驗(yàn)(harib08e)
6 高速計數(shù)器(harib08f)
7 消除閃爍(1)(harib08g)
8 消除閃爍(2)(harib08h)
第12天 定時器(1)
1 使用定時器(harib09a)
2 計量時間(harib09b)
3 超時功能(harib09c)
4 設(shè)定多個定時器(harib09d)
5 加快中斷處理(1)(harib09e)
6 加快中斷處理(2)(harib09f)
7 加快中斷處理(3)(harib09g)
第13天 定時器(2)
1 簡化字符串顯示(harib10a)
2 重新調(diào)整FIFO緩沖區(qū)(1)(harib10b)
3 測試性能(harib10c~harib10f)
4 重新調(diào)整FIFO緩沖區(qū)(2)(harib10g)
5 加快中斷處理(4)(harib10h)
6 使用“哨兵”簡化程序(harib10i)
第14天 高分辨率及鍵盤輸入
1 繼續(xù)測試性能(harib11a~harib11c)
2 提高分辨率(1)(harib11d)
3 提高分辨率(2)(harib11e)
4 鍵盤輸入(1)(harib11f)
5 鍵盤輸入(2)(harib11g)
6 追記內(nèi)容(1)(harib11h)
7 追記內(nèi)容(2)(harib11i)
第15天 多任務(wù)(1)
1 挑戰(zhàn)任務(wù)切換(harib12a)
2 任務(wù)切換進(jìn)階(harib12b)
3 做個簡單的多任務(wù)(1)(harib12c)
4 做個簡單的多任務(wù)(2)(harib12d)
5 提高運(yùn)行速度(harib12e)
6 測試運(yùn)行速度(harib12f)
7 多任務(wù)進(jìn)階(harib12g)
第16天 多任務(wù)(2)
1 任務(wù)管理自動化(harib13a)
2 讓任務(wù)休眠(harib13b)
3 增加窗口數(shù)量(harib13c)
4 設(shè)定任務(wù)優(yōu)先級(1)(harib13d)
5 設(shè)定任務(wù)優(yōu)先級(2)(harib13e)
第17天 命令行窗口
1 閑置任務(wù)(harib14a)
2 創(chuàng)建命令行窗口(harib14b)
3 切換輸入窗口(harib14c)
4 實(shí)現(xiàn)字符輸入(harib14d)
5 符號的輸入(harib14e)
6 大寫字母與小寫字母(harib14f)
7 對各種鎖定鍵的支持(harib14g)
第18天 dir命令
1 控制光標(biāo)閃爍(1)(harib15a)
2 控制光標(biāo)閃爍(2)(harib15b)
3 對回車鍵的支持(harib15c)
4 對窗口滾動的支持(harib15d)
5 mem命令(harib15e)
6 cls命令(harib15f)
7 dir命令(harib15g)
第19天 應(yīng)用程序
1 type命令(harib16a)
2 type命令改良(harib16b)
3 對FAT的支持(harib16c)
4 代碼整理(harib16d)
5 第一個應(yīng)用程序(harib16e)
第20天 API
1 程序整理(harib17a)
2 顯示單個字符的API(1)(harib17b)
3 顯示單個字符的API(2)(harib17c)
4 結(jié)束應(yīng)用程序(harib17d)
5 不隨操作系統(tǒng)版本而改變的API(harib17e)
6 為應(yīng)用程序自由命名(harib17f)
7 當(dāng)心寄存器(harib17g)
8 用API顯示字符串(harib17h)
第21天 保護(hù)操作系統(tǒng)
1 攻克難題——字符串顯示API(harib18a)
2 用C語言編寫應(yīng)用程序(harib18b)
3 保護(hù)操作系統(tǒng)(1)(harib18c)
4 保護(hù)操作系統(tǒng)(2)(harib18d)
5 對異常的支持(harib18e)
6 保護(hù)操作系統(tǒng)(3)(harib18f)
7 保護(hù)操作系統(tǒng)(4)(harib18g)
第22天 用C語言編寫應(yīng)用程序
1 保護(hù)操作系統(tǒng)(5)(harib19a)
2 幫助發(fā)現(xiàn)bug(harib19b)
3 強(qiáng)制結(jié)束應(yīng)用程序(harib19c)
4 用C語言顯示字符串(1)(harib19d)
5 用C語言顯示字符串(2)(harib19e)
6 顯示窗口(harib19f)
7 在窗口中描繪字符和方塊(harib19g)
第23天 圖形處理相關(guān)
1 編寫malloc(harib20a)
2 畫點(diǎn)(harib20b)
3 刷新窗口(harib20c)
4 畫直線(harib20d)
5 關(guān)閉窗口(harib20e)
6 鍵盤輸入API(harib20f)
7 用鍵盤輸入來消遣一下(harib20g)
8 強(qiáng)制結(jié)束并關(guān)閉窗口(harib20h)
第24天 窗口操作
1 窗口切換(1)(harib21a)
2 窗口切換(2)(harib21b)
3 移動窗口(harib21c)
4 用鼠標(biāo)關(guān)閉窗口(harib21d)
5 將輸入切換到應(yīng)用程序窗口(harib21e)
6 用鼠標(biāo)切換輸入窗口(harib21f)
7 定時器API(harib21g)
8 取消定時器(harib21h)
第25天 增加命令行窗口
1 蜂鳴器發(fā)聲(harib22a)
2 增加更多的顏色(1)(harib22b)
3 增加更多的顏色(2)(harib22c)
4 窗口初始位置(harib22d)
5 增加命令行窗口(1)(harib22e)
6 增加命令行窗口(2)(harib22f)
7 增加命令行窗口(3)(harib22g)
8 增加命令行窗口(4)(harib22h)
9 變得更像真正的操作系統(tǒng)(1)(harib22i)
10 變得更像真正的操作系統(tǒng)(2)(harib22j)
第26天 為窗口移動提速
1 提高窗口移動速度(1)(harib23a)
2 提高窗口移動速度(2)(harib23b)
3 提高窗口移動速度(3)(harib23c)
4 提高窗口移動速度(4)(harib23d)
5 啟動時只打開一個命令行窗口(harib23e)
6 增加更多的命令行窗口(harib23f)
7 關(guān)閉命令行窗口(1)(harib23g)
8 關(guān)閉命令行窗口(2)(harib23h)
9 start命令(harib23i)
10 ncst命令(harib23j)
第27天 LDT與庫
1 先來修復(fù)bug(harib24a)
2 應(yīng)用程序運(yùn)行時關(guān)閉命令行窗口(harib24b)
3 保護(hù)應(yīng)用程序(1)(harib24c)
4 保護(hù)應(yīng)用程序(2)(harib24d)
5 優(yōu)化應(yīng)用程序的大。╤arib24e)
6 庫(harib24f)
7 整理make環(huán)境(harib24g)
第28天 文件操作與文字顯示
1 alloca(1)(harib25a)
2 alloca(2)(harib25b)
3 文件操作API(harib25c)
4 命令行API(harib25d)
5 日文文字顯示(1)(harib25e)
6 日文文字顯示(2)(harib25f)
7 日文文字顯示(3)(harib25g)
第29天 壓縮與簡單的應(yīng)用程序
1 修復(fù)bug(harib26a)
2 文件壓縮(harib26b)
3 標(biāo)準(zhǔn)函數(shù)
4 非矩形窗口(harib26c)
5 bball(harib26d)
6 外星人游戲(harib26e)
第30天 高級的應(yīng)用程序
1 命令行計算器(harib27a)
2 文本閱覽器(harib27b)
3 MML播放器(harib27c)
4 圖片閱覽器(harib27d)
5 IPL的改良(harib27e)
6 光盤啟動(harib27f)
第31天 寫在開發(fā)完成之后
1 繼續(xù)開發(fā)要靠大家的努力
2 關(guān)于操作系統(tǒng)的大小
3 操作系統(tǒng)開發(fā)的訣竅
4 分享給他人使用
5 關(guān)于光盤中的軟件
6 關(guān)于開源的建議
7 后記
8 畢業(yè)典禮
9 附錄
第15天
多任務(wù)(1)
挑戰(zhàn)任務(wù)切換(harib12a)
任務(wù)切換進(jìn)階(harib12b)
做個簡單的多任務(wù)(1)(harib12c)
做個簡單的多任務(wù)(2)(harib12d)
提高運(yùn)行速度(harib12e)
測試運(yùn)行速度(harib12f)
多任務(wù)進(jìn)階(harib12g)
1 挑戰(zhàn)任務(wù)切換(harib12a)
“話說,多任務(wù)到底是啥呢?”我們今天的內(nèi)容,就從這個問題開始吧。
多任務(wù),在英語中叫做“multitask”,顧名思義就是“多個任務(wù)”的意思。簡單地說,在Windows等操作系統(tǒng)中,多個應(yīng)用程序同時運(yùn)行的狀態(tài)(也就是同時打開好幾個窗口的狀態(tài))就叫做多任務(wù)。
對于生活在現(xiàn)代社會的各位來說,這種多任務(wù)簡直是理所當(dāng)然的事情。比如你會一邊用音樂播放軟件聽音樂一邊寫郵件,郵件寫到一半忽然有點(diǎn)東西要查,便打開Web瀏覽器上網(wǎng)搜索。這對于大家來說這些都是家常便飯了吧。可如果沒有多任務(wù)的話會怎么樣呢?想寫郵件的時候就必須關(guān)掉正在播放的音樂,要查東西的時候就必須先保存寫到一半的郵件,然后才能打開Web瀏覽器……光想象一下就會覺得太不方便了。
然而在從前,沒有多任務(wù)反倒是普遍的情形(那個時候大家不用電腦聽音樂,也沒有互聯(lián)網(wǎng))。在那個年代,電腦一次只能運(yùn)行一個程序,如果要同時運(yùn)行多個程序的話,就得買好幾臺電腦才行。
就在那個時候,誕生了最初的多任務(wù)操作系統(tǒng),大家都覺得太了不起了。從現(xiàn)在開始,我們也要準(zhǔn)備給“紙娃娃系統(tǒng)”添加執(zhí)行多任務(wù)的能力了。連這樣一個小不點(diǎn)兒操作系統(tǒng)都能夠?qū)崿F(xiàn)多任務(wù),真是讓人不由地感嘆它生逢其時呀。
稍稍思考一下我們就會發(fā)現(xiàn),多任務(wù)這個東西還真是奇妙,它究竟是怎樣做到讓多個程序同時運(yùn)行的呢?如果我們的電腦里面裝了好多個CPU的話,同時運(yùn)行多個程序倒也順理成章,但實(shí)際上就算我們只有一個CPU,照樣可以實(shí)現(xiàn)多任務(wù)。
其實(shí)說穿了,這些程序根本沒有在同時運(yùn)行,只不過看上去好像是在同時運(yùn)行一樣:程序A運(yùn)行一會兒,接下來程序B運(yùn)行一會兒,再接下來輪到程序C,然后再回到程序A……如此反復(fù),有點(diǎn)像日本忍者的“分身術(shù)”呢(笑)。
為了讓這種分身術(shù)看上去更完美,需要讓操作系統(tǒng)盡可能快地切換任務(wù)。如果10秒才切換一次,那就連人眼都能察覺出來了,同時運(yùn)行多個程序的戲碼也就穿幫了。再有,如果我們給程序C發(fā)出一個按鍵指令,正巧這個瞬間系統(tǒng)切換到了程序A的話,我們就不得不等上20秒,才能重新輪到程序C對按鍵指令作出反應(yīng)。這實(shí)在是讓人抓狂。ǹ蓿
在一般的操作系統(tǒng)中,這個切換的動作每0.01~0.03秒就會進(jìn)行一次。當(dāng)然,切換的速度越快,讓人覺得程序是在同時運(yùn)行的效果也就越好。不過,CPU進(jìn)行程序切換(我們稱為“任務(wù)切換”)這個動作本身就需要消耗一定的時間,這個時間大約為0.0001秒左右,不同的CPU及操作系統(tǒng)所需的時間也有所不同。如果CPU每0.0002秒切換一次任務(wù)的話,該CPU處理能力的50%都要被任務(wù)切換本身所消耗掉。這意味著,如果同時運(yùn)行2個程序,每個程序的速度就只有單獨(dú)運(yùn)行時的1/4,這樣你會覺得開心嗎?如果變成這種結(jié)果,那還不如干脆別搞多任務(wù)呢。
相比之下,即便是每0.001秒切換一次任務(wù),單單在任務(wù)切換上面也要消耗CPU處理能力的10%。大概有人會想,10%也沒什么大不了的吧?可如果你看看速度快10%的CPU賣多少錢,說不定就會恍然大悟,“對啊,只要優(yōu)化一下任務(wù)切換間隔,就相當(dāng)于一分錢也不花,便換上了比現(xiàn)在更快的CPU嘛……”(笑),你也就明白了浪費(fèi)10%也是很不值得的。正是因?yàn)檫@個原因,任務(wù)切換的間隔最短也得0.01秒左右,這樣一來只有1%的處理能力消耗在任務(wù)切換上,基本上就可以忽略不計了。
關(guān)于多任務(wù)是什么的問題,已經(jīng)大致講得差不多了,接下來我們來看看如何讓CPU來處理多任務(wù)。
當(dāng)你向CPU發(fā)出任務(wù)切換的指令時,CPU會先把寄存器中的值全部寫入內(nèi)存中,這樣做是為了當(dāng)以后切換回這個程序的時候,可以從中斷的地方繼續(xù)運(yùn)行。接下來,為了運(yùn)行下一個程序,CPU會把所有寄存器中的值從內(nèi)存中讀取出來(當(dāng)然,這個讀取的地址和剛剛寫入的地址一定是不同的,不然就相當(dāng)于什么都沒變嘛),這樣就完成了一次切換。我們前面所說的任務(wù)切換所需要的時間,正是對內(nèi)存進(jìn)行寫入和讀取操作所消耗的時間。
接下來我們來看看寄存器中的內(nèi)容是怎樣寫入內(nèi)存里去的。下面這個結(jié)構(gòu)叫做“任務(wù)狀態(tài)段”(task status segment),簡稱TSS。TSS有16位和32位兩個版本,這里我們使用32位版。顧名思義,TSS也是內(nèi)存段的一種,需要在GDT中進(jìn)行定義后使用。
參考上面的結(jié)構(gòu)定義,TSS共包含26個int成員,總計104字節(jié)(摘自CPU的技術(shù)資料),我特意把它們分成4行來寫。從開頭的backlink起,到cr3為止的幾個成員,保存的不是寄存器的數(shù)據(jù),而是與任務(wù)設(shè)置相關(guān)的信息,在執(zhí)行任務(wù)切換的時候這些成員不會被寫入(backlink除外,某些情況下是會被寫入的)。后面的部分中我們會用到這里的設(shè)定,不過現(xiàn)在你完全可以先忽略它。
第2行的成員是32位寄存器,第3行是16位寄存器,應(yīng)該沒必要解釋了吧……不對,eip好像到現(xiàn)在還沒講過呢。EIP的全稱是"extended instruction pointer",也就是"擴(kuò)展指令指針寄存器"的意思。這里的"擴(kuò)展"代表它是一個32位寄存器,也就是說其對應(yīng)的16位版本叫做IP,類比一下的話,跟EAX與AX之間的關(guān)系是一樣的。
EIP是CPU用來記錄下一條需要執(zhí)行的指令位于內(nèi)存中哪個地址的寄存器,因此它才被稱為"指令指針"。如果沒有這個寄存器,記性不好的CPU就會忘記自己正在運(yùn)行哪里的程序,于是程序就沒辦法正常運(yùn)行了。每執(zhí)行一條指令,EIP寄存器中的值就會自動累加,從而保證一直指向下一條指令所在的內(nèi)存地址。
說點(diǎn)題外話,JMP指令實(shí)際上是一個向EIP寄存器賦值的指令。JMP 0x1234這種寫法,CPU會解釋為MOV EIP,0x1234,并向EIP賦值。也就是說,這條指令其實(shí)是篡改了CPU記憶中下一條該執(zhí)行的指令的地址,蒙了CPU一把。這樣一來,CPU在讀取下一條指令時,就會去讀取0x1234這個地址中的指令。你看,這不就相當(dāng)于是做了一個跳轉(zhuǎn)嗎?
對了,如果你在匯編語言里用MOV EIP,0x1234這種寫法是會出錯的,還是不要嘗試的好。在匯編語言中,應(yīng)該使用JMP 0x1234來代替MOV EIP,0x1234。
如果在TSS中將EIP寄存器的值記錄下來,那么當(dāng)下次再返回這個任務(wù)的時候,CPU就可以明白應(yīng)該從哪里讀取程序來運(yùn)行了。
按照常識,段寄存器應(yīng)該是16位的才對,可是在TSS數(shù)據(jù)結(jié)構(gòu)中卻定義成了int(也就是DWORD)類型。我們可以大膽想象一下,說不定英特爾公司的人將來會把段寄存器變成32位的,這樣想想也挺有意思的呢(笑)。
第4行的ldtr和iomap也和第1行的成員一樣,是有關(guān)任務(wù)設(shè)置的部分,因此在任務(wù)切換時不會被CPU寫入。也許你會想,那就和第1行一樣,暫時先忽略好了--但那可是絕對不行的!如果胡亂賦值的話,任務(wù)就無法正常切換了,在這里我們先將ldtr置為0,將iomap置為0x40000000就好了。
關(guān)于TSS的話題暫且先告一段落,我們回來繼續(xù)講任務(wù)切換的方法。要進(jìn)行任務(wù)切換,其實(shí)還得用JMP指令。JMP指令分為兩種,只改寫EIP的稱為near模式,同時改寫EIP和CS的稱為far模式,在此之前我們使用的JMP指令基本上都是near模式的。不記得CS是什么了?CS就是代碼段(code segment)寄存器啦。
說起來我們其實(shí)用過一次far模式的JMP指令,就在asmhead.nas的"bootpack啟動"的最后一句(見8.5節(jié))。
JMP DWORD 2*8:0x0000001b
這條指令在向EIP存入0x1b的同時,將CS置為2*8(=16)。像這樣在JMP目標(biāo)地址中帶冒號(:)的,就是far模式的JMP指令。
如果一條JMP指令所指定的目標(biāo)地址段不是可執(zhí)行的代碼,而是TSS的話,CPU就不會執(zhí)行通常的改寫EIP和CS的操作,而是將這條指令理解為任務(wù)切換。也就是說,CPU會切換到目標(biāo)TSS所指定的任務(wù),說白了,就是JMP到一個任務(wù)那里去了。
CPU每次執(zhí)行帶有段地址的指令時,都會去確認(rèn)一下GDT中的設(shè)置,以便判斷接下來要執(zhí)行的JMP指令到底是普通的far-JMP,還是任務(wù)切換。也就是說,從匯編程序翻譯出來的機(jī)器語言來看,普通的far-JMP和任務(wù)切換的far-JMP,指令本身是沒有任何區(qū)別的。
好了,枯燥的講解就到這里,讓我們實(shí)際做一次任務(wù)切換吧。我們準(zhǔn)備兩個任務(wù):任務(wù)A和任務(wù)B,嘗試從A切換到B。
首先,我們需要創(chuàng)建兩個TSS:任務(wù)A的TSS和任務(wù)B的TSS。
本次的HariMain節(jié)選
struct TSS32 tss_a, tss_b;
向它們的ldtr和iomap分別存入合適的值。
本次的HariMain節(jié)選
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;
接著將它們兩個在GDT中進(jìn)行定義。
本次的HariMain節(jié)選
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32);
set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);
將tss_a定義在gdt的3號,段長限制為103字節(jié),tss_b也采用類似的定義。
現(xiàn)在兩個TSS都創(chuàng)建好了,該進(jìn)行實(shí)際的切換了。
我們向TR寄存器存入3 * 8這個值,這是因?yàn)槲覀儎偛虐旬?dāng)前運(yùn)行的任務(wù)定義為GDT的3號。TR寄存器以前沒有提到過,它的作用是讓CPU記住當(dāng)前正在運(yùn)行哪一個任務(wù)。當(dāng)進(jìn)行任務(wù)切換的時候,TR寄存器的值也會自動變化,它的名字也就是"task register"(任務(wù)寄存器)的縮寫。我們每次給TR寄存器賦值的時候,必須把GDT的編號乘以8,因?yàn)橛⑻貭柟揪褪沁@樣規(guī)定的。如果你有意見的話,可以打電話找英特爾的大叔投訴哦(笑)。
給TR寄存器賦值需要使用LTR指令,不過用C語言做不到。唉,各位是不是都已經(jīng)見怪不怪了啊?啥?你早就料到了?(笑)所以說,正如你所料,我們只能把它寫進(jìn)naskfunc.nas里面。
本次的HariMain節(jié)選
load_tr(3 * 8);
本次的naskfunc.nas節(jié)選
_load_tr: ; void load_tr(int tr);
LTR [ESP+4] ; tr
RET
對了,LTR指令的作用只是改變TR寄存器的值,因此執(zhí)行了LTR指令并不會發(fā)生任務(wù)切換。
要進(jìn)行任務(wù)切換,我們必須執(zhí)行far模式的跳轉(zhuǎn)指令,可惜far跳轉(zhuǎn)這事C語言還是無能為力,這種語言還真是不方便啊。沒辦法,這個函數(shù)我們也得在naskfunc.nas里創(chuàng)建。
本次的naskfunc.nas節(jié)選
_taskswitch4: ; void taskswitch4(void);
JMP 4*8:0
RET
也許有人會問,在JMP指令后面寫個RET有意義嗎?也對,通常情況下確實(shí)沒意義,因?yàn)橐呀?jīng)跳轉(zhuǎn)到別的地方了嘛,后面再寫什么指令也不會被執(zhí)行了。不過,用作任務(wù)切換的JMP指令卻不太一樣,在切換任務(wù)之后,再返回這個任務(wù)的時候,程序會從這條JMP指令之后恢復(fù)運(yùn)行,也就是執(zhí)行JMP后面的RET,從匯編語言函數(shù)返回,繼續(xù)運(yùn)行C語言主程序。
另外,如果far-JMP指令是用作任務(wù)切換的話,地址段(冒號前面的4*8的部分)要指向TSS這一點(diǎn)比較重要,而偏移量(冒號后面的0的部分)并沒有什么實(shí)際作用,會被忽略掉,一般來說像這樣寫0就可以了。
現(xiàn)在我們需要在HariMain的某個地方來調(diào)用taskswitch(),可到底該寫在哪里呢?唔,有了,就放在顯示"10[sec]"的語句后面好了。也就是說,程序啟動10秒以后進(jìn)行任務(wù)切換。
本次的HariMain節(jié)選
} else if (i == 10) { /* 10秒計時器*/
putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
taskswitch4(); /*這里! */
} else if (i == 3) { /* 3秒計時器 */
大功告成了?不對,我們還沒準(zhǔn)備好tss_b呢。在任務(wù)切換的時候需要讀取tss_b的內(nèi)容,因此我們得在TSS中定義好寄存器的初始值才行。
本次的HariMain節(jié)選
tss_b.eip = (int) &task_b_main;
tss_b.eflags = 0x00000202; /* IF = 1; */
tss_b.eax = 0;
tss_b.ecx = 0;
tss_b.edx = 0;
tss_b.ebx = 0;
tss_b.esp = task_b_esp;
tss_b.ebp = 0;
tss_b.esi = 0;
tss_b.edi = 0;
tss_b.es = 1 * 8;
tss_b.cs = 2 * 8;
tss_b.ss = 1 * 8;
tss_b.ds = 1 * 8;
tss_b.fs = 1 * 8;
tss_b.gs = 1 * 8;
乍看之下,貌似會有很多看不懂的地方吧,我們從后半段對寄存器賦值的地方開始看。這里我們給cs置為GDT的2號,其他寄存器都置為GDT的1號,asmhead.nas的時候也是一樣的。也就是說,我們這次使用了和bootpack.c相同的地址段。當(dāng)然,如果你用別的地址段也沒問題,不過這次我們只是想隨便做個任務(wù)切換的實(shí)驗(yàn)而已,這種麻煩的事情還是以后再說吧。
繼續(xù)看剩下的部分,關(guān)于eflags的賦值,如果把STI后的EFLAGS的值通過io_load_eflags賦給變量的話,該變量的值就顯示為0x00000202,因此在這里就直接使用了這個值,僅此而已。如果還有看不懂的地方,大概就是eip和esp的部分了吧。
在eip中,我們需要定義在切換到這個任務(wù)的時候,要從哪里開始運(yùn)行。在這里我們先把task_b_main這個函數(shù)的內(nèi)存地址賦值給它。
本次的bootpack.c節(jié)選
void task_b_main(void)
{
for (;;) { io_hlt(); }
}
這個函數(shù)只執(zhí)行了一個HLT,沒有任何實(shí)際作用,后面我們會對它進(jìn)行各種改造,現(xiàn)在就先這樣吧。
task_b_esp是專門為任務(wù)B所定義的棧。有人可能會說,直接用任務(wù)A的棧不就好了嗎?那可不行,如果真這么做的話,棧就會混成一團(tuán),程序也無法正常運(yùn)行。
本次的HariMain節(jié)選
int task_b_esp;
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
總之先寫成這個樣子了。我們?yōu)槿蝿?wù)B的棧分配了64KB的內(nèi)存,并計算出棧底的內(nèi)存地址。請各位回憶一下向棧PUSH數(shù)據(jù)(入棧)的動作,ESP中存入的應(yīng)該棧末尾的地址,而不是棧開頭的地址。
好了,我們已經(jīng)講解得夠多了,現(xiàn)在總算是萬事俱備啦,馬上"make run"一下吧。這個程序如果運(yùn)行正常的話應(yīng)該是什么樣子呢?嗯,啟動之后的10秒內(nèi),還是跟以前一樣的,10秒一到便執(zhí)行任務(wù)切換,task_b_main開始運(yùn)行。因?yàn)閠ask_b_main只有一句HLT,所以接下來程序就全部停止了,鼠標(biāo)和鍵盤也應(yīng)該都沒有反應(yīng)了。
唔……這樣看起來好像很無聊啊,算了,總之我們先來"make run"吧。10秒鐘的等待還真是漫長……哇!停了停了!
看來我們的首次任務(wù)切換獲得了圓滿成功。
……