Chapter2 数据表达和MIPS汇编语言
2.1 二进制表示方法
针对于有符号整数,可以使用原码、补码和反码的方式进行表示。
2.1.1 反码与原码
对于一个数的有符号数,若其为正数则反码为其本身。例如正数0 001的反码为0 001与之前保持一致;负数1 001的反码为 1 110,即符号位不变,数值位按位取反。
然而,对于3位的反码而言,我们可以得到+0和-0两种0的表示方式0 00和1 11,即0的表示方式不唯一。为此引入补码的概念。
2.1.2 补码与原码
对于一个数的有符号数,若其为正数则补码为其本身;若一个数为负数,则其补码的运算过程为:
补码=反码+1
补码 = 反码 + 1
补码=反码+1
例如:
| 步骤 | 结果 |
|---|---|
| 初始值 | 1 0001 = -15 |
| 按位取反 | 1 1110 |
| 加一 | 1 1110 + 1 = 1 1111 |
此外,一个n+1位的补码的表示范围为−2n-2^n−2n至2n−12^n-12n−1.
对于一个数的补码转为原码的过程为:首先将除符号位外的每一个按位取反,随后加一。
2.1.3 进位和溢出
涉及到二进制补码的加减法绕不开的就是进位和溢出的判断。进位就不过多赘述,主要看溢出:
1.溢出的条件
溢出的条件为两个符号相同的二进制数进行加法或减法。一个正数加一个负数永远不可能发生溢出。一个正数(符号位位0)加一个正数(符号位为0)的结果反而是一个负数(符号位为1),例如0 111 + 0 001 = 1 000,则此时发生溢出。一个负数(符号位为1)加一个负数(符号位为1)的结果反而是一个正数(符号位为0)。
2.判断运算过程中是否溢出(主要针对减法💔)
从大一开始我这里就一直很不明白在减法的时候怎么判断是否发生了溢出,数字逻辑只讲了加法,计算机导论也只讲了加法,但是考试的时候考得就是减法,考场上就纯靠转为十进制运算好来判断,速度实在是太慢了😱。
举一个例子:-3-(-2)=-1。我们可以得到-3的补码为1 101,-2的补码为1 110。对于-3-(-2)=-1本身而言我们可以将其变形为-3+2=-1,注意看这里是将减法转变为了加法,同时-2取反为2。那么一切就好说了,运算步骤为:
| 步骤 | 表达式 |
|---|---|
| -3的补码 | 1 101 |
| -2的补码 | 1 110 |
| 将-2的补码全部取反 | 0 001 |
| 将上一步中得到的值加一 | 0 010 |
| 直接与-3的补码相加正好得到结果-1 | 1 111 |
再举一个例子:2-(-3)=5。对于-3-(-2)=-1本身而言我们可以将其变形为2+3=5。运算步骤为:
| 步骤 | 表达式 |
|---|---|
| 2的补码 | 0 010 |
| -3的补码 | 1 101 |
| -3的补码全部取反 | 0 010 |
| 将上一步中得到的值加一 | 0 011 |
| 用-3的补码直接加上2的补码得到-5 | 0 101 |
再举一个例子:-3-2=-5。到这里其实就很好说了,直接用两个符号数相减就可以了,运算步骤为:
| 步骤 | 表达式 |
|---|---|
| -3的补码 | 1 101 |
| 2的补码 | 0 010 |
| 用-3的补码直接减去2的补码得到-5 | 1 011 |
再举一个例子:3-2=1。这个其实就更好说了,直接用两个符号数相减就可以了,运算步骤为:
| 步骤 | 表达式 |
|---|---|
| 3的补码 | 0 011 |
| 2的补码 | 0 010 |
| 用3的补码直接减去2的补码得到 | 0 001 |
2.1.4 符号位扩展
根据二进制数的符号位填充高位。正数前面可添加任意多的0,负数前面可添加任意多的1。这里最主要的是和MIPS语言中的SLL,SRL和SRA指令关联最大,后文展开。
2.1.6 补码的十六进制表示
用15减去各位数字后再加一。例如6A3D的补码为95C3 = 95C2 + 1;FFFF的反码为0001 = 0000 + 1。
2.2 MIPS语言寄存器
在这里就不对MIPS指令的定义进行介绍了,只对精简指令集和复杂指令集进行一个简单的区分:
| 精简指令集RISC | 复杂指令集CISC |
|---|---|
| ARM、MIPS、PowerPC、RISC-V | x86 |
2.2.1 各个寄存器的功能
| 寄存器编号 | 寄存器符号 | 功能描述 |
|---|---|---|
| 0 | zero | 值0 |
| 1 | $at | 在伪指令展开的时候常用的寄存器。常用的伪指令有blt小于则转移指令 ,再如li $t0, 0x12345678装入32位立即数指令,再如move指令 |
| 2~3 | $v0 - $v1 | 存储函数的返回值 |
| 4~7 | $a0 - $a3 | 调用函数时的参数,例如void fun(int a, int b, int c) |
| 8~15 | $t0 - $t7 | 可以方便使用的寄存器,但是需要注意各个函数调用的t寄存器,所以在调用不同函数的时候一定要保存起来(常用的方式是栈) |
| 16~23 | $s0 - $s7 | 可以方便使用的寄存器,与$t0 - $t7寄存器最大的区别在于程序调用其他函数的前后其中内容是不变的 |
| 29 | $sp | 栈指针,指向栈的位置 |
| 31 | $ra | 返回函数调用前的地址,最常用的指令为jr $ra |
2.2.2 大端和小端
1.小端模式
小端模式是将数据的地位放在低地址空间,数据的高位放在高地址的空间中。例如存放二进制数1011-0100-1111-0110-1000-1100-0001-0101:
| 地址编号 | 内存 | 备注 |
|---|---|---|
| 0 | 0001-0101 | 低地址 |
| 1 | 1000-1100 | |
| 2 | 1111-0110 | |
| 3 | 1011-0100 | 高地址 |
| 4 |
在从小端模式中读取时从低地址开始读取,首先读出0001-0101,随后读出1000-1100-0001-0101…以此类推。
2.大端模式
大端模式是将数据的高位存放在低地址空间,数据的低位存放在高地址空间。例如例如存放二进制数1011-0100-1111-0110-1000-1100-0001-0101:
| 地址编号 | 内存 | 备注 |
|---|---|---|
| 0 | 1011-0100 | 低地址 |
| 1 | 1111-0110 | |
| 2 | 0110-1000 | |
| 3 | 0001-0101 | 高地址 |
| 4 |
在从大端模式中读取时从高地址开始读取,首先读出0001-0101,随后读出0110-1000-0001-0101…依次类推。
3.个人总结
两种数据存储方式其实可以看做是从左往右看数据或者从右往左看数据。小端存储模式中借助内存的低地址空间存储数据的地位更符合人的思维逻辑——“越小的放在越下面”,也颇有一种“队列”的感觉;大端存储模式的存储和读取的过程则颇有一种“栈”的感觉。
2.3 MIPS指令格式
MIPS指令有R型、J型和I型三种。其中,R型的最高6为的操作操作码都是000000,具体功能通过最低位的6为funct功能码进行区别。此外,不同的J型和I型的操作码互不相同。
I型指令是含有16位立即数的指令,常见的有lw, sw, beq, bne指令。
J型指令包含26位的伪地址,常见的有j, jal指令。注意:jr指令属于R型指令而不是J指令!!!
R型指令包含三个寄存器。
2.4 MIPS程序语言(将C/C++转为MIPS)
拿到一段C程序,首先判断其是否为叶函数。叶函数是不再调用其他函数的函数。如果是叶函数的话可以不用考虑分配栈帧进行。如果不是叶函数的话那必定要分配栈来存储重要寄存器数据:$ra, $a*, $t*, v。
2.4.1 for循环的MIPS语言
for (int i = 0; i < 10; i++)
{
j = j + 1;
}
解析for循环的语句可以概括为:
for (Init; Test; Update)
Body
那么等效的汇编语言代码格式为:
li $t0, 0 # i = 0
li $t1, 0 # j = 0
li $t2, 10 # for循环上线次数为10
j Test
Loop:addi $t1, $t1, 1
addi $t0, $t0, 1
Test: beq $t0, $t2, OtherProgramm
j Loop
OtherProgramm:......
2.4.2 while循环的MIPS语言
while (i < 10)
{
j = j + 1;
}
解析while循环的语句可以概括为:
while (Test)
Body
那么等效的汇编语言代码格式为:
li $t0, 0 # i = 0
li $t1, 0 # j = 0
li $t2, 10 # for循环上线次数为10
j Test
Loop:addi $t1, $t1, 1
addi $t0, $t0, 1
Test:beq $t0, $t2, OtherProgram
j Loop
OtherProgram:......
2.4.3 if语言的MIPS语言
if (i == j)
{
f = g + h;
}
else
{
f = g - h;
}
考虑将上述代码转为MIPS语言等效代码:
beq $s3, $s4, True
subu $s0, $s1, $s2
j OtherProgram
True: add $s0, $s1, $s2
OtherProgram: ...
2.4.4函数调用的MIPS语言
函数调用MIPS语言需要用到$a0 - $a3几个寄存器来分别传入函数的参数,使用$ra寄存器来保存函数的返回地址,使用$v0-$v1寄存器来保存函数的返回值(return)。尤其是非叶函数时因为函数中的函数在调用时会改变上述几个寄存器的存储值,则此时需要栈帧来保存这些值会改变的寄存器中的值。
例如C代码:
funcA (int a, int b, int c, int d)
{
a = d + funcB(b, c, a);
return a + b;
}
其等效的MIPS语言代码为:
#准备调用funcA
move $a0, $t0
move $a1, $t1
move $a2, $t2
move $a3, $t3
jal funcA
funcA:
#保存关键变量
addi $sp, $sp, -20
sw $ra, 0($sp)
sw $t0, 4($sp)
sw $t1, 8($sp)
sw $t2, 12(4sp)
sw $t3, 16(4sp)
#准备调用funcB
move $a0, $t1
move $a1, $t2
move $a2, $t0
jal funcB
#恢复数据
lw $t3, 16(4sp)
lw $t2, 12(4sp)
lw $t1, 8($sp)
lw $t0, 4($sp)
#a = d + funcB
add $t0, $t3, $v0
#return
move $v0, $t0
lw $ra, 0($sp)
j $ra
2.4.5 汉诺塔的MIPS语言(综合使用上述所有结构)
最后附上汉诺塔可运行代码:
.data
msg: .asciiz "Move x : from x to x\n"
.text
move_one:
la $t0,msg
add $a1,$a1,'0'
sb $a0,14($t0)
sb $a1,5($t0)
sb $a2,19($t0)
li $v0,4 # syscall 4(print_str)
la $a0,msg
syscall
jr $ra
hanoi: #implement hanoi function here
addi $sp,$sp,-24
sw $ra,0($sp)
sw $a0,4($sp)
sw $a1,8($sp)
sw $a2,12($sp)
sw $a3,16($sp)
sw $t1,20($sp)
li $t0,1
lw $t1,4($sp)
beq $t0,$t1,jumptoelse
addi $a0,$t1,-1
lw $a1,8($sp)
lw $a2,16($sp)
lw $a3,12($sp)
jal hanoi
lw $a0,8($sp)
move $a1,$t1
lw $a2,16($sp)
jal move_one
addi $a0,$t1,-1
lw $a1,12($sp)
lw $a2,8($sp)
lw $a3,16($sp)
jal hanoi
lw $ra,0($sp)
lw $a0,4($sp)
lw $a1,8($sp)
lw $a2,12($sp)
lw $a3,16($sp)
lw $t1,20($sp)
addi $sp,$sp,24
jr $ra
jumptoelse:
lw $a0,8($sp)
li $a1,1
lw $a2,16($sp)
jal move_one
lw $ra,0($sp)
lw $a0,4($sp)
lw $a1,8($sp)
lw $a2,12($sp)
lw $a3,16($sp)
lw $t1,20($sp)
addi $sp,$sp,24
jr $ra
main:
addi $sp,$sp,-4
sw $ra,0($sp)
li $a0,5
li $a1,'A'
li $a2,'B'
li $a3,'C'
jal hanoi
lw $ra,0($sp)
addi $sp,$sp,4
jr $ra # retrun to caller
1603

被折叠的 条评论
为什么被折叠?



