計算機指令
算術運算指令
add a, b, c# a = b + c
設計原則一——對指令進行規整化設置
- 簡化實現
- 獲得更高的性能,更低的成本
代碼示例
- C語言代碼
f = (g + h) - (i + j)
- MIPS
add t0, g, h # temp : t0 = g + h
add t1, i, j # temp : t1 = i + j
sub f, t0, t1 #final: f = t0 - t1
參與算術邏輯運算的變量必須是寄存器變量,對於MIPS(x 32)指令集來說,有32個寄存器,每個寄存器長,它們:
- 用於存儲最常用到的變量
- 每個寄存器的編號爲0~31
- 每個寄存器中32位數據稱作一個字
約定符號:
- $t0,$t1,…,$t9 用於表示臨時變量
- $s0,$s1,…,$s 7表示需要保存的變量
設計原則二——更小就會更快
- 傾向於將算術運算放到寄存器中運行,以加快速度
- 內存的存儲單元通常是百萬級的
前面的代碼,根據約定符號進行矯正:
add $t0, $s1, $s2
add $t1, $s3, $s4
sub $s0, $t0, $t1
可以看到,在指令集中,只有個存儲空間,這並不足以支持所有運算,所以很多的數據是存儲在內存中的,我們將這些被儲存在內存中的數據稱爲_Memory Operands。內存的操作數是用來保存複雜的操作數,這是由寄存器的空間過小導致的。
算數邏輯運算不能直接對內存中的數據進行運算,這就需要我們先從內存中讀取(load)數據,然後再進行處理,最終再將操作的結果寫入(write)內存。在對內存的操作中,都是通過字節尋址的(每八位分配一個地址)。值得注意的是,每個字節由組成,而寄存器中每一位長度是,這就表明,我們在知道首地址的情況下,想要尋找第個元素時,需要將偏移量。
MIPS中數據的對齊方式是:大端對齊。什麼是大端對齊呢?
- 存儲一個數據,需要四個字節,這也就對應着內存上的四個存儲單元。
- 這四個存儲單元有他們相應的標號
- 高內存地址放整數的低位,低內存地址放整數的高位,這種方式叫正着放,術語叫大端對齊
- 與大端對齊相反的是小端對齊,說白了就是反着存
Memory Operand 小例子
C code
g = h + A[8]
其中,$s1 is g,$s2 is h,$s3 is the base address of A
MIPS code
lw $t0, 32($s3) # 讀取A[8],32來源於偏移量*4 = 8*4 = 32
add $s1, $s2, $t0
Memory Operand 小例子2
C code
A[12] = h + A[8]
其中,$s2 is h,$s3 is the base address of A
MIPS code
lw $t0, 32($s3)
add $t0, $s2, $t0
sw $t0, 48($s3)#寫入
可以看出,寄存器和內存的交互主要通過 ,兩條語句,進行數據的讀取、寫入,對於編譯器而言,選擇哪些變量放到寄存器中,哪些放到內存中,是非常關鍵的問題,同時也是非常困難的問題。
立即數
所謂立即數,即所使用的變量是一個常數,他是包含在指令中的。如:
addi $s3, $s3, 4
這就相當於C語言中的
s3 += 4;
立即數操作中沒有減法,因爲減掉一個數就相當於加上這個數的相反數,即:
addi $s3, $s3, -4
就可以完成C語言中如下功能
s3 -= 4;
**支持的數字範圍:**立即數操作的常亮僅支持有符號的16位整數,即:這個區間。如果需要一個32位整數,那麼這個整數就只能被先放到內存中,然後再被寄存器讀取。
這裏就引出了第三個設計原則:加快高概率事件(Make the Common Case Fast)
-
小的常數是常用的
-
使用常用的數,不需要從內存中讀取
數字的類型
-
無符號整數(unsigned int)
- 用於表示地址
- 或是非負數
-
有符號整數
- 正負數
-
浮點數
- 單精度浮點數(float)
- 雙精度浮點數(double)
有符號數的表示
- 補碼
- 源碼
補碼和源碼之間有+0和-0的區別。
MIPS中特殊的取值——0
MIPS中0號寄存器永遠爲0,只能讀取,不能寫入。我們常用$zero來代表上述的零號計算器,利用這個寄存器,我們可以進行下面的操作:
add $t2, $s1, $zero
addi $t2, $zero, 100
事實上,MIPS中是沒有初始化的方法的,在上面的代碼中,我們用$s1的值初始化了$t2,然後將100賦值給$t2,這種語法的設計減少了很多冗餘的語句。
有符號數和無符號數的表示
無符號二進制數計算公式:
這個老掉牙了,沒啥好說的,一個長度爲n的無符號二進制數,能表示這個區間內的整數。
有符號數的表示:
- 源碼的表示:最高爲作爲符號位
- 0代表符號爲正
- 1代表符號爲負
- 源碼錶示存在的問題:同時存在正0和負0
補碼的表示:這一部分我之前看的時候基本上都是硬背的,現在聽到這種講法才恍然大悟(菜是原罪)
假設補碼錶示的二進制數有n位,標號爲,那麼補碼到十進制數的計算公式爲:
它對應的取值範圍是:
在整數運算中,大多數情況下,使用的是補碼的形式。下面有一些特殊的補碼數字(幫助理解用的,不用想哪裏特殊):
十進制 | 二進制 |
---|---|
0 | 0000 0000 … 0000 |
-1 | 1111 1111 … 1111 |
最小的數 | 1000 0000 … 0000 |
最大的數 | 0111 1111 … 1111 |
一般情況下,MIPS指令集下的運算都是對有符號數進行運算,除非你顯式的告訴計算機要進行無符號數運算,需使用addu操作。
常見操作:
- 將數字取反:將數字的每一位取反,再對最後一位加1。
- 優點:可以用已有的加法電路,做簡單的拓展即可作爲減法電路使用。
- 符號位擴展:
- 應用場景:如:使用指令時,得到的是一個16位數字,要將該數字拓展爲32位數字,纔可以與其他數字進行算術運算。
- 無符號數的拓展方法:
- 直接補零
- 有符號數的拓展方法:
- 正數:在數字前方補0
- 負數:在數字前方補1
指令的表示
還記得我們第一章的時候講過,彙編語言多數時間執行着將高級語言翻譯成機器語言(machine code) 工作。下面是一些寄存器名稱和用途的的對應表:
其中,1號寄存器被稱爲$at,它是爲彙編程序預留的;26-27號寄存器被稱爲$k0,$k1,他們是保留給操作系統的。
指令集分爲六大類:
- 算數邏輯運算
- 內存訪問
- 分支和跳轉指令
- 浮點運算指令 --使用協處理器完成
- 內存管理指令
- 特殊指令
0-31號寄存器是我們(程序員)能夠訪問的到的寄存器,還有我們訪問不到的寄存器:
- PC:用於存放當前程序正在執行的指令
- HI & LO:在乘除法運算時做臨時儲存
mips32中指令集架構的格式
R format
主要用途:用於表示算數邏輯運算,分爲:
他們分別代表:
- op:用於表示當前的操作類型
- rs,rt:表示用於運算的兩個源操作數,對應寄存器的編號
- rd:目標操作數,對應寄存器的編號
- shamt:針對移位運算,記錄移位的次數
- funct:是opCode的拓展
例子:
add $t0, $s1, $s2
對應的指令爲:
special | $s1 | $s2 | $t0 | 0 | add |
---|---|---|---|---|---|
0 | 17 | 18 | 8 | 0 | 32 |
000000 | 10001 | 10010 | 01000 | 00000 | 100000 |
I format
I-format可以用於之前的操作、或是條件跳轉等指令。
其中
- rs/rt :源操作或目標操作寄存器
- 在進行運算時:補碼形式的二進制數
- 在進行尋址時:表示內存地址
設計規則四——Good design demands good compromises
這種設計方法看似折中、不利於譯碼,但將長度固定在了32位,爲了統一,增加了一部分譯碼的複雜性,達到了整體的統一,同時,這種設計方法,降低了後期電路設計的複雜性。
J format
後面再講
馮諾依曼計算機設計的兩個關鍵特性
1.指令和數據都是二進制串,無法區分
2.程序時能夠被改寫的
存儲程序相關的基本概念:
-
程序可以被保存爲二進制文件,這樣的特性使得一個程序可以從一個電腦搬到另一個電腦上使用,這一個叫做“二進制的兼容性”(deepin-wine)
-
爲了保證對已經編譯好了的軟件的繼承,指令集架構應當圍繞着少數幾個大的指令集架構發展。
邏輯運算指令
一些對應的語言:
移位運算(Shift Operations)
special | rs | rt | rd | shamt | funct |
---|---|---|---|---|---|
6 bits | 5 bits | 5 bits | 5 bits | 5 bits | 6 bits |
C語言中
$s1 = $s2 << 10;
$s1 = $s2 >> 10;
與MIPS中:
sll $1,$2,10
srl $1,$2,10
相同
其中: shamt-用於記錄移動多少位
-
邏輯左移運算:
- 左移,並將空出來的部分用零填充
- 左移位,相當於乘上
-
邏輯右移運算:
NOR運算
NOR就是not,or,這是一個三目運算符,使用方法如下:
nor $t0, $t1, $t2
這個運算表示
MIPS中沒有單獨的取反操作,如果想要對$t1取反存入到$t0中,方法如下:
nor $t0, $t1, $zero
在上述代碼中,進行的是的操作,其中,任何一個數和零進行或運算,還是他本身,再對它本身取反就可以得到的結果。
條件指令(分支與循環)
常見的控制指令:
beq re, rt, L1
bne rs, rt, L1
j L1
他們對應的C語言語法是:
#beq re, rt, L1
if(re == rt){
goto L1;
}
#bne rs, rt, L1
if(re != rt){
goto L1;
}
#j L1,無條件跳轉
goto L1;
跳轉的目標指令,與當前的beq/bne之間,不能超過正負,同時,在寫C和C++的時候除特殊情況外應當避免/減少goto語句的使用,因爲使用goto語句不當可能會讓程序表意不明,更加混亂。
一個小例子(選擇結構)
老規矩,先看C/C++
if(i==j){
f = g + h;
}else {
f = g - h;
}
對應的MIPS
#s3 is i, s4 is j, s0 is f, s1 is g, s2 is h.
bne $s3, $s4, Else#判斷i,j是否相等,若不等就直接跳過下面兩行
add $s0, $s1, $s2#若相等,就繼續執行到這一行,完成f=g+h
j Exit#執行完f = g - h之後跳過下一條語句直接退出
Else: sub $s0, $s1, $s2#執行f = g - h
Exit:
從上面的代碼中可以看出,MIPS的標記本身並沒有改變代碼的執行順序,僅僅是對某行代碼做了標記而已,如果沒有三條語句的話,就算程序中有標記,MIPS還是會順序執行的。
第二個例子(循環結構)
Ccode
while(save[i] == k) i += 1;
MIPS Code
#i is $s3, k is $s5, 地址被保存在$s6中
Loop: sll $t1, $s3, 2 #偏移量 = i * 4
add $t1, $t1, $s6# 當前地址= 起始地址 + 偏移量
lw $t0, 0($t1)#讀取數據到t0
bne $t0, $s5, Exit
addi $s3, $s3, 1# s3 = s3 + 1
j Loop
Exit:
沒啥好講的了,自行體會。
更多條件指令
MIPS Code
slt rd, rs, rt
slti rt, rs, constant
C Code
#slt rd, rs, rt
if(rs < rt ){
rd = 1;
}else {
rd = 0;
}
#slti rt, rs, constant
if(re < constant){
rd = 1;
}else {
rd = 0;
}
這個語句可以和beq一起用,來進行在大於、小於等情況下的選擇結構。
小練習
if(i>=j){
i = i + 1;
}else {
j = j + 1;
}
將上述代碼轉化成mips指令集中的code,i,j–>$s1,$s2
我的答案:
slt $t0, i, j # if i>=j 0
beq $t0, $zero, Else #if t0==zero --> i>=j goto Else:j=j+1,else go on
addi $s1, $s1, 1
j Exit
Else: addi $s2, $s2, 1
Exit: ...
有符號和無符號數字的比較
無符號數進行比較:
sltu,sltui,其實說白了就是後面加個u就是無符號數操作,再加個i就是常數操作。
函數的調用和函數的返回
基本塊,是一個指令序列,在這個序列中是沒有分支指令的,也沒有其他分支指令的跳轉指令。
函數調用的基本步驟:
- 將函數相關的參數放入寄存器中(load 指令或者是邏輯運算指令)
- 將寄存器的控制權交給函數相關的進程。關鍵
- 申請,並獲得存儲空間(堆棧)
- 執行函數中的指令
- 將返回的結果放回寄存器中關鍵
- 將結果返回到調用的地址中
jal ProcedureLabel
將寄存器控制權交給相關的進程:
將ProcedureLabel的下一個地址放到$ra中,然後跳轉到目標地址。(這裏$ra用於記錄返回值返回到哪裏)
jr $ra
將$ra複製到程序計數器中,這條語句也可以用於其他的跳轉用途。
這段他講的好抽象,我有點沒聽懂。
下面有一個實例。
Leaf Procedure Example(不調用其它函數的函數)
C Code
int leaf_example(int g, int h, int i, int j){
int f;
f = (g + h) - (i + j);
return f;
}
我就直接貼最後的代碼了:
leaf_example:
addi $sp, $sp, -4#壓棧,往下壓四個字節
sw $s0, 0($sp)#將s0放入壓好的棧中
add $t0, $a0, $a1
add $t1, $a2, $a3
sub $s0, $t0, $t1
add $v0, $s0, $zero#將結果放入用於返回的參數$v0中
lw $s0, 0($sp)#恢復$s0
addi $sp, $sp, 4#恢復堆棧位置
jr $ra#返回
說實話,除了jr都看懂了,那個確實沒看懂。
None-Leaf Procedures(函數的嵌套調用)
調用者需要將一些信息存儲到堆棧中:
- 被使用的寄存器($s0-$s7)
- 他的返回地址
- 在函數、過程中用到的臨時變量
在返回之前,將堆棧中數據取出進行恢復。
if fact(int n){
if(n < 1){
return 1;
}
return n*fact(n-1);
}
對應的mips,參數n放到$a0中,結果放到$v0
fact:
addi $sp, $sp, -8
sw $ra, 4($sp)
sw $a0, 0($sp)
slti $t0, $a0, 1#判斷是否小於1
beq $t0, $zero, L1#如果大於等於1,那麼跳到後面去
addi $v0, $zero, 1#如果小於1,那麼將1付給v0
addi $sp, $sp, 8#從棧空間中出棧兩個
jr $ra#返回
L1:
addi $a0, $a0, -1#減一
jal fact# 從新跳轉到fact部分去執行
lw $a0, 0($sp)#將堆棧中把兩個參數取出
lw $ra, 4($sp)
addi $sp, $sp, 8#恢復堆棧
mul $v0, $a0, $v0#相乘
jr $ra#返回
棧上的變量
堆棧用於存儲臨時變量。
圖中$sp指向堆棧可用地址,申請棧的時候,向下申請。$fp的存在使得便於恢復棧。
內存中一個程序分爲不同的段:
- 代碼段:用於存放指令
- 靜態數據:執行過程中不會變的數據
- 堆:C語言中new出來的東東,需要手動釋放
- 棧:臨時變量
思考:
- 遞歸能否轉循環
- 答案是可以的,就直接模擬它的棧