天天看點

《作業系統真象還原》——0.25 指令集、體系結構、微架構、程式設計語言

本節書摘來自異步社群《作業系統真象還原》一書中的第0章,第0.25節,作者:鄭鋼著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

指令集是什麼?表面上看它是一套指令的集合。集合的意思顯而易見,那咱們說說什麼是指令。

在計算機中,cpu隻能識别0、1這兩個數,甚至它都不知道數是什麼,它隻知道要麼“是”,要麼“不是”,恰好用0、1來表示這兩種狀态而已。

人發明的東西逃不出人的思維,是以,先看看我們人類的語言是怎麼回事。

不同的語言對同一種事物有不同的名字,這個名字其實就是代碼。比如說人類的好朋友:狗,咱們在中文裡稱之為狗,但在英文中它被稱為dog,雖然用了兩種語言,但其描述的都是這種會汪汪叫、對人類無比忠誠的動物。人是怎樣識别小狗的呢?識别資訊來自聽覺、視覺等,這是因為人天生具備處理聲音和圖像的能力,能夠識别出各種不同的聲音和顔色不同的圖像。可是計算機隻能處理0、1這兩個數,是以讓計算機識别某個事物,隻有用01這兩個數來定義。也就是說,要用0、1來為各種事物編碼。

為了更好地說明指令集,咱們這裡不再用現有的語言舉例子,當然也不是要自創指令集。下面舉個簡單的例子來示範指令集的模型。

咱們拿表達式a=b+c為例。假設a、b、c都是記憶體變量的值,它們的位址分别是0x3000、0x3004、0x3008。在此用ra表示寄存器a,rb表示寄存器b,rc表示寄存器c。

完成這個加法的步驟是先将b和c載入到ra和rb寄存器中,再将兩個寄存器的值相加後送入寄存器ra,之後再将寄存器ra的值寫入到位址為0x3000的記憶體中。

步驟有了,咱們再設計完成這些步驟的指令。

步驟1:将記憶體中的資料載入到寄存器,咱們假設它的指令名為load。

《作業系統真象還原》——0.25 指令集、體系結構、微架構、程式設計語言

步驟2:兩個寄存器的加法指令,假設指令名為add。

步驟3:将寄存器中的内容存儲到記憶體,假設指令名為store。

以上指令名都是假設的,名字可以任意取,因為cpu不識别指令名。指令名是編譯器用來給人看的,為的是友善人來程式設計,cpu它隻認編碼。目前cpu中的指令,無論是哪種指令集,都由操作碼和操作數兩部分組成(有些指令即使指令格式中沒有列出操作數,也會有隐含的操作數)。咱們也采用這種操作碼+操作數的思路,分别為這兩部分編碼。

咱們先為操作碼設計編碼。

《作業系統真象還原》——0.25 指令集、體系結構、微架構、程式設計語言

接下來為操作數編碼,操作數一般是立即數、寄存器、記憶體等,咱們這裡主要是為寄存器編碼。

《作業系統真象還原》——0.25 指令集、體系結構、微架構、程式設計語言

好啦,操作碼和操作數都有了,其實指令集已經完成了。不過在一長串的二進制01中,哪些是操作碼,哪些是操作數呢?這就是指令格式的由來啦。我們人為規定個格式,規定操作碼和操作數的大小及位置,然後在cpu硬體電路中寫死這些規則,讓cpu在硬體一級上識别這些格式,進而能識别出操作碼和操作數。

假設我們的指令格式最大支援三個寄存器參數和一個立即數參數。其中操作碼和各寄存器操作數各占1位元組,立即數部分占4位元組。各條指令并不是完全按照此格式填充,不同的指令有不同的參數,隻有操作碼部分是固定的,其他操作數部分是可選的。當cpu在譯碼階段識别出操作碼後,cpu自然知道該指令需要什麼樣的操作數,這是寫死在硬體電路中的,是以不同的指令其機器碼長度很可能不一緻。

為了示範指令集模型,我們在上面假設了寄存器名、指令名、格式。按理說這對于指令集來說已經全了,不過,為友善咱們了解編譯器,不如咱們再假設個指令的文法吧,咱們這裡學習intel的文法格式:“指令目的操作數,源操作數”。目的操作數在左,源操作數在右,此指派順序比較直覺。intel想表達的是 a=b這種語序,如a=b,便是mov a,b。

以上三個步驟的機器碼按照十六進制表示為:

《作業系統真象還原》——0.25 指令集、體系結構、微架構、程式設計語言

以上自定義的指令便是按照咱們假設的文法來生成的。對于機器碼的大小,由于指令不同,需要的操作數也不同,是以機器碼大小也不同。另外,機器碼中的立即數是按照x86架構的小端位元組序寫的,這一點大家要注意。小端位元組序是數值中的低位在低位址,高位在高位址,數位以位元組為機關。前面有一小節說明大小端位元組序問題。

步驟2的機器碼為01 00 01 10。操作碼占1位元組,cpu識别出第1位元組的二進制01是add指令,知道此指令的操作數是3個寄存器,并且第1個寄存器操作數是目的寄存器,另外兩個寄存器是源操作數(這都是我們假定的,并且是寫死在硬體中的規則,不同的指令有不同的規則,您也可以創造出記憶體和寄存器混合作為操作數的加法指令)。于是到第2位元組去讀取寄存器編碼,發現其值為二進制00,就是寄存器ra對應的編碼。接着到下一個位元組處繼續讀出寄存器編碼,發現是二進制01,也就是寄存器rb,rc同理。于是将寄存器rb和rc的值相加後存入到寄存器ra。

步驟3中,機器碼為10 00 0c300000,cpu讀取機器碼的第1 位元組發現其為二進制10,知道其為指令store,于是便确定了,目的操作數是個立即數形式的記憶體位址,源操作數是個寄存器。接着到指令格式中的寄存器操作數1的位置去讀取寄存器編碼,發現其值為00,這就是寄存器ra的編碼。機器碼中剩下的部分便作為立即數,這樣便将寄存器ra的值寫入到記憶體0x0000300c中了。

以上指令集的模型,确實太過于簡單了,也許稱之為模型都非常勉強。現實中的指令格式要遠遠複雜得多。下面我們看看目前世面上的指令集有哪些。

最早的指令集是cisc(complex instruction set computer),意為複雜指令集計算機。從名字上看,這套指令集相當複雜,當初這套指令集問世的時候,它的研發者們都沒想過要給它起名,隻是因為後來出現了相對精簡高效的指令集,是以人們為了加以區分,才将最初的這套相對複雜的指令集命名為cisc,而後來精簡高效的指令集稱為risc(reduced instruction set computer)。

cisc和risc并不是具體的指令集,而是兩種不同的指令體系,相當于指令集中的門派,是指令的設計思想。舉個例子,就像中醫與西醫,中醫講究從整體上調理身體,西醫則更多的是偏向局部。這就是兩種不同的醫療思路,類似于cisc和risc這兩種指令體系。那什麼是指令集呢?拿中醫舉例,像華佗、張仲景這兩位醫聖,他們雖然都是基于中醫的思想治病,但醫術各有特色,水準也不盡相同,這就相當于不同的指令集。一會兒咱們會介紹具體的指令集。

為什麼說cisc複雜呢?

首先,因為它是最早的指令集,當初都是摸着石頭過河,肯定有一些瑕疵在裡面。其次,當初的程式員都是用彙編語言開發程式,他們當然希望彙編語言強大啦,盡量多一些指令,盡量一個指令能多幹幾件事,是以指令集中的指令越來越多,越來越複雜。不過這樣的好處是程式員同學很爽。最後,cisc是intel使用的指令集,intel公司在相容性方面做得最好,指令集在發展的過程中,還要相容過去有瑕疵的古董,以至于最後的指令集變得有點“奇形怪狀”了。

作為後起之秀的risc,借鑒了前輩cisc的經驗,取其精華,棄其糟粕,當然要更好更輕量啦。它是怎麼來的呢?

