寫在前面
本文是根據"MIPS Assembly Language Programming CS50 Discussion and Project Book. Daniel J. Ellard"總結的。其中有大量的翻譯文體以及個人的看法想法,當然,內容沒有書上那么詳盡。
這一章節會涉及MIPS的變量的聲明、數據的輸入輸出、取地址、分支跳轉語句(用以實行循環、判斷等),基本上對應於任何一門高級語言的最基本操作。
簡介
機器語言
匯編語言
因為機器和匯編語言關系很近,每個不同的機器體系結構通常都有自己的匯編語言(事實上,每個體系結構可能有幾個),並且每個都是唯一的。用 assember
(而不是機器語言)編程的優勢在於,匯編語言更容易讓人閱讀和理解。
開始編程
在這一章節中不會正式介紹所有指令,只是為了熟悉匯編和部分算法
1. 初步了解匯編
涉及到的新指令/標簽
#
/**/
li
add
main
addi
syscall
正文
1.1 注釋
在開始編寫程序的可執行語句之前,我們需要編寫一個描述程序應該做什么的注釋。
#和/**/都可以寫注釋
1.2 尋找正確的指令
由於MIPS體系結構的指令相對較少,很快你就會記住你需要的所有指令,但是隨着你開始,你需要花一些時間瀏覽指令列表,尋找那些你可以用來做你想做的事情的指令。這些可以在第二部分找到。
- 加法運算需要三個操作數(operant)
li
(load immediate value):將32位常量(32-bit constant)放入指定寄存器中
# add.asm # begin of add.asm li $t1,1 #load 1 into register $t1 add $t0,$t1,2 #$t0 = $t1 + 2
1.3 補全程序
上面
(1)Label和main
使用不同形式的Label可以很好地將指令分類,在執行程序之前,我們需要告訴匯編程序從哪里開始。在SPIM
中,程序的執行從帶有標簽main的位置開始。
內存中的一個位置可能有多個標簽。因此,為了告訴 SPIM
應該將label main分配給程序的第一條指令。
# add.asm # begin of add.asm main: li $t1,1 #load 1 into register $t1 add $t0,$t1,2 #$t0 = $t1 + 2 # end of add.asm
(2)syscall調用
程序的結尾與C類似,在C中可以調用exit函數來停止程序的執行,停止MIPS程序的一種方法是使用類似於在C中調用exit的方法。
使用 syscall
告訴 SPIM
它應該停止執行的程序,以及執行許多其他有用的事情的方法。syscall
指令暫停程序的執行,並將控制權傳輸到操作系統。然后,操作系統查看寄存器$v0的內容,以確定程序要求它執行的操作。
這通過在執行syscall指令之前將10(exit syscall的編號)放入 $v0
來完成的
# add.asm # begin of add.asm main: li $t1,1 #load 1 into register $t1 add $t0,$t1,2 #$t0 = $t1 + 2 # exit li $v0, 10 syscall # end of add.asm
2. 了解syscall
新指令/標簽
.data
.text
move
syscall 1
syscall 5
正文
算法中我們還不知道該怎么做的部分是從用戶那里讀取數字,並打印出,這兩個操作都可以通過系統調用完成。
2.1 讀取和打印整數
在C中,我們如果要打印輸出一個數字:printf("%d",x);
, 但是那么在 mips
是沒有這樣的一個print指令的,唯一一個可以用於輸出的指令是syscall,在特定條件下它是可以起到輸出的作用的。它的使用要配合其他的寄存器($v0),$v0中儲存不同的值在調用syscall時會有不同的作用
$v0 = 1, syscall -> print_in (output)
$v0 = 10, syscall -> exit
(1)syscall 1
我們現在知道怎樣將syscall的作用定義為打印輸出(即完成了%d部分),但是我們要打印輸出哪個寄存器的內容呢(即找出需要被打印的x)?在mips中syscall只能打印$a0中的內容(無法指定被打印的寄存器),為此,我們需要將x(位於某個寄存器)的值存儲至$a0寄存器,供syscall調用和輸出。MIPS中有一個move指令,它將一個寄存器的內容復制到另一個寄存器中。
# print_in: 1 addi $t0, $zero, 1 move $a0,$t0 #不推薦使用li給$a0賦值,那樣就不是打印$t0的內容了,沒有意義 li $v0,1 # 將1存儲至$v0中,提示syscall的作用為打印整數 syscall
(2)syscall 5
# read and print Integer # $t0 -used to hold the first number # $t1 -used to hold the second number # $t2 -used to hold the sum of the $t1 and $t2 # $v0 -syscall paramenter and return value # $a0 -syscall parameter # start main: # get the first number from user, put into $to li $v0,5 #load syscall read_int(5 represents this) into $v0 syscall #make the syscall # 這之后在控制台輸入一個整數並回車,用戶輸入的數據將被存儲在$v0中 move $t0,$v0 #move the number(5) read into $t0 # get the second number from user, put into $t1 li $v0,5 #load syscall read_int(5 represents this) into $v0 syscall #make the syscall move $t1,$v0 #move the number(5) read into $t1 # sum, put into $t2 add $t2,$t0,$t1 # print out $t2 move $a0,$t2 # move the number to print into $a0 li $v0,1 # load syscall print_int into $v0 syscall # make the syscall # exit li $v0,10 # syscall code 10 is for exit syscall # end
2.2 hello world
新指令/標簽
la
.asciiz
.ascii
.byte
$v0 = 4, syscall -> print_string (output)
我們需要將被打印字符串 "hello world" 的地址放入$a0中,將$v0的值設為4,再進行syscall的調用輸出即可。接下來我們會學習如何定義一個(字符串)變量並使用它。
像是在C語言中一樣,MIPS中也可以對地址進行調用,而這里我們定義了變量后如果想要對這個變量調用可以通過訪問其變量名存儲的地址來實現。指令la可以獲取並存儲變量地址。
字符串“Hello World”不應該是程序的可執行部分(包含要執行的所有指令)的一部分,該部分稱為程序的文本段 (text segment
)。相反,字符串應該是程序使用的數據的一部分,按照慣例,它存儲在數據段中。MIPS匯編程序允許程序員通過使用幾個匯編程序指令來指定在程序中存儲每個項的段。
為了在數據段中放置一些東西,我們需要做的就是在定義它之前放置一個· .data
。.data
指令和下一個 .text
我們還需要知道如何定義和為空結束的字符串分配空間。在MIPS匯編程序中,這可以使用 .asciiz
(ASCII,以零結尾的字符串)指令來完成。對於以非空結尾的字符串,可以使用 .ascii
指令(directive)
.data hello_msg: .asciiz "hello world\n" # 這里\n依舊有效 .text main: la $a0, hello_msg # load the addr of hello_msg into register $a0 li $v0,4 syscall # exit li $vo,10 syscall
數據段中的數據被組裝到相鄰的位置。因此,有很多方法可以聲明字符串“Hello World\n”並獲得相同的准確輸出。嘗試各種方法的輸入輸出可以很快地熟悉這門語言。
# Method 1 .data hello_msg: .ascii "Hello" .ascii " " .ascii "World" .ascii "\n" .byte 0 # a 0 byte # Method 2 .data hello_msg: .byte 0x48 # hex for ASCII "H" .byte 0x65 # hex for ASCII "e" .byte 0x6C .byte 0x6C .byte 0x6F # ... # and so on .byte 0xA # hex for ASCII newline .byte 0x0 # hex for ASCII NULL
2.3 條件執行
新指令/標簽
bgt
正文
我們將編寫的下一個程序將探討在MIPS匯編語言中實現條件執行的問題。
下面我們先用占位符(placeholder comment)代表這個操作表示出該算法:
# start .text main: # Get first number from user, put into $t0. syscall # make the syscall. move $t0, $v0 # move the number read into $t0. # Get second number from user, put into $t1. li $v0, 5 # load syscall read_int into $v0. syscall # make the syscall. move $t1, $v0 # move the number read into $t1. # (placeholder comment) # put the larger of $t0 and $t1 into $t2. # Print out $t2. move $a0, $t2 # move the number to print into $a0. li $v0, 1 # load syscall print_int into $v0. syscall # make the syscall. # exit li $v0, 10 # syscall code 10 is for exit. syscall # make the syscall. # end
分支指令之一是 bgt
。bgt
指令有三個參數。前兩個是數字,最后一個是標簽。
如果第一個數字大於第二個數字,則應在標簽處繼續執行;
# bgt, branch greater than bgt $t0, $t1, t0_bigger # if &to>$t1, branch to t0_bigger move $t2, $t1 # else copy $t1 into $t2 b endif # and then branch to endif # b endif:一次判斷賦值后直接跳轉至結尾防止重復賦值 t0_bigger: move $t2, $t0 # copy $t0 into $t2 endif:
完整的程序:
# start .text main: # the first number from user/(console) li $v0,5 syscall move $t0,$v0 # the second number from user/(console) li $v0,5 #由於第一個數的獲取,此時的$v0可能不是5,需要重新賦值 syscall move $t1,$v0 # judge bgt $t0,$t1,int_greater # if move $a0,$t1 # else li $v0,1 # set $v0 syscall # print(make syscall) b endif # branch to end int_greater: move $a0,$t0 # ifSo-then li $v0,1 # set $v0 syscall # print(make syscall) endif: # exit li $v0,10 # set $v0 syscall # exit(make syscall) # end
至此,本次內容基本結束,一些更為復雜的數據結構(如數組的聲明)和算法(如對應於C語言中的switch)可以通過對下一章介紹的指令集的內容自行研究。