1. 程式人生 > >MIPS 通用暫存器 + 指令

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. load/store
  la $t0, val_1 複製val_1表示的地址到t0暫存器中     注: val_1是個Label
 lw $t2, ($t0) t0暫存器中的值作為地址,把這個地址起始的Word 複製到t2 中
 lw $t2, 4($t0) t0暫存器中的值作為地址, 把這個地址再加上偏移量4後 所起始的Word 複製到t2 中
 sw $t2, ($t0) 把t2暫存器中值(1 Word),儲存到t0的值所指向的RAM中
 sw $t2, -12($t0) 把t2暫存器中值(1 Word),儲存到t0的值再減去偏移量12, 所指向的RAM 中

2. 算數運算指令
  算數運算指令的所有運算元都是暫存器,不能直接使用RAM地址或間接定址。
  運算元的大小都為 Word (4-Byte)
  指令格式與例項 註釋
  move $t5, $t1       // $t5 = $t1;
  add $t0, $t1,       // $t2 $t0 = $t1 + $t2; 帶符號數相加
  sub $t0, $t1,       // $t2 $t0 = $t1 - $t2; 帶符號數相減
  addi $t0, $t1, 5    // $t0 = $t1 + 5;
  addu $t0, $t1, $t2  // $t0 = $t1 + $t2; 無符號數相加
  subu $t0, $t1, $t2  // $t0 = $t1 - $t2; 無符號數相減
  mult $t3, $t4       // $t3 * $t4, 把64-Bits 的積,儲存到Lo,Hi中。即: (Hi, Lo) = $t3 * $t4;
  div $t5, $t6        // Lo = $t5 / $t6 (Lo為商的整數部分); Hi = $t5 mod $t6 (Hi為餘數)
  mfhi $t0            // $t0 = Hi
  mflo $t1            // $t1 = Lo

3. 分支跳轉指令
 分支指令格式與例項 註釋
  b target 無條件的分支跳轉,將跳轉到target 標籤處
  beq $t0, $t1, target       // 如果 $t0 == $t1, 則跳轉到target 標籤處
  blt $t0, $t1, target       // 如果 $t0 < $t1,  則跳轉到target 標籤處
  ble $t0, $t1, target       // 如果 $t0 <=$t1,  則跳轉到target 標籤處
  bgt $t0, $t1, target       // 如果 $t0 > $t1,  則跳轉到target 標籤處
  bge $t0, $t1, target       // 如果 $t0 >= $t1, 則跳轉到target 標籤處
  bne $t0, $t1, target       // 如果 $t0 != $t1, 則跳轉到target 標籤處

4. 跳轉指令
 指令格式與例項 註釋
  j target          // 無條件的跳轉, 將跳轉到target 標籤處
  jr $t3            // 跳轉到t3暫存器所指向的地址處(Jump Register)

5. 子函式呼叫指令
 指令格式與例項 註釋
  jal sub_routine_label 執行步驟:
  a. 複製當前的PC(Program Counter)到$ra暫存器中。 因為當前的PC 值就是子函式執行完畢後的返回
       地址。
  b. 程式跳轉到子程式標籤sub_routine_label處。  
  注:子函式的返回,使用 jr $ra  
  如果子函式內又呼叫了其他的子函式,那麼$ra的值應該被儲存到堆疊中。 因為$ra的值總是對應著當前執
    行的子函式的返回地址。