計算機組成與設計(2)-----指令_2
思維導圖
一.計算機硬體對函式的支援
函式是通過指定的輸入引數根據一定的處理方法從而得出輸出引數的過程.
1.1函式的執行過程
函式或者方法會有輸入引數,方法體,返回值等特徵.組合語言中為這些特徵設計了一些暫存器.
- $a0-$a3:用於儲存輸入引數
- $v0-$v1:用於儲存返回值
- $ra:儲存返回地址,具體地址是函式呼叫指令地址的下一個位元組的地址,是為了返回函式呼叫地址而存在.
- 程式計數器PC:用於儲存當前執行的指令地址,使用函式呼叫指令是,$ra其實就是被賦值為PC+4的值.
函式的呼叫也有特定的指令支援
- 連線跳轉指令jal:用於跳轉到某個函式所在的地址,他會將此時的PC+4的值賦給$ra,以便函式執行後返回.
- 強制跳轉指令jr:跳轉至指定的暫存器位置,一般用於jr $ra,標識函式執行後返回.
現在函式的執行過程就比較清晰了,在函式的呼叫處呼叫jal指令,然後執行過程,最後通過jr指令返回呼叫地址.
1.2暫存器原始值的儲存
- 棧指標$sp:儲存棧頂的棧幀的地址.
- 幀指標$fp:同$sp一起確定一個函式在棧中的位置,可以根據這個值獲取母函式的$sp及$fp,確定其界限,還可以儲存區域性變數.
圖是盜用的,幀指標就是$fp,棧指標就是$sp
當呼叫一個函式時,我們需要儲存其引數,返回值,返回地址.然而,當這個函式中又呼叫了其他的函式時,就沒有那麼多的暫存器以供使用了,所以,會使用一個先進後出的資料結構-----棧,儲存母函式的相關資料.
棧是先進後出的有序佇列.與專有的暫存器$sp配合.輸入資料成為入棧(push),入棧時的順序是高地址>低地址,輸出資料成為出棧(pop)順序則和入棧時相反.一般的儲存約定是不儲存臨時暫存器$t0-$t9,儲存保留暫存器$s0-$s7.
1.3巢狀函式例項
下面使用一個遞迴函式呼叫的例項來理解函式是如何被呼叫的.
//以C函式舉例
int fact (int n){
if(n < 1){
return 1;
}else{
return (n * fact(n - 1));
}
}
以下是編譯後的指令
//開始呼叫fact方法 fact: addi $sp,$sp,-8 //保留連個資料4 * 2,所以將棧指標的地址減去8個位元組 sw $ra,4($sp) //儲存返回地址 sw $a0,0($sp) //儲存引數n //判斷n是否小於1 slti $t0,$a0,1 //判斷n是否小於1 beq $t0,$zero,L1 //if n>=1 跳轉至L1 //如果n < 1 addi $v0,$zero,1 //儲存返回值 addi $sp,$sp,8 //出棧兩個元素 jr $ra //返回到函式呼叫地址 //如果n >= 1 L1:addi $a0,$a0,-1 //儲存n-1的值 jal fact //遞迴呼叫此方法 //遞迴呼叫函式後,返回地址,返回值,引數$a0都已經被覆蓋,需要通過棧獲取 lw $a0,0($sp) //恢復輸入引數值 lw $ra,4($sp) //恢復返回地址值 addi $sp,$sp,8 //棧的大小減去8個位元組,以恢復此時這個函式所對應的棧的位置 //進行返回 mul $v0,$a0,$v0 //儲存返回值 jr $ra //返回到函式呼叫地址
1.4在堆中為新資料分配空間
C程式需要在記憶體中對靜態變數和動態資料的分配提供空間.一般約定如下:
- 保留區:記憶體低位即地址從0開始的一部分是保留的.
- 正文:通常稱程式碼段.地址在保留區之上
- 靜態資料:存放靜態資料,配合$gp暫存器使用,地址在正文之上.
- 動態資料:可以稱為堆,存放動態的資料.方向向著棧的地址延伸,地址在靜態資料之上
- 棧:存在於位置最高處,方向向著動態資料取延伸,地址在動態資料之上,如果此兩者相互接觸,表示沒記憶體了,就會發生記憶體洩漏.
二.字串的存取
字元可能會使用不同的編碼方式(字符集)進行儲存,以便被轉化為二進位制格式.
- ASCII:每個字元佔用一個byte即8個bit(8位),所以採用lb,sb,lbu進行存取
- UTF-8:除了ASCII部分的子集是8bit外,其餘的佔用16個bit,即半個字,所以採用lh,lhu,sh進行存取
- UTF-16:不同於UTF-8,所有的字元都佔用16bit
- UTF-32:所有的字元佔用一個字即32bit
三.MIPS中32位立即數和定址
3.1 32位立即數
通過上篇指令的分享我們知道,指令是具有格式的,不會把32位地址都用來表示立即數,比如在addi中.那麼當需要32位立即數是應該怎麼做呢?會使用到兩個特殊的指令.
//如何載入32位的立即數0000 0000 0011 1101 0000 1001 0000 0000
//立即數高位指令,用於指定暫存器中前16位即高位的值
lui $s0,61 //61的二進位制表示是0000 0000 0011 1101,指定此暫存器的前16位
ori $s0,2304 //2304的二進位制表示是1101 0000 1001 0000 0000,指定此暫存器的後16位
3.2定址模式
在遇到的命令中,一些命令需要跳轉到其他地址繼續執行.而跳轉過程中不同命令跳轉地址的確定方式方法是不同的.
- 立即數定址:運算元是位於指令自身中的常數
- 暫存器定址:運算元是暫存器,比如jr
- 基址定址:運算元在記憶體中,其地址是指令中基址暫存器和常數的和,比如lw
- PC相對定址:地址是PC和指令中常數的和,比如所有的條件分支跳轉指令,beq,bnq等,一般來說,跳轉目的地址都接近於跳轉地址
- 偽直接定址:跳轉地址由指令中26位欄位和PC高4位相連而成,比如j.跳轉地址舉例太遠時使用,會將PC的高四位和運算元的28位作為第28位結合構成跳轉地址.
圖中箭頭指向的是定址的單位.一般用字即32bit定址,可以比半字或者位元組定址有更大的範圍.
四.並行與指令:同步
現在的電腦都是多核處理器,會存在資料競爭的情況,即存在兩個及其以上的執行緒同時對一個地址進行訪問或寫入請求,且其中至少有一個寫入請求.資料競爭會導致讀取資料出錯.而解決的辦法就是同步,採用加鎖和解鎖實現.對一個區域加鎖,則其他的執行緒就不可進行寫入操作,這個區域就稱為互斥區.
實現的原理則是假設存在一個單元表示某個鎖的狀態,1是被鎖狀態,0是自由狀態.通過一個讀取指令讓1和這個狀態值進行交換,如果交換後的值是1,則表示這個區域已經被加鎖.如果是0,則表示加鎖成功.然後在使用一個寫入指令寫入值,並返回一個狀態值,如果返回的狀態值為1,則表示寫入成功,以上操作是原子操作,如果返回的狀態值是0,則表示在此期間,有其他的執行緒對這個區域進行了寫入操作,原子操作失敗,再次重複以上操作.
原子操作例項:
//模擬進行一次原子操作,先讀取,在寫入
again:addi $t0,$zero,1 //設定暫存器的值為1
ll $t1,0($s1) //連線取數指令,會讀取$s1地址處的值,並標記此地址,如果在條件存數指令sc執
//行前有其他執行緒對此地址進行了寫入操作,sc指令就會失敗
sc $t0,0($s1) //條件存數指令,將$t0的值寫入$s1,並將$t0的值賦值為1或0,1表示成功,0表示失敗
beq $t0,$zero,again //如果存入失敗,表示原子操作失敗,重新執行.
所以在ll和sc指令之間的操作是一次原子性操作,但是,其中的指令需要慎重考慮.
五.翻譯並執行程式
不同語言被編譯的過程並不相同,下面是傳統的C語言和java語言的編譯過程.
5.1C語言的翻譯
- C語言的翻譯過程大致分為4部,並需要4個工具.
1.編譯器:將C語言轉化為組合語言
2.彙編器:將組合語言轉化為目標檔案,目標檔案含有機器語言指令和將資料和指令正確放入記憶體所需要的資訊.目標檔案一般含有:
- 目標檔案頭:描述大小和位置
- 程式碼段:機器語言程式碼
- 靜態資料段:生命週期內分配的資料
- 重定位資訊:標記了一些在程式載入如記憶體是依賴於絕對地址的指令和資料
- 符號表:未定義的剩餘標記,如外部引用
- 除錯資訊:包含一份說明目標檔案如何編譯的簡明描述,這樣,偵錯程式可以將機器指令關聯到C原始檔.
3.連結器:首先,每一個函式都是單獨編譯的,這樣如果函式改變則只需要重新編譯這一部分的函式即可,而不用重新編譯和彙編整個 程式.連結器的功能就是講獨立彙編的機器語言拼接在一起,他會尋找舊的地址,然後用新的地址代替他們.其工作步驟大 致分為3步:
- 將程式碼和資料模組象徵性的放入記憶體
- 決定資料和指令標籤的地址
- 修補內部和外部引用
4.載入器:連結器執行完畢後會生成一個可執行檔案.載入器會將其放入記憶體中以準備執行.
- java語言
編譯器會將java程式碼編譯成class檔案,java虛擬機器會讀取此class檔案以執行.JIT是及時編譯工具,他會將解釋的程式碼段翻譯成宿主計算機上的機器語言以提高效率.
最後,用一個氣泡排序程式作為完整的例子理解指令.
//首先,氣泡排序需要一個子函式,用以交換兩者的值
C程式碼:
void swap(int v[],int k){
int temp;
temp = v[k];
v[k] = v[k + 1];
v[k + 1] = temp;
}
//編譯後的程式碼
swap: sll $t1,$a1,2 //k*4獲取v[k]的位元組地址的偏移量
add $t1,$a0,$t1 //偏移量加上陣列地址獲取v[k]的地址
lw $t0,0($t1) //獲取v[k]的值
lw $t2,4($t1) //獲取v[k + 1]的值
sw $t2,0($t1) //存入v[k]的值
sw $t0,4($t1) //存入v[k + 1]的值
然後是氣泡排序的C程式碼
//氣泡排序的C程式碼
void sort (int v[],int n){
int i,j;
for(i = 0;i < n;i += 1){
for(j = i - 1;j >= 0 && v[j] > v[j + 1]; j += 1){
swap(v,j);
}
}
}
//編譯後的程式碼
//首先,儲存暫存器的值
sort:addi $sp,$sp,-20
sw $ra,16($sp)
sw $s3,12($sp)
sw $s2,8($sp)
sw $s1,4($sp)
sw $s0,0($sp)
//排序過程
//移動引數
move $s2,$a0 //獲取陣列地址v[]
move $s3,$a1 //獲取偏移量n
//迴圈外部
move $s0,$zero //$s0指定值為0,i值
for1tst:slt $t0,$s0,$s3 //第一層迴圈判定,i < n
beq $t0,$zero,exit1 //如果i >= n,則跳轉至exit1
//迴圈內部
addi $s1,$s0,-1 //j = i -1
for2tst:slti $t0,$s1,0 //第二程迴圈判定,j < 0
bne $t0,$zero,exit2 //如果j < 0,跳轉至exit2
sll $t1,$s1,2 //獲取偏移量
add $t2,$s2,$t1 //獲取v[j]的地址
lw $t2,0($t2) //獲取v[j]值
lw $t4,49$t2) //獲取v[j + 1]的值
slt $t0,$t4,$t3 //判斷v[j + 1] < v[j]
beq $t0,$zero,exit2 //如果v[j + 1] >= v[j],跳轉至exit2
//如果v[j + 1] < v[j],傳遞引數並呼叫swap函式
move $a0,$s2 //傳入陣列地址
move $a1,$s1 //傳入偏移量
jal swap //呼叫swap函式
//迴圈內部
addi $s1,$s1,-1 //j = j -1
j for2tst //返回第二層迴圈
//迴圈外部
addi $s0,$s0,1 //i = i +1
j for1tst //返回第一層迴圈
//恢復暫存器的值
lw $s0,0($sp)
lw $s1,4($sp)
lw $s2,8($sp)
lw $s3,12($sp)
lw $s4,16($sp)
addi $sp,$sp,20
//過程返回
jr $ra //返回函式呼叫地址