3.2 x86體系結構
計算機組成
3 指令系統體系結構
3.2 x86體系結構
X86是商業上最為成功,影響力最大的一種體系結構。但從技術的角度看,它又存在著很多的問題,那我們就來一起分析X86這種體系結構的特點。
要探討x86體系結構,我們就得從8086開始說起。8086是英特爾在1978年推出的一款16位的微處理器。
它內部的通用寄存器為16位,對外有16根數據線和20根地址線,因此可以訪問的存儲單元數量為2的20次方,也就是1MByte。而cpu發到存儲器的地址,我們稱為物理地址,8086的物理地址采用了段加偏移的方式,關於這一點我們在後面進行解釋。8086作為微處理器,在一個芯片上集成了原先由多個部件組成的CPU的功能,因此可以以低廉的價格,很小的體積和功耗,提供相對不錯的性能,這也為個人計算機的發展奠定了基礎。
1981年IBM就推出了自己的個人計算機pc5150。它采用了8086的簡化版本8088,這是一款八位寬的微處理器,此後個人計算機的市場迅速增長,反過來也推動了x86微處理器的發展。
1982年英特爾推出了80286,由於應用程序對內存空間的需求越來越大,286將地址總線擴展到了24位,可以尋址16兆的內存空間,而且更多的任務也要求更高效的管理模式,286引入了保護模式,雖然這個機制還不完善,但是一個非常重要的進步。為了兼容8086,286仍然保留了8086的工作模式,這就被稱實模式,而兼容為之前處理器開發的軟件成為x86能夠成功的關鍵因素之一。
實模式又稱為實地址模式。運行在實模式下的x86處理器就像是一個更快的8086,而且現在所有x86處理器在加電或復位後首先進入實模式。在實模式中會運行系統初始化程序,為後面進入保護模式做好準備。現在的個人計算機在啟動之後,cpu首先會通過主板上的芯片組,從BIOS芯片中取出指令進行執行,這段程序就是運行在實模式下的。
那即使286將地址總線擴大到了24位,很快也無法滿足應用程序的需求,所以英特爾在1985年將x86體系結構升級到32位,並將之命名為IA32,IA是Intel Architecture的縮寫。
386是x86系列中的第一款32位微處理器,它的內部真正支持了32位的算術和邏輯運算,並提供了32位的通用寄存器,地址總線也擴展到了32位,這樣就可以尋址到\(2^{32}\),也就是 4GByte 的內存空間。386還在286的基礎上改進了保護模式,此外還增加了虛擬8086模式,以運行兼容8086的程序。
在386之後,x86的微處理器的主要工作模式就是保護模式。保護模式為多任務的執行提供了很好的支持和保護,同時可以訪問4G字節的物理存儲空間,這相比之前是一個很大的進步,可以在相當長的時間內滿足軟件的需求。而且從386開始還引入了虛存的概念,可以對存儲空間進行更好的管理。簡單說來保護模式,可以讓操作系統加強對應用軟件的控制,使得系統運行更加安全高效。
現在在系統啟動後,會首先進入實模式,在初始化完成後,通過設置cpu中的控制寄存器,進入保護模式,操作系統和應用程序都運行在保護模式下。如果需要運行兼容8086的程序,可以從保護模式下切換到虛擬8086模式。在這種情況下,如果發生中斷或異常,是會回到保護模式處理,處理完成後,再返回虛擬8086模式。不管在哪種模式下,經過復位以後都會重新從實模式開始啟動。
32位的x86,也就是IA32體系結構,維持了很長時間,並逐漸占據了市場的主導地位,但是隨著應用對內存空間的需求越來越大,32位的體系結構已經難以滿足要求。在x86向64位升級的過程中,x86-64的體系結構是最先由AMD提出來的。而英特爾當時正專註於另一種64位的體系結構,稱為IA64,但它與IA32是不兼容的。
這就是第一款64位的x86微處理器,AMD的皓龍。它可以訪問高於4G字節的存儲器,從而解決內存空間受限的問題,其實x86擴展到64位實在說不上是一個亮點。因為在當時已經有很多體系結構都已經升級到了64位,但是由於x86一直堅持著向前兼容的設計原則,聚集了大量的應用程序,形成了很好的生態環境,因此AMD的x86-64的更重要的一點是,它能夠兼容32位的x86程序,而且運行這些程序不會降低性能。與此對應的是英特爾提出的IA64體系結構,這是基於這個結構的安騰處理器,它與IA32體系結構並不兼容,只是提供了一種模式,可以運行32位的x86程序,但是會大大降低性能,這就成為了安騰不受歡迎的重要原因之一。中間曲線圖是英特爾對安騰銷售的預期,這條藍色的線是97年的預測,第二條線是98年的預測,第三條線是99年的預測,實際的銷售情況是這條橙色的線所表示的,所以最後英特爾也回過頭來才用AMD的擴展方案。
這就是x86-64,x86-64支持原有的32位的x86的運行模式,並將之命名為傳統模式。新增的模式稱為長模式。長模式又分為兩個子模式,64位模式和兼容模式。在兼容模式下,原有的x86程序不需要重新編譯,就可以高效的運行,這也是x86-64得以成功的關鍵。
x86體系結構從16位演變到64位,其中有很多的變化,我們來看兩個重要的方面。第一是寄存器模型的變化。
這是8086所使用的寄存器。我們分別來看它各自的功能。
在通用計算機中,前四個又稱為數據寄存器,它們都是16位寄存器,但也可以當做兩個八位寄存器使用。大多數算術運算和邏輯運算都會使用到這些數據寄存器,用於臨時保存數據,而且它們除了作為通用的用途之外,還有一些專門的用途。
接下來的四個寄存器,又被稱為指針寄存器和變址寄存器。SP和BP這兩個寄存器用於堆棧操作,SI和DI這兩個寄存器用於串操作。這是它們名稱的含義,之後在介紹相關的指令時,會講解它們的具體用途。當然這些寄存器也可以當做普通的數據寄存器使用。
我們再來看標誌寄存器,標誌寄存器當中包含的若幹標誌位,標誌位分為兩大類。
狀態標誌用於反映cpu的工作狀態。比如說執行完加法運算後,如果產生進位,就會將標誌寄存器當中體現進位的狀態位置為有效。
而控制標誌則是對cpu的運行起特定的控制作用。例如,如果讓cpu采用單步的方式,而不是連續執行指令的方式運行,則需將標誌寄存器當中的某個控制標誌置為有效。
這是8086的標誌位,實際只使用了其中九個比特。紅色這些是狀態標誌,例如比特0就是進位標誌。另外三個紫色是控制標誌。這些標誌位我們也會結合著指令進行講解。
從8086到IA32,寄存器模型也由16位擴展到了32位。通用寄存器都在原有的基礎上增加了高16位,擴展成了32位。例如,原來的AX擴展成32位以後,新的名稱為EAX。程序中使用EAX寄存器就是一個32位寄存器,但仍然可以使用AX訪問16位,也可以使用AH或者AL訪問其中的特定的八位。再來看指令指針寄存器也從16位擴展到32位,標誌寄存器也是同樣。但是段寄存器的長度沒有擴展,而是增加了兩個16位的段寄存器。
我們再來看x86-64對寄存器模型的擴展。x86-64將大多數寄存器擴展成了64位,例如EAX擴展了高32位以後成為了RAX,和之前一樣,不但可以使用64位的RAX寄存器,同時也可以使用EAX、AX等等。需要註意的是,段寄存器仍然沒有擴展。此外,x86-64還增加了八個64位的通用寄存器,命名為R8到R15。
然後我們來看x86體系結構演變過程中的另一個要點,就是存儲器的尋址。
8086依靠指令指針寄存器,也就是IP寄存器進行尋址。IP寄存器中保存了一個內存地址,指向當前需要取出的指令。當cpu從內存中取出一個指令後,IP寄存器會自動增加指向下一個指令的地址。程序員不能直接操作IP寄存器,但是可以通過編寫轉移等指令來改變IP寄存器的內容。我們知道8086是16位的,我們使用IP寄存器可以訪問到的內存空間就是2的16次方,也就是64k個字節單元。但其實8086對外有20位的地址線,尋址範圍是1兆的字節單元。那如何解決這個矛盾呢?
8086中提供了段寄存器來解決這個問題。8086一共提供了四個段寄存器:CS是代碼段寄存器,DS是數據段寄存器,ES是附加段,SS是堆棧段。
現在我們就來看8086的物理地址是如何生成的?我們在編寫程序時直接給出的地址稱為偏移地址,又叫偏移量,這是16位的。而段寄存器中存放了段基址,這一對地址就被稱為邏輯地址。其中段基址在計算時首先被左移四位,然後和偏移量相加,這樣就得到了一個20位的物理地址,這也就是從邏輯地址生成物理地址的過程。二進制的左移四位,實際相當於十進制的乘以16,所以物理地址相當於段基址乘以16,再加上偏移量。這就是8086段加偏移的物理地址生成方式。
用這樣的方式,8086對存儲器進行分段式的管理,每個段最大為2的16次方,也就是64k個字節,他們在編程時使用邏輯地址,這樣的好處在於,如果我們將程序在存儲器當中變換位置,那程序中原來編寫的偏移地址都不需要改動,只需要修改段寄存器的內容就可以了。而且還要說明,不同的邏輯段實際上是可以有重疊的。這也為高效的利用存儲器的空間提供了便利。
我們來看一個實例。如果我們寫一條指令 MOV AX, [3000H],但實際上在段加偏移的模式下,操作數是默認存放在DS也就是數據段寄存器所指向的段中,讓我們假設事先已經在DS段寄存器當中存入了2000H。那麽實際的物理地址應該采用段寄存器乘以16,加上偏移量的方式得到最終我們所用的物理地址是23000H。
我們來看左邊這個存儲器的片段,假設代碼段在這裏,其中有一條指令就是我們現在所用的MOV指令。第一個字節是操作碼,後兩個字節就是指令中指定的偏移地址。那cpu在取回這條指令後,發現要從存儲器中取得一個數據,因此cpu會取出DS寄存器的內容,並將其左移四位後,與指令編碼中的偏移量相加,從而得到了23000這個地址,然後用這個地址去訪問存儲器。因為我們的目標寄存器AX是16位的,所以從存儲器中取出兩個字節,並依照著高地址放在高字節,低地址放在低字節的原則,存放到AX當中。這就完成了這條指令的執行。這樣段加偏移的方式實際上是非常復雜的,這是在內部的運算器和通用寄存器只有16位的情況下,又想訪問超過16位的地址空間所想出的一個折中的辦法,算是在簡陋的條件下用了一個巧妙的方法,從而提供了較好的性能。但是如果不僅限於微處理器,計算機中是早就采用了32位的地址空間的。
比如60年代的大型計算機 IBM S/360,在其比較早的型號中就使用了32位的通用寄存器,cpu對外發出32位的地址。
我們再來看IA32體系結構的存儲器尋址。我們以指令的尋址為例,原先16位的IP寄存器已經擴展為了32位的EIP寄存器。當然在實模式下面還是使用 CS:IP 這樣的邏輯地址。而在保護模式下,因為ERP寄存器的尋址能力已經達到了 \(2^{32}\),也就是4G個字節單元,這和CPU對外的地址總線的寬度是一致的,也就是說EIP寄存器完全有能力訪問到外部存儲器的每一個字節單元,但在保護模式下,IA32仍然采用了與之前形式上一致的尋址模式 CS:EIP 這樣的邏輯地址的組合,但是在這種模式下,段寄存器 CS 的含義已經發生了變化。
實際在保護模式下,段基值是放在內存中的。這是一個存儲器的片段,這些數據以八個字節為一個單位,我們稱為一個描述符。我們來看其中一個描述符,字節2、3、4、7,一共四個字節,保存了一個基地址。而cpu中,CS段寄存器不再保存段基址,而是指向這個描述符的地址,但是我剛才說過CS只有16位,它無法在4G空間中尋址,所以CPU當中還增加了一個寄存器,叫 GDTR。而 GDTR 中則保存了這個全局描述符表的起始地址,CS 段寄存器中則保存了相對於這個起始地址的偏移量,所以我們可以這麽理解,cpu在取指令時,先將 GDTR 中保存的地址取出,並於CS寄存器當中的內容相加,得到一個內存地址,用這個地址去訪問存儲器,獲得了這個描述符對應的八個字節,再將其中的基地址部分提取出來,與EIP寄存器中保存的偏移地址相加,就得到了真正要訪問的地址。當然在這個描述中還描述了這個段有多長,也就是段界限;以及對這個段的內容是否能讀寫,也就是權限;還有一些其它的信息。CPU通過這些信息可以由硬件來判斷,當前要執行的這條指令是否符合這個段的要求,從而起到了保護的作用。我們可以看到分段的方式變得越來越復雜,雖然它提供了很好的保護機制,但是代價也是不小的。而且在分段的基礎上,還可以采用分頁等機制,進行更細粒度的管理,所以在有些系統上就對分段進行了簡化的處理,讓所有的程序都運行在一個段上,也就是不改變段寄存器的內容,而在x86-64上得到了進一步的簡化。
圖中一個x86-64當中對應的描述符,我們可以看到,很多的字節都被固定設置為零。也就是說,這時候已經沒有了段基址和段界限,所有的代碼段都是從地址0開始的。在描述符中只設置了這個段對應的權限和若幹的控制位。
這些就是x86體系結構演變過程中的一些要點。現在我們已經了解了x86體系結構的基本特點,之後,我們將通過分析x86的具體指令,來進一步學習這種體系結構。
3.2 x86體系結構