汇编语言之MIPS汇编

简介

咱们知道x86架构cpu用于PC端和工作站较多,ARM架构cpu常见于手机和单片机,那么MIPS架构的cpu主要在哪些设备可以找到它们的身影呢?

  • 中国龙芯
  • PS游戏机

学习环境搭建

寄存器

在mips中通用寄存器用$开头表示,一共有32个

寄存器编号 寄存器名 寄存器用途
$0 $zero 永远返回0
$1 $at 保留寄存器
$2-$3 $v0-$v1 一般用于存储表达式或者函数的返回值(value的简写)
$4-$7 $a0-$a3 参数寄存器(Argument简写)
$8-$15 $t0-$t7 一般用于存储临时变量(temp简写)
$16-$23 $s0-$s7 存放子函数调用过程需要被保留的数据(saved values)
$24-$25 $t8-$t9 属性同$t0-$t7
$26-$27 $k0-$k1 一般存储中断函数返回值
$28 $gp GlobalPointer简写
$29 $sp 栈指针,指向栈顶(Stack Pointer简写)
$30 $s8/$fp (Save / Frame Pointer)帧指针
$31 $ra 一般用于存储函数返回地址(return address简写)

寄存器编号和别名一一对应,同一个寄存器可以有两种不同表示方法:$0或者$zero

  • program counter (PC) 无法直接修改,通过跳转指令可以改动
  • HI 和 LO :这两个寄存器特别用来保存乘法、除法、乘法累加的结果。

MIPS汇编中的分段处理

1
2
3
.data #数据段

.text #代码段

传送指令

  1. 加载立即数指令 li

li(load immediate) :用于将立即数传送给寄存器

1
li $t0,1  ;十六进制数据使用0x前缀表示
  1. 加载地址指令 la

la(load address) :用于将地址传送至寄存器中, 多用于通过地址获取数据段中的地址

1
2
3
4
5
.data 
msg: .ascii "hello world"

.text
la $a0,msg # 将字符串数据所在的地址赋值给$a0寄存器
  1. 寄存器数据传送指令move

用于将一个寄存器中的数据传送至另一个寄存器当中

1
move $t0,$t1  # 将寄存器$t1中的数据传送至$t0

系统服务指令 syscall

在C语言中输出文本可以使用printf函数,但是汇编中没有printf这么一说,如果想要输出文本,需要借助syscall指令

如果想要输出一个数字1,那么syscall指令从$a0寄存器中取出需要输出的数据

因此, 你在执行syscall指令之前需要将数据提前放入$a0之中:

1
2
li $a0,1
syscall

同时,还需要指定输出的数据类型,数据类型的指定保存在$v0寄存器中

1
2
# $v0=1, syscall--->print_int
# $v0=4, syscall--->print_string

$v0存入1,表示syscall$a0中的数据当做数字输出

$v0存入4,表示syscall$a0中的数据当做数据的地址,然后输出对应的数据

syscall指令读写对照表

Service Code in $v0 Arguments Result
print integer 1 $a0 = integer to print
print float 2 $f12 = float to print
print double 3 $f12 = double to print
print string 4 $a0 = address of null-terminated string to print
read integer 5 $v0 contains integer read
read float 6 $f0 contains float read
read double 7 $f0 contains double read
read string 8 $a0 = address of input buffer $a1 = maximum number of characters to read See note below table
sbrk (allocate heap memory) 9 $a0 = number of bytes to allocate $v0 contains address of allocated memory
exit (terminate execution) 10
print character 11 $a0 = character to print See note below table
read character 12 $v0 contains character read
open file 13 $a0 = address of null-terminated string containing filename $a1 = flags $a2 = mode $v0 contains file descriptor (negative if error). See note below table
read from file 14 $a0 = file descriptor $a1 = address of input buffer $a2 = maximum number of characters to read $v0 contains number of characters read (0 if end-of-file, negative if error). See note below table
write to file 15 $a0 = file descriptor $a1 = address of output buffer $a2 = number of characters to write $v0 contains number of characters written (negative if error). See note below table
close file 16 $a0 = file descriptor
exit2 (terminate with value) 17 $a0 = termination result See note below table
Services 1 through 17 are compatible with the SPIM simulator, other than Open File (13) as described in the Notes below the table. Services 30 and higher are exclusive to MARS.
time (system time) 30 $a0 = low order 32 bits of system time $a1 = high order 32 bits of system time. See note below table
MIDI out 31 $a0 = pitch (0-127) $a1 = duration in milliseconds $a2 = instrument (0-127) $a3 = volume (0-127) Generate tone and return immediately. See note below table
sleep 32 $a0 = the length of time to sleep in milliseconds. Causes the MARS Java thread to sleep for (at least) the specified number of milliseconds. This timing will not be precise, as the Java implementation will add some overhead.
MIDI out synchronous 33 $a0 = pitch (0-127) $a1 = duration in milliseconds $a2 = instrument (0-127) $a3 = volume (0-127) Generate tone and return upon tone completion. See note below table
print integer in hexadecimal 34 $a0 = integer to print Displayed value is 8 hexadecimal digits, left-padding with zeroes if necessary.
print integer in binary 35 $a0 = integer to print Displayed value is 32 bits, left-padding with zeroes if necessary.
print integer as unsigned 36 $a0 = integer to print Displayed as unsigned decimal value.
(not used) 37-39
set seed 40 $a0 = i.d. of pseudorandom number generator (any int). $a1 = seed for corresponding pseudorandom number generator. No values are returned. Sets the seed of the corresponding underlying Java pseudorandom number generator (java.util.Random). See note below table
random int 41 $a0 = i.d. of pseudorandom number generator (any int). $a0 contains the next pseudorandom, uniformly distributed int value from this random number generator’s sequence. See note below table
random int range 42 $a0 = i.d. of pseudorandom number generator (any int). $a1 = upper bound of range of returned values. $a0 contains pseudorandom, uniformly distributed int value in the range 0 = [int] [upper bound], drawn from this random number generator’s sequence. See note below table
random float 43 $a0 = i.d. of pseudorandom number generator (any int). $f0 contains the next pseudorandom, uniformly distributed float value in the range 0.0 = f 1.0 from this random number generator’s sequence. See note below table
random double 44 $a0 = i.d. of pseudorandom number generator (any int). $f0 contains the next pseudorandom, uniformly distributed double value in the range 0.0 = f 1.0 from this random number generator’s sequence. See note below table
(not used) 45-49
ConfirmDialog 50 $a0 = address of null-terminated string that is the message to user $a0 contains value of user-chosen option 0: Yes 1: No 2: Cancel
InputDialogInt 51 $a0 = address of null-terminated string that is the message to user $a0 contains int read $a1 contains status value 0: OK status -1: input data cannot be correctly parsed -2: Cancel was chosen -3: OK was chosen but no data had been input into field
InputDialogFloat 52 $a0 = address of null-terminated string that is the message to user $f0 contains float read $a1 contains status value 0: OK status -1: input data cannot be correctly parsed -2: Cancel was chosen -3: OK was chosen but no data had been input into field
InputDialogDouble 53 $a0 = address of null-terminated string that is the message to user $f0 contains double read $a1 contains status value 0: OK status -1: input data cannot be correctly parsed -2: Cancel was chosen -3: OK was chosen but no data had been input into field
InputDialogString 54 $a0 = address of null-terminated string that is the message to user $a1 = address of input buffer $a2 = maximum number of characters to read See Service 8 note below table $a1 contains status value 0: OK status. Buffer contains the input string. -2: Cancel was chosen. No change to buffer. -3: OK was chosen but no data had been input into field. No change to buffer. -4: length of the input string exceeded the specified maximum. Buffer contains the maximum allowable input string plus a terminating null.
MessageDialog 55 $a0 = address of null-terminated string that is the message to user $a1 = the type of message to be displayed: 0: error message, indicated by Error icon 1: information message, indicated by Information icon 2: warning message, indicated by Warning icon 3: question message, indicated by Question icon other: plain message (no icon displayed) N/A
MessageDialogInt 56 $a0 = address of null-terminated string that is an information-type message to user $a1 = int value to display in string form after the first string N/A
MessageDialogFloat 57 $a0 = address of null-terminated string that is an information-type message to user $f12 = float value to display in string form after the first string N/A
MessageDialogDouble 58 $a0 = address of null-terminated string that is an information-type message to user $f12 = double value to display in string form after the first string N/A
MessageDialogString 59 $a0 = address of null-terminated string that is an information-type message to user $a1 = address of null-terminated string to display after the first string N/A

使用syscall指令输出helloworld示例:

1
2
3
4
5
6
7
8
.data 

msg: .ascii "hello world\0" #类似于C语言中 char* msg="hello world"

.text
la $a0,msg
li $v0,4
syscall

数据定义

  1. 定义整型数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # 打印Integer数据
    .data
    age: .word 23 #一个字长数据32位

    .text
    li $v0,1
    lw $a0,age
    syscall


    #========================#
    #加法运算
    .data
    number1: .word 2
    number2: .word 5

    .text
    lw $t0,number1
    lw $t1,number2

    add $t2,$t0,$t1

    li $v0,1
    move $a0 $t2
    syscall

    # 乘法运算 使用mul
  2. 定义Float数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #打印float数据
    .data
    PI: .float 3.14

    .text
    li $v0,2
    lwc1 $f12,PI
    syscall

    #float数据 算数运算
    .data
    f1: .float 3.14
    f2: .float 3.15
    .text
    li $v0,2
    lwc1 $f1,f1
    lwc1 $f2,f2
    add.s $f3,$f2,$f1 #带.s后缀的指令皆为浮点单精度指令
    mov.s $f12,$f3
    syscall
  3. 定义Double数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #打印double数据
    .data
    ayDouble: .double 7.20
    zeroDouble: .double 0.0
    .text

    ldc1 $f2,ayDouble
    ldc1 $f0,zeroDouble
    li $v0,3
    add.d $f12,$f2,$f0 #带.d后缀的指令皆为浮点双精度指令
    syscall
  4. 定义字符串数据

    1
    2
    3
    4
    5
    6
    7
    #打印double数据
    .data
    msg: .ascii "hello world"
    .text
    li $v0,4
    la $a0,msg
    syscall

用户输入

  1. 字符串输入

    1
    2
    3
    4
    5
    6
    7
    .data
    userInput: .space 20 #声明一块空间 默认存放0
    .text
    li $v0,8
    la $a0,userInput #将用户的输入存放至userInput中
    li $a1,20 #限制用户输入, 一旦超过20个默认回车
    syscall
  2. 整型数据输入

    1
    2
    3
    4
    # 输入的结果系统会存放在$v0寄存器
    .text
    li $v0,5
    syscall
  1. 浮点型数据输入

    1
    2
    3
    4
    #以float为例  输入的结果会存放在$f0寄存器
    .text
    li $v0,6
    syscall

单精度和双精度

单精度数(float型)在32位计算机中存储占用4字节,也就是32位,有效位数为7位,小数点后6位。

双精度数(double型)在32位计算机中存储占用8字节,也就是64位,有效位数为16位,小数点后15位。

浮点寄存器

在mips中一共有32个浮点寄存器(其中包含16个双精度浮点寄存器),用于单独处理浮点数

函数声明和调用

  1. 函数声明

    • 格式

      1
      2
      3
      函数名:
      函数体
      jr $ra #$ra寄存器中保存着调用指令下一条代码所在的地址
    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      .data

      .text
      show:
      li $v0,1
      li $a0,3
      syscall

      jr $ra ;jump registers
  1. 函数调用

    • 格式

      1
      jal 函数名
    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      .data

      .text
      jal show #调用函数时将下一条指令的地址存放至$ra寄存器

      #结束程序
      li $v0,10
      syscall

      show:
      li $v0,1
      li $a0,3
      syscall

      jr $ra
  2. 函数传参和返回值

    在mips汇编中,函数的参数一般放在$a系列寄存器当中,最多对应4个参数,超过4个部分使用栈储存. 函数的变量和返回值一般放在$v系列寄存器当中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #需求:定义加法函数 并调用获取返回值int sum(int v,int b)
    main:
    addi $a1,$zero,50
    addi $a2,$zero,100

    jal add

    li $v0,1
    move $a0,$v1
    syscall

    #结束程序
    li $v0,10
    syscall

    add:
    add $v1,$a1,$a2
    jr $ra
  3. 嵌套函数

    由于每执行jal调用一次函数, 就会刷新$ra寄存器中的值,因此,在嵌套函数调用之前,需要临时保存上一次$ra中的值,使用栈空间临时保护即可

栈操作

  1. 栈空间拉伸和平衡

    1
    2
    3
    addi $sp,$sp,-4 #栈拉伸 拉伸4个字节空间

    addi $sp,$sp,4 #栈平衡
  2. 入栈和出栈

    1
    2
    3
    sw $s0 ,0($sp) #入栈   往内存中写入数据

    lw $s0, 0($sp) #出栈 从内存中读取数据
  3. 嵌套函数使用栈保护$ra代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    main:
    addi $a1,$zero,50
    addi $a2,$zero,100

    jal add

    li $v0,1
    move $a0,$v1
    syscall

    #结束程序
    li $v0,10
    syscall

    add:
    addi $sp,$sp,-4 #栈拉伸
    sw $ra ,0($sp) #入栈

    jal sub
    add $v1,$a1,$v1

    lw $ra, 0($sp) #出栈
    addi $sp,$sp,4 #栈平衡
    jr $ra

    sub:
    sub $v1,$a1,$a2
    jr $ra

内存空间布局

从mars中可以查看到内存分布起始物理地址

image-20210108155849172

转成图后:

栈的伸缩在mips和x86架构中是由高地址往低地址进行伸缩, 在arm架构中可升序也可降序

内存碎片

在内存动态分配(heap区)过程中容易出现一些小且不连续的空闲内存区域,这些未被使用的内存称作内存碎片

分类:

  • 内部碎片:比如数据在内存中采用4个字节对齐的方式进行存储, 比如我们申请一块3个字节的空间用于存储一个数据,但是系统给我们分配了4个字节空间,这时多出来的一个字节的空间就被称之为内部碎片
  • 外部碎片:在我们进行内存回收和分配的时候容易出现外部碎片,比如我连续申请了三块4个字节的内存空间,当我释放第二块内存空间然后紧接着申请一块8个字节的空间,此时由于之前释放的4个字节空间太小无法使用,这就造成了内存块空闲,这种碎片叫做外部碎片

PC 寄存器

称作 程序计数寄存器(Program Counter Register) :用于存储程序即将要执行的指令所对应在内存中的实际物理地址, 如果改变该值可以让指令跳转到我们想要跳转的地方

如何修改pc寄存器中的值

使用以下转移指令

  1. jr指令

    1
    jr 寄存器  #$ra寄存器实际上就是保存着函数调用前的pc寄存器的值
  2. jal指令

    1
    jal 标号  #跳转的同时给$ra寄存器赋值
  3. j指令

    1
    j 标号   #直接跳转

内存数据的读写

  1. 从指定内存中读取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    #整型数据
    lw $t0,0x10010000 #读取4个字节数据 赋值给t0寄存器
    ld $t0,0x10010000 #读取8个字节数据 赋值给t0和t1寄存器

    #单精度浮点数据
    lwc1 $f0,0x10010000


    #双精度浮点数据
    ldc1 $f0,0x10010000


    #字符数据
    ### 由于字符数据是以ascii码16进制的形式存放到内存中,因此只能获取到ascii码值,该值属于整型数据,直接使用lw或者ld即可
    lw $t0,0x10010000 #读取4个字节数据 赋值给t0寄存器
    ld $t0,0x10010000 #读取8个字节数据 赋值给t0和t1寄存器

    从内存中读取数据的宽度取决于寄存器的大小,由于32位cpu寄存器最大存储32位数据,因此lw $t0表示一次性读取4个字节的数据到$t0寄存器, 如果想要连续读取八个字节的数据,那么需要使用ld $t0,表示一次性读取8个字节的数据到$t0,$t1连个连续的寄存器,

  2. 往指定内存中写入数据

    1. 第一种 数据定义的同时指定物理地址
    1
    2
    3
    4
    5
    6
    .data 0x10010020 ;将以下定义的数据存放在0x10010020这个物理地址
    .ascii "a"
    .ascii "b"

    .data 0x10010000
    .ascii "c"
    1. 第二种 在代码段中使用指令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #整型数据
    li $s1,4
    sw $s1,0x10010000 ;将$s1寄存器中的数据存入0x10010000这个物理地址

    #单精度浮点数
    .data
    f1: .float 3.14

    .text
    lwc1 $f2,f1
    swc1 $f2,0x10010000


    #双精度浮点数
    .data
    d1: .double 3.14

    .text
    ldc1 $f2,d1
    sdc1 $f2,0x10010000

以上直接使用的是简单粗暴的十六进制表示物理地址,很多时候内存的地址会保存在寄存器中,你可能会看到以下写法:

1
2
3
4
5
6
7
8
9
10
11
lw $s1, $s2
sw $s1, $s2

或者
lw $s1, 20($s2)
sw $s1, 20($s2) ;将地址往高位偏移20个字节 相当于sw $s1, 20+$s2


或者
lw $s1, -20($s2)
sw $s1, -20($s2) ;将地址往低位偏移20个字节

注意: 往指定内存中读取写入数据时,代码段不允许直接写入和读取

一维数组的定义

数组本质上就是多个数据的集合,在内存中按照一定顺序排列,角标即为每个数据的偏移值,在mips中内存数据是按照4个字节进行对齐的,也就是说一个数据最少占用4个字节内存空间,因此数组中数据之间的偏移量固定为n*4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.data
array: .space 20 #别名的另外一种用法 通过array(寄存器)这种格式 寄存器中存放地址偏移地址量

.text
# $t0寄存器存放角标值 $s1中存放需要存入的值
li $s1,1
li $t0,0
sw $s1,array($t0) #相当于 sw $s1,array+$t0

li $s1,2
li $t0,4
sw $s1,array($t0)

li $s1,3
li $t0,8
sw $s1,array($t0)

数组的打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.data
array: .space 20

.text
#初始化数组中的数据
li $s1,1
li $t0,0
sw $s1,array($t0)

li $s1,2
li $t0,4
sw $s1,array($t0)

li $s1,3
li $t0,8
sw $s1,array($t0)


#查找角标为2的数值
getData:
la $s1 ,array
li $a0,2
mul $a0,$a0,4
add $s1,$s1,$a0
lw $a0,0($s1)
li $v0,1
syscall

#将角标临时置为0 方便下面循环操作
li $t0,0
while:
beq $t0,12,exit
lw $t2,array($t0)

addi $t0,$t0,4

li $v0,1
move $a0,$t2
syscall
j while

exit:
li $v0,10
syscall
快速初始化数组数据的方法
1
2
.data
array: .word 20 :3 #批量定义3个整型数据20

分支跳转指令

  1. 整型数据分支比较跳转
  • bgt(branch if greater than):用于大于比较
1
2
3
4
bgt $t0,$t1,sub # 如果$t0中的数据大于$t1,则跳转到sub分支,执行sub中的代码,否则,按照顺序执行bgt下面的代码, sub是一个代号,可以自定义


sub:
  • beq(branch equal):用于等于比较
1
2
3
4
beq $t0,$t1,sub # 如果$t0中的数据等于$t1,则跳转到sub分支,执行sub中的代码,否则,按照顺序执行bgt下面的代码, sub是一个代号,可以自定义


sub:
  • ble(branch if less than):用于小于比较
1
2
3
4
ble $t0,$t1,sub # 如果$t0中的数据小于$t1,则跳转到sub分支,执行sub中的代码,否则,按照顺序执行bgt下面的代码, sub是一个代号,可以自定义


sub:

练习1: 将以下c代码转换成mips汇编代码:

1
2
3
4
5
6
7
8
scanf("%d",$a);
scanf("%d",$b);

if(a>b){
printf("YES");
}else{
printf("NO");
}

汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 用$t0指代a ,$t1指代b

.data
msg_yes: .ascii "YES\0" # \0表示字符串结尾
msg_no: .ascii "NO\0"


.text
li $v0,5 #控制syscall为读取integer状态
syscall # 此时io控制台显示光标,可输入数字,回车后将输入的数字保存在$v0中
move $t0,$v0 #由于接下来还需要使用$v0 ,为避免数据被覆盖掉 将输入的数据转移到$t0中进行临时保存

li $v0,5
syscall
move $t1,$v0


bgt $t0,$t1,sub
li $v0,4
la $a0,msg_no
syscall

#结束程序
li $v0,10
syscall
sub:
li $v0,4
la $a0,msg_yes
syscall

练习2: 将以下c代码转换成mips汇编代码:

1
2
3
4
5
6
7
8
9
10
11
//求累加之和
//1+2+3+.....+100

int i=1;
int s=0;

while(i<=100){
s=s+i;
i=i+1;
}
printf("%d",s);

汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 用$t0指代i ,$t1指代s

.text
li $t0 ,1
li $t1 ,0


loop:
# s=s+i;
add $t1,$t1,$t0
add $t0,$t0,1

ble $t0,100,loop


move $a0,$t1
li $v0,1
syscall
  1. 浮点型数据分支比较
  • 小于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .data
    num1: .float 3.14
    num2: .float 3.16

    .text

    lwc1 $f0,num1
    lwc1 $f1,num2

    c.lt.s $f0,$f1 #关键代码
    bc1t sub #bc1t表示条件满足 bc1f表示不满足条件


    sub:
  • 等于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .data
    num1: .float 3.14
    num2: .float 3.16

    .text

    lwc1 $f0,num1
    lwc1 $f1,num2

    c.eq.s $f0,$f1 #关键代码 c表示coproc协处理寄存器 s表示single单精度
    bc1t sub #bc1t表示条件满足 bc1f表示不满足条件


    sub:
  • 小于等于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .data
    num1: .float 3.14
    num2: .float 3.16

    .text

    lwc1 $f0,num1
    lwc1 $f1,num2

    c.le.s $f0,$f1 #关键代码
    bc1t sub #bc1t表示条件满足 bc1f表示不满足条件


    sub:

    以上是单精度浮点数据的比较示例,如果是双精度,只需将结尾.s改成.d即可

mips多文件开发

在文件A中定义函数

1
2
3
4
5
fun:
li $v0,1
li $a0,1
syscall
jr $ra

在文件B中使用关键字.include引用A文件中的函数

1
2
3
4
.text
jal fun

.include "A.asm"

所有文件必须在同一目录下

  1. 宏替换

    全局替换,使用我们之前学过的.include伪指令进行替换

  2. 宏匹配

在汇编中,如果我们要依次打印1,2,3三个整数,那么汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print1:
li $v0,1
li $a0,1
syscall
jr $ra

print2:
li $v0,1
li $a0,2
syscall
jr $ra

print2:
li $v0,1
li $a0,3
syscall
jr $ra

我们发现使用标签的方式定义函数,当函数体内容存在不确定变量值时,代码非常冗余, 如果使用高级语言进行封装的话,我们一般一个函数就搞定了:

1
2
3
void print(int a){
print(a);
}

有没有办法使得汇编能像高级语言一样简洁呢?

在MARS中给我们提供了一个扩展伪指令,叫做宏匹配

宏匹配使用的格式如下:

1
2
3
.macro 别名
#汇编指令...
.end_macro

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
li $v0,10
syscall

#比如我们要对以上两行指令使用宏匹配进行封装

#封装结果为
.macro exit
li $v0,10
syscall
.end_macro


#在代码中引用
.text
exit #直接使用别名调用

如果我们要封装一个打印整型数据的函数,那么我们可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
#封装结果为
.macro print_int(%param)
li $v0,1
li $a0,%param
syscall
.end_macro


#在代码中引用
.text
print_int(1) #直接使用别名调用
print_int(2)
print_int(3)

这样是不是和高级语言没什么区别啦

打印字符串封装示例:

1
2
3
4
5
6
7
8
9
10
11
.macro print_str (%str)
.data
myLabel: .asciiz %str
.text
li $v0, 4
la $a0, myLabel
syscall
.end_macro

print_str ("test1")
print_str ("test1")

然后结合我们之前学过的多文件开发,完全可以将这个封装好的函数单独放在一个文件中,直接在头部.include就行

  1. 宏定义

    全局定义,如果我们想给一个数据或者寄存器,甚至是一行代码取个别名,然后在代码中使用别名的方式指代,那么可以使用宏定义指令.eqv 别名的好处就是方便我们进行记忆

    1
    2
    3
    .eqv  LIMIT      20 #给20这个立即数取个别名为LIMIT
    .eqv CTR $t2
    .eqv CLEAR_CTR add CTR, $zero, 0

    当我们有以下代码:

    1
    2
    3
    4
    .text
    li $v0,1
    add $t2, $zero, 0
    li $t0,20

    如果我们使用宏定义,我们可以写成如下:

    1
    2
    3
    4
    5
    6
    7
    8
    .eqv  LIMIT      20 #给20这个立即数取个别名为LIMIT
    .eqv CTR $t2
    .eqv CLEAR_CTR add CTR, $zero, 0

    .text
    li $v0,1
    CLEAR_CTR
    li $t0,LIMIT

:宏定义宏匹配必须先定义后使用,也就是说定义的代码需要放在前头

二维数组的定义

二维数组其实就类似于我们数学中的二维坐标系,我们如果要定位一个点的话可以使用(x,y)来表示,在计算机的世界里,二维中所有的点都按照顺序依次存放在内存当中

假设我们将第一维当做行,第二维当做列,那么排列的方式有以下两种:

第一种是 行不动,列轮动

内存地址 二维坐标
0x00000020 arr[2][2]
0x0000001C arr[2][1]
0x00000018 arr[2][0]
0x00000014 arr[1][2]
0x00000010 arr[1][1]
0x0000000C arr[1][0]
0x00000008 arr[0][2]
0x00000004 arr[0][1]
0x00000000 arr[0][0]

这种方式获取实际地址的公式为:

1
2
3
4
5
6
addr=baseAddr+(rowIndex*colSize+colIndex)*dataSize

实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度

比如:我要计算arr[2][1]的实际物理地址, 那么
实际地址=0x00000000+(2*3+1)*4=0x00000000+0x0000001C=0x0000001C

第二种是 列不动,行轮动

内存地址 二维坐标
0x00000020 arr[2][2]
0x0000001C arr[1][2]
0x00000018 arr[0][2]
0x00000014 arr[2][1]
0x00000010 arr[1][1]
0x0000000C arr[0][1]
0x00000008 arr[2][0]
0x00000004 arr[1][0]
0x00000000 arr[0][0]

这种方式获取实际地址的公式为:

1
2
3
4
5
6
addr=baseAddr+(colIndex*rowSize+rowIndex)*dataSize

实际地址=首地址+(第几列*行数+第几行)*数据占用的宽度

比如:我要计算arr[2][1]的实际物理地址, 那么
实际地址=0x00000000+(1*3+2)*4=0x00000000+0x00000014=0x00000014

使用mips汇编实现二维数组定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


#以下是以 行不动 列轮动的方式实现
.data
da: .word 1,2,3,5,6,7,10,11,12
array: .space 36

.text
# void initArry(*arr,row,col,index)
initArry:
#首地址
la $a0,array
#第几行
li $a1,0
#第几列
li $a2,0
#存第几个数
li $a3,0

while:
# arr[x][y]
bgt $a1,2,exit
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-12
sw $a1,8($sp)
sw $a2,4($sp)
sw $a3,0($sp)

jal saveDataToMemory
#从栈中恢复局部变量值
lw $a1,8($sp)
lw $a2,4($sp)
lw $a3,0($sp)
add $sp,$sp,12

#累加计数
add $a3,$a3,4
#列轮动
addi $a2,$a2,1
bgt $a2,2,sub

j while

sub:
li $a2,0
add $a1,$a1,1
j while

saveDataToMemory:
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-4
sw $ra,0($sp)

#计算数据存放的物理地址
jal getAddr

#获取需要存放的数据
lw $t0,da($a3)
#将数据存入指定位置中
sw $t0,0($v0)

lw $ra,0($sp)
add $sp,$sp,4
jr $ra

#算法部分
getAddr:
#实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度
mul $a1,$a1,3
add $a2,$a2,$a1
mul $a2,$a2,4
add $v0,$a2,$a0

jr $ra


exit:
#程序结束之前测试数据能否正常取出
jal getDataFromArry
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出值为11
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

列不不动 行轮动方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


#以下是以 列不动,行轮动的方式实现
.data
da: .word 1,2,3,5,6,7,10,11,12
array: .space 36

.text
# void initArry(*arr,row,col,index)
initArry:
#首地址
la $a0,array
#第几行
li $a1,0
#第几列
li $a2,0
#存第几个数
li $a3,0

while:
# arr[x][y]
bgt $a2,2,exit
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-12
sw $a1,8($sp)
sw $a2,4($sp)
sw $a3,0($sp)

jal saveDataToMemory
#从栈中恢复局部变量值
lw $a1,8($sp)
lw $a2,4($sp)
lw $a3,0($sp)
add $sp,$sp,12

#累加计数
add $a3,$a3,4
#行轮动
addi $a1,$a1,1
bgt $a1,2,sub

j while

sub:
li $a1,0
add $a2,$a2,1
j while

saveDataToMemory:
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-4
sw $ra,0($sp)

#计算数据存放的物理地址
jal getAddr

#获取需要存放的数据
lw $t0,da($a3)
#将数据存入指定位置中
sw $t0,0($v0)

lw $ra,0($sp)
add $sp,$sp,4
jr $ra

#算法部分
getAddr:
#实际地址=首地址+(第几列*行数+第几行)*数据占用的宽度
mul $a2,$a2,3
add $a1,$a1,$a2
mul $a1,$a1,4
add $v0,$a1,$a0

jr $ra


exit:
#程序结束之前测试数据能否正常取出
jal getDataFromArry
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出7
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

更为简便的方法实现二维数组的搭建

由于数组中数据是在内存中连续进行排列存储的,那么我们可以之间将数据 依次存入内存之中,然后使用算法进行数据获取即可(以下示例皆采用 行不动,列动 的方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


#由于数组中的数据是存放在堆内存中,需要在程序执行时动态分配


.text
#堆地址从0x10040000开始
la $a0,0x10040000
#将数据按照顺序存放至内存中
jal initData

#获取数据
jal getDataFromArry
j exit

initData:
li $s1,1
sw $s1,0($a0)

li $s1,2
sw $s1,4($a0)

li $s1,3
sw $s1,8($a0)

li $s1,5
sw $s1,12($a0)

li $s1,6
sw $s1,16($a0)

li $s1,7
sw $s1,20($a0)

li $s1,10
sw $s1,24($a0)

li $s1,11
sw $s1,28($a0)

li $s1,12
sw $s1,32($a0)


jr $ra


#算法部分
getAddr:
#实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度
mul $a1,$a1,3
add $a2,$a2,$a1
mul $a2,$a2,4
add $v0,$a2,$a0

jr $ra


exit:
#程序退出
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出值为11
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

再简化一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


.data
da: .word 1,2,3,5,6,7,10,11,12
# 假如以上数据是动态写入的 当做数组中的数据来用

.text
#那么只需要提供首地址然后配合算法就能获取到指定坐标的数据
la $a0,da
jal getDataFromArry
j exit


#算法部分
getAddr:
#实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度
mul $a1,$a1,3
add $a2,$a2,$a1
mul $a2,$a2,4
add $v0,$a2,$a0

jr $ra


exit:
#程序退出
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出值为11
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

按照正常的编程思维,我们一般使用第一种行不动 列动的存储方式 第一维为行,第二维为列,如果你想改成行动存储方式,有两种方法:要么将数据的存储顺序进行变动,配合第二种算法,要么将第二维当成行,第一维当成列,配合第二种算法进行处理

Mips汇编指令汇总表

类别

指令名称

实例

含义

注释

英文注解

加法

add $s1,  $s2,  $s3

$s1 = $s2 + $s3

三个寄存器操作数

addition 加法

减法

sub $s1,  $s2,  $s3

$s1 = $s2 - $s3

三个寄存器操作数

subtraction 减法

立即数加法

addi $s1, $s2,  20

$s1 = $s2 + 20

用于加常数数据

add immediate 立即加法

取字

lw $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将一个字从内存中取到寄存器中

load word 加载字

存字

sw $s1, 20 ($s2)

Memory[$s2 + 20] = $s1

将一个字从寄存器中取到内存中

store word 存储字

取半字

lh $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将半个字从内存中取到寄存器中

load halfword 加载半字

取无符号半字

lhu $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将半个字从内存中取到寄存器中

load halfword unsigned

存半字

sh $s1, 20 ($s2)

Memory[$s2 + 20] = $s1

将半个字从寄存器中取到内存中

stroe halfword 存储半字

取字节

lb $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将一字节从内存中取到寄存器中

load byte

取无符号字节

lbu $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将一字节从内存中取到寄存器中

load byte unsigned

存字节

sb $s1, 20 ($s2)

Memory[$s2 + 20] = $s1

将一字节从寄存器中取到内存中

store byte

取链接字

ll $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

取字作为原子交换的前半部

load linked

存条件字

sc $s1, 20 ($s2)

Memory[$s2 + 20] = $s1;

$s1 = 0 or 1

存字作为原子交换的后半部分

store conditional

取立即数的高位

lui $s1, 20

$s1 = 20 * 216

取立即数并放到高16位

load upper immediate

and $s1, $s2, $s3

$s1 = $s2 & $s3

三个寄存器操作数按位与

and

or $s1, $s2, $s3

$s1 = $s2 | $s3

三个寄存器操作数按位或

or

或非

nor $s1, $s2, $s3

$s1 = ~ ($s2 | $s3)

三个寄存器操作数按位或非

not or

立即数与

andi $s1, $s2, 20

$s1 = $s2 & 20

和常数按位与

and immediate

立即数或

ori $s1, $s2, 20

$s1 = $s2 | 20

和常数按位或

or immediate

逻辑左移

sll $s1, $s2, 10

$s1 = $s2 << 20

根据常数左移相应位

set left logical

逻辑右移

srl $s1, $s2, 10

$s1 = $s2 >> 20

根据常数右移相应位

set right logical

相等时跳转

beq $s1, $s2, 25

if ($s1 == $s2) go to 

PC + 4 + 25 * 4

相等检测:

和PC相关的跳转

branch on equal

不相等时跳转

bne $s1, $s2, 25

if ($s1 != $s2) go to 

PC + 4 + 25 * 4

不相等检测:

和PC相关的跳转

branch on not equal

小于时跳转

slt $1, $s2, $3

if ($s2 < $s3) $s1 = 1; 

else $s1 = 0

比较是否小于

set less than

无符号数比较小时置位

sltu $1, $s2, $3

if ($s2 < $s3) $s1 = 1; 

else $s1 = 0

比较是否小于无符号数

set less than unsigned

无符号数小于立即数时置位

slti $1, $s2, 20

if ($s2 < 20) $s1 = 1; 

else $s1 = 0

比较是否小于常数

set less than immediate

无符号数比较小于无符号立即数时置位

sltiu $1, $s2, 20

if ($s2 < 20) $s1 = 1; 

else $s1 = 0

比较是否小于无符号常数

set less than immediate unsigned

跳转

j 2500

go to 2500 * 4

跳转到目标地址

jump

跳转至寄存器所指位置

jr $ra

go to $ra

用于switch语句,以及过程调用

jump register

跳转并链接

jal 2500

$ra = PC + 4;

go to 2500 * 4;

用于过程调用(方法)

正常的执行流程执行完A自然要执行B指令,现在要跳转执行C方法,这时就把B地址存入寄存器中,执行完C后跳转到B

jump and link

### Mips内存结构图:

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%