cisc不是做得很全很強嗎,可是很多時候,程式員并不會用到那些複雜的指令和尋址方式,即使用到了,編譯器有時候為了優化,未必“全”将其編譯為複雜的形式。這就導緻了cpu中的複雜的指令和尋址方式無用武之地。根據二八定律,指令集中20%的簡單指令占了程式的80%,而指令集中80%的複雜指令占了程式的20%。根據這個特性,處理器及指令集被重新設計,保留了那些基本常用的指令,減少了硬體電路的複雜性。這樣,大部分指令都能在一個時鐘周期内完成,更有利于提升流水線的效率。而且,指令采用了定長編碼,這樣譯碼工作更容易了。由于其太優秀了,後來的處理器,如mips,arm,power都采用risc指令體系,做得最好的就是mips處理器,它嚴格遵守risc思想,業界公認其優雅。

我們常用的cpu是intel和amd公司的産品,它們用的指令集便是基于cisc思想的x86。amd的x86指令架構是intel授權給他們的,為差別于此,intel在官方手冊上稱自己的指令集為ia32。

雖然amd采用的也是x86指令集,但intel可沒把硬體實作方法也告訴amd,否則amd的cpu和intel的cpu不就完全一樣了嗎,人家intel也不肯呢。指令集是一套約定,裡面規定的是有哪些指令、指令的二進制編碼、指令格式等,如何實作這套約定,這是硬體自己的事。打個比方,這就像和朋友約好了在某餐廳吃飯,咱是坐車去,還是走着去,這是咱們的事,與吃飯是無關的。說白了,在intel的cpu上運作的軟體也能夠在amd的cpu上運作,原因就是它們共用了同用一套指令集,也就是對二進制編碼達成了共識。它們面對相同的需求,可能采取了不同的行動,但都完成了任務。比如機器碼是b80000,intel的cpu經過譯碼,知道這是将0指派給寄存器ax,相當于彙編語言mov ax,0。amd的cpu在譯碼時,也得将此機器碼認為是将0指派給寄存器ax。至于它們在實體上是怎麼将0傳入寄存器ax中的,這是它們各自實作的方式,與指令集無關。它們各自實作的方式,就叫微架構。

總結一下,指令集是具體的一套指令編碼,微架構是指令集的實體實作方式。

發展到後來,x86指令集越來越複雜。它本屬于cisc體系,但由于效率低下,最終在其内部實作上采取了risc核心,即一條cisc指令在譯碼時,分解成多條risc指令,這樣其執行效率便可與risc媲美啦。

目前市面上常見的指令集有五種,除x86是cisc指令體系外,arm、mips、power、c6000都是risc指令體系的指令集。

cpu與指令集是對應的,一種cpu隻能識别一種指令集,是以很多cpu都以其支援的指令集來稱呼。比如arm、mips,它們本身是cpu名稱,又是指令集名稱。

arm主要用在手機中,作為手機的處理器。power是ibm用于伺服器上的處理器。c6000是數字信号處理器,廣泛用于視訊處理。而mips雖然本身很優秀,但其在各領域起步都較晚,并沒有廣泛應用的領域。

由于mips本身的優越性,龍芯用的就是mips指令集,有沒有人問,為什麼咱們自主研發的cpu還要用人家國外的指令集?就不能也研發出一套指令集嗎?能倒是能,不過語言不通用。就像我自己可以發明一門語言,語言本身沒什麼問題,問題是我用自己發明的語言和别人交流,誰聽得懂呢,誰又願意去學這門語言呢?大家都很忙,不通用的東西沒人願意花精力去學。如果龍芯也自立門戶創造新的指令集,那有誰願意給它寫編譯器呢?即使有了編譯器,作業系統也要重新編譯釋出,應用程式也要重新編譯釋出,指令集背後不僅是個計算機生态鍊,更重要的是全球經濟鍊。

平時所說的程式設計語言,雖然其上層表現各異,歸根結底是要在具體的cpu上運作的,是以必須由編譯器按照該cpu的指令集,翻譯成符合該cpu的指令。說到這,不得不說一下交叉編譯,本質上交叉編譯就是用在a平台上運作的編譯器,編譯出符合b平台cpu指令集的程式,編譯出的程式直接能在b平台上運作啦。這裡的平台指的就是cpu指令體系結構。

繼續閱讀