MIPS學習筆記(一)


寫在前面

  本文是根據"MIPS Assembly Language Programming CS50 Discussion and Project Book. Daniel J. Ellard"總結的。其中有大量的翻譯文體以及個人的看法想法,當然,內容沒有書上那么詳盡。

  這一章節會涉及MIPS的變量的聲明、數據的輸入輸出、取地址、分支跳轉語句(用以實行循環、判斷等),基本上對應於任何一門高級語言的最基本操作。

簡介

機器語言

  正如我們在前一章中所看到的,計算機指令可以表示為位序列。一般來說,這是程序可能的最低表示級別——每條指令都相當於CPU的單個不可分割的動作。這種表示被稱為機器語言,因為它是機器可以直接“理解”的唯一形式

匯編語言

  一個高一層的表示(而且對人類來說更容易使用)稱為匯編語言。匯編語言與機器語言有着非常密切的關系,通常有一種直接的方法將匯編語言編寫的程序翻譯成機器語言。(此算法通常由一個名為匯編程序(assembler)的程序實現。)

  因為機器和匯編語言關系很近,每個不同的機器體系結構通常都有自己的匯編語言(事實上,每個體系結構可能有幾個),並且每個都是唯一的。用 assember(而不是機器語言)編程的優勢在於,匯編語言更容易讓人閱讀和理解。

開始編程

在這一章節中不會正式介紹所有指令,只是為了熟悉匯編和部分算法

1. 初步了解匯編

涉及到的新指令/標簽

#

/**/

li

add

main

addi

syscall

正文

下面由一個add.asm為例開始了解匯編(語法結構等等),且暫時不嚴格要求指令之間的不同之處(后續會介紹具有類似功能的指令也會有一些差異)

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 補全程序

  上面兩條指令執行我們想要的計算,但它們並不構成一個完整的程序。與C類似,匯編語言程序必須包含一些附加信息,這些信息告訴匯編程序的開始結束位置。此信息的確切形式因匯編程序而異(請注意,對於給定的體系結構,可能有多個匯編程序,而對於MIPS體系結構,可能有多個匯編程序)。本教程假設 SPIM 被用作匯編和運行時環境。

(1)Label和main

  標簽(Label)是內存中地址的符號名稱。在MIPS程序集中,標簽是符號名(遵循與C符號名相同的約定),后跟冒號(colon)。

  使用不同形式的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

  注意,SPIM 匯編程序不允許將指令名用作標簽。因此,不允許使用名為add的標簽,因為存在同名指令。(當然,由於指令名都很短而且相當通用(li, lw等),因此它們通常不會生成非常描述性的標簽名。)

(2)syscall調用

  程序的結尾與C類似,在C中可以調用exit函數來停止程序的執行,停止MIPS程序的一種方法是使用類似於在C中調用exit的方法。

  但是,與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

syscall 10

 正文

   算法中我們還不知道該怎么做的部分是從用戶那里讀取數字,並打印出,這兩個操作都可以通過系統調用完成。

 2.1 讀取和打印整數

  在C中,我們如果要打印輸出一個數字:printf("%d",x);, 但是那么在 mips 是沒有這樣的一個print指令的,唯一一個可以用於輸出的指令是syscall,在特定條件下它是可以起到輸出的作用的。它的使用要配合其他的寄存器($v0),$v0中儲存不同的值在調用syscall時會有不同的作用

$v0 = 1, syscall -> print_in (output)

$v0 = 5, syscall -> read_in (into $v0, input)

$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

syscall 4

正文

$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

b endif, endif 為標簽名

正文

我們將編寫的下一個程序將探討在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

分支指令之一是 bgtbgt 指令有三個參數。前兩個是數字,最后一個是標簽。

如果第一個數字大於第二個數字,則應在標簽處繼續執行;

否則將在下一條指令處繼續執行。

另一方面,b 指令只是分支到給定的標簽。(無條件跳轉)

占位符所在位置的程序:

# 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)可以通過對下一章介紹的指令集的內容自行研究。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM