《操作系統真象還原》讀書筆記 第3章

0x1 地址、section、vstart

0x1.1 什麼是地址

地址只是數字,描述各種符號在源程序中的位置,它是源代碼文件中各符號偏移文件開頭的距離。由於指令和變量所佔內存大小不同,故他們相對於文件開頭偏移量參差不齊。源碼文件中各符號地址是由編譯器來規劃的。
編譯器的工作就是給各符號編址。編譯器根據所在硬件平臺特性,將源代碼中的每一個符號(指令和數據)都按照硬件平臺的特性分配空間,在不考慮對齊情況下,這些符號都在空間上彼此相鄰,連續分佈,它們在程序中距第一個符號的距離便是他們載程序中的地址。(ps:跟文件文件偏移一個意思)
本質上,程序中各種數據結構的訪問,就是通過“該數據結構的起始地址+該數據結構所佔內存大小決定的”來實現的。這就解釋了爲什麼要給出變量類型,因爲變量類型規定了變量所佔內存的大小,每種類型都有其對應的內存容量。
程序中定義的任何一個變量,在編譯後的可執行文件中都會佔據一席之地。此變量在文件中的位置是編譯器來安排的。編譯器無論怎麼安排程序中的數據,必然有一個先後順序,而佔據第一位的數據,其地址便是整個程序的起始地址,在它後面的數據依次排開。

0x1.2 什麼是section

編譯器提供的關鍵字Section只是爲了讓程序員在邏輯上將程序劃分成幾個部分,因爲它是僞指令,CPU不知道這是什麼東西。一般section的應用場所是根據不同的屬性人爲地將程序劃分幾部分,如數據放在一個section中,指令放在另一個section中,這樣程序員編便將指令和數據分開了,使代碼清晰明瞭,更易維護。程序如何劃分並沒有規定,完全看程序員喜好,甚至可以利用section把程序切的零碎不堪。

0x1.3 什麼是vstart

vstart是實模式下的虛擬起始地址。作用是爲section的數據指定一個虛擬的起始地址,也就是根據此地址,在文件中是找不到相關數據的,是虛擬的,假的,文件中所有符號都不在這個地址上。它只是告訴編譯器將所有的區段起始地址替換成程序員所設置的地址,再無它意。編譯器沒有加載功能,只有編譯功能。真正的程序加載是由加載器來做的。加載器會讀取程序員設置的vstart地址,並把程序加載在vstart設置的內存地址上。
mbr用vstart=0x7c00來修飾的原因,是因爲開發人員知道mbr要被j加載器(BIOS)加載到物理地址0x7c00,mbr後續的物理地址都是0x7c00+。另外,BIOS進入mbr是通過jmp 0:0x7c00來實現的,故此是cs已經變成0,相當於平坦模式了,只不過此時平坦模式大小隻有65535字節,不是4GB。所以mbr中個數據編譯出來的地址(大於等於0x7c00)實際上都成了偏移地址,這樣“0*16:0x7c00+”來訪問到被加載到0x7c00的mbr是正確無誤的。所以,用vstart的時機是:我預先知道我的程序將來被加載到某地址處。程序只有加載到非0地址時vstart纔是有用的,程序默認起始地址是0。
由於實模式下回使用段地址乘16,所以我們在實模式下設置vstart時要事先除16。注意,保護模式下段寄存器中的值是不用乘16的。

0x2 CPU的實模式

0x2.1 CPU的工作原理

CPU大體分爲三部分:控制單元、運算單元、存儲單元。
控制單元是CPU的控制中心,CPU需要經過它的幫助才知道自己下一步需要做什麼。而控制單元大致分爲指令寄存器IR(Instruction Register)、指令譯碼器ID(Instruction Decoder)、操作控制器OC(Operation Controller)組成。程序被加載入內存後,也就是指令這時候都在內存裏了,指令指針寄存器IP指向內存中下一條待執行指令的地址,控制單元根據IP寄存器的指向,將位於內存中的指令逐個裝載到指令寄存器中,但是它還是不知道這些指令是幹什麼的。然後指令譯碼器將位於指令寄存器中的指令按照指令格式來解碼,分析操作碼是什麼,操作數在哪裏之類的。操作碼就是大家平時用的mov、jmp等。尋址方式又有好多種,如基址尋址、變址尋址等,操作數類型中記錄的是用哪些寄存器之類的。如果在指令中用到了立即數,就要將其記錄到指令格式中立即數的部分,如果尋址用了偏移量,就要將此偏移量記錄到指令格式中的偏移量部分。
存儲單元是指CPU內部的L1、L2緩存及寄存器,待處理的數據就存放在這些存儲單元中,這裏的數據是指令中的操數。
爲什麼數據已經在內存中了還非得CPU內存再整這麼個數據單元?
原因是緩存基本上都是採用的SRAM(Static RAM)存儲器,從名字上看就知道它是一種具有靜態存取功能的存儲器。SRAM與DRAM不同,它不用刷新電路也能保存內部數據,這就是靜態的含義。但是它集成度較低,相同容量之下,SRAM的體積比DRAM要大很多。
運算單元負責算數運算(加減乘除)和邏輯運算(比較、位移),它從控制單元哪裏接受命令(信號)並執行。
CPU的總流程:控制單元要取下一條待運行的指令,該指令地址在程序技術期PC中,在x86CPU上,程序技術器就是cs:ip。於是讀取ip寄存器後,將此地址送上地址總線,CPU根據此地址便得到了指令,並將其存入到指令寄存器IR中。這時指令譯碼器ID根據指令格式檢查指令寄存器中的指令,先確定操作碼是什麼,在檢查操作數類型,若是在內存中,就將相應的操作數從內存中取出來放入自己的存儲單元,若操作數在寄存器中就直接使用。湊齊了操作碼操作數後,操作控制寄存器給運算單元下令,於是運算單元便真正開始執行指令了。

0x2.2 實模式下的寄存器

寄存器是一種物理存儲元件,只不過它比一般存儲介質要快,所以在CPU內部有好多這樣的寄存器來給CPU存儲數據。
緩存成功解決了速度不匹配設備之間的數據傳輸,並且在一般情況下IO是整個系統的瓶頸,緩存的出現,有效減少了低速IO設備的訪問頻率,從而大幅度提升了速度。
段寄存器CPU是通過段基址+段內偏移地址的形式來訪問內存的。段寄存器就是用來存儲段基址的,它的作用就是指定一片內存的起始地址,故也稱爲段基地址寄存器。
訪問內存,是要通過地址總線,給地址總線一個數字(也就是地址),地址總線就能找到改數字爲地址的內存。可是這個數字哪來的呢?對於首次訪問內存之前,其內存地址肯定是要放在與內存不同的存儲介質中更合適,也更容易。如果用內存來存儲內存地址,首先訪問內存就是問題。那麼提交給地址總線的數字從哪來的?初次訪問內存時,該地址要麼用立即數,要麼存儲在某個存儲器中能讓CPU取出來再訪問內存,肯定不能用內存本身來存。由於寄存器比內存更高級,CPU更能接受,所以就用寄存器來存儲內存地址。由於要指定的是內存中的一段區域的起始地址,所以稱爲段基址寄存器,也稱段寄存器,無論在實模式下還是保護模式下,他們都是16位。
代碼段就是把所有指令都連續排放在一起了,形成了一個全部都是指令的區域,裏面存儲的是指令的操作碼及尋址方式等。該區域可以在磁盤上的文件中,也可以是被加載後的內存中,總之是與段指令區域。他們內部都是緊湊挨着的,內容形式完全一樣,只是存放的介質不一樣。代碼段寄存器cs就是用來指向內存中這段指令區域的起始地址。
數據段和代碼段類似,只是這段區域中的內容不是指令,而是純粹的數據,也就是說裏面存儲的是程序運行所需要的數據,數與指令的操作數。數據段寄存器DS便是用來指向此數據區域的起始地址。
ip寄存器是不可見寄存器,cs段寄存器是可見寄存器。這兩個寄存器相互配合就指向了CPU要執行的指令地址。
立即數就是直接存在指令中的常數,CPU在讀取指令時就可以直接將這個數拿出來當作指令解析,不用再去找寄存器和內存,題現了其高效性。

0x2.3 實模式下的ret

在實模式下調用call指令時,CPU會判斷要修改的PC是否跨段訪問操作。如果有就將cs:ip全部保留到棧中,如果沒有跨段就只保留ip。ret只將棧頂2字節數據彈出,並用它爲ip寄存器賦值。只餘內容正確性應由程序員自己控制。ret只置換了IP寄存器,也就是說不用換段基址,屬於近返回。
retf彈出棧頂4字節數據,棧頂處的2字節用來替換IP寄存器,另外的2字節用來替換CS寄存器。同樣retf也不會去檢查從棧頂往上的4字節內容是不是偏移地址和段基地址,它只負責彈出它們,並將它們分別在如代碼段寄存器CS和指針寄存器IP。由程序員負責棧中數據的正確性。retf稱爲遠返回。

0x2.4 實模式下的call

在8086處理器中,有兩個指令用於改變流程。一個是jmp,另一個是call。它們的區別是jmp不會像call一樣保存返回有效地址。
16爲模式相對近調用
call指令所調用函數和當前代碼段是同一個段,即在64KB空間內,所以只給出段內偏移地址就好,不用給出段基地址。
指令中的立即數地址可以是被調用的函數名、標號、立即數,函數名同標號一樣,最終會被轉換成一個實際數字地址。如call near prog_name,不過千萬不要誤會編譯後的操作數最終會被編譯器轉換爲一個絕對地址,在編譯後的機器碼的操作數中,它是指令相對於目標地址的偏移量,是個地址差。也就是說,假如proc_name被編譯器分配的地址是0x1234,call指令最終操作數並不是0x1234,而時目標地址減去當前call指令的地址,所得差再減去此指令長度3,最終的結果纔是call相對於近調用指令操作數。
由於此操作數是相對並不是目標函數的絕對地址,只是對於目標函數地址的相對增量,所以此操作數並不能直接被CPU使用(“直接”就是操作數以立即數形式給CPU後,CPU拿來就用,不用轉換)。CPU在實際執行中還要將此增量還原成絕對地址。所以此相對近調用並不能稱爲“直接”相對近調用。
既然是相對量,就有正負之分。如果目標地址比當前call指令大,地址相對量則爲證書。如果目標地址比當前call指令地址小,地址相對量便爲負數。由此可見操作數是一個有符號數。由於段是個16位大小的空間,所以,正負數的範圍是-32768~32767。
爲什麼CPU要使用相對地址呢?
這是和硬件相關的內容,在同一段內的函數調用(近調用),必須要用相對地址的形式,這是硬件設計問題,工程師們只設計了這一種形式。偏移地址或絕對地址只是個數字,從數值上無法區分這是哪類地址,硬件一律認爲給他的操作數就是相對地址(即使輸入的是絕對地址)。想要讓CPU工作正確,就要確保給它輸入的是真正的相對地址。
以下是示例代碼。

call near near_proc
jmp $
addr dd 4
near_proc:
	mov eax,0x1234
	ret

輸入nasm -o call.bin call.S將彙編代碼編譯成二進制文件call.bin。
使用xxd命令查看call.bin文件裏有什麼,下面是作者提供的xxd腳本註釋我們可以直接使用。

#usage: sh xxd.sh 文件 起始地址 長度 
xxd -u -a -g 1 -s $2 -l $3 $1 

#-u  use upper case hex letters. Default is lower case.
#
#-a | -autoskip
#	    toggle autoskip: A single ’*’ replaces nul-lines.  Default off.
#
#-g bytes | -groupsize bytes
#     separate the output of every <bytes> bytes (two hex characters or eight bit-digits each) by a whitespace.  Specify -g 0 to
#    suppress grouping.  <Bytes> defaults to 2 in normal mode and 1 in bits mode.  Grouping does not  apply  to  postscript  or
#    include style.
#
#-c cols | -cols cols
#            format <cols> octets per line. Default 16 (-i: 12, -ps: 30, -b: 6). Max 256.
#
#-s [+][-]seek
#   start at <seek> bytes abs. (or rel.) infile offset.  + indicates that the seek is relative to the current stdin file position 
#   (meaningless when not reading from stdin).  - indicates that the seek should be that many characters from the end of
#   the input (or if combined with +: before the current stdin file position).  
#   Without -s option, xxd starts at  the  current file position.

查看編譯後的二進制文件大小ll -b call.bin,發現是13字節。
在這裏插入圖片描述
接下來用xxd命令查看二進制文件的內容。輸入xxd -g 1 -u call.bin讓每組以一個字節顯示。
在這裏插入圖片描述
CPU遇到機器碼0xe8,它就會知道這是相對近調用指令,其操作數是兩個字節的數字,總共長度是3字節。EB FE是jmp $的機器碼,EB是操作碼,FE是操作數,由於操作數是有符號的,所以表示-2。04000000是定義的4字節數據addr dd 4。B8 3412這三個字節中B8是mov的操作碼,3412是立即數。
第二種調用方式
16位實模式間接絕對近調用
“間接”是指目標函數地址沒有直接給出,要麼地址在寄存器中,要麼在內存中,總之不以立即數的形式出現。
“絕對”是指目標函數地址是絕對地址,不像“16位相對近調用”中的那樣是相對地址。
還有一點,這也是近調用,即只能調用同一個段代碼中的函數,依然是隻給出段內偏移就好。不用給出段基地址。
指令的一般形式是“call寄存器尋址”或“call內存尋址”,如call ax,call [0x1234]。不同的指令形式對應不同的操作碼,“call 內存地址”對應的操作碼是ff16,機器碼是ff16+16位內存地址。機器碼除了與尋址方式有關外,還和寄存器名字有關,如“call ax”的機器碼是ffd0,“call cx”的機器碼是ffd1,其他形式的機器碼或操作嗎不單獨列出。
此調用形式也是近調用,調用名稱中和“近”有關的就可以用near。near也可以省略。由於是近調用,並沒有跨段,所以call指令只要保留IP寄存器的值就好,將其壓入棧後再用新的地址偏移替換IP的值。
注意,寄存器尋址中,若在寄存器名稱前添加數據類型僞指令,編譯器會報警告:“warming: register size specification ignored”。警告信息字面上的意思是寄存器大小被忽略。只是提示警告,不影響編譯,編譯的機器碼依然是正確的。
near的意思同數據類型僞指令word一樣,是指在內存地址處取2字節內容,或者將操作數強制轉換位2字節。可以認爲像near、short、far,這些用在調用或轉移中的修飾符。每種數據類型大小不同,即表示數的範圍不同,用不同的範圍來表示不同的調用或轉移範圍。
near若在寄存器前面,如call near ax,表示在寄存器讀取2字節,相當於給ax寄存器中的值做了類型轉換。由於near的範圍可正可負,是個有符號數範圍,所以它不等同於數據類型word。在這種情況下,編譯器發現16位的寄存器的值精度被破壞了(寄存器中原值沒變,被提取出來的數被強制類型轉換了)。
同理far、short也一樣,far表示4字節、short表示1字節,如果在寄存器前用這些數據類型,如call far ax或call short ax,編譯器同樣會發出警告。
第三種調用方式
16位實模式直接絕對遠調用
何爲直接?直接就是操作數在指令中直接給出,是立即數。
在各種轉移指令中,凡是包含“直接”,都意指不需要經過寄存器或內存,操作數以立即數的形式給出。凡是包含“遠”,就意指要跨段了,目標函數和當前指令不在同一個段中。
由於是遠調用,所以cs和ip都要用新的,call指令將來還是要回來的,所以要在棧中保留回來的路,即先把老的cs寄存器壓入棧,再把老的IP寄存器壓入棧後,用新的CS和IP寄存器替換。
指令形式是:call far 段基址(立即數):段內偏移地址(立即數)
形式如call 0:far_proc
遠調用call要用retf來配合。
CPU不會判斷新的段值是否和舊的段值相等與否,而是讓它加載,它就會進行加載。不會考慮新舊段基址是否相同。
注意這種遠調用的方式,填寫的是段內絕對地址。
第四種調用方式
16位實模式間接絕對遠調用
這回看名字就知道,使用寄存器或內存存儲絕對地址的調用方式。(作者的介紹總算沒白費),還要注意一點,遠跳的話需要4個字節,1個寄存器絕對存不下,所以至少需要兩個寄存器。又因爲寄存器資源十分寶貴,所以乾脆只支持內存尋址。
16位間接絕對遠調用的指令格式是:call far 內存尋址,如call far [bx],call far [0x1234],操作碼是ff1e,前(低)2字節是段內偏移地址,後(高)2字節是段基址。在此調用方式中一定要加個關鍵字far,否則就和第2種間接絕對近調用一樣了。

0x2.5 實模式下的jmp

無條件跳轉,是指“生硬地”改變CPU的航線,將程序流轉移到新的位置。CPU的航線是cs和ip,所以jmp指令也是通過修改這兩個寄存器來爲CPU導航。
jmp指令和call的唯一區別就是隻修改cs和ip,不保存它們的值,所以跳轉到新的地址後沒辦法再回來。
和call一樣,按遠近(是否跨段)來劃分,大致分爲兩類,近轉移、遠轉移。不過在轉移方式中,還有個更近的,叫短轉移。
一共有5類轉移方式
16位實模式相對短轉移
其實在介紹call指令的時候我們就見識過相對短轉移了,就是那個常用的jmp $ 。指令格式是jmp short 立即數地址。
此處立即數地址可能是標號,因爲標號只是更爲人性化的立即數形式,在編輯階段將被分配爲某個地址。
和call指令一樣,既然是相對的,那麼這個操作數就是相對增量,有正負之分。相對短轉移的機器碼大小是2字節,操作碼是0xeb,可知其字節大小。操作數佔大小1字節。
16位實模式相對近轉移
該相對轉移就是比正常的短轉移擴大了轉移範圍。
16位實模式間接絕對近轉移
16位實模式直接絕對遠轉移
形式jmp cs:ip
16位實模式間接絕對遠轉移
形式jmp far [addr]

0x2.6 標誌寄存器flags

有條件轉移指令jcc的條件轉移是依賴標誌寄存器中的標誌位的,實模式下的標誌寄存器是flags。
flags寄存器中存儲的信息只是結果的特徵,即標誌,並不是真正的結果,結果可以存儲在內存中。
flags寄存器是16位寬,保護模式下對其拓展(extend)成32位的eflags寄存器。
以下標誌位僅在80286以上CPU有效,相對8088,它支持特權級和多任務。
第12~13位,IOPL位,即Input Output Privilege Level,這用在有特權級概念的CPU中。有4個任務特權級,即特權級0/1/2/3。故IOPL要佔用2位來表示這4個特權級。
第14位爲NT,即Nest Task,意爲任務嵌套標誌位。8088支持多任務,一個任務就是一個進程。當一個任務中又嵌套了另一個任務(進程)時,此NT位爲1,否則爲0。
以下標誌位80386以上的CPU才支持。
第16位RF位,即Resume Flag,恢復標誌位。該標誌位用於程序調試,指示是否接收調試故障,它需要與調試器一起使用。當RF位1時忽略調試故障,爲0時接受。
第17位VM位,即Virtual 8086 Model,意爲虛擬8086模式。這是實模式向保護模式的產物,現在已經沒有了。
第18爲爲AC位,即Alignment Check,意爲對齊檢查。檢擦程序中的數據活指令其內存地址是否是偶數,是否是是16、32的整數倍,沒有餘數,這樣硬件每次對地址以自增地方式(每次加2、16、32等)訪問內存時,自增後的地址正好對齊數據所在的起始地址上,這就是對齊原理。對齊不是軟件邏輯要求,而是硬件要求,如果訪問是16或32的整數倍,硬件上好處理,所以運行較快。若AC位爲1時,則進行地址對齊檢查,爲0時不檢查。
以下標誌位值對80586(奔騰)以上CPU有效
第19位爲VIF位,即Virtual Interrupt Flag,意爲虛擬中斷標誌位,虛擬模式下的中斷標誌。
第20位爲VIP位,即Virtual Interrupt Pending,意爲虛擬中斷掛起標誌位。在多任務下,位操作系統提供的虛擬中斷掛起信息,與VIF位配合。
第21位爲ID位,即Identification,意思爲識別標誌位。系統經常要判斷CPU型號,若ID爲1,表示當前CPU支持CPU id指令,這樣便能獲取CPU的型號、廠商等信息。若ID爲0,則表示當前CPU不支持CPU id指令。
其餘剩下的22~31 爲都沒有實際用途,純粹是佔用位,爲了將來拓展。
實模式最後被保護模式淘汰的原因,最主要是安全隱患。
在實模式下,用戶程序和操作系統可以說是同一特權的程序,因爲實模式下沒有特權級,他處處和操作系統平起平坐,所以可以執行一些破壞性指令。(各種病毒程序就是典型的例子)。

0x3 讓我們直接對顯示器說點什麼

0x3.1 CPU如何與外設通信——IO接口

現實生活中的各種硬件由各種廠商負責提供,它們的種類繁多,原理個不行相同。他們都有字節的特性,數據格式不同,有的外設用串行數據,有的是用並行數據,並且讓他們都在自己的時序下工作,無論他們的速度如何,在CPU看來都太慢了。
而且CPU不可能記住每個硬件的訪問方式,而且CPU熟讀那麼快,外設的速度沒法與CPU相匹配,爲了減少自己的等待時間,還得爲這些低速設備準備數據緩衝區。CPU用的信號都是TTL電平,外設大多數都是機電設備,機電設備可不能用TTL電平驅動,CPU系統總線上傳送的都是並行數據(所以你聽到的都是8位、16位、32位CPU),外設可是並行、串行都有,還得轉換格式,不可能讓CPU去一一適應它們,否則CPU做的工作就太多了。
於是CPU設計師們給CPU和外設之間添加了一個代理,CPU以後想要訪問各種設備只需要找到對應的代理,讓這個代理再進行數據格式的轉換髮給外設處理即可。什麼速度不匹配、緩衝區的問題都由代理解決。舉個例子,如果CPU想要跟串行設備通信,CPU就同串行接口進行通信,把數據發給它後,數據再經由串行接口發給串行設備,串行設備有了反饋後,把數據發送給串行接口,串行接口再返回給CPU,並行設備也是如此。
任何不兼容問題都可以通過增加一層IO接口代理的方式解決,IO接口形式不限,它可以是一個電路板,也可以是塊芯片,甚至可以是一個插槽,它的作用就是在CPU和外設間做協調轉換,如CPU和外設之間速度不匹配,它就起到了變速箱的作用,CPU和外設信號不通,它就是翻譯機。
再具體點就是,音箱中的聲卡就是驅動音響設備的IO接口。顯卡也同樣是一種IO接口,它是用來驅動顯示器的。現在電腦的聲卡和顯卡已經被集成在主板芯片組中了,我們常見的集成聲卡和集成顯卡就是。
IO接口時接連CPU與外部設備的邏輯控制部件,可以分爲硬件和軟件兩部分,硬件部分功能是協調CPU和外設之間的種種不匹配,如雙方由於速度不匹配,那IO接口就實現數據緩衝以減少等待時間,數據格式不匹配,IO接口就進行格式轉換。IO接口實際上內部也是由軟件控制工作的,這就是所謂的邏輯部分,所以軟件是指用來控制接口電路工作的驅動程序以及完成內部數據傳輸所需要的程序。
IO接口芯片又分爲可編程接口芯片和不可編程接口芯片。
接口的作用是連接處理器和外部設備,如果外部設備工作很簡單,不需要設定就直接可以執行功能就使用不可編程芯片,不可編程芯片是很簡單的IO接口。
當我們需要一個IO接口實現多種模式和多種複雜功能時,且允許多種不同外設都可以連接該接口進行通信,這時就需要用計算機指令告訴IO接口,那些設備連接在IO接口上,此IO接口的工作模式等。這種通過軟件選擇IO接口上的功能、工作模式的做法,稱爲“IO接口控制編程”。這通常時用端口讀寫指令in/out實現的。
爲了簡化CPU對外設的訪問工作,大家統一約定好IO接口的功能:
1)設置數據緩衝,解決CPU與外設間的速度不匹配
CPU和外設速度上的差異可以通過設置緩衝區來解決,數據先存儲在緩衝區裏,等待需要時就傳送出去。
2)設置信號電平轉換電路
CPU和外設的信號電平不同,CPU所用信號電平事TTL電平,而外設大多數是機電設備,不能用TTL電平驅動,可以在接口電路中設置電平轉換電路來解決。
3)設置數據格式轉換
外設的多種多樣決定了輸出的信息可能是數字信號、模擬信號等,而CPU只能處理數字信號。數字信號需要經過數/模轉換(D/A)成模擬量才能被送到外設以及驅動硬件。模擬量也同樣需要經過模/數(A/D)轉換成數字信號才能被處理。
4)設置時序控制來同步CPU和外部設備
接口電路要協調CPU和硬件的兩種不同的時間計發。
5)提供地址譯碼
CPU同多個硬件硬件打交道,每個硬件反饋信息信息也很多,所以一個IO接口必須包含多個端口,即IO接口上的寄存器,來存儲這些信息內容。但同一時刻,只能有一個端口和CPU數據交互,這就需要IO接口提供地址譯碼電路,使CPU可以選中某個端口,使其可以訪問數據總線。
CPU通過總線訪問各個物理設備。CPU只能同一時間訪問到一個物理設備對應IO接口的某一個端口。當多個物理設備都想要和CPU進行通信時,還需要添加一層專門用來篩選IO接口的電路來仲裁IO接口,它的名字就叫做輸入輸出控制中心(I/O control hub,ICH),也就是南橋芯片。對應名字的北橋芯片的上部分就是散熱片,下面的纔是真正的北橋。由於南橋和北橋是成對出現的,南橋主要用於鏈接低速設備,北橋用於鏈接高速設備。
CPU通過內部總線連接到南橋芯片的內部,這個內部總線是專用的,他只通向位於南橋中的CPU接口。在南橋內部集成了一些IO接口,如並口硬盤PATA(就是我們平時說的IDE硬盤)、串口硬盤SATA、USB、PCI設備、電源管理等設備接口是直接內置在南橋的內部。
爲了支持非必要設備的拓展,南橋提供了專用拓展的接口,這就是PCI接口。在主板上有很多插槽,他們就是預留的pci接口,pci設備可以即插即用。由於他們延伸到了南橋外面,且可以很多pci設備連接上來,這條延長的pci接口變成了PCI總線。總線就是外設都要連接的,用於傳輸數據、行爲控制的電線。
IO接口設計之初,就要被設計成要通過寄存器方式通CPU通信,其內部有專用數據交互的寄存器,只不過這裏所說的寄存器位於IO接口中,爲了區別與CPU內存的寄存器,IO接口中的寄存器就稱爲端口(這可不是網絡應用程序所開的那種端口)。
IO接口是CPU與硬件的橋樑,一端是CPU,另一端是硬件。端口是IO接口開放給CPU的端口,一般的IO接口都有一組端口,每個端口都有自己的用途,甚至有時,一個端口在不同情況下有不同用途。可見IO端口另一端的硬件還是很複雜的。
端口也是寄存器,寄存器有數據寬度,有8位、16位、32位,各個設備是不同的這取決於生產廠商。
訪問端口的方法:
既然外設中的ROM可以通過內存映射來訪問,端口也可以,把一些內存作爲端口的映射,訪問這些內存就相當於訪問這些端口。還可以將端口獨立編址,把所有端口從0開始編號,位於IO端口上的所有端口號都是連續的。
IA32體系系統中,因爲用於存儲端口號的寄存器是16位的,所以最大有65536個端口,即0~65535。
要是通過內存映射,端口就可以用mov指令來操作。但由於用的是獨立編址,所以就不能把它當作內存來操作,因此CPU提供了專門的指令來幹這事,in和out。
in指令用於從端口中讀取數據,其一般形式是:
1)in al,dx
2)in ax,dx
其中al和ax用來存儲從端口獲取的數據,dx是指端口號。
這是固定用法,只要用in指令,源操作數(端口號)必須是dx,而目的操作數是用al,還是ax,取決於dx端口代指的寄存器是8位寬度,還是16位寬度。
out指令用於端口中寫數據,其一般形式是:
1)out dx,al
2)out dx,ax
3)out 立即數,al
4)out 立即數,ax
注意,這裏與in指令相反,in指令的源操作數是端口號,而out指令中的目的操作數是端口號。
in和out指令的共性
1)在以上兩個指令中,dx只做端口號之用,無論其是操作數或目的操作數。
2)in指令從端口讀數據,可以認爲端口是數據源,所以端口出現在源操作數的位置。讀出來的數據要有個“目的地”來存放,所以in指令中存放數據的地方出現在“目的操作數”位置。
out指令是把數據寫入端口指向的寄存器,在這裏,端口是數據的“目的地”,所以端口出現在目的操作數的位置。待寫入的數據總有個“來源”,所以out指令中的“源操作數”是數據來源。
3)在以上兩個指令的兩個操作數中,無論是對於源操作數,還是目的操作數,除端口外,那個作爲數據的操作數,一律用al寄存器存儲8位寬度的數據,用ax寄存器存儲16位寬度的數據。
4)in指令中,端口號只能用dx寄存器。
5)out指令中,可以選用dx寄存器或立即數充當端口號。

0x3.2 顯卡概述

上一章直接在文本模式下,對顯卡映射內存書寫字符串讓其在顯示器上顯示的方式現在早就不用了。
mbr運行在實模式下,所以在實模式下也可以用BIOS的0x10中斷打印字符串,因爲中斷向量表只存在於實模式下,BIOS中斷是要依賴中斷向量的。不適用於接下來要進入的保護模式,也就沒有BIOS中斷向量表。其次,不希望有更多依賴BIOS軟件。
某些IO接口也叫適配器,適配器是驅動某一外部設備的功能模塊。顯卡也稱爲顯示適配器,不過歸根結底它就是IO接口,專門用來連接CPU和顯示器。我們想要操作顯示器,沒有直接的辦法,只能通過它IO接口——顯卡。
顯卡是pci設備,所以是安裝在主板上的pci槽上的,pci總線式共享並行架構,並行數據就要保證數據發送好必須同時到達目的地,因爲這關係到數據的順序。例如8位並行總線就需要同時發送這8位,接收方也要同時接收這8位才行。雖然並行效率很高,但是對於要保證同時接收n位數據,這是有困難的,隨着並行數據位寬越來越大,這種困難也越來越明顯。於是串行傳輸很好地解決了這一問題,一次只發一位,這樣順序就解決了,數據到目的地看再組合到一起就成了。於是就有了PCI Express總線,這就是串行設備簡稱pcie。現在的顯卡都是串口的了。不要絕得傳輸速度一定是並行快,因爲傳輸速度一部分取決於並行數據量,一部分還要取決於傳輸頻率。串口顯卡雖然一次只能傳輸1位,但它的傳輸頻率高。

0x3.3 顯卡、顯存、顯示器

爲了能看到圖像,我們需要顯示器。無論哪種顯示器,它都是由顯卡控制的,我們沒必要了解液晶顯示和普通CRT顯示器的差別。無論哪種顯卡,它提供給我們的編程接口都是一樣的:IO端口和顯存。
顯存是由顯卡提供的,他是位於顯卡內部的一塊內存,所以稱它爲顯存。標註了DDR 512M,DDR2 1G這些都是指顯存大小。顯卡的工作就是不斷的讀取這塊內存,隨後將其內容發送到顯示器。
顯示器上多彩的圖案,說明顯卡可以讓顯示器工作再圖形模式,能在顯示器上看到Linux終端上的黑屏白字,說明顯卡可以讓顯示器工作在字符模式。屏幕是有密密麻麻的像素組成的,顯存中的每一位都對應了一個像素點。
在黑白圖像模式中,顯存位與像素是1對1,因爲只有兩種顏色,所以只要現存中對應位置是1,屏幕上響應像素就被點亮,呈現的是白色。若該位爲0,該像素就不會被點亮,只要不管該像素就是黑色,所以用黑色壁紙當桌面,纔是在物理上保護顯示器。
文本模式的屏幕上其實是由兩個字節來表示一個字符的,低字節是ASCII碼,高字節是對應的字節屬性,這就是爲什麼文本模式還能顯示顏色的原因。
高字節中,低4位是字符前景色,高4位是字符的背景色。

0x3.4 改進MBR,直接操作顯卡

;主引導程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTION equ 0x2
;---------------------------------------------------------
SECTION MBR vstart=0x7c00
	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00
	mov ax,0xb800
	mov gs,ax
;清屏
;利用0x06功能號,上卷全部行,則可清屏
;---------------------------------------------------------
;INT 0x10	功能號:	0x06	功能描述:	上卷窗口
;---------------------------------------------------------
;輸入:
;AH 功能號= 0x06
;AL = 上卷行數(如果爲0,表示全部)
;BH = 上卷行屬性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;無返回值
	mov ax,0600h
	mov bx,0700h
	mov cx,0
	mov dx,184fh
	int 10h
	;輸出背景色綠色,前景色紅色
	mov byte [gs:0x00],'1'
	mov byte [gs:0x01],0xA4;A表示綠色背景閃爍,4表示前景色爲紅色
	
	mov byte [gs:0x02],' '
	mov byte [gs:0x03],0xA4

	mov byte [gs:0x04],'M'
	mov byte [gs:0x05],0xA4
	
	mov byte [gs:0x06],'B'
	mov byte [gs:0x07],0xA4

	mov byte [gs:0x08],'R'
	mov byte [gs:0x09],0xA4

	jmp $	;通過死循環懸停再此

	times 510-($-$$) db 0
	db 0x55,0xaa

編譯後重新將mbr.bin寫入虛擬磁盤
在這裏插入圖片描述運行bochs,查看是否有綠色背景的字符。發現右上角有綠色背景,紅色字符串在不停閃爍。
在這裏插入圖片描述
在這裏插入圖片描述

0x3.5 MBR操作硬盤

0x3.5.1 硬盤控制器端口

首先給出作者提供的硬盤控制器端口表,這個表種名爲Device的端口就是專門指定要操作硬盤的端口。
在這裏插入圖片描述
端口可以分爲兩組,Command Block registers 和 Control Block registers。Command Block registers用於向硬盤驅動器寫入命令或者從硬盤控制器獲取硬盤狀態,Control Block registers用於控制硬盤狀態。
端口是按照通道給出的,一個通道上分別由主、從兩塊硬盤,都用這些端口號。想要操作某通道上的某塊硬盤,需要單獨指定對應8位的與磁盤相對應的寄存器,不過由於只是指定某個磁盤,只需要1位就夠了(寄存器是很寶貴的),所以除了指定磁盤,其中第4位,便是指定通道上的主或從硬盤,0爲主盤,1爲從盤(一個通道上有兩個分支,主盤通道分支和從盤通道分支)
在這裏插入圖片描述
端口用途在都硬盤和寫硬盤時還是有區別的,比如拿Primary通道上的0x1F1端口來說,讀操作時若讀失敗,裏面存儲的時失敗狀態信息,所以稱爲error寄存器,並且0x1F2端口中存儲未讀的扇區數。在寫操作時error寄存器又變成了feature寄存器,此寄存器用於寫命令的參數。
Data寄存器在名字上我們就知道它是用於管理數據的,我們讀取和寫入的數據都通過這個寄存器獲取。爲了讓讀寫數據更快,此寄存器被設計爲16位。在讀硬盤時,硬盤準備好數據後,硬盤控制器比將其放在內部的緩衝區,不斷讀此寄存器便是讀取緩衝區的全部數據。在寫硬盤時,我們把數據源源不斷的送到此端口,數據便被存入緩衝裏,硬盤發現緩衝區中有數據了,編講此數據寫入相應的扇區。
讀磁盤時,端口0x171或0x1F1的寄存器叫Error寄存器,只在讀取硬盤失敗時纔有效,裏面纔有失敗的信息,尚未讀取的扇區數載Sector count寄存器中。在寫磁盤時,有些命令需要添加額外的參數,此寄存器就是用來填寫寫入命令參數的。
Sector count 寄存器是用來指定待讀取或待寫入的扇區數。硬盤每完成一個扇區,就會將此寄存器的值減1,所以如果中間讀或寫操作失敗了,該寄存器中的值便是未完成的扇區。這是8位寄存器,最大值爲255,若指定爲0則表示要操作256個扇區。
硬盤中的扇區在物理上是用“柱面-磁頭-扇區”來定位的(Cylinder Head Sector),簡稱CHS,但是每次我們都要事先計算好扇區的具體位置過於麻煩。於是我們希望有一套對於人來說比較直觀的尋址方法,我們希望磁盤中扇區從0開始依次遞增編號,不用考慮扇區所在物理結構。這是一種邏輯上爲扇區編址的方法,全程邏輯塊地址(Logical Block Address)。
LBA有兩種,一種是LBA28,用28個位bit來描述一個扇區的地址。最大尋址範圍是2的28次方等於268435456個扇區,每個扇區是512個字節,最大支持128G。另一種是LBA48,用48bit來描述一個扇區的地址,最大可尋址範圍是2的48次方,等於281474976710656個扇區,乘以512字節後,最大支持131072TB,即128PB。
LBA寄存器有LBA low、LBA mid、LBA high 三個,它們三個都是8位寬度的。LBA low寄存器用來u才能出28位地址的0 ~ 7位,LBA mid寄存器用來存儲第 8 ~ 15 位,LBA high寄存器存儲第16 ~ 23位。剩下不夠的位數引出Device寄存器。
device寄存器是個雜項,它的寬度是8位。在此寄存器的低4位用來存儲LBA地址的24~27位。結合上面的LBA寄存器。第4位用來指定通道上的主盤或從盤,0代表主盤,1代表從盤。第6位用來設置是否啓用LBA方式,1代表啓動LBA模式,0代表啓用CHS模式。另外兩位固定爲1,稱爲MBS位,不需要關注。
在讀硬盤是,端口0x1F7或0x177的寄存器名稱是Status,它是8位寄存器,用來給出硬盤的狀態信息。第0位是ERR位,如果此位爲1,表示命令出錯了,具體原因可見error寄存器。第3位是data request位,如果此位爲1,表示硬盤已經把數據準備好了,主機可以把數據讀出來。第6位是DRDY,表示硬盤就緒,此位是在對硬盤診斷時使用的,表示硬盤檢測正常,可以繼續執行一些命令。第7位BSY,表示硬盤是否繁忙,吐過位1表示硬盤繁忙,此寄存器其他位都無效。另外4位暫不關注。
在寫硬盤時,端口0x1F7或0x177寄存器的名稱是command,此寄存器用來存儲讓硬盤執行的命令,只要把命令寫進此寄存器,硬盤就開始工作了。在書上的系統中,主要使用了3個命令。
1)identify:0xEC,硬盤識別
2)read sector: 0x20,讀扇區
3)write sector: 0x30,寫扇區
常用寄存器端口示意圖,左邊是device,右邊是status寄存器。
在這裏插入圖片描述

0x3.5.2 常用硬盤操作方法

最權威的使用方法是去參考ATA手冊。
不管是讀硬盤,還是寫硬盤,都不是一個指令就能完成的。相關寄存器都要設置。要是讀硬盤,首先要告訴讀哪個扇區,讀幾個扇區,用哪種模式尋址。寫硬盤也是一樣。
一般操作硬盤最主要順序就是command寄存器一定得是最後寫,因爲一旦command寄存器被寫入後,硬盤就開始幹活了。其他寄存器順序不是很重要。
1)先選擇通道,往該通道的sector count寫入待操作扇區數
2)往該通道上三個LBA寄存器寫入扇區起始地址的低24位
3)往device寄存器中寫入LBA地址的24~27位,並置第6位爲1,使其爲LBA模式,設置第4位選擇主盤或從盤
4)往該通道上command寄存器中寫入操作命令
5)讀取該通道上的status寄存器,判斷硬盤工作是否完成
6)如果以上步驟是讀硬盤進入下一個步驟,否則完工。
7)將硬盤數據讀出
硬盤完工後,它已經準備好了數據,怎麼獲取呢?一般常用的數據傳送方式如下:
1)無條件傳送方式
2)查詢傳送方式
3)中斷傳送方式
4)直接存儲器存儲方式(DMA)
5)I/O處理機傳送方式
第1種,應用此方式的數據源設備一定是隨時準備好了數據的,CPU隨時取隨時拿都沒問題,CPU讀取數據時不用打招呼。
第2種,也稱爲程序I/O、PIO(Programming Input/Output Model),是指傳輸前,由程序先去檢測設備的狀態。數據源設備在一定的條件下纔可以傳輸數據,這類設備通常是低速設備,比CPU慢很多。CPU需要數據時,先檢查該設備狀態,如果狀態爲“準備好了可以發送”,CPU再去獲取數據。硬盤有status寄存器,裏面保存了工作狀態,所以對於硬盤可以用此方式來獲取數據。
第3種“中斷傳送方式”,也稱爲中斷驅動I/O。上面提到的“查詢傳送方式”有這樣的缺陷,由於CPU需要不斷查詢設備狀態,所以意味着只有最後一刻的查詢纔有意義的,之前的查詢都是發生在數據尚未準備好的時間段裏,所以所效率不高,僅對於不要求速度的系統可以採用。可以改進的地方是如果數據源設備將數據準備好後再通知CPU來取,這樣效率就變高了。通知CPU可以採用中斷的方式,當數據源設備準備好數據後,它通過發中斷來通知CPU來拿數據,這樣就避免了查詢花費大量的時間,效率高。
第4種“直接存儲器存取方式(DMA)”。在中斷傳送方式種,雖然極大地提高了CPU的利用率,但通過中斷方式通知CPU,CPU就要通過壓棧保護現場,還要執行傳輸指令,最後還要恢復現場。這種方式不讓CPU參與傳輸,完全由數據設備和內存直接傳輸。CPU直接到內存種拿數據,DMA是硬件,不是軟件概念,所以需要DMA控制器才行。
第5種“I/O處理機傳送方式”。爲了完全解放CPU,在DMA控制器種把數據交換、組合、校驗的功能都融合了起來,然後再引入除了DMA以外的硬件。於是I/O機理就誕生了,聽名字就知道它是專門處理IO的,並且他其實是一種處理器,只不過用的時另一套擅長IO的指令系統,隨時可以處理數據。有了IO機理的幫忙,CPU甚至不知道由數據傳輸這回事,同樣,這也需要單獨的硬件來支持。

0x3.6 讓MBR使用硬盤

在前幾章內容裏,我們都是在操作MBR的內容。但是MBR只有512字節,這塊空間不足以支撐操作系統的所有寄存功能,所以我們需要繼續傳遞cs:ip。

0x3.6.1 改造MBR

我們的MBR受限於512字節大小,沒辦法爲內核準備好環境,更沒辦法將內核成功加載到內存並運行。所以我們要在另一個程序中完成初始環境以及加載內核的任務,這個程序我們稱爲loader,即加載器。這塊MBR負責從磁盤上把loader加載到內存,並將接力棒交給它。
由於MBR是佔據了磁盤的0扇區(以邏輯LBA方式,扇區從0開始編號,若以物理CHS方式,扇區從1開始編號),第1扇區是空閒的,可以用,但是相隔太近,需要一段空間。將loader放到第2扇區。MBR從第二扇區把它讀出來。讀出來後原則是只要有空閒的地方都可以放置,之前的實模式下內存佈局中,0x500 ~ 0x7BFF和0x7E00 ~ 0x9FBFF這兩段內存區域都可以。
首先,loader中要定義一些數據結構(如GDT全局描述符表),這些結構式爲將來內核使用的,所以loader加載到內存後不能被覆蓋。
其次,隨着不斷添加功能,內核會越來越強大,其所在的內存地址也會向越來越高的地方發展,難免會超過可用區域的上限,所以儘量把loader放在低處,多留出空間給內核。

;主引導程序
;----------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00
	mov ax,0xb800
	mov gs,ax

;清屏
;利用 0x06功能號,上卷全部行,則可清屏
;----------------------------------------------------------
;INT 0x10	功能號:0x06		功能描述:上卷窗口
;----------------------------------------------------------
;輸入
;AH 功能號= 0x06
;AL = 上卷行數(如果爲0,表示全部)
;BH = 上卷行屬性
;(CL,CH) = 窗口左上角(x,y)位置
;(DL,DH) = 窗口右下角(x,y)位置
;無返回值
	mov ax,0600h
	mov bx,0700h
	mov cx,0
	mov dx,184fh
	int 10h
	;輸出背景色綠色,前景色紅色
	mov byte [gs:0x00],'1'
	mov byte [gs:0x01],0xA4;A表示綠色背景閃爍,4表示前景色爲紅色
	
	mov byte [gs:0x02],' '
	mov byte [gs:0x03],0xA4

	mov byte [gs:0x04],'M'
	mov byte [gs:0x05],0xA4
	
	mov byte [gs:0x06],'B'
	mov byte [gs:0x07],0xA4

	mov byte [gs:0x08],'R'
	mov byte [gs:0x09],0xA4

	mov eax,LOADER_START_SECTOR		;起始扇區lba地址
	mov bx,LOADER_BASE_ADDR			;寫入的地址
	mov cx,1						;待讀入的扇區數
	call rd_disk_m_16				;以下讀取程序的起始部分(一個扇區)
	
	jmp LOADER_BASE_ADDR
;----------------------------------------------------------
;功能:讀取硬盤n個扇區
rd_disk_m_16:
;----------------------------------------------------------
									; eax=LBA 扇區號
									; bx=將數據寫入的內存地址
									; cx=讀入的扇區數
	mov esi,eax						; 備份eax
	mov di,cx						; 備份cx
;讀寫硬盤:
;1:設置要讀取的扇區數
	mov dx,0x1f2
	mov al,cl
	out dx,al						;讀取扇區數
	
	mov eax,esi						;恢復ax

;2:將LBA 地址存入0x1f3 ~ 0x1f6
	
	;LBA地址7~0 位寫入端口0x1f3
	mov dx,0x1f3
	out dx,al
	;LBA地址15~8位寫入0x1f4
	mov cl,8
	shr eax,cl
	mov dx,0x1f4
	out dx,al
	;LBA地址23~16位寫入端口0x1f5
	shr eax,cl
	mov dx,0x1f5
	out dx,al
	
	shr eax,cl
	and al,0x0f						;lba 第24~27
	or al,0xe0						;設置7~4位爲1110,表示lba模式
	mov dx,0x1f6
	out dx,al
;3:0x1f7端口寫入讀命令 0x20
	mov dx,0x1f7
	mov al,0x20
	out dx,al
;4:檢測硬盤狀態
.not_ready:
	;同一端口,寫時表示寫入命令字,讀時表示讀入硬盤狀態
	nop
	in al,dx
	and al,0x88						;3位爲1表示硬盤控制器已準備好數據傳輸,第7位爲1表示硬盤繁忙
	cmp al,0x8
	jnz	.not_ready					;若未準備好繼續等待
;5:0x1f0端口讀取數據
	mov ax,di
	mov dx,256
	mul dx
	mov cx,ax
; di爲要讀取的扇區數,一個扇區有512字節,每次讀入一個字節共需di*512/2次,所以di*256
	mov dx,0x1f0
.go_on_read:
	in ax,dx
	mov [bx],ax
	add bx,2
	loop .go_on_read
	ret
times 510-($-$$) db 0
db 0x55,0xaa

要包含的文件boot.inc

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

將程序編譯,這裏-I代表添加依賴的目錄,boot.inc就放在這個目錄下
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
接下來編寫loader加載器的代碼

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

; 輸出背景顏色綠色,前景色紅色,並且跳動的字符串“1 MBR”
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $

輸入指令進行編譯nasm -I ./include/ -o loader.bin loader.S
將生成的loader.bin寫入虛擬磁盤的第二扇區。第0扇區是MBR。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

發佈了16 篇原創文章 · 獲贊 1 · 訪問量 1108
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章