博客 / 詳情

返回

Y 分鐘速成 MIPS Assembly

源代碼下載: MIPS-cn.asm

MIPS(Microprocessor without Interlocked Pipeline Stages)彙編語言是為了配合約翰·雷洛伊·亨尼西於1981年設計的 MIPS 微處理器範式而設計的,這些 RISC 處理器用於嵌入式系統,例如網關和路由器。

閲讀更多

# 註釋用一個 '#' 表示

# 一行中 '#' 之後的所有文本都會被彙編器的詞法分析器忽略

# 程序通常包含 .data 和 .text 部分

.data # 數據存儲在內存中(在RAM中分配)
      # 類似於高級語言中的變量

  # 聲明遵循( 標籤: .類型 值 )的聲明形式
  hello_world: .asciiz "Hello World\n"      # 聲明一個 null 結束的字符串
  num1: .word 42                            # 整數被視為字
                                            # (32位值)

  arr1: .word 1, 2, 3, 4, 5                 # 字數組
  arr2: .byte 'a', 'b'                      # 字符數組(每個1字節)
  buffer: .space 60                         # 在 RAM 中分配空間
                                            # (不清除為0)

  # 數據類型的大小
  _byte: .byte 'a'                          # 1字節
  _halfword: .half 53                       # 2字節
  _word: .word 3                            # 4字節
  _float: .float 3.14                       # 4字節
  _double: .double 7.0                      # 8字節

  .align 2                                  # 數據的內存對齊
                                            # 其中數字(應是2的冪)表示幾字節對齊
                                            # .align 2 表示字對齊(因為 2^2 = 4 字節)

.text                                       # 這部分包括指令和程序邏輯
.globl _main                                # 聲明一個全局指令標籤
                                            # 其他文件都可以訪問

  _main:                                    # MIPS 程序按順序執行指令
                                            # 這條標籤下的代碼將首先執行

    # 打印 "hello world"
    la $a0, hello_world                     # 加載存儲在內存中的字符串地址
    li $v0, 4                               # 加載 syscall 的值
                                            # (數字代表要執行哪個 syscall)
    syscall                                 # 使用給定的參數($a0)執行指定的 syscall

    # 寄存器(用於在程序執行期間保存數據)
    # $t0 - $t9                             # 臨時寄存器,用於過程內部的中間計算
                                            # (過程調用時不保存)

    # $s0 - $s7                             # 保留寄存器(被保留的寄存器,過程調用時保存)
                                            # 通常保存在棧中

    # $a0 - $a3                             # 參數寄存器,用於傳遞過程的參數
    # $v0 - $v1                             # 返回寄存器,用於向調用過程返回值

    # 存取指令
    la $t0, label                           # 將內存中由 label 指定的值的地址複製到寄存器 $t0 中
    lw $t0, label                           # 從內存中複製一個字
    lw $t1, 4($s0)                          # 從寄存器中存儲的地址複製一個字
                                            # 偏移量為4字節(地址 + 4)
    lb $t2, label                           # 把一個字節複製到寄存器 $t2 的低階部分
    lb $t2, 0($s0)                          # 從 $s0 的源地址複製一個字節
                                            # 偏移量為0
    # 同理也適用於 'lh' (取半字)

    sw $t0, label                           # 將字存儲到由 label 映射的內存地址中
    sw $t0, 8($s0)                          # 將字存儲到 $s0 指定的地址中
                                            # 偏移量為8字節
    # 同理也適用於 'sb' (存字)和 'sh' (存半字)。'sa'不存在

### 數學 ###
  _math:
    # 記住要將值加載到寄存器中
    lw $t0, num                             # 從數據部分
    li $t0, 5                               # 或者從一個立即數(常數)
    li $t1, 6
    add $t2, $t0, $t1                       # $t2 = $t0 + $t1
    sub $t2, $t0, $t1                       # $t2 = $t0 - $t1
    mul $t2, $t0, $t1                       # $t2 = $t0 * $t1
    div $t2, $t0, $t1                       # $t2 = $t0 / $t1
                                            # (MARS 的某些版本可能不支持)
    div $t0, $t1                            # 執行 $t0 / $t1。
                                            # 用 'mflo' 得商,用 'mfhi' 得餘數

    # 移位
    sll $t0, $t0, 2                         # 按位左移立即數(常數值)2
    sllv $t0, $t1, $t2                      # 根據一個寄存器中的變量值左移相應位
    srl $t0, $t0, 5                         # 按位右移
                                            # (不保留符號,用0符號擴展)
    srlv $t0, $t1, $t2                      # 根據一個寄存器中的變量值右移相應位
    sra $t0, $t0, 7                         # 按算術位右移(保留符號)
    srav $t0, $t1, $t2                      # 根據一個寄存器中的變量值右移相應算數位

    # 按位運算符
    and $t0, $t1, $t2                       # 按位與
    andi $t0, $t1, 0xFFF                    # 用立即數按位與
    or $t0, $t1, $t2                        # 按位或
    ori $t0, $t1, 0xFFF                     # 用立即數按位或
    xor $t0, $t1, $t2                       # 按位異或
    xori $t0, $t1, 0xFFF                    # 用立即數按位異或
    nor $t0, $t1, $t2                       # 按位或非

## 分支 ##
  _branching:
    # 分支指令的基本格式通常遵循 <指令> <寄存器1> <寄存器2> <標籤>
    # 如果給定的條件求值為真,則跳轉到標籤
    # 有時向後編寫條件邏輯更容易,如下面的簡單的 if 語句示例所示

    beq $t0, $t1, reg_eq                    # 如果 $t0 == $t1,則跳轉到 reg_eq
                                            # 否則執行下一行
    bne $t0, $t1, reg_neq                   # 當 $t0 != $t1 時跳轉
    b branch_target                         # 非條件分支,總會執行
    beqz $t0, req_eq_zero                   # 當 $t0 == 0 時跳轉
    bnez $t0, req_neq_zero                  # 當 $t0 != 0 時跳轉
    bgt $t0, $t1, t0_gt_t1                  # 當 $t0 > $t1 時跳轉
    bge $t0, $t1, t0_gte_t1                 # 當 $t0 >= $t1 時跳轉
    bgtz $t0, t0_gt0                        # 當 $t0 > 0 時跳轉
    blt $t0, $t1, t0_gt_t1                  # 當 $t0 < $t1 時跳轉
    ble $t0, $t1, t0_gte_t1                 # 當 $t0 <= $t1 時跳轉
    bltz $t0, t0_lt0                        # 當 $t0 < 0 時跳轉
    slt $s0, $t0, $t1                       # 當 $t0 < $t1 時結果為 $s0 (1為真)

    # 簡單的 if 語句
    # if (i == j)
    #     f = g + h;
    #  f = f - i;

    # 讓 $s0 = f, $s1 = g, $s2 = h, $s3 = i, $s4 = j
    bne $s3, $s4, L1 # if (i !=j)
    add $s0, $s1, $s2 # f = g + h

    L1:
      sub $s0, $s0, $s3 # f = f - i

    # 下面是一個求3個數的最大值的例子
    # 從 Java 到 MIPS 邏輯的直接翻譯:
    # if (a > b)
    #   if (a > c)
    #     max = a;
    #   else
    #     max = c;
    # else
    #     max = b;
    #   else
    #     max = c;

    # 讓 $s0 = a, $s1 = b, $s2 = c, $v0 = 返回寄存器
    ble $s0, $s1, a_LTE_b                   # 如果 (a <= b) 跳轉到 (a_LTE_b)
    ble $s0, $s2, max_C                     # 如果 (a > b && a <= c) 跳轉到 (max_C)
    move $v0, $s0                           # 否則 [a > b && a > c] max = a
    j done                                  # 跳轉到程序結束

    a_LTE_b:                                # 當 a <= b 時的標籤
      ble $s1, $s2, max_C                   # 如果 (a <= b && b <= c) 跳轉到 (max_C)
      move $v0, $s1                         # 如果 (a <= b && b > c) max = b
      j done                                # 跳轉到 done

    max_C:
      move $v0, $s2                         # max = c

    done:                                   # 程序結束

## 循環 ##
  _loops:
    # 循環的基本結構是一個退出條件和一個繼續執行的跳轉指令
    li $t0, 0
    while:
      bgt $t0, 10, end_while                # 當 $t0 小於 10,不停迭代
      addi $t0, $t0, 1                      # 累加值
      j while                               # 跳轉回循環開始
    end_while:

    # 二維矩陣遍歷
    # 假設 $a0 存儲整數 3 × 3 矩陣的地址
    li $t0, 0                               # 計數器 i
    li $t1, 0                               # 計數器 j
    matrix_row:
      bgt $t0, 3, matrix_row_end

      matrix_col:
        bgt $t1, 3, matrix_col_end

        # 執行一些東西

        addi $t1, $t1, 1                  # 累加列計數器
      matrix_col_end:

      # 執行一些東西

      addi $t0, $t0, 1
    matrix_row_end:

## 函數 ##
  _functions:
    # 函數是可調用的過程,可以接受參數並返回所有用標籤表示的值,如前所示

    main:                                 # 程序以 main 函數開始
      jal return_1                        # jal 會把當前程序計數器(PC)存儲在 $ra
                                          # 並跳轉到 return_1

      # 如果我們想傳入參數呢?
      # 首先,我們必須將形參傳遞給參數寄存器
      li $a0, 1
      li $a1, 2
      jal sum                             # 現在我們可以調用函數了

      # 遞歸怎麼樣?
      # 這需要更多的工作
      # 由於 jal 會自動覆蓋每次調用,我們需要確保在 $ra 中保存並恢復之前的程序計數器
      li $a0, 3
      jal fact

      li $v0, 10
      syscall

    # 這個函數返回1
    return_1:
      li $v0, 1                           # 將值取到返回寄存器 $v0 中
      jr $ra                              # 跳轉回原先的程序計數器繼續執行


    # 有2個參數的函數
    sum:
      add $v0, $a0, $a1
      jr $ra                              # 返回

    # 求階乘的遞歸函數
    fact:
      addi $sp, $sp, -8                   # 在棧中分配空間
      sw $s0, ($sp)                       # 存儲保存當前數字的寄存器
      sw $ra, 4($sp)                      # 存儲先前的程序計數器

      li $v0, 1                           # 初始化返回值
      beq $a0, 0, fact_done               # 如果參數為0則完成

      # 否則繼續遞歸
      move $s0, $a0                       # 複製 $a0 到 $s0
      sub $a0, $a0, 1
      jal fact

      mul $v0, $s0, $v0                   # 做乘法

      fact_done:
        lw $s0, ($sp)
        lw $ra, ($sp)                     # 恢復程序計數器
        addi $sp, $sp, 8

        jr $ra

## 宏 ##
  _macros:
    # 宏可以實現用單個標籤替換重複的代碼塊,這可以增強程序的可讀性
    # 它們絕不是函數的替代品
    # 它們必須在使用之前聲明

    # 用於打印換行符的宏(這可以被多次重用)
    .macro println()
      la $a0, newline                     # 存儲在這裏的新行字符串
      li $v0, 4
      syscall
    .end_macro

    println()                             # 彙編器會在運行前複製此代碼塊

    # 參數可以通過宏傳入。
    # 它們由 '%' 符號表示,可以選擇起任意名字
    .macro print_int(%num)
      li $v0, 1
      lw $a0, %num
      syscall
    .end_macro

    li $t0, 1
    print_int($t0)

    # 我們也可以給宏傳遞立即數
    .macro immediates(%a, %b)
      add $t0, %a, %b
    .end_macro

    immediates(3, 5)

    # 以及標籤
    .macro print(%string)
      la $a0, %string
      li $v0, 4
      syscall
    .end_macro

    print(hello_world)

## 數組 ##
.data
  list: .word 3, 0, 1, 2, 6                 # 這是一個字數組
  char_arr: .asciiz "hello"                 # 這是一個字符數組
  buffer: .space 128                        # 在內存中分配塊,不會自動清除
                                            # 這些內存塊彼此對齊

.text
  la $s0, list                              # 取 list 的地址
  li $t0, 0                                 # 計數器
  li $t1, 5                                 # list 的長度

  loop:
    bgt $t0, $t1, end_loop

    lw $a0, ($s0)
    li $v0, 1
    syscall                                 # 打印數字

    addi $s0, $s0, 4                        # 字的大小為4字節
    addi $t0, $t0, 1                        # 累加
    j loop
  end_loop:

## INCLUDE ##
# 使用 include 語句可以將外部文件導入到程序中
# (它只是將文件中的代碼放入 include 語句的位置)
.include "somefile.asm"

有建議?或者發現什麼錯誤?在 Github 上開一個 issue,或者發起 pull request!

原著 Stanley Lim,並由 0 個好心人修改。
© 2022 Stanley Lim
Translated by: Liu Yihua
本作品採用 CC BY-SA 3.0 協議進行許可。

user avatar _raymond 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.