匯編語言是使用符號格式表示指令,可直接面向機器內存以及寄存器的程序設計語言。本部分主要學習內容包括:
1、以MIPS指令系統為例,學習並掌握MIPS指令集及匯編語言,能夠使用MIPS匯編語言編寫順序、分支、循環等具有特定功能的結構化程序,掌握函數調用及棧操作等匯編程序設計方法。該部分內容請參考《數字設計和計算機體系結構》第6章
MIPS體系結構的寄存器
-
MIPS體系結構定義了32個寄存器
-
通用寄存器標志由$符號開頭,寄存器表示有兩種方式
-
直接使用該寄存器對應的編號,例如:從$0到$31
-
使用對應的寄存器名稱,例如:$t0,$t1,……, $sp等
-
-
每個寄存器都有其特定的作用,有相應的規范,可參考《數字設計和計算機體系結構》第6章表6-1
MIPS匯編語句
-
MIPS匯編中一般有3類語句,通常是一個語句一行
(1)可執行指令(instruction),是處理器生成在運行時執行的機器碼,告訴處理器該做什么,一條可執行指令對應一條機器指令
(2)偽指令(Extended (pseudo) instruction)和宏,簡化編程人員的工作,由編譯器編譯成一條或多條可執行指令,常見的偽指令如move,la,li等(相應的偽指令在Mars中的Help中有解釋)
(3)匯編器指令(偽操作)(Directives),不會被編譯器編譯成可執行指令(或機器指令),會被翻譯成預處理指令,用於指示編譯器工作,用來定義段、分配內存變量等;匯編器指令不是指令集的一部分
-
MIPS匯編語言指令格式: [ 標簽:] 操作符 [ 操作數 ] [ #注釋 ]
-
標簽:(可選),標記內存地址,必須跟冒號。通常在數據和代碼段中出現
-
操作符:定義操作(比如 add,sub,sll等)
-
操作數:指明操作需要的數據;可以是寄存器、內存變量或常數(即立即數);大多數指令有3個操作數
-
#注釋:由 # 開頭在1行內結束,提高程序可讀性離不開它,使用它是一個好編程習慣的體現。
-
-
指令中出現的常量數值被稱為立即數(有的操作符對應的操作數是常數:addi,lw,sw等,具體可以瀏覽Mars中的Help)
-
load指令和store指令
由於MIPS只能對寄存器與立即數進行運算,因此必須有特定的數據傳輸指令實現主存單元與寄存器的數據交換
並且可以區分一下lw,lb,lh,sw,sb,sh的區別
-
-
LOAD類指令:主存單元→寄存器
-
STORE類指令:寄存器→主存單元
-
-
跳轉指令(用來實現判斷以及循環)
-
-
b類型指令就是通過相應的寄存器判斷是否符合跳轉條件,符合就跳轉到相應的標志上去(后文有講標志)
例:beq $s0,$t0,loop(更多b類指令請瀏覽Help)
-
-
-
j類型指令只能單獨跳轉到相應的標志位,且跳轉范圍不如jar,jalr等指令
loop:
xxxx
xxxxx
j loop
-
這里的跳轉需要注意$ra寄存器,他會記錄每一次跳轉之后的PC+4值。(這里的PC可以簡單理解為當前程序執行的代碼,+4就是下一條代碼的位置)所以在函數調用中用到了j類指令,注意維護$ra的值(jr $ra是返回記錄的相應位置)
-
-
-
循環的例子(可參考《數字設計和計算機體系結構》6.4.4節)
-
常見指令示例請參考《數字設計和計算機體系結構》第6章
MIPS匯編程序結構
匯編程序的結構一般包括數據段和代碼段
-
數據段以匯編器指令.data為開始標志
-
代碼段以匯編器指令.text為開始標志
基本程序模板
.data # 數據段,主要是數據變量聲明
...
.text # 代碼段
main: # 主程序入口
...
li $v0,10 #退出程序
syscall
可以注意到這段模板中main這個標志(lable)。在mips代碼段中,需要跳轉或函數調用的時候都需要標志的指示,來判斷相應跳轉的位置。
數據聲明(數據定義)
-
為變量的存儲划分內存
-
可能會有選擇的為數據分配名字(標簽)
-
-
語法
-
[名字:] 匯編器指令 初始值[,初始值]....
var1: .word 10
-
所有的初始值在內存中以二進制數據存儲
-
-
數據匯編器指令
-
.byte匯編器指令,以8位字節存儲數值表
-
.half 匯編器指令,以16位(半字長)存儲數值表
-
.word 匯編器指令,以32位(一個字長)存儲數值表
-
.word w:n匯編器指令,將32位數值w存入n個邊界對齊的連續的字中
-
.float匯編器指令,以單精度浮點數存儲數值表
-
.double 匯編器指令,以雙精度浮點數存儲數值表
-
-
字符串偽指令
-
.ascii 匯編器指令,為一個ASCII字符串分配字節序列
-
.asciiz 匯編器指令,與.ascii偽指令類似,但字符串以NULL結尾
-
.space n 匯編器指令,為數據段中n個未初始化的字節分配空間
-
字符串中的特殊字符(按照C語言的約定),“新行:\n,Tab:\t,引用:\”
-
數據定義的例子:
注:上圖應該為.byte .asciiz .word
-
匯編程序為標簽(變量)構建符號表
-
為數據段的每一個標簽計算地址
-
內存對齊和字節排序
-
對齊:地址是空間大小的整數倍
-
字的地址是4的整數倍
-
地址的2位最低有效位必須是00
-
-
半字的地址是2的整數倍
-
-
.align n匯編器指令,對下一個定義的數據做2^n字節對齊
-
字節序和端
-
處理器對一個字內的字節排序有兩種方法
-
小端字節排序
-
內存地址=最低有效字節的地址,例子:Intel IA-32,Alpha
-
-
大端字節排序
-
內存地址=最高有效字節的地址,例子:SPARC,PA-RISC
-
-
MIPS可以操作以上兩種字節序
-
函數調用(過程調用)
-
swap 過程(C程序)
-
翻譯成 MIPS 匯編語言
-
-
調用swap過程:swap(a,10)
-
將數組a的地址和10(第10個元素)作為參數傳遞
-
調用swap過程,保存返回地址 $31 = $ra
-
執行swap過程,返回對返回地址的控制
-
void swap(int v[] , int k)
{ int temp;
temp = v[k];
v[k] = v[k+1];
v[k+1] = temp;
} //C-code
swap:
sll $t0,$a1,2 # $t0 = k*4
add $t0,$t0,$a0 # $t0 = v+k*4
lw $t1,0($t0) # $t1 = v[k]
lw $t2,4($t0) # $t2 = v[k+1]
sw $t2,0($t0) # v[k] = $t2
sw $t1,4($t0) # v[k+1] = $t1
jr $ra # return
參數:
$a0 = v[]的地址
$a1 = k,
返回地址在$ra中
函數調用中兩個重要的指令jal和jr
-
JAL(Jump-and-Link):調用指令
-
寄存器$ra=$31被JAL用來保存返回地址($ra=PC+4)
-
通過偽直接尋址轉跳
-
-
JR(Jump Register):返回指令
-
跳轉到在寄存器Rs(PC=Rs)中存儲的地址所在指令
-
-
JALR(Jump-and-Link Register)
-
在Rd=PC+4中存儲返回地址
-
跳轉到在寄存器Rs(PC=Rs)中存儲的地址所在過程
-
用於調用方法(地址僅在運行時可知)
-
參數傳遞
-
匯編語言中的參數傳遞比高級語言中復雜
-
將所有需要的參數放置在一個可訪問的存儲區域
-
然后調用過程
-
-
會用到兩種類型的存儲區域
-
寄存器:使用通用寄存器(寄存器方法);內存:使用棧(棧方法)
-
-
參數傳遞的兩種常用機制
-
值傳遞:傳遞參數值:引用傳遞:傳遞參數的地址
-
按照約定,參數傳遞通過寄存器實現
$a0 =$4 .. $a3=$7用來做參數傳遞
$v0=$2. . $v1=$3用來表示結果數據
-
其它的參數/結果可以放在棧中
-
-
運行時棧用於
-
不適合使用寄存器時用來存儲變量/數據結構
-
過程調用中保存和恢復寄存器
-
實現遞歸
-
-
運行時棧通過軟件規范實現
-
棧指針 $sp = $29(指向棧頂):幀指針 $fp = $30(指向過程幀)
-
棧
-
由於寄存器只有32個,因此編譯器絕大多數情況下不可能把函數需要的所有局部變量都分配在寄存器
棧指針使用的順序是從上往下存儲。棧指針$sp一般在函數調用的時候會用到,在每次調用函數的時候,要將相應的$ra寄存器的值以及相應保存當前狀態的寄存器的值存入到棧中,在需要返回的時候,又將相應的值取出,進行返回。
請參考《數字設計和計算機體系結構》第6章 6.4.6節
遞歸
首先需要理解遞歸本身的性質或運行流程,在此基礎上進行mips編寫
如有疑問,也可瀏覽課程網站觀看快速排序講解視頻
-
遞歸流程:判斷是否終止遞歸、進入終止輸出並回溯 或 進入包含遞歸模塊的核心部分
-
遞歸模塊重點:遞歸終止條件、遞歸進入變量存儲、遞歸回溯
-
以實現下列C代碼的遞歸程序為例
-
$ra中存儲着jal指令的下一指令,即本次遞歸結束時的下一條指令地址
sw $ra, 0($sp) # 保存返回地址
addi $sp, $sp, -4 # 棧指針下移四字節
sw index, 0($sp) # 保存函數參數index
addi $sp, $sp, -4 # 棧指針下移四字節
sw i, 0($sp) # 保存函數參數i
addi $sp, $sp, -4 -
按照存儲順序的逆序將本遞歸的參數取回來繼續完成本次遞歸函數
sw $ra, 0($sp) # 保存返回地址
addi $sp, $sp, -4 # 棧指針下移四字節
sw index, 0($sp) # 保存函數參數index
addi $sp, $sp, -4 # 棧指針下移四字節
sw i, 0($sp) # 保存函數參數i
addi $sp, $sp, -4 # 棧指針下移四字節 -
遞歸回溯后的操作:
bne $t4, 8, nxt
li $s1, 0
nxt:
add $s2, $s1, $s0
sw $s2, fib_array($t4)
move $s1, $s0 #前前一個
move $s0, $s2 # $s0 前一個
jr $ra
-
系統調用
-
系統調用可以簡單的理解為發揮執行輸入,輸出,結束程序的作用(具體也瀏覽Help)
-
參數所使用的寄存器:$v0, $a0,$a1
-
返回值使用: $v0
詳細用法(部分):
Service | Code in $v0 |
---|---|
print integer | 1 |
print float | 2 |
print double | 3 |
print string | 4 |
read integer | 5 |
read float | 6 |
read double | 7 |
read string | 8 |
sbrk(allocate heap memory) | 9 |
exit(teminate execution) | 10 |
e.g. Print out integer value contained in register $t2
li $v0, 1
move $a0, $t2
syscall
e.g. Read integer value, store in RAM location with label int_value (presumably declared in data section)
li $v0, 5
syscall #輸入一個整數。並把整數值賦給$v0寄存器
e.g. Print out string (useful for prompts)
.data
string1 .asciiz "Print this.\n"
.text
li $v0, 4
la $a0, string1 #將要打印的字符串地址賦值 $a0 = address(string1)
syscall
e.g. To indicate end of program, use exit system call
li $v0, 10 # system call code for exit = 10
syscall
課下題目分析
最大公約數
-
考察程序基本結構是否理解(輸入,輸出,判斷條件)
-
考察通過用寄存器保存相應的值並進行計算
-
考察對於相關指令的使用
-
循環的建立
字符串逆置
-
考察對於字符串的讀寫(參考syscall里的用法)
-
難度較為簡單,不用想復雜了
漢諾塔
-
考察對於棧的了解
-
考察對於遞歸的理解
-
考察jal,jr,$ra以及$sp的理解與使用
-
難度較為復雜,不過在了解完遞歸后,會非常清晰