已故的蘭卡斯特大學的 Doug Shepherd 教授(我的老師)曾經(jīng)告訴我,他曾經(jīng)從無到有寫過一個 OS,之后就再也沒有碰到任何他感覺惱人的編程問題,以及無法進行下去的研究。今天,我們理所當然的在我們的日常工作使用這些奇妙的機器,而無需了解任何底層的內(nèi)容,不需要了解軟件是如何與其進行交互的。 在這里,我們將專注與廣泛使用的 x86 架構的 CPU,我們將拋開所有的軟件,遵循 Doug 早期的步伐,按照如下過程學習:
注意,從實用操作系統(tǒng)的角度,本指南并非旨在擴展,而是旨在將來自多個來源的信息片段匯集到一個完整且連貫的文檔中,從而為您提供底層編程實踐經(jīng)驗,比如 OS 是如何編寫的,以及在編寫的時候會遇到的問題。本指南采用的方法比較獨特,因為特定的語言和工具(例如匯編,C,Make等)不是重點,而是被視為達到目的的手段:我們將學會如何通過這些手段達到我們主要的目標。 這項工作不是作為替代品,而是作為其他優(yōu)秀工作的墊腳石,例如 Minix 項目和一般的操作系統(tǒng)開發(fā)。 架構和啟動2.1 啟動現(xiàn)在,開始我們的旅途! 當我們啟動計算機時,它必須在沒有任何 OS 的幫助下完成初始化。然而,它必須從已經(jīng)加載的永久存儲器(比如硬盤等)中加載 OS 我們不久就會發(fā)現(xiàn),啟動階段,計算機提供的功能非常有限:在這個階段,甚至一個簡單的文件系統(tǒng)都非常奢侈(比如讀寫一個硬盤的問題),但是我們連這個都沒有。幸運的是,我們有 BIOS (the Basic Input/Output Software):一系列軟件列程,這些最初是從芯片加載到內(nèi)存,并在電源開啟的那一刻被初始化。 BIOS 提供對關鍵設備(比如屏幕、鍵盤和硬盤)的自動檢測和基本控制。 當 BIOS 完成對設備的底層測試(最主要的是檢測掛載的內(nèi)存是否工作正常)之后,它必須啟動在某個設備中存儲的 OS。這里,我們要注意,BIOS 不能簡單從硬盤掛載一個代表 OS 的文件,因為 BIOS 沒有文件系統(tǒng)。 BIOS 必須從物理設備的特定地址讀取特定區(qū)塊的數(shù)據(jù)(通常大小是 512 字節(jié))。 所以,BIOS 最初的階段是硬盤的第一個區(qū)塊找到 OS(比如,0頭0道0扇區(qū)),這一區(qū)塊被稱為啟動區(qū)塊。因為某些硬盤可能沒有包含 OS(可能這些硬盤存儲了另外一些內(nèi)容),所以對于 BIOS,檢測一個硬盤的區(qū)塊是否包含啟動代碼還是只是包含簡單數(shù)據(jù)是非常重要的。注意 CPU 并不區(qū)分數(shù)據(jù)和代碼,兩者都會被解釋為 CPU 指令,只是代碼是一些對應 CPU 指令的有用算法實現(xiàn)。 對于 BIOS 簡單的理解是,啟動區(qū)塊的最后兩個字節(jié)內(nèi)容必須是 從這里開始,我們開始控制計算機的執(zhí)行。 2.2 啟動代碼我們可以使用二進制編輯器,來寫原始字節(jié)值到文件中(一個標準的文本編輯器會轉換字符比如‘A‘成一個 ASCII 編碼的值)。因此我們可以制作一個簡單合法的啟動區(qū)塊。 啟動區(qū)塊的機器代碼,每個字節(jié)16進制展示: e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[ 29 more lines with sixteen zero-bytes each ]00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa注意有三個重要的地方:
要十分關注大小端機制。你可能很奇怪為什么魔法數(shù)字 編譯器和匯編器能幫助我們隱藏大小端的細節(jié),比如說一個 16 位的數(shù)據(jù)會被自動轉換成正確的格式。但是,有時候了解大小端很重要,比如說當尋找 bug 的時候,想要知道一個字節(jié)是如何被存放在存儲設備上的。 這可能是計算機能運行的最小的程序,不管怎樣,它是合法的。我們可以通過兩種方式測試。第二種方式最安全也最適合我們的實驗目的:
如果計算機啟動以后一直處于等待狀態(tài),沒有“沒有找到 OS” 這樣的信息的話,說明上述代碼已經(jīng)被成功加載和執(zhí)行。這行代碼主要做的就是無限循環(huán),沒有這個循環(huán),CPU 就會去執(zhí)行內(nèi)存中的下一個指令,大部分情況下是隨機的未初始化的字節(jié)。這會導致 CPU 進入一些非法的狀態(tài),甚至有可能會令 BIOS 運行一些列程去格式化你的磁盤! 記住,是我們編程,然后計算機盲目的執(zhí)行我們的指令,直到斷電。所以我們要確保他執(zhí)行我們設計好的代碼而不是一些在內(nèi)存中隨機存在的字節(jié)。在底層,我們有很多權限和能力去控制計算機。我們來開始學習這些吧! 2.3 CPU 仿真有許多方便的第三方工具,幫助我們測試這些底層代碼,而不需要不斷的啟動機器,或者冒著重要數(shù)據(jù)被丟失的風險。比如說使用 CPU 仿真器,有 Bochs 和 Qemu。不像虛擬機(比如 VMWare 和 VirtualBox)會嘗試優(yōu)化性能,并且借助宿主計算機直接在 CPU 上執(zhí)行指令,仿真器包含一個模擬 CPU 架構的程序,使用變量來表示 CPU 寄存器,并用高層的控制結構來模擬底層調(diào)整等??偟膩碚f,它更慢,但是更適合開發(fā)和測試這樣的系統(tǒng)。 注意,為了讓仿真器做任何有用的事,需要編寫代碼并被編譯成磁盤鏡像文件來運行。一個鏡像文件就是原生數(shù)據(jù)(比如機器代碼和字節(jié)),并會被寫進硬盤、CD 或者 U 盤中。甚者有些仿真器能夠從下載或者 CD 中的鏡像文件中成功的啟動和運行一個真實的 OS(雖然這種情況最適合的方案還是虛擬機技術)。 仿真器翻譯底層顯示設備的指令成像素,并在桌面上渲染,然后你就能在真實的顯示器上看到了。 總之,對于這篇文檔的練習,在仿真器上能成功運行的機器代碼也能成功的在真實架構的設備上運行,唯一的區(qū)別就是慢了點。 2.3.1 Bochs:x86 CPU 仿真器Bochs 需要我們在本地目錄配置一個文件 下面展示了一個我們將用到的 Bochs 的配置文件的樣子: 為了用 Bochs 測試我們的啟動代碼,只需要輸入 $bochs一個簡單的實驗,試著改變 BIOS 的魔法數(shù)字然后重寫運行 Bochs。 因為 Bochs CPU 仿真器和真實的很接近,完成在 Bochs 上的測試之后,你可以在真實設備上啟動之前的代碼,你會發(fā)現(xiàn)它會運行的更快。 2.3.2 QEMUQEMU 和 Bochs 很像。但是它更加的高效,并且除了 x86 之外的其他 CPU 架構。不過他的文檔沒有 Bochs 豐富。它不需要配置文件就可以運行,比如這樣: 2.4 16進制表示法我們前面已經(jīng)看過一個 16進制的例子。理解為什么16進制在底層編程經(jīng)常被使用到很重要。 首先,可能會問,我們理解10進制是那么自然,為什么不用10進制呢?我不是這方面的專家,不過很可能因為大部分人有十個手指,所以我們習慣用十進制。 十進制的基底是10,有十個不同的數(shù)字符號。16進制的基底是16,需要16個數(shù)字符號,所以需要額外6個符號,一個簡單的做法是使用字符,比如 1,2,...8,9,a,b,c,e,d,f,其中符號 d 表示 13。 為了區(qū)別不同的進制,16進制表示的時候會在前面加 計算機表示一個數(shù)使用的是一系列位(二進制位),因為計算機基本只能區(qū)別兩個電路狀態(tài):0 和 1,就好像計算機只有兩個手指一樣。所以為了表示一個大于1的數(shù)字,計算機需要將一系列位給組合起來,就像我們表示大于9的數(shù)字用兩個或者更多的位(比如 456,23...)。 為了簡化期間,特定數(shù)量的一系列位會被稱為 所以,16進制的優(yōu)勢在于,一系列位的表示會相當?shù)拈L,很難書寫,但是很容易被轉換成更短的16進制表示,并且,我們將4位二進制數(shù)字表示成一位16進制數(shù)字,而不是將所有位表示成一個數(shù)字(不管是16進制還是32或者64進制),因為這樣更加簡單。下面的圖清楚的展示了這個: 將二進制數(shù)字轉換成十進制和16進制: ![]() 將二進制數(shù)字轉換成十進制和16進制 引導扇區(qū)編程(16位模式)即使有樣例代碼,你也毫無疑問會覺得在二進制編輯器編寫機器代碼是很令人沮喪的。你必須記住或者經(jīng)常查閱,某些特定的機器碼在 CPU 上的不同功能。幸運的是,匯編語言可以更加用戶友好,同時能表達特定的機器碼在 CPU 上的作用。 在這一章,我們會研究引導扇區(qū)編程,讓我們能夠在熟悉匯編的同時又能夠在功能匱乏的引導階段把我們的程序運行起來。 重識引導扇區(qū)現(xiàn)在,讓我們用匯編語言重新構建一個扇區(qū)代碼(而不是之前那樣直接用機器代碼),因為用匯編可以很好的表達底層變量。 使用匯編器,我們可以將匯編代碼轉換成真實的機器代碼: $nasm boot_sect.asm -f bin -o boot_sect.bin
注意,我們這里使用 除了保存這個文件到引導扇區(qū)然后重啟機器,我們也可以很方便的用 Bochs 測試我們的程序: $bochs或者,我們也可以使用 QEMU: 除此之外,也可以使用虛擬機加載該鏡像文件,或者將該鏡像文件寫入到可啟動的介質(zhì)(比如 U 盤),然后從真實的計算機上啟動它。注意將鏡像文件寫入介質(zhì),不是簡單的將它添加到介質(zhì)的文件系統(tǒng)中:你必須借助合適的工具將它從底層直接寫入扇區(qū)。 如果我們了解匯編器轉換的真是機器代碼,可以運行下面的命令,它會將二進制內(nèi)容轉換成16進制格式,方便閱讀: $od -t x1 -A n boot_sect.bin運行這個命令,你會看到之前熟悉的機器代碼。 祝賀你??!你剛用匯編器寫了一個啟動代碼!我們將會知道,所有的 OS 必須用這種方式啟動,然后才能使用高層的抽象(比如高層語言,c/c++)。 16位模式CPU 廠商必須保證他們的產(chǎn)品能夠兼容以前的 CPU,這導致一些老的軟件,在特定的老的 OS 上,能夠運行在更現(xiàn)代的 CPU 上。 Intel 提供的兼容解決方案是模擬老的 CPU:Intel 8086。這款 CPU 支持模擬16位指令并且沒有內(nèi)存保護機制。內(nèi)存保護對于現(xiàn)代的 OS 的穩(wěn)定非常重要。因為它允許 OS 嚴格限制用戶進程訪問內(nèi)核內(nèi)存,無論是故意的還是有意的。因為這會令用戶進程規(guī)避 OS 的安全機制,甚者令整個系統(tǒng)面臨風險。 所以,為了向后兼容,對于 CPU,支持現(xiàn)代 OS 的更高級的32或者64位保護模式的同時,又能通過16位初始化啟動,讓老的 OS 繼續(xù)運行,是非常重要的。在后面我們會詳細介紹如何從16位模式過度到32位保護模式。 通常,我們說 CPU 是16位的,指的是它一次只能執(zhí)行最長是16位的指令。比如,一個16位 CPU 有一個特別的指令能夠在一個 CPU 周期內(nèi)將兩個16位的數(shù)字加起來。如果一個進程需要將兩個32位數(shù)字相加的話,那么比起16位,它需要更多的 CPU 周期。 首先,我們會研究16位模式環(huán)境,因為所有的 OS 都是從此開始的。后面我們會學習32位保護模式,以及這樣的好處。 額,你好?現(xiàn)在我們開始寫一個簡單的啟動代碼,只是簡單的打印信息到屏幕上。為此,我們需要學習一些基本的 CPU 工作概念和如何使用 BIOS 管理屏幕設備 首先,讓我們思考我們這里要做什么。我們想要在屏幕上打印一個字符。但是我們不知道如何使用屏幕設備,因為可能有很多不同種類的屏幕設備,并且有不同的接口。這就是為什么使用 BIOS 的原因。因為 BIOS 已經(jīng)做了自動檢測硬件機制,至少很明顯,啟動階段 BIOS 就能在屏幕上打印信息。或許這能幫我們一手。 所以,接下來,我們希望請求 BIOS 能為我們打印一些字符,但是怎么做?這里沒有 Java 庫幫助我們打印信息到屏幕,這簡直就是做夢。但是我們可以確定,在計算機內(nèi)存的某個地方, BIOS 機器代碼知道如何打印信息到屏幕。真相是,我們可以知道 BIOS 在內(nèi)存中的代碼,并用某種方式執(zhí)行它。但顯示很糟糕,因為不同的機器 BIOS 內(nèi)部的細節(jié)會有所不同。 在這里,我們使用基本的計算機機制:中斷。 中斷中斷是一種讓 CPU 暫時停止當前正在處理的任務,并轉而去執(zhí)行更高優(yōu)先級的執(zhí)行,完成之后再返回處理原先的任務的機制。一個中斷可以通過軟件中斷觸發(fā)(比如 int 0x10),或者被一些更高優(yōu)先級任務的硬件設備觸發(fā)(比如讀取網(wǎng)絡設備的輸入數(shù)據(jù))。 每一種中斷被表示成在中斷向量表中的索引。中斷向量表是被 BIOS 初始化的,并位于內(nèi)存的其實地址(比如在物理內(nèi)存的 0x0 位置),包含的內(nèi)容是一些地址指針指向 ISR (中斷服務例程),一個 ISR 是一系列機器指令,很像我們的啟動代碼,不過處理的是一些獨特的中斷服務。(比如從磁盤或者網(wǎng)絡讀取數(shù)據(jù)) BIOS 會添加一些它自己的 ISR 到中斷向量表中,來處理計算機某些方面的任務。比如,中斷 不過,為 BIOS 的每一個例程分配一個中斷是很浪費的。所以 BIOS 比如使用 switch 語句,根據(jù)預先在某個 CPU 寄存器( 寄存器就像我們在高層語言中使用變量,如果在某個例程中能夠暫時存儲數(shù)據(jù)會很有用。所有的 x86 CPU 都有4中不同目的的寄存器, 注意 有時候,處理一個字節(jié)會更加方便,所以這些寄存器允許我們獨立設置它的高位和低位字節(jié): mov ax, 0 ; ax -> 0x0000, or in binary 0000000000000000mov ah, 0x56 ; ax -> 0x5600 mov al, 0x23 ; ax -> 0x5623mov ah, 0x16 ; ax -> 0x1623小結回憶一下,我們將要用 BIOS 為我們在屏幕上打印字符。通過設置 下面的代碼展示了完成的啟動區(qū)塊代碼。注意,在這種情況下,只需要設置 下面的原生機器碼展示了上述匯編代碼經(jīng)過匯編處理的結果: b4 0e b0 48 cd 10 b0 65 cd 10 b0 6c cd 10 b0 6c cd 10 b0 6f cd 10 e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa上述代碼是 CPU 真實執(zhí)行的代碼,如果你驚訝于你要付出如此多的努力和理解來完成這么一個程序,那么請記住,這些指令和 CPU 十分相關,它們看起來簡單,但是執(zhí)行卻非???。你已經(jīng)開始理解計算機的工作方式了,因為它就是這樣字的。 你好,世界!現(xiàn)在我們開始嘗試寫一個稍微有點不同的但更高級一點的“你好”程序。這會引入一點其他的知識,比如基本的 CPU 知識和內(nèi)存布局。 內(nèi)存,地址以及標簽我們之前提到過 CPU 是如何獲得以及執(zhí)行內(nèi)存中的指令的,也知道 BIOS 是如何加載 512 字節(jié)的啟動代碼到內(nèi)存中的,并且也完成了它的初始化,讓 CPU 無限循環(huán)的執(zhí)行第一條指令。 所以,我們的啟動區(qū)塊代碼在內(nèi)存的某個地方,那么具體哪里呢?我們可以想象主內(nèi)存是一個很長的字節(jié)序列,能夠被通過地址(比如索引)被訪問到。如果我們想知道內(nèi)存中第 54 字節(jié)的內(nèi)容,那么 54 是我們的地址,為了簡單方便起見,經(jīng)常被表示成16進制格式: 我們啟動代碼的開始處,也就是機器碼的最開始在內(nèi)存的某個地址處,并且是 BIOS 幫我們放在那兒的。我們可以假設,除非我們知道,那么 BIOS 應該在內(nèi)存的開始處加載我們的代碼,即地址 BIOS 一般總是加載啟動代碼到地址 ![]() 經(jīng)典的啟動后底層內(nèi)存布局 打印 X現(xiàn)在我們要開始玩一個游戲叫做“找到這個字節(jié)”,通過這個,我們會描述內(nèi)存映射、匯編代碼中的標簽,以及知道 BIOS 加載到哪里了。我們將會寫一個匯編程序,會持有一個字節(jié)的字符數(shù)據(jù),然后我們會嘗試在屏幕上打印改字符。為此,我們需要知道絕對內(nèi)存地址,這樣我們才能加載它到 首先,當我們在程序中定義數(shù)據(jù)的時候,我們用前置標簽( b4 0e b0 1e cd 10 a0 1e 00 cd 10 bb 1e 00 81 c3 00 7c 8a 07 cd 10 a0 1e 7c cd 10 e9 fd ff 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa上述是匯編器生成的機器代碼,可以發(fā)現(xiàn)程序中的 X,它的16進制表示是 如果我們運行程序,會發(fā)現(xiàn),只有后面兩種方式成功打印了 X 第一次嘗試的問題是,它試圖把偏移地址加載到 那么為什么第二次嘗試也失敗了呢?問題是,CPU 對待偏移地址是認為它是距離內(nèi)存的起始位置的偏移。而不是我們被加載代碼開始的偏移。第二次嘗試實際上會導致它訪問中斷向量表。 第三次嘗試中,我們相信 BIOS 是把我們的代碼加載到了內(nèi)存地址 第四次嘗試,做的有點“小聰明”,通過預先計算 BIOS 加載啟動代碼中 “X” 的地址,得到 第四次嘗試提醒我們?yōu)槭裁礃撕灪苡杏?。如果沒有標簽的話,我們需要手動計算匯編器生成的機器碼中的地址,然后再去更新代碼中的相應內(nèi)容,再重新用匯編器生成代碼。 現(xiàn)在我們看到了 BIOS 實際上將我們的啟動代碼加載到了地址 在代碼中總是手動計算標簽的內(nèi)存偏差地址是很不方便的。所以,如果你在代碼的起始處添加下列代碼的話,很多匯編器會在匯編的時候自動糾正標簽的引用地址,該代碼告訴匯編器你希望代碼被加載到內(nèi)存的何處。 問題1: 當添加 定義字符串假設你想打印預定義的信息,(比如,“正在啟動中...”),你要如何在匯編代碼中定義這樣字符呢?我們要記住,我們的計算機不知道什么是字符串,一個字符串只是內(nèi)存中一系列的數(shù)據(jù)單元(比如字節(jié)、字等)。 匯編中,我們可以如下方式定義字符串: my_string: db ’Booting OS’我們之前看到過 有一件事我們要知道,知道一個字符串能多長的重要性不亞于它存放在哪兒。因為我們要編寫處理這些字符串的代碼,所以了解如何得知字符串的長度是很重要的。有幾種可能的方式,但是實際上匯編器,會將字符串定義為非空終結符。這里的意思是,字符串的最后一個字節(jié)是0: 后面遍歷一個字符串的時候,可能會打印每個字符,我們可以很容易的知道我們時候到達字符串的末尾了。 使用棧當面臨底層計算的時候,我們經(jīng)常聽到很多人會討論棧,好像這個東西很特殊一樣。棧其實只是為了解決下面這個不便:CPU 只有有限的寄存器用于暫時的變量存儲,但我們經(jīng)常需要比寄存器數(shù)量更多的臨時存儲。我們當然可以使用內(nèi)存,不過通過內(nèi)存地址讀寫是不方便的,尤其當我們不在乎數(shù)據(jù)被存放的真實地方。不久我們很看到,在函數(shù)調(diào)用中的參數(shù)傳遞中是非常有用的。 CPU 提供兩個指令,允許我們存取棧頂?shù)臄?shù)據(jù): 棧是通過兩個特殊的 CPU 寄存器實現(xiàn)的: 下面的啟動代碼展示了棧的使用 問題2: 下面的啟動代碼將會以什么順序打?。?'C' 這個字符會存放在哪個絕對內(nèi)存地址?你可以改代碼來驗證你的想法,不過一定要解釋為什么是這樣。 ;; A simple boot sector program that demonstrates the stack. ;mov ah, 0x0e ; int 10/ah = 0eh -> scrolling teletype BIOS routinemov bp, 0x8000 ; Set the base of the stack a little above where BIOS mov sp, bp ; loads our boot sector - so it won’t overwrite us.push ’A’ ; Push some characters on the stack for later push ’B’ ; retreival. Note, these are pushed on aspush ’C’ ; 16-bit values, so the most significant byte ; will be added by our assembler as 0x00.pop bx ; Note, we can only pop 16-bits, so pop to bx mov al, bl ; then copy bl (i.e. 8-bit char) to alint 0x10 ; print(al)pop bx ; Pop the next value mov al, bl int 0x10 ; print(al)mov al, [0x7ffe] ; To prove our stack grows downwards from bp, ; fetch the char at 0x8000 - 0x2 (i.e. 16-bits) int 0x10 ; print(al)jmp $ ; Jump forever. ; Padding and magic BIOS number.times 510-($-$$) db 0 dw 0xaa55控制結構如果我們不知道如何寫基本的控制代碼,比如 這些高層的控制語句最終會被轉換成 jump 語句。事實上,我們前面已經(jīng)看過最簡單的代碼了: 或者上述的等價的代碼: jmp $ ; jump to address of current instruction這個指令提供了一個無條件轉移功能(它總是 jump),不過我們更希望根據(jù)某個條件跳轉(比如不斷循環(huán)直到循環(huán)十次等等)。 在匯編語言中實現(xiàn)條件跳轉是這樣的:首先執(zhí)行一個比較指令,然后執(zhí)行一個特殊的條件轉移指令 在 C 或者 Java 語言中,看起來像這樣: if(ax == 4) { bx = 23;} else { bx = 45;}我們可以從上面的匯編代碼中看到,在幕后, 基于 問題3: 從高層語言的角度規(guī)劃跳轉代碼,然后用匯編語言替換會很有用。試一下轉換下列的偽匯編代碼為真實的匯編代碼,使用 mov bx , 30if (bx <= 4) { mov al, ’A’} else if (bx < 40) { mov al, ’B’} else { mov al, ’C’}mov ah, 0x0e ; int=10/ah=0x0e -> BIOS tele-type output int 0x10 ; print the character in aljmp $; Padding and magic number. times 510-($-$$) db 0dw 0xaa55調(diào)用函數(shù)在高層語言中,我們會將一個大問題寫成一個通用目的函數(shù)(比如打印信息,寫文件等等),然后我們會在代碼中不斷的使用它,一般是通過改變傳遞給函數(shù)的參數(shù)來獲取不同的輸出。從 CPU 角度,函數(shù)就是跳轉到某個有用的例程的地址處,然后再跳轉回到跳轉之前的下一條指令。 我們可以模擬一個函數(shù)的調(diào)用像這樣: 首先,注意我們是如何使用 不幸的是,上面這種方式我們需要明確的告訴它當結束的時候要返回到哪里,這樣的話,就不能從任意的地方調(diào)用這個函數(shù)了(它總是返回到同樣的地址,在這里就是 從參數(shù)傳遞的方式借鑒一下,調(diào)用者代碼可用存放一個準確的返回地址(比如,調(diào)用之后的那行代碼)在某個公認的地方。然后被調(diào)用者可以跳轉回那個地址。 CPU 會用 ......mov al, ’H’ ; Store ’H’ in al so our function will print it. call my_print_function......my_print_function: mov ah, 0x0e ; int=10/ah=0x0e -> BIOS tele-type output int 0x10 ; print the character in al ret我們的函數(shù)基本上很完美了,不過有一個很丑陋的問題,盡早的認識到會很有幫助。當我們在匯編代碼中調(diào)用一個函數(shù)的時候,比如一個打印函數(shù),這個函數(shù)的內(nèi)部很可能會用幾個寄存器去幫助它的執(zhí)行的實現(xiàn)(事實上,由于寄存器資源稀有,它幾乎一定會這樣做的),所以當我們的代碼從函數(shù)調(diào)用返回的時候,我們之前存放在 一個明智的守規(guī)矩的函數(shù)會立刻將任何它想要使用的寄存器的內(nèi)容 push 到棧中,然后在它要返回的時候馬上 pop(重新恢復寄存器在調(diào)用之前的值) 它們。因為一個函數(shù)很可能會使用許多通用寄存器,CPU 實現(xiàn)了兩個方便的指令, 包含文件有時候,你可能會想在多個程序中復用你的代碼。 %include 'my_print_function.asm' ; this will simply get replaced by ; the contents of the file...mov al, ’H’ ; Store ’H’ in al so our function will print it. call my_print_function小結我們已經(jīng)了解了一下 CPU 和匯編相關的知識,現(xiàn)在可用開始編寫一個稍微復雜有點的 “Hello, word” 啟動程序了。 問題4: 將這一節(jié)學到的內(nèi)容利用起來,來寫一個函數(shù)打印以0結尾的字符串,這個函數(shù)可以用如下的方式使用: 為了好的分數(shù),請注意函數(shù)要小心處理寄存器,并且最好每行代碼都有相應的注釋來闡述你的理解。 總結我們好像仍然沒有什么很大的進展。不過,因為我們要工作的環(huán)境比較特殊,所以這還好,也很正常。如果你到現(xiàn)在為止的理解的話,我們的進展就很順利。 護士,幫幫我!!目前,我們已經(jīng)成功的讓計算機打印我們加載到內(nèi)存中的字符和字符串,很快,我們會試著從磁盤加載數(shù)據(jù)。如果我們能夠展示存儲在任意內(nèi)存地址處的16進制的格式的數(shù)據(jù)的話,這對我們實際想要加載的東西很有幫助。記住,我們沒有奢侈的好用的開發(fā)環(huán)境,也沒有調(diào)試器幫助我們一行行調(diào)試觀察代碼。當我們犯錯的時候,計算機給我們唯一的最好的反饋是什么也沒有發(fā)生,所以我們要仔細。 我們已經(jīng)完成了一個例程來打印字符串。現(xiàn)在我們要拓展那個想法,一個打印16進制格式的例程,這對于我們在底層環(huán)境工作會很有用。 我們仔細想想要怎么做,首先思考一下我們會怎樣用這個例程呢?在高級語言中,我們可能會像這樣: mov dx, 0x1fb6 ; store the value to print in dx call print_hex ; call the function; prints the value of DX as hex.print_hex:......ret既然我們想要屏幕上打印字符串,我們可能可以復用我們之前的打印函數(shù)去做真正的打印工作,所以我們主要的工作是如何轉換在 問題5實現(xiàn) 讀取磁盤我們已經(jīng)介紹了 BIOS,以及嘗試了一下計算機底層的開發(fā),但是有個小問題擋在我們開發(fā) OS 的路上:BIOS從磁盤的第一個扇區(qū)加載我們的啟動代碼,但這幾乎是它能加載的所有了。如果我們的 OS 代碼很龐大怎么辦?比如說大于 512 字節(jié)? OS 通常不會是一個 512 字節(jié)大小的。所以,第一件要做的事就是,將它們剩余的代碼從磁盤引導到內(nèi)存中,然后開始執(zhí)行。幸運的是,就像之前提示的一樣,BIOS 提供了一些例程允許我們管理在磁盤上的數(shù)據(jù)。 基于段的擴展內(nèi)存當 CPU 運行在16位真實環(huán)境中時,寄存器最大的大小是16位,這意味著,我們能引用的最大的內(nèi)存地址是 為了繞過這個限制,CPU 設計者添加了一些特殊的寄存器, 關于段地址最惱人的一件事是:相鄰的段總是會發(fā)生16字節(jié)的重疊,所以不同的段和偏移計算出來的絕對地址有時候會一樣。但是,在遇到這個問題之前,我們暫時了解到這里。 為了計算絕對地址,CPU 會將段寄存器中的值乘以16,然后加上你提供的偏移地址。因為我們用的是16進制,當將一個數(shù)乘16時,我們只需要簡單的將兩個0添加到左邊(原文有誤,說一個0,應該是兩個0),比如 0x42 * 16 = 0x4200.所以如果我們設置 下面展示了一個等價于我們使用 ;; A simple boot sector program that demonstrates segment offsetting ;mov ah, 0x0e ; int 10/ah = 0eh -> scrolling teletype BIOS routinemov al, [the_secret]int 0x10 ; Does this print an X?mov bx, 0x7c0 ; Can ’t set ds directly , so set bxmov ds, bx ;then copy bx to ds.mov al, [the_secret] int 0x10 ; Does this print an X?mov al, [es:the_secret] ; Tell the CPU to use the es (not ds) segment.int 0x10 ; Does this print an X?mov bx, 0x7c0mov es, bxmov al, [es:the_secret]int 0x10 ; Does this print an X?jmp $ ; Jump forever.the_secret: db 'X'; Padding and magic BIOS number. times 510-($-$$) db 0dw 0xaa55由于這里我們沒有時候 注意至少在16位模式下,CPU 的一個限制,一個看起對的指令 所以,基于段的地址允許我們訪問到更多的內(nèi)存,大于1MB(0xffff * 16 + 0xffff)。后面當我們轉到32位保護模式時,我們會了解到如何訪問更多的內(nèi)存。目前對于16位模式,了解這些已經(jīng)夠了。 磁盤驅動工作方式硬盤驅動包含一個或多個堆疊起來的盤,盤下面有個讀寫頭,就像老式播放器,(為了增加容量,所以將幾個盤堆疊起來)訪問磁頭會從某個特定的盤的表面經(jīng)過。因為某個特定的盤可以在它兩個表面都被讀寫,一個讀寫磁頭會有一個在盤上面,一個在盤下面。下圖展示了經(jīng)典的硬盤驅動的內(nèi)部結構,并展示了堆疊的磁盤和暴露出來的磁頭。 ![]() 硬盤內(nèi)部結構 注意這里的描述的內(nèi)容在軟磁盤上也一樣適用,不過沒有堆疊的磁盤,只有一個磁盤。 金屬外表的磁盤使得它們表面的特定區(qū)域可以被磁頭磁化,或反磁化,所以能夠有效的永久存儲任何狀態(tài)。因此如何描述將要被讀寫的數(shù)據(jù)在磁盤表面的精確地址很重要。目前使用 CHS (Cylinder-Head-Sector)來表示磁盤的數(shù)據(jù)地址。這是一個有效的 3D 坐標系統(tǒng):
如下圖所示: ![]() 硬盤內(nèi)部結構 使用 BIOS 讀取磁盤不久我們會知道,不同的設備需要使用不同的例程。比如,軟盤在使用之前我們需要手動為磁盤下的讀寫磁頭開啟關閉發(fā)動裝置。大部分的硬盤設備有很多實用的自動化本地芯片,不過設備是如何連接 CPU 的總線技術(比如 ATA/IDE,SATA,SCSI,USB 等)影響了我們?nèi)绾问褂盟鼈?。幸運的是,BIOS 提供了幾種磁盤例程將所有這些不同抽象化為一般的磁盤設備。 我們想要使用的這個 BIOS 例程就是通過 注意,出于某種原因(比如,我們索引一個扇區(qū)但是沒有考慮磁盤的限制,嘗試讀一個不存在的扇區(qū),軟盤沒有被考慮在內(nèi)),BIOS 可能會讀取磁盤失敗,所以知道如何檢測這種情況很重要,不然,我們可能覺得我們已經(jīng)讀了一些數(shù)據(jù),但事實上,目的地址的內(nèi)存仍然包含的是一些隨機的字節(jié)數(shù)據(jù)。幸運的是,BIOS 會更新某些寄存器讓我們知道這些失敗的情況: ......int 0x13 ; Issue the BIOS interrupt to do the actual read.jc disk_error ; jc is another jumping instruction, that jumps ; only if the carry flag was set.; This jumps if what BIOS reported as the number of sectors; actually read in AL is not equal to the number we expected.cmp al, <no. sectors expected >jne disk_errordisk_error : mov bx, DISK_ERROR_MSG call print_string jmp $; Global variables DISK_ERROR_MSG: db 'Disk read error!', 0小結像早前解釋的,能夠從磁盤讀取很多字節(jié)對于啟動我們的 OS 是很重要的。所以這里,我們使用這一節(jié)學到的內(nèi)容來實現(xiàn)了一個有用的例程,這個例程的作用是從磁盤簡單的讀取緊隨啟動代碼之后的前面n個扇區(qū)內(nèi)容: 我們可以寫一個啟動代碼測試上述代碼: ; Read some sectors from the boot disk using our disk_read function[org 0x7c00]mov [BOOT_DRIVE], dl ; BIOS stores our boot drive in DL, so it’s ; best to remember this for later.mov bp, 0x8000 ; Here we set our stack safely out of the mov sp, bp ; way, at 0x8000mov bx, 0x9000 ; Load 5 sectors to 0x0000(ES):0x9000(BX)mov dh, 5 ; from the boot disk.mov dl, [BOOT_DRIVE] call disk_loadmov dx, [0x9000] ; Print out the first loaded word, whichcall print_hex ; we expect to be 0xdada , stored ; we expect to be 0xdada , storedmov dx , [0 x9000 + 512 ; Also, print the first word from thecall print_hex ; 2nd loaded sector: should be 0xfacejmp $%include '../print/print_string.asm' ; Re-use our print_string function%include '../hex/print_hex.asm' ; Re-use our print_hex function%include 'disk_load.asm'; Include our new disk_load function; Global variables BOOT_DRIVE: db 0; Bootsector padding times 510-($-$$) db 0 dw 0xaa55; We know that BIOS will load only the first 512-byte sector from the disk, ; so if we purposely add a few more sectors to our code by repeating some; familiar numbers, we can prove to ourselfs that we actually loaded those ; additional two sectors from the disk we booted from.times 256 dw 0xdada times 256 dw 0xface |
|
|
來自: 東耳果果 > 《電腦 機器人 太空 激光》