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的值,通常通過堆棧。

對於浮點寄存器的用法,也有一個相應的標准的約定。我們將在7.5節。在這里,我們已經介紹了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、MIPS的內存映射、中斷等功能都做到了協處理器0中,浮點運算做到了協處理器1 中。

6、MIPS寄存器非常多,對於表達式求值很有利,不過調度算法就復雜了。而且寄存器雖然有約定俗成的用法,但實際上並沒有限制。

7、MIPS指令為定長的,很統一,給我的“感覺”非常好。

    最終,個人體會,在MIPS體系下思考又是另一種感覺,由於棧是全手工管理,就不用考慮push、pop是否匹配以及操作數大小,但手工管理棧要求頭腦非 常清晰;由於寄存器多了,就更多的考慮寄存器調度,如何發揮出所有寄存器的潛力;也不用去費心思選擇尋址方式。MIPS在寄存器使用、棧、存儲方面提供了 更高的靈活性,設計程序可以更加自由,但同時也增大了交流、學習的難度,這點與Intel嚴格的體系結構完全相反。

    從MIPS的特性看來,由於MIPS指令集簡單,容易設計和實現,尺寸可以做小,因此MIPS的方向除了嵌入式外,應該是多核心,提高並行度,主要面向並 發性高的應用,如服務器。而在桌面應用方面,目前沒有x86的優勢明顯。速度是一方面,MIPS的應用少,指令集太精簡、對程序員的友好程度不夠好也是一 個原因。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM