mips體系結構特點(轉載非原創)

MIPS指令特點:
1、所有指令都是32位編碼;
2、有些指令有26位供目標地址編碼;有些則只有16位。因此要想加載任何一個32位值,就得用兩個加載指令。16位的目標地址意味着,指令的跳轉或子函數的位置必須在64K以內(上下32K);
3、所有的動作原理上要求必須在1個時鐘週期內完成,一個動作一個階段;
4、有32個通用寄存器,每個寄存器32位(對32位機)或64位(對64位機);
5、本身沒有任何幫助運算判斷的標誌寄存器,要實現相應的功能時,是通過測試兩個寄存器是否相等來完成的;
6、所有的運算都是基於32位的,沒有對字節和對半字的運算(MIPS裏,字定義爲32位,半字定義爲16位);
7、沒有單獨的棧指令,所有對棧的操作都是統一的內存訪問方式。因爲push和pop指令實際上是一個複合操作,包含對內存的寫入和對棧指針的移動;

8、由於MIPS固定指令長度,所以造成其編譯後的二進制文件和內存佔用空間比x86的要大,(x86平均指令長度只有3個字節多一點,而MIPS是4個字節);

9、尋址方式:只有一種內存尋址方式。就是基地址加一個16位的地址偏移;

10、內存中的數據訪問必須嚴格對齊(至少4字節對齊)

11、跳轉指令只有26位目標地址,再加上2位的對齊位,可尋址28位的空間,即256M。意思即是說,在一個C程序內,goto語句只能跳轉到它之前的128M和之後的128M這個地址空間之內

12、條件分支指令只有16位跳轉地址,加上2位的對齊位,共18位尋址空間,即256K。意思即是說,在一個C程序內,if語句只能跳轉到它之前的128K和之後的128K這個地址空間之內;

13、MIPS默認不把子函數的返回地址(就是調用函數的受害指令地址)存放到棧中,而是存放到$31寄存器中;這對那些葉子函數有利。如果遇到嵌套的函數的話,有另外的機制處理;

14、流水線效應。由於採用了高度的流水線,結果產生了一些對程序員來說可見的效應,需要注意。最重要的兩個效應就是分支延遲效應和載入延遲效應。
    a 任何一個分支跳轉語句後面的那條語句叫做分支延遲槽。實際上在程序執行到分支語句時,當他剛把要跳轉到的地址填充好(到代碼計數器裏),還沒完成本條 指令,分支語句後面的那個指令就執行了。這是因爲流水線效應,幾條指令同時在執行,只是處於不同的階段。具體看書上說提前半條指令執行,沒看懂。分支延遲 槽常用被利用起來完成一些參數初始化等相關工作,而不是被浪費了。
    b 載入延遲是這樣的。當執行一條從內存中載入數據的命令時,是先載入到高速緩衝中,然後再取到寄存器中,這個過程相對來說是比較慢的。在這個過程完成之 前,可能已經有幾條在流水線上的指令被執行了。這幾條在載入指令後被執行的指令就被稱作載入延遲槽。現在就有一個問題,如果後面這幾條指令要用到載入指令 所載入的那個數據怎麼辦?一個通用的辦法是,把內部鎖加在數據載入過程上,這樣,當後面的指令要用這條指令時,就只有先停止運行(在ALU階段),等這條 數據載入指令完成了後再開始運行。

*MIPS指令的五級流水線:每條指令都包含五個執行階段。
第一階段:從指令緩衝區中取指令。佔一個時鐘週期;
第二階段:從指令中的源寄存器域(可能有兩個)的值(爲一個數字,指定$0~$31中的某一個)所代表的寄存器中讀出數據。佔半個時鐘週期;
第三階段:在一個時鐘週期內做一次算術或邏輯運算。佔一個時鐘週期;
第四階段:指令從數據緩衝中讀取內存變量的階段。從平均來講,大約有3/4的指令在這個階段沒做什麼事情,但它是指令有序性的保證(爲什麼是保證,我還沒看清楚?)。佔一個時鐘週期;
第五階段:存儲計算結果到緩衝或內存的階段。佔半個時鐘週期;
=> 所以一條指令要佔用四個時鐘週期;

15、MIPS的虛擬地址內存映射空間
a  0x0000 0000 ~ 0x7fff ffff
用戶級空間,2GB,要經MMU(TLB)地址翻譯。kuseg。可以控制要不要經過緩衝。

b  0x8000 0000 ~ 0x9fff ffff
kseg0. 這塊區域爲操作系統內核所佔的區域,共512M。使用時,不經過地址翻譯,將最高位去掉就線性映射到內存的低512M(不足的就裁剪掉頂部)。但要經過緩衝區過渡。

c  0xa000 0000 ~ 0xbfff ffff
kseg1. 這塊區域爲系統初始化所佔區域,共512M。使用時,不經過地址翻譯,也不經過緩衝區。將最高3位去掉就線性映射到內存的低512M(不足的就裁剪掉頂部)。

d  0xc000 0000 ~ 0xffff ffff
kseg2. 這塊區域也爲內核級區域。要經過地址翻譯。可以控制要不要經過緩衝。

16、MIPS的協處理器
CP0:這是MIPS芯片的配置單元。必不可少,雖然叫做協處理器,但是通常都是做在一塊芯片上。絕大部分MIPS功能的配置,緩衝的控制,異常/中斷的控制,內存管理的控制都在這裏面。所以是一個完整的系統所必不可少的;

17、 MIPS的高速緩衝
MIPS一般有兩到三級緩衝,其中第一級緩衝數據和指令分開存儲。這樣的好處是指令和數據可以同時存取,提高效率。但缺點是提高了複雜度。第二級緩衝和第三級緩衝(如果有的話)就不再分開存放啦。

緩衝的單元叫做緩衝行(cache line)。每一行中,有一個tag,然後後面接的是一些標誌位和一些數據。緩衝行按順序線性排列起來,就組成了整個緩衝。

cache line的索引和存取有一套完整的機制。
18、MIPS的異常機制
精確異常的概念:在運行流程中沒有任何多餘效應的異常。即當異常發生時,在受害指令之前的指令被完全執行,而受害指令及後面的指令還沒開始執行(注:說受 害指令及後面的指令還沒做任何事情是不對的,實際上受害指令是處於其指令週期的第三階段剛完成,即ALU階段剛完成)。精確異常有有助於保證軟件設計上不 受硬件實現的影響。

CP0中的EPC寄存器用於指向異常發生時指令跳轉前的執行位置,一般是受害指令地址。當異常時,是返回這個地址繼續執行。但如果受害指令在分支延遲槽中,則會硬件自動處理使EPC往回指一條指令,即分支指令。在重新執行分支指令時,分支延遲槽中的指令會被再執行一次。

精確異常的實現對流水線的流暢性是有一定的影響的,如果異常太多,系統執行效率就會受到影響。

*異常又分常規異常和中斷兩類。常規異常一般爲軟件的異常,而中斷一般爲硬件異常,中斷可以是芯片內部,也可以是芯片外部觸發產生。

異常發生時,跳轉前最後被執行的指令是其MEM階段剛好被執行完的那條指令。受害指令是其ALU階段剛好執行完的那條指令。

異常發生時,會跳到異常向量入口中去執行。MIPS的異常向量有點特殊,它一般只個2個或幾個中斷向量入口,一個入口給一般的異常使用,一個入口給 TLB miss異常使用(這樣的話,可以省下計算異常類型的時間。在這種機制幫助下,系統只用13個時鐘週期就可以把TLB重填好)。

CP0寄存器中有個模式位,SR(BEV),只要設置了,就會把異常入口點轉移到非緩衝內存地址空間中(kseg1)。

MIPS系統把重啓看作一個不可迴歸的異常來處理。
冷啓動:CPU硬件完全被重新配置,軟件重新加載;
熱啓動:軟件完全重新初始化;

MIPS對異常的處理的哲學是給異常分配一些類型,然後由軟件給它們定義一些優先級,然後由同一個入口進入異常分配程序,在分配程序中根據類型及優先級確定該執行哪個對應的函數。這種機制對兩個或幾個異常同時出現的情況也是適合的。

下面是當異常發生時MIPS CPU所做的事情:
a 設置EPC指向迴歸的位置;
b 設置SR(EXL)強迫CPU進入kernel態,並禁止所有中斷響應。
c 設置Cause寄存器,以使軟件可以得到異常的類型信息;還有其它一些寄存器在某些異常時會被設置;
d CPU開始從異常入口取指令,然後以後的所有事情都交由軟件處理了。

k0和k1寄存器用於保存異常處理函數的地址。
異常處理函數執行完成後,會回到異常分配函數那去,在異常分配函數裏,有一個eret指令,用於迴歸原來被中斷的程序繼續執行;eret指令會原子性地把中斷響應打開(置SR(EXL)),並把狀態級由kernel轉到user級,並返回原地址繼續執行。

19、中斷
MIPS CPU有8個獨立的中斷位(在Cause寄存器中),其中,6個爲外部中斷,2個爲內部中斷(可由軟件訪問)。一般來說,片上的時鐘計數/定時器,會連接到一個硬件位上去。

SR(IE)位控制全局中斷響應,爲0的話,就禁止所有中斷;
SR(EXL)和SR(ERL)位(任何一個)如果置1的話,會禁止中斷;
SR(IM)有8位,對應8箇中斷源,要產生中斷,還得把這8位中相應的位置1纔行;

中斷處理程序也是用通用異常入口。但有些新的CPU有變化。

*在軟件中實現中斷優先級的方案
a 給各種中斷定優先級;
b CPU在運行時總是處於某個優先級(即定義一個全局變量);
c 中斷髮生時,只有等於高於CPU優先級的中斷優先級才能執行;(如果CPU優先級處於最低,那麼所有的中斷都可以執行);
d 同時有多箇中斷髮生時,優先執行優先級最高的那個中斷程序;

20、大小端問題
硬件上也有大端小端問題,比如串口通訊,一個字節一個字節的發,首先是低位先發出去。
還有顯卡的顯示,比如顯示黑白圖像,在屏幕上一個點對應顯存中的一位,這時,這個位對應關係就是屏幕右上角那個點對應顯存第一個字節的7號位,即最高位。第一排第8位點對應第一個字節的0號位。

21、MIPS上的Linux運行情況

用戶態和核心態:在用戶態,不能隨意訪問內核代碼和數據存放區,只能訪問用戶態空間和內核允許訪問(通過某種機制)的內核頁面。也不能執行CP0相關的指令。用戶態要執行內核的某些服務,就得用系統調用(system_call),在系統調用的最後,是一個eret指令。

任何時候Linux都有至少一個線程在跑,Linux一般不禁止中斷。發生中斷時,其環境是從被中斷線程借來的。

中斷服務程序(ISR)應該短小。

MIPS Linux系統上半地址空間只能用內核特權級訪問。內核不通過TLB地址翻譯。

所有線程都共用相同的內核地址空間,但只有同一組線程才用同一個用戶地址空間(指向同一個mm_struct結構)。

如果物理內存高於512M,那麼不能用kseg0和kseg1來映射高於512M的內存部分。只能用kseg2來映射。kseg2要經過TLB。

從某個方面說,內核就是一組供異常處理函數調用的子程序。內核中,線程調度器就是這樣一個小的子程序。由各個線程(異常處理程序也可以算作一個特殊的線程,換他書上的說法)調用。

MIPS Linux有異常模式,而x86上沒有這個概念。

異常要小心操作。不是僅用軟件鎖就能解決的。

21、原子操作
MIPS爲支持操作系統的原子操作,特地加了一組指令 ll/sc。它們這樣來使用:

先寫一句
atomic_block:
ll XX1, XXX2
….
sc XX1, XXX2
beq XX1, zero, automic_block
….

在ll/sc中間寫上你要執行的代碼體,這樣就能保證寫入的代碼體是原子執行的(不會被搶佔的)。

其實,ll/sc兩語句自身並不保證原子執行,但他耍了個花招:
用一個臨時寄存器XX1,執行ll後,把XXX2中的值載入XX1中,然後會在CPU內部置一個標誌位,我們不可見,並保存XXX2的地址,CPU會監視 它。在中間的代碼體執行的過程中,如果發現XXX2的內容變了(即是別的線程執行了,或是某個中斷髮生了),就自動把CPU內部那個標誌位清0。執行sc 時,把XX1的內容(可能已經是新值了)存入XXX2中,並返回一個值存入XX1中,如果標誌位還爲1,那麼這個返回的值就爲1;如果標誌位爲0,那麼這 個返回值就爲0。爲1的話,就表明這對指令中間的代碼是一次性執行完成的,而不是中間受到了某些中斷,那麼原子操作就成功了;爲0的話,就表明原子操作沒 成功,執行後面beq指令時,就會跳轉到ll指令重新執行,直到原子操作成功爲止。

所以,我們要注意,插在ll/sc指令中間的代碼必須短小。

據經驗,一般原子操作的循環不會超過3次。

22、系統調用 syscall
系統調用也通過異常入口進入系統內核,選擇8號異常代碼處理函數進行處理,進入系統調用分配函數後,還要根據傳進來的參數再一次分配到具體的功能函數上去。系統調用傳遞參數是在寄存器中進行的。

系統調用號存放在v0中,參數存放在a0-a3。如果參數過多,會有另一套機制來處理。系統調用的返回值通常放在v0中。如果系統調用出錯,則會在a3中返回一個錯誤號。

23、異常入口點位於kseg0的底部,是硬件規定的。

24、注意:地址空間的0x0000 0000是不能用的,從0開始的一個或多個頁不會被映射。

25、內存分頁映射有以下優點:
a 隱藏和保護數據;
b 分配連續的地址給程序;
c 擴展地址空間;
d 按需求載入代碼和數據(通過異常方式);
e 便於重定位;
f 代碼和數據在線程中共享,便於交換數據;

所有的線程是平等的,所有的線程都有自己的內存管理結構體;運行於同一地址空間的線程組,共享有大部分這種數據結構。在線程中,保存有本地址空間已經使用的頁面的一個頁表,用來記錄每個已用的虛頁與實際物理頁的映射關係;

26、ASID是與虛擬頁高位配合使用。用於描述在TLB和Cache中的不同的線程,只有8位,所以最多隻能同時運行256個線程。這個數字一般來說是夠的。如果超過這個數目了,就要把Cache刷新了重新裝入。所以,在這點上,與x86是不同的。

27、MIPS Linux的內存駐留頁表結構
用的是兩級頁表,一個頁表目錄,一個頁表,頁表中的每一項是一個 EntryLo0-1。
(這與x86方式類似)。而沒有用MIPS原生設計的方案。

28、TLB的refill過程-硬件部分
a CPU先產生一個虛擬地址,要到這個地址所對應的物理地址上取數據(或指令)或寫數據(或指令)。
低13位被分開來。然後高19位成爲VPN2,和當前線程的ASID(從EntryHi(ASID)取)一起配合與TLB表中的項進行比較。(在比較過程中,會受到PageMask和G標誌位的影響)
b 如果有匹配的項,就選擇那個。虛擬地址中的第12位用於選取是用左邊的物理地址項還是用右邊的物理地址項。
然後就會考察V和D標誌位,V標誌位表示本頁是否有效,D表示本頁是否已經髒了(被寫過)。
如果V=0,或D=1,就會引發翻譯異常,BadVAddr會保存現在處理的這個虛擬地址,EntryHi會填入這個虛擬地址的高位,還有Context中的內容會被重填。
然後就會考察C標誌位,如果C=1,就會用緩衝作中轉,如果C=0,就不使用緩衝。
這幾級考察都通過了之後,就正確地找到了那個對應的物理地址。
c 如果沒有匹配的項,就會觸發一個TLB refill異常,然後後面就是軟件的工作了;

29、TLB的refill過程-軟件部分
a 計算這個虛擬地址是不是一個正確的虛擬地址,在內存頁表中有沒有與它對應的物理地址;如果沒有,則調用地址錯誤處理函數;
b 如果在內存頁表中找到了對應的物理地址,就將其載入寄存器;
c 如果TLB已經滿了,就用random選取一個項丟棄;
d 複製新的項進TLB。

30、MIPS Linux中標誌內存頁已經髒了的方式與x86不同。它要耍個把戲:
a 當一個可寫的頁第一次載入內存中時(從磁盤載入?載入的時候就分配一個物理頁,同時就分配個對應的虛擬頁,並在內存頁表中添一個Entry),將其Entry的D標誌位清0;
b 然後,當後面有指令要寫這個頁時,就會觸發一個異常(先載入TLB中判斷),我們在這個異常處理函數中把內存頁表項中的標誌位D置1。這樣後面的就可以寫了。並且,由於這個異常把標誌位改了,我們認爲這個物理頁是髒的了。
c 至於TLB中已經有的那個Entry拷貝還要修改它的D標誌位,這樣這次寫入操作才能繼續入下進行。

31、MIPS中的C語言參數傳遞機制?

32、MIPS中的堆棧結構及在內存中的分佈?


指令長度和寄存器個數
MIPS的所有指令都是32位的,指令格式簡單。不像x86那樣,x86的指令長度不是固定的,以80386爲例, 其指令長度可從1字節(例如PUSH)到17字節,這樣的好處代碼密度高,所以MIPS的二進制文件要比x86的大大約20%~30%。而定長指令和格式 簡單的好處是易於譯碼和更符合流水線操作,由於指令中指定的寄存器位置是固定的,使得譯碼過程和讀指令的過程可以同時進行,即固定字段譯碼。
32 個通用寄存器,寄存器數量跟編譯器的的要求有關。寄存器分配在編譯優化中是最重要的優化之一(也許是做重要的)。現在的寄存器分配算法都是基於圖着色的技 術。其基本思想是構造一個圖,用以代表分配寄存器的各個方案,然後用此圖來分配寄存器。粗略說來就是使用有限的顏色使圖中相臨的節點着以不同的顏色,圖着 色問題是個圖大小的指數函數,有些啓發式算法產生近乎線形時間運行的分配。全局分配中如果有16個通用寄存器用於整型變量,同時另有額外的寄存器用於浮點 數,那麼圖着色會很好的工作。在寄存器數教少時候圖着色並不能很好的工作。
   問: 既然不能少於16個,那爲什麼不用64個呢?
答: 使用64個或更多寄存器不但需要更大的指令空間來對寄存器編碼,還會增加上下文切換的負擔。除了那些很大不能感非常複雜的函數,32個寄存器就已足夠保存 經常使用的數據。使用更多的寄存器並不必要,同時計算機設計有個原則叫“越小越快”,但是也不是說使用31個寄存器會比32個性能更好,32個通用寄存器 是流行的做法。
指令格式
所有MIPS指令長度相同,都是32位,但爲了讓指令的格式剛好合適,於是設計者做了一個折衷:所有指令定長,但是不同的指令有不同的格式。MIPS指令有三種格式:R格式,I格式,J格式。每種格式都由若干字段(filed)組成,圖示如下:
I型指令
      6    5     5     16
   ------|-----|-----|------------------|
   | op | rs | rt   | 立即數操作 |
       ------|-----|-----|------------------|
加載/存儲字節,半字,字,雙字
條件分支,跳轉,跳轉並鏈接寄存器
R型指令
      6    5     5     5     5     6
   ------|-----|-----|-----|-----|--------|
   |op | rs   | rt   | rd |shamt|funct |
   ------|-----|-----|-----|-----|---------|
寄存器-寄存器ALU操作
讀寫專用寄存器
J型指令
      6             26
   ------|------------------------------|
   |op   |  跳轉地址          |
       ------|------------------------------|
跳轉,跳轉並鏈接
陷阱和從異常中返回

各字段含義
op:指令基本操作,稱爲操作碼。
rs:第一個源操作數寄存器。
rt:第二個源操作數寄存器。
rd:存放操作結果的目的操作數。
shamt:位移量
funct:函數,這個字段選擇op操作的某個特定變體。
  
所有指令都按照着三種類型之一來編碼,通用字段在每種格式中的位置都是相同的。
    這種定長和簡單格式的指令編碼很規則,很容易看出其機器碼,例如:
add $t0,$s0,$s1
    表示$t0=$s0+$s1,即16號寄存器(s0)的內容和17號寄存器(s1)的內容相加,結果放到8號寄存器(t0)。
    指令各字段的十進制表示爲
   ------|-----|-----|-----|-----|------|
   |   0 | 16 | 17 |   8 |   0 |   32 |
   ------|-----|-----|-----|-----|------|
op=0和funct=32表示這是加法,16=$s0表示第一個源操作數(rs)在16號寄存器裏,17=$s1表示第二個源操作數(rt)在17號寄存器裏,8=$t0表示目的操作數(rd)在8號寄存器裏。
把各字段寫成二進制,爲
------|-----|-----|-----|-----|------|
   |000000|10000|10001|01000|00000|100000|
------|-----|-----|-----|-----|------|
這就是上述指令的機器碼(machine code),可以看出是很有規則性的。

通用寄存器(GPR)
有32個通用寄存器,$0到$31:
$0: 即$zero,該寄存器總是返回零,爲0這個有用常數提供了一個簡潔的編碼形式。MIPS編譯器使用slt,beq,bne等指令和由寄存器$0獲得的0 來 產生所有的比較條件:相等,不等,小於,小於等於,大於,大於等於。還可以用add指令創建move僞指令,即
move $t0,$t1
實際爲
add $t0,$0,$t1
焦林前輩提到他移植fpc時move指令出錯,轉而使用add代替的。
   使用僞指令可以簡化任務,彙編程序提供了比硬件更豐富的指令集。
$1:即$at,該寄存器爲彙編保留,剛纔說到使用僞指令可以簡化任務,但是代價就是要爲彙編程序保留一個寄存器,就是$at。
由 於I型指令的立即數字段只有16位,在加載大常數時,編譯器或彙編程序需要把大常數拆開,然後重新組合到寄存器裏。比如加載一個32位立即數需要 lui(裝入高位立即數)和addi兩條指令。像MIPS程序拆散和重裝大常數由彙編程序來完成,彙編程序必需一個臨時寄存器來重組大常數,這也是爲彙編 保留$at的原因之一。
$2..$3:($v0-$v1)用於子程序的非浮點結果或返回值,對於子程序如何傳遞參數及如何返回,MIPS範圍有一套約定,堆棧中少數幾個位置處的內容裝入CPU寄存器,其相應內存位置保留未做定義,當這兩個寄存器不夠存放返回值時,編譯器通過內存來完成。
$4..$7:($a0-$a3)用來傳遞前四個參數給子程序,不夠的用堆棧a0-a3和v0-v1以及ra一起來支持子程序/過程調用,分別用以傳遞參數,返回結果和存放返回地址。當需要使用更多的寄存器時,就需要堆棧(stack)了,MIPS編譯器總是爲參數在堆棧中留有空間以防有參數需要存儲。
$8..$15:($t0-$t7)臨時寄存器,子程序可以使用它們而不用保留。
$16..$23:($s0-$s7)保存寄存器,在過程調用過程中需要保留(被調用者保存和恢復,還包括$fp和$ra),MIPS提供了臨時寄存器和保存寄存器,這樣就減少了寄存器溢出(spilling,即將不常用的變量放到存儲器的過程),編譯器在編譯一個葉(leaf)過程(不調用其它過程的過程)的時候,總是在臨時寄存器分配完了才使用需要保存的寄存器。
$24..$25:($t8-$t9)同($t0-$t7)
$26..$27:($k0,$k1)爲操作系統/異常處理保留,至少要預留一個。 異常(或中斷)是一種不需要在程序中顯示調用的過程。MIPS有個叫異常程序計數器(exception program counter,EPC)的寄存器,屬於CP0寄存器,用於保存造成異常的那條指令的地址。查看控制寄存器的唯一方法是把它複製到通用寄存器裏,指令mfc0(move from system control)可以將EPC中的地址複製到某個通用寄存器中,通過跳轉語句(jr),程序可以返回到造成異常的那條指令處繼續執行。仔細分析一下會發現個有意思的事情:
爲 了查看控制寄存器EPC的值並跳轉到造成異常的那條指令(使用jr),必須把EPC的值到某個通用寄存器裏,這樣的話,程序返回到中斷處時就無法將所有的 寄存器恢復原值。如果先恢復所有的寄存器,那麼從EPC複製過來的值就會丟失,jr就無法返回中斷處;如果我們只是恢復除有從EPC複製過來的返回地址外 的寄存器,但這意味着程序在異常情況後某個寄存器被無端改變了,這是不行的。爲了擺脫這個兩難境地,MIPS程序員都必須保留兩個寄存器$k0和$k1,供操作系統使用。發生異常時,這兩個寄存器的值不會被恢復,編譯器也不使用k0和k1,異常處理函數可以將返回地址放到這兩個中的任何一個,然後使用jr跳轉到造成異常的指令處繼續執行
$28:($gp)C語言中有兩種存儲類型,自動型和靜態型,自 動變量是一個過程中的局部變量。靜態變量是進入和退出一個過程時都是存在的。爲了簡化靜態數據的訪問,MIPS軟件保留了一個寄存器:全局指針 gp(global pointer,$gp),如果沒有全局指針,從靜態數據去裝入數據需要兩條指令:一條有編譯器和連接器計算的32位地址常量中的有效位;令一條才真正裝 入數據。全局指針只想靜態數據區中的運行時決定的地址,在存取位於gp值上下32KB範圍內的數據時,只需要一條以gp爲基指針的指令即可。在編譯時,數 據須在以gp爲基指針的64KB範圍內。
$29:($sp)MIPS硬件並不直接支持堆棧, 例如,它沒有x86的SS,SP,BP寄存器,MIPS雖然定義$29爲棧指針,它還是通用寄存器,只是用於特殊目的而已,你可以把它用於別的目的,但爲 了使用別人的程序或讓別人使用你的程序,還是要遵守這個約定的,但這和硬件沒有關係。x86有單獨的PUSH和POP指令,而MIPS沒有,但這並不影響 MIPS使用堆棧。在發生過程調用時,調用者把過程調用過後要用的寄存器壓入堆棧,被調用者把返回地址寄存器$ra和保留寄存器壓入堆棧。同時調整堆棧指 針,當返回時,從堆棧中恢復寄存器,同時調整堆棧指針。
$30:($fp)GNU MIPS C編譯器使用了偵指針(frame pointer),而SGI的C編譯器沒有使用,而把這個寄存器當作保存寄存器使用($s8),這節省了調用和返回開銷,但增加了代碼生成的複雜性。
$31:($ra)存放返回地址,MIPS 有個jal(jump-and-link,跳轉並鏈接)指令,在跳轉到某個地址時,把下一條指令的地址放到$ra中。用於支持子程序,例如調用程序把參數 放到$a0~$a3,然後jal X跳到X過程,被調過程完成後把結果放到$v0,$v1,然後使用jr $ra返回。
在調用時需要保存的寄存器爲$a0~$a3,$s0~$s7,$gp,$sp,$fp,$ra。
跳轉範圍
J 指令的地址字段爲26位,用於跳轉目標。指令在內存中以4字節對齊,最低兩個有效位不需要存儲。在MIPS中,每個地址的最低兩位指定了字的一個字 節,cache映射的下標是不使用這兩位的,這樣能表示28位的字節編址,允許的地址空間爲256M。PC是32位的,那其它4位從何而來呢?MIPS的 跳轉指令只替換PC的低28位,而高4位保留原值。因此,加載和鏈接程序必須避免跨越256MB,在256M的段內,分支跳轉地址當作一個絕對地址,和 PC無關,如果超過256M(段外跳轉)就要用跳轉寄存器指令了。
同樣,條件分支指令中的16位立即數如果不夠用,可以使用PC相對尋址,即用分支指令中的分支地址與(PC+4)的和做分支目標。由於一般的循環和if語句都小於2^16個字(2的16次方),這樣的方法是很理想的。

 

0 zero 永遠返回值爲0
1 at 用做彙編器的暫時變量
2-3 v0, v1 子函數調用返回結果
4-7 a0-a3 子函數調用的參數
8-15 t0-t7 暫時變量,子函數使用時不需要保存與恢復
24-25 t8-t9
16-25 s0-s7 子函數寄存器變量。子函數必須保存和恢復使用過的變量在函數返回之前,從而調用函數知道這些寄存器的值沒有變化。
26,27 k0,k1 通常被中斷或異常處理程序使用作爲保存一些系統參數
28 gp 全局指針。一些運行系統維護這個指針來更方便的存取“static“和”extern"變量。
29 sp 堆棧指針
30 s8/fp 第9個寄存器變量。子函數可以用來做楨指針
31 ra 子函數的返回地□

這些寄存器的用法都遵循一系列約定。這些約定與硬件確實無關,但如果你想使用別人的代碼,編譯器和操作系統,你最好是遵循這些約定。

寄存器名約定與使用

*at: 這個寄存器被彙編的一些合成指令使用。如果你要顯示的使用這個寄存器(比如在異常處理程序中保存和恢復寄存器),有一個彙編directive可被用來禁止彙編器在directive之後再使用at寄存器(但是彙編的一些宏指令將因此不能再可用)。

*v0, v1: 用來存放一個子程序(函數)的非浮點運算的結果或返回值。如果這兩個寄存器不夠存放需要返回的值,編譯器將會通過內存來完成。詳細細節可見10.1節。


*a0-a3: 用來傳遞子函數調用時前4個非浮點參數。在有些情況下,這是不對的。請參考10.1細節。

* t0-t9: 依照約定,一個子函數可以不用保存並隨便的使用這些寄存器。在作表達式計算時,這些寄存器是非常好的暫時變量。編譯器/程序員必須注意的是,當調用一個子函數時,這些寄存器中的值有可能被子函數破壞掉。

*s0-s8: 依照約定,子函數必須保證當函數返回時這些寄存器的內容必須恢復到函數調用以前的值,或者在子函數裏不用這些寄存器或把它們保存在堆棧上並在函數退出時恢復。這種約定使得這些寄存器非常適合作爲寄存器變量或存放一些在函數調用期間必須保存原來值。

* k0, k1: 被OS的異常或中斷處理程序使用。被使用後將不會恢復原來的值。因此它們很少在別的地方被使用。

* gp: 如果存在一個全局指針,它將指向運行時決定的,你的靜態數據(static data) 區域的一個位置。這意味着,利用gp作基指針,在gp指針32K左右的數據存取,系統只需要一條指令就可完成。如果沒有全局指針,存取一個靜態數據區域的 值需要兩條指令:一條是獲取有編譯器和loader決定好的32位的地址常量。另外一條是對數據的真正存取。爲了使用gp, 編譯器在編譯時刻必須知道一個數據是否在gp的64K範圍之內。通常這是不可能的,只能靠猜測。一般的做法是把small global data (小的全局數據)放在gp覆蓋的範圍內(比如一個變量是8字節或更小),並且讓linker報警如果小的全局數據仍然太大從而超過gp作爲一個基指針所能存取的範圍。

並不是所有的編譯和運行系統支持gp的使用。

*sp: 堆棧指針的上下需要顯示的通過指令來實現。因此MIPS通常只在子函數進入和退出的時刻才調整堆棧的指針。這通過被調用的子函數來實現。sp通常被調整到這個被調用的子函數需要的堆棧的最低的地方,從而編譯器可以通過相對於sp的偏移量來存取堆棧上的堆棧變量。詳細可參閱10.1節堆棧使用。

* fp: fp的另外的約定名是s8。如果子函數想要在運行時動態擴展堆棧大小,fp作爲楨指針可以被子函數用來記錄堆棧的情況。一些編程語言顯示的支持這一點。彙編編程員經常會利用fp的這個用法。C語言的庫函數alloca()就是利用了fp來動態調整堆棧的。

如果堆棧的底部在編譯時刻不能被決定,你就不能通過sp來存取堆棧變量,因此fp被初始化爲一個相對與該函數堆棧的一個常量的位置。這種用法對其他函數是不可見的。

* ra: 當調用任何一個子函數時,返回地址存放在ra寄存器中,因此通常一個子程序的最後一個指令是jr ra.

子函數如果還要調用其他的子函數,必須保存ra的值,通常通過堆棧。

對於浮點寄存器的用法,也有一個相應的標準的約定。在這裏,我們已經介紹了MIPS引入的寄存

 

1、 MIPS指令集的確很RISC,數據類的僅有load、store和move,當然按操作數的長短分許多lw、lh等等,但實際上就這三個。運算類的也僅 僅完成基本功能,也根據操作數長短分了許多子指令。跳轉類更少,要麼無條件跳轉,要麼根據操作數跳轉。這些指令確實屬於最常用的80%的。相比Intel 的LEA等指令,由於個人習慣,很少用,而AAD、AAA等指令,我幾乎沒用過。

2、MIPS指令較少,但彙編器爲了方便使用,定義了許多 僞指令,如li、ror等。最終會被擴展成多條實際指令。這樣一來,好處就是能省力,但壞處就是對彙編器要求較高,而且對機器指令反彙編後難以還原爲僞指 令(反彙編器面對lui $at, 0xABCD和ori r, $at, 0xEF00似乎不能自作主張的將其視作li, r, 0xABCDEF00);反彙編出來的指令條數多,不利於hack(或許又是好事)。

3、MIPS的尋址方式最簡單,僅有寄存器加偏移尋址方式(內嵌16位立即數尋址不算在內),這對於飽受Intel的八種尋址方式折磨的人來說是天大的好事。

4、MIPS沒有棧操作指令,雖然有約定俗稱的$sp。在做遞歸調用時必須手工管理棧,調用子程序時沒有自動壓棧的call指令,只能用jal。這對於用慣了intel的PUSH和POP的人又會是一場噩夢。

5、M
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章