易错部分

1. 函数内,跳转到另一个函数

在函数内进入另一个函数时,必须将 a0等本函数的参数和一些变量如t0,t1、返回地址ra等都要存入栈中。否则容易出事情。
特别是t0,我错了好几次了

一、基础

nop

Nop
就是告诉处理器这个周期什么也不做。一般在程序员手敲汇编程序时可能用不到,但是编译器可能会用到。
例如:在流水线处理器中(并行、乱序执行等不考虑),由于架构原因,在执行分支指令时,处理器的PC寄存器不能立即跳转。因此在PC寄存器跳转,即分支发生之前,处理器 还可以执行一条指令,这便是所谓的延迟槽。
为了降低汇编程序员敲代码的复杂性or提高程序可读性,延迟槽的利用便成了编译器的任务,而无需程序员操心。在编译器编译汇编程序时,会自动将之前的一条指令放到延迟槽中执行,以充分压榨处理器性能。但有的时候,之前的指令放到延迟槽里面会产生与正常顺序执行不同的结果,为了程序运行结果的正确性,这个多出来的CPU周期便什么也不能做。此时,编译器加入nop指令。

指令集跳转范围

根据指令集对j型指令的定义,跳转的索引值有26位,而且操作时后面补了两个零,其能跳转到的指令范围为2^26条,即2^28B。在高级语言转化为汇编语言的过程中,b类跳转指令通常对应于循环,因而一般是小范围的跳转;而j类跳转指令通常用于过程调用,可能会在整个程序的代码段范围内跳转。如果跳转范围超过256MB,则需要多条j指令进行“接力”或使用jr。jr是跳转到寄存器里存的值,不补零,(每一个地址存储一个字节的数据),所以可以达到2^32B。而对B类的指令,跳转有16位立即数,且操作时后面补零,2^16条指令,即2^18B.

立即数

对于addi和ori指令,他们的立即数都是16位的,不同之处在于addi中的立即数是有符号数,而ori中的立即数是无符号数。对于有符号立即数,第一位是符号位,也就是说addi指令的立即数最多只能表示一个15位的数,而0x8165是16位的数,所以数据溢出了。
// 那么由于汇编指令的参数都是用正负号来区分正负数,指令的立即数又是有范围的,所以输入的数据必须在 -2^15 ~ 2^15 - 1,输入这样的数据,才能正确转化为16位的带符号立即数。

通过指令集对ori指令的解释我们知道,ori指令第三个立即数参数是16位,使用的是无符号扩展。
因此ori指令的立即数参数不存在负数的情况,立即数的范围是 0x0000~0xffff 即 0~65535

导出机器码和数据的指令

java -jar /Users/g1613108/Desktop/Mars4_5.jar a db mc CompactDataAtZero dump 0x00003000-0x00003004 HexText object.txt source.asm

解析,HexTest表示是十六进制,object.txt是导出的目标文件,source.asm是源文件,0x00003000-0x00003004是导出的机器码

常见指令

