MIPS 彙編/MIPS 細節
MIPS 有 32 個通用暫存器和另外 32 個浮點暫存器。暫存器都以美元符號 ($) 開頭。浮點暫存器命名為 $f0、$f1、...、$f31。通用暫存器既有名稱又有編號,如下所示。在 MIPS 組合語言程式設計時,最好使用暫存器名稱。
| 編號 | 名稱 | 註釋 |
|---|---|---|
| $0 | $zero | 始終為零 |
| $1 | $at | 保留給彙編器 |
| $2, $3 | $v0, $v1 | 分別為第一個和第二個返回值 |
| $4, ..., $7 | $a0, ..., $a3 | 函式的前四個引數 |
| $8, ..., $15 | $t0, ..., $t7 | 臨時暫存器 |
| $16, ..., $23 | $s0, ..., $s7 | 儲存暫存器 |
| $24, $25 | $t8, $t9 | 更多臨時暫存器 |
| $26, $27 | $k0, $k1 | 保留給核心(作業系統) |
| $28 | $gp | 全域性指標 |
| $29 | $sp | 堆疊指標 |
| $30 | $fp | 幀指標 |
| $31 | $ra | 返回地址 |
一般來說,有很多暫存器可以在程式中使用:十個**臨時暫存器**和八個**儲存暫存器**,以及 arg $a 和返回值 $v 暫存器。臨時暫存器是通用暫存器,可以自由地用於算術和其他指令(稱為被覆蓋),而儲存暫存器必須在函式呼叫之間保持其值。(如果要使用一個儲存暫存器,則必須在過程進入時儲存它,並在過程退出時恢復它)。處理呼叫保留的 $s0..7 暫存器的最簡單方法是根本不碰它們。
臨時暫存器名稱都以 $t 開頭。例如,有 $t0、$t1 ... $t9。這意味著有 10 個臨時暫存器可以使用,而不用擔心儲存和恢復其內容。儲存暫存器名為 $s0 到 $s7。
**零暫存器**,名為 $zero ($0),是一個靜態暫存器:它始終包含值為零。此暫存器不能用作儲存操作的目標,因為它的值是硬連線的,不能由程式更改。
還有一些暫存器,大多數指令不能直接訪問。其中包括程式計數器 (PC),它儲存正在執行的指令的地址(由 JAL 讀取以計算返回地址,由跳轉和分支寫入),以及“hi”和“lo”暫存器,它們用於乘法和除法,其結果大於 32 位(乘法可能導致 64 位乘積,除法導致商和餘數)。有一些特殊指令用於將資料移入和移出 hi 和 lo 暫存器。
有 3 種指令格式:R 指令、I 指令、J 指令。
R 指令採用三個引數:兩個源暫存器(**rt** 和 **rs**)和一個目標暫存器(**rd**)。R 指令使用以下格式編寫
- 指令 rd, rs, rt
其中每一個代表如下
| rd | 目標暫存器說明符 |
| rs | 源暫存器說明符 |
| rt | 源/目標暫存器說明符 |
例如:
add $t0, $t1, $t2
將 $t1 和 $t2 的值相加,並將結果儲存在 $t0 中。
當彙編成機器碼時,R 指令表示如下
| 操作碼 | rs | rt | rd | shamt | func |
|---|---|---|---|---|---|
| 6 位 | 5 位 | 5 位 | 5 位 | 5 位 | 6 位 |
對於 R 格式指令,**操作碼**或“操作碼”始終為零。**rs**、**rt** 和 **rd** 分別對應於兩個源暫存器和一個目標暫存器。**shamt** 用於移位指令而不是 **rt**,以簡化硬體。在彙編中,要將 $t4 中的值左移兩位並將結果放入 $t5 中
sll $t5, $t4, 2
由於所有 R 格式指令的操作碼都是零,**func** 向硬體指定要執行的確切 R 格式指令。上面的 add 示例將編碼如下
opcode rs rt rd shamt funct 000000 01001 01010 01000 00000 100000
由於它是一個 R 格式指令,所以前六位(操作碼)為 0。接下來的 5 位對應於 rs,在本例中為 $t1。從上面的表格中,我們發現 $t1 是 $9,其二進位制表示為 01001。同樣,接下來的五位編碼為 $t2 = $10 = 01010。目標是 $t0 = $8 = 01000。我們沒有執行移位,所以 shamt 為 00000。最後,由於 add 指令的 func 為 100000。
對於上面的移位示例,操作碼欄位再次為 0,因為這是一個 R 格式指令。rs 欄位在移位中未使用,因此我們將接下來的五位保留為 0。rt 欄位為 $t4 = $12 = 01100。rd 欄位為 $t5 = $13 = 01101。移位量,shamt,為 2 = 00010。最後,sll 的 func 欄位為 000000。因此,sll $t5, $t4, 2 的編碼為
opcode rs rt rd shamt funct 000000 00000 01100 01101 00010 000000
I 指令採用兩個暫存器引數和一個 16 位“立即數”值。立即數是儲存為指令一部分而不是儲存在記憶體中的值。這使得訪問常量比將常量放在記憶體中然後載入它們快得多(因此得名)。I 格式指令與 R 格式指令一樣,首先指定目標暫存器(**rt**)。接下來是一個源暫存器(**rs**),最後是立即數。
- 指令 rt, rs, imm
例如,假設我們想要將值 5 新增到暫存器 $t1 中,並將結果儲存在 $t0 中
addi $t0, $t1, 5
或將比較和分支到附近的標籤(範圍為 16 位有符號位移,左移 2 位)。彙編器計算 `(目標 - 分支指令地址 + 4) >> 2` 作為立即數
beq $t0, $zero, t0_equals_zero_branch_target
I 格式指令在機器碼中表示如下
| 操作碼 | rs | rt | imm |
|---|---|---|---|
| 6 位 | 5 位 | 5 位 | 16 位 |
**操作碼**指定請求的操作。**rs** 和 **rt** 各為 5 位,與 R 格式指令相同,位置也相同。**imm** 欄位儲存立即數。根據指令的不同,立即數常量可以是符號擴充套件的或零擴充套件的。如果需要 32 位立即數,則存在一個特殊的指令 **lui**(“載入上部立即數”),用於將立即數載入到暫存器的上部 16 位中。然後,該暫存器可以與另一個 16 位立即數進行邏輯 或運算,以將最終值儲存在該暫存器中。然後,該值可以在普通的 R 格式指令中使用。以下指令序列將位模式 0101 0101 0101 ... 儲存到暫存器 $t0 中
lui $t0, 0x5555 ori $t0, $t0, 0x5555
通常,彙編器會自動以這種方式拆分 32 位常量,因此如果編寫 li $t0, 0x5555555,程式設計師就不必擔心。一些彙編器(如啟用了擴充套件偽指令模式的 MARS)甚至支援 addi / addiu[檢查拼寫] / xori / 等等的較大立即數,這樣。
上面的 addi 示例將編碼如下。addi 指令的操作碼為 001000。源暫存器 $t1 的編號為 9,即二進位制的 01001。目標暫存器 $t0 的編號為 8,即二進位制的 01000。5 在二進位制中為 101,因此機器碼中的 addi $t0, $t1, 5 為
opcode rs rt imm 001000 01001 01000 0000 0000 0000 0101
addi 和 addiu[檢查拼寫] 將其 16 位立即數符號擴充套件到 32 位(因此可以表示 -32768 .. 32767 的有符號二進位制補碼值,即 0..0x7fff 和 0xffff8000 .. 0xffffffff 的無符號值)
按位布林邏輯指令,如 andi、ori 和 xori,將立即數零擴充套件(因此可以使用 0..65535 的值)
使用 I 格式的其他指令包括載入/儲存(地址 = 暫存器 + 帶符號的 16 位位移)、立即比較(例如 slti $t1, $t0, 1234,在暫存器中生成 0 或 1)、lui 用於將立即數載入到暫存器的較高 16 位,以及分支(但不是跳轉)指令。
J 指令用於將程式流程轉移到 PC 暫存器周圍 256MB 區域內的給定硬編碼絕對地址。J 指令幾乎總是用標籤編寫:彙編器和連結器會將標籤轉換為數值。J 指令只接受一個引數:要跳轉到的地址。
(真正針對 PC 的相對控制轉移可以使用 'b label' 完成,'b label' 可用於位置無關程式碼。MIPS 分支指令是 I 格式指令,具有左移 2 位的 16 位相對位移,這與跳轉不同。)
指令 地址
有兩種 J 格式指令:j 和 jal。後者將在後面討論。j(“跳轉”)指令告訴處理器立即跳到 地址 指示的指令。例如,要跳轉到 label1
j label1
J 格式指令被編碼為
| 操作碼 | 地址 |
|---|---|
| 6 位 | 26 位 |
在 MIPS32 機器上,地址是 32 位寬的,因此 26 位可能不足以指定要跳轉到的指令。幸運的是,由於所有指令都是 32 位(4 個位元組)寬的,我們可以假設所有指令都從可被 4 整除的位元組地址開始(實際上,載入程式保證了這一點)。在二進位制中,可被 4 整除的數字以兩個零結尾(就像十進位制中可被 100 整除的數字總是以兩個零結尾)。因此,我們可以允許彙編器省略最後兩個零,並讓硬體重新插入它們。這實際上使地址欄位變成了 30 位。最後 4 位將從下一條指令的地址中借用,因此我們不能讓程式跨越 256MB 邊界,因為跨越邊界的跳轉將需要改變最高 4 位。(當 J 指令正在處理時,PC 已經更新為指向下一條指令,即分支延遲槽。)
在上面的示例中,如果 label1 指定了地址為 120 或二進位制的 1111000 的指令,我們可以用機器碼對上面的跳轉示例進行編碼。j 的操作碼為 2 或二進位制的 10,我們必須截斷跳轉地址的最後兩位,將其變為 11110。因此,j 120 的機器碼如下所示。
opcode |---------------addr-----------| 000010 0000 0000 0000 0000 0000 0111 10