slti (set less than imm) slti t0,a0, 2当a0比2小,t0为1
bgtz(branch on greater than zero),bgtz rt, else
beq(branch when equal) beq t0,0, else当两寄存器相等时,跳转到else标签位置
bgez(branch greater or euqal than zero)
blez(branch less or equal than zero)
bltz(branch less than zero)
bne(branch not equal) bne t0,0, label
li(load imm) li $v0, 1 赋值立即数
la(load address) la rt, label,将地址或者标签存入寄存器
sw(store Word), sw a0, 4(sp)把a0存入了$sp+4,而必须保证$sp+4必须为4的倍数
lw(load Word),加载字
lb(load Byte), lb rt, offset($a0),将该地址存储的1字节数据存入rt。
sb(store Byte), sb rt, offset(base),将rt存入后面,存字节,可以不写offset
加载字节,只将某地址存储的一个字节的数据拿出,存放在寄存器中,如 0x00001000存储的字为0x12345678, 我们从0x00001001 加载字节,则 寄存器得到的值为 0x00000056。(注意小端存储),从0x00001000加载字节,那就是0x00000078
存储字节,例如0x00003000存放0x12345678,那么将0x00003000存储字节,就是存储的0x00000078
sh(store halfword),存半字,sh rt, offset(base)将rt的值存入offset(base),保证offset+$base为2的倍数
nop(no operation) sll(shift left logical) 0,0, 0其指令码也是0x00000000
sll(shift left logical),逻辑左移,sll rd, rt, 5 rt逻辑左移5位存入rd
ori(or imm) 或立即数,or rt, rs, immediate,立即数进行零扩展
addi和addiu,这里addiu不是真的加无符号立即数,其本意是相对addi不考虑溢出, addiu rt, rs immediate
lui(load upper imm),立即数加载到高位,lui rt, immediate,操作是将立即数左移16位,赋值给rt。
jr(jump register) jr $ra,函数结束,返回地址
jal(jump and link), jal label,简化:jal把pc+4,这个地址存在了ra即31寄存器内,用GPR[$31]查看这个值。并跳转到标签对应的位置。利用jr $ra 来返回到pc+4。相当于执行了函数

jalr rd, rs,将rs地址赋值给pc,将pc+4赋值给rd。即跳转到rs,返回地址存在rd。
sub(subtract)减去 sub rd, rs, rt
div rs rt,其中$LO存放商,$HI存放余数

mult rs, rt,有符号乘,其中低32位存LO寄存器,高32位存在HI寄存器

multu 无符号乘法。

move指令,用一个寄存器的值赋给另一个寄存器。
mflo rd, 读取lo寄存器到rd (move from lo register)
mfhi rd,读取hi寄存器到rd
mthi rd, 写hi寄存器 (move to hi register)
mtlo rd, 写lo寄存器
sra 算术右移。 sra rd, rt, s

声明数组和字符串

.data
m1: .space 280
m2: .space 280
space: .asciiz  " "  (.asciiz自动添加结束标志)
nextline: .asciiz "\n"

汇编实例

.data
.text
.globl main
main:
    addi t0,0, 100
    ori t1,0, 200
    add t2,t1, t2
    subt3, t2,t1
    lui t4, 233
    oriv0, 1
    ori a0, 2333
    mthit1
    syscall
    nop
    loop:
        j loop
        nop

指令的格式

  1. R型
    1. 如 add,sub,sll, jalr
    2. 格式为 op, rs, rt, rd, shamt, func
  2. I型
    1. 特点是有16位的立即数
    2. addi subi ori beg bgtz sw lw,必须用负号来标记负数
    3. op, rs, rt, offset or immediate(16bits)
  3. J型
    1. j jal
    2. op, address(26 bits)
  • 解读
    · op:也称opcode、操作码,用于标识指令的功能。CPU需要通过这个字段来识别这是一条什么指令。不过,由于op只有6位,不足以表示所有的MIPS指令,因此在R型指令中,有func字段来辅助它的功能。

· func: 用于辅助op来识别指令。

· rs、rt、rd: 通用寄存器的代号,并不特指某一寄存器。范围是0~31,用机器码表示就是00000~11111。

· shamt:移位值,用于移位指令。

· offset:地址偏移量。

· immediate:立即数。

· address:跳转目标地址,用于跳转指令

syscall

li v0, i
syscallv0 = 1, print integer in a0v0 = 2, print float in f12v0 = 3, print duoble in f12v0 = 4, print string at the address of the a0,必须有结束标志v0 = 5, read integer to v0v0 = 6, read float to f0v0 = 7, read double to f0v0 = 8, read string,必须自己指定读入地址和长度,在a0存入起始地址,在a1存入读入最大的字符串长度
v0 = 10, 退出程序v0 = 11,输出字符,in a0v0 = 12, 读入字符 to $v0

小端存储

每一个地址存储一个字节的数据。而32位机器中,每条命令(包括nop)都是32位的,即4个字节,即0x12345678,八个十六进制位,所以0x00存储第一条命令的首地址,下一条命令的首地址就存储在0x04。

小端存储指的是首地址存储最低两位,即 0x00存储 0x78 , 0x01存储0x56, 0x02存储0x34, 0x03存储0x12。大端存储指的是首地址存储最高的两十六进制位。

扩展指令和伪指令

寄存器

$0 名字为 $zero, 常量0
$1 名字为 $at,保留给汇编器
$2-$3 名字为 $v0-$v1: 函数调用返回值
$4-$7 名字为 $a0-$a3:函数调用参数
$8-$15 名字为 $t0-$t7 临时变量
$16-$23 名字为 $s0-$s7 存储的变量
$24-$25 名字为 $t8-$t9 临时变量
$28 名字为 $gp 全局指针 global pointer
$29 名字为 $sp 堆栈指针 stack pointer
$30 名字为 $fp 帧指针 frame pointer
$31 名字为 $ra: 返回地址 return address

$a0...$a3用来做参数传递
$v0...$v1用来做存储结果数据
如果函数参数较少,传入参数存放在 $a0-$a3寄存器内,如果函数参数过多,如十个,就要用堆栈 $sp 寄存器存储.
  • 三个特殊寄存器
  1. PC(Program Counter):这个寄存器想必已经在理论课上讲解过了。它用于存储当前CPU正在执行的指令在内存中的地址。需要注意的是,这个寄存器的值不能用常规的指令进行取值和赋值,但这并不意味着不能做到,只是麻烦一些。那么,怎样对其取值、赋值呢?这可以作为一个思考的题目,在之后的学习中自行探索。(提示:跳转)

  2. HI:这个寄存器用于乘除法。它被用来存放每次乘法结果的高32位,也被用来存放除法结果的余数。

  3. LO:HI的孪生兄弟。它被用来存放每次乘法结果的低32位,也被用来存放除法结果的商。

for 循环/while

.text
li t1, 100            #n = 100
lit2, 0              #i
for_begin1:            #for(int i=0;i<n;i++)
slt t3,t2, t1      #{
beqt3, 0, for_end1   
nop               
#do something
addit2, t2, 1       #i++
j for_begin1
nop                    #}
for_end1:
liv0, 10
syscall
#该程序实现的是输入数组元素,并排序后输出
.data
    array: .space 400
    message_input_n: .asciiz "请输入数组的长度\n"
    message_input_array: .asciiz "请输入整数\n"
    message_output_array: .asciiz "排序后的数组为:\n"
    space: .asciiz " "
    stack: .space 100

.globl main   # 定义全局标签 并勾选  Initialize Program Counter to gloabl 'main' if defined


.text

input:
    la a0, message_input_n   # 将a0赋值为input_n标签对应地址
    liv0, 4       # 表示输出a0的地址指向字符串  print string  
    syscall

    li v0, 5       # 读入整数,存入v0,即元素的个数   read integer      syscall      movet0, v0   # 将读入的值v0存入t0

    lit1, 0       # t1就是i
    for_i_begin:
        slt t2,t1, t0  # if(t1t2, zero, for_1_end # 如果t1 = t0 那么久结束
        nop  # 延时槽

        lat2, array  
        li t3, 4
        multt3, t1
        mflot3
        addu t2,t2, t3

        laa0, message_input_array
        li v0, 4  # print string
        syscall

        liv0, 5  #  read integer in v0
        syscall

        swv0, (t2)

        addit1, t1, 1  # i++
        j for_i_begin
        nop
    for_1_end:
        movev0, t0  #v0是函数的返回值
        jr ra    # 这里不完善,因为没有人调用这个函数,所以不该有返回
        nop

output:
    movet0, a0  # 这里默认函数调用a0是传入的参数,即元素的个数 --> t0

    laa0, message_output_array 
    li v0, 4
    syscall  # 输出时的提示语句

    lit1, 0  # t1 就是i
    for_2_begin:
        sltt2, t1,t0 # t1 < t0 --> t2 = 1
        beq t2,zero, for_2_end  #
        nop

        la t2, array
        sllt3,t1,2       # t3 = t1*4
        addut2, t2,t3

        lw a0, (t2)
        li v0 1  # print integer 输入数据
        syscall 

        lwa0, space
        li v0, 4  #输出空格
        syscall

        addit1, t1, 1 # i++
        j for_2_begin
        nop    for_2_end:
        jrra
        nop

sort:
    addiu sp,sp, -32  # 分配栈空间 ,栈指针移动的大小与使用的栈空间来决定
    move t0,a0  # 传入参数a0,存入t0,即个数
    li t1, 0      # t1就是i
    for_4_begin:
        sltt2, t1,t0 
        beq t2,zero, for_4_end
        nop

        la t2, array
        sllt3,t1,2       # t3 = t1*4
        addut2, t2,t3  # t2为当前地址

        sw t2, 28(sp)
        sw t1, 24(sp)
        sw t0, 20(sp)
        sw ra, 16(sp)

        move a0t0
        move a1t1

        jal findmin
        nop

        lw ra, 16(sp)
        lw t0, 20(sp)
        lw t1, 24(sp)
        lw t2, 28(sp)

        #将得到的返回值与该地址的数据交换
        lw t3, (v0)  # v0是返回值, 是最小值的地址
        lw t4, (t2)  # t2是当前值的地址
        sw t3, (t2)
        sw t4, (v0) 

        addi t1,t1, 1
        j for_4_begin
        nop
    for_4_end:
        addiu sp,sp, 32  # 维护栈指针的大小
        jr ra
        nop

findmin:
    lat0, array
    sll a0,a0, 2   # a0为传入的参数,因为sort函数没有改变 a0的值
    subia0, a0, 4
    addut0, t0,a0  # t0为最后一个元素的地址

    lwt1, (t0)      #t1存储最小值
    move t2,t0      # 令t2为最小值的地址

    movet3, t0     # t3是i(即最后一个元素的地址)
    lat0, array      # t0是首地址
    sll a1,a1, 2    #  a1是计算到了第几个元素
    addu t0,t0, a1  # t0是查找范围的首地址
    for_3_begin:
        sget4, t3,t0  # set on greater and equal if(t3 >= t0) then t4 = 1
        beq t4,zero, for_3_end #
        nop

        lw t5, (t3)  

        slt t6,t5, t1  # set on less than        beqt6, zero, if_1_else  # branch on equal            nop
            movet1, t5
            movet2, t3
            j if_1_end
            nop
        if_1_else:
        if_1_end:

        subit3, t3, 4
        j for_3_begin
        nop
    for_3_end:
        movev0, t2
        jrra
        nop


main:
    la sp, stack
    addiusp, sp, 100
    addiusp, sp, -20

    jal input
    nop
    movet0, v0   # 返回值v0是元素的个数,存入了t0
    movea0, t0  #参数
    swt0, 16(sp)  # 用栈维护t0,从高字节开始。t0占用了从 sp+16到sp+19。栈底的位置
    jal sort
    nop
    lwt0, 16(sp)

    movea0, t0  # 传参
    jal output
    nop

    addiusp, sp, 20   # 问题 addiu不是加无符号数吗???答:“无符号”是一个误导,其本意是不考虑溢出。

    liv0, 10
    syscall

里面有栈的使用 栈底在高地址,入栈后,栈指针向低地址移动。子过程执行前后需要移动栈指针sp
1. 计算好栈帧大小
2. 栈指针始终指向栈顶
3. 过程开始时分配栈空间 (addiu
sp, sp, -32)
4. 过程结束时回收栈空间 (addiu
sp, sp, 32)
5. 以栈指针为基址进行栈的存取(sw
t0, 24($sp))

二、各种例子

1. loop

while(t0 <= s0)

.text
    li  v0,5
    syscall         # 输入一个整数,输入的数存到v0中
    move s0,v0    # 赋值,s0=v0
    li  s1,0       #s1 用于存储累加的值,s1=0
    lit0,1       # t0是循环变量
loop:
    bgtt0,s0,loop_end    # 这里用了一个扩展指令bgt,当t0>s0的时候跳转到loop_end 这里表示while(t0<=s0){}
    adds1,s1,t0 # s1=s1+t0
    addit0,t0,1   #t0=t0+1
    j   loop        # 无条件跳转到loop标签
loop_end:
    movea0,s1     # 赋值,a0=s1
    liv0,1       # v0=1,在syscall中会输出a0的值
    syscall         
    li  v0,10      #v0=10
    syscall         # 结束程序

2. 数组

.data
array:  .space  40  # 存储这些数需要用到数组,数组需要使用10*4=40字节
                    # 一个int整数需要占用4个字节,需要存储10个int整数
                    # 因此,array[0]的地址为0x00,array[1]的地址为0x04
                    # array[2]的地址为0x08,以此类推
str:    .asciiz "The numbers are:\n"
space:  .asciiz " "

.text
    li  v0,5
    syscall             # 输入一个整数
    moves0,v0        #s0 is n
    li  t0,0           #t0 循环变量
loop_in:
    beq t0,s0,loop_in_end # t0==s0的时候跳出循环 while(t0 < s0){}
    li  v0,5
    syscall             # 输入一个整数
    sllt1,t0,2       #t1=t0<<2,即t1=t0*4
    swv0,array(t1)  # 把输入的数存入地址为array+t1的内存中
    addi t0,t0,1      # t0=t0+1
    j   loop_in         # 跳转到loop_in
loop_in_end:

    la  a0,str
    liv0,4
    syscall             # 输出提示信息

    li  t0,0
loop_out:
    beqt0,s0,loop_out_end
    sllt1,t0,2       #t1=t0<<2,即t1=t0*4
    lwa0,array(t1)      # 把内存中地址为array+t1的数取出到a0中
    liv0,1
    syscall             # 输出a0
    laa0,space
    li  v0,4
    syscall             # 输出一个空格
    addit0,t0,1
    j   loop_out
loop_out_end:
    liv0,10
    syscall             # 结束程序

3. 二维数组

C++声明二维数组
int[][] arr = new int[16][16]

除了使用.space声明二维数组,还可以
.data
arr: .word 0:256 # 声明了256个字的存储空间。共1024字节
经过实验(没查到官方解释= =),.word冒号前的数指为所开空间初始化的值,冒号后的数指所开空间字的个数。所以.word 0:63指开63个字大小的空间,其值均为0,当n=8时存在越界访问的情况

matrix标签的地址没有赋值给寄存器,而是直接使用,令寄存器为偏移量

.data
matrix: .space  256 # int matrix[8][8]   8*8*4字节
            # matrix[0][0] 的地址为0x00, matrix[0][1] 的地址为0x04,……
            # matrix[1][0] 的地址为0x20, matrix[1][1] 的地址为0x24,……
            # ……
str_enter:  .asciiz "\n"
str_space:  .asciiz " "

# 这里使用了宏,%i为存储当前行数的寄存器,%j为存储当前列数的寄存器
# 把 (%i*8+%j)*4 存入%ans寄存器中
.macro  getindex(%ans,%i,%j)
    sll %ans,%i,3   # %ans=%i*8
    add %ans,%ans,%j    # %ans=%ans+%j
    sll %ans,%ans,2 # %ans=%ans*4
.end_macro

.text
    li  v0,5
    syscall
    moves0,v0            # 行数
    liv0,5
    syscall
    move s1,v0            # 列数

    # 这里使用了循环嵌套
    li  t0,0           #t0是一个循环变量
in_i:                   # 这是外层循环
    beq t0,s0,in_i_end
    li  t1,0           #t1是另一个循环变量
in_j:                   # 这是内层循环
    beq t1,s1,in_j_end
    li  v0,5
    syscall             # 注意一下下面几行,在Execute页面中Basic列变成了什么
    getindex(t2,t0,t1)       # 这里使用了宏,就不用写那么多行来算(t0*8+t1)*4了
    sw  v0,matrix(t2)     # matrix[t0][t1]=v0
    addit1,t1,1
    j   in_j
in_j_end:
    addit0,t0,1
    j   in_i
in_i_end:

    # 这里使用了循环嵌套,和输入的时候同理
    lit0,0
out_i:
    beq t0,s0,out_i_end
    li  t1,0
out_j:
    beqt1,s1,out_j_end
    getindex(t2,t0,t1)
    lw  a0,matrix(t2)     # a0=matrix[t0][t1]
    liv0,1
    syscall
    la  a0,str_space
    liv0,4
    syscall             # 输出一个空格
    addi    t1,t1,1
    j   out_j
out_j_end:
    la  a0,str_enter
    liv0,4
    syscall             # 输出一个回车
    addi    t0,t0,1
    j   out_i
out_i_end:

    li  $v0,10
    syscall

4. 宏的使用

  1. 不带参数的宏
    .macro done
    li $v0, 10
    syscall
    .end_macro
    
  2. 带参数的宏
    .macro  getindex(%ans,%i,%j)
        sll %ans,%i,3
        add %ans,%ans,%j
        sll %ans,%ans,2
    .end_macro
    
  3. 类似#define的宏定义
    .eqv EQV_NAME string// 将EQV_NAME替换为string
    

5. 栈的使用stack

$sp始终指向栈底(高地址)。
1. 如果要函数的参数多于3个,可以使用栈来传递

  1. 要在函数内套用另一个函数,可以将本函数的一些参数用栈存起来
out_func:
    sw ra, (sp)
    subi sp,sp, 4
    sw ao, (sp)
    subi sp,sp, 4

    jal in_func

    addi sp,sp, 4
    lw t0, (sp)
    addi sp,sp, 4
    lw ra, (sp)

    v0存返回值
    jrra

6. 递归调用

int f(int n){
    if(n == 1) return 1;
    return n * f(n-1);
}
int main(){
    printf("%d\n",f(5));
}

main:
    li v0, 5  # 读取n
    syscall
    moves0, v0
    movea0, s0
    jal factorial

    movea0, v0
    liv0, 1
    syscall
    li v0, 10
    syscall

factorial:
    bnea0, 1, work
    li v0, 1
    jr31 # 这里还应该存一下栈

work:
    move t0,a0

    sw ra, (sp)
    subi sp,sp, 4
    sw t0, (sp)
    subi sp,sp, 4

    subi t1,t0, 1
    move a0,t1
    jal factorial

    addi sp,sp, 4
    lw t0, (sp)
    addi sp,sp,4
    lw ra, (sp)

    mult t0,v0
    mflo v0
    jr31

7.if else

.text
li t1, 100               #t1=100
lit2, 200               #t2=200
slt t3,t1, t2         #if(t1<t2) t3=1 
beqt3, 0, if_1_else    #
nop
#do something

j if_1_end                #jump to end
nop

if_1_else:
#do something else

if_1_end:
liv0, 10  
syscall

8.双重while或for循环

要保证内层循环每次开始前要初始化变量j

li t0 ,1
lit1, 1
while_out:

    bgt t0,s0, while_out_end 
    li t1, 1     while_in:
        bgtt1, s1, while_in_end
            #do something
        addit1, t1, 1   # j++
        j while_in
    while_in_end:

    addit0, t0, 1  # i++
    lit1, 1
    j while_out
while_out_end:

gmm