add a, b, c# a = b + c
设计原则一——对指令进行规整化设置
代码示例
f = (g + h) - (i + j)
add t0, g, h # temp : t0 = g + h
add t1, i, j # temp : t1 = i + j
sub f, t0, t1 #final: f = t0 - t1
参与算术逻辑运算的变量必须是寄存器变量,对于MIPS(x 32)指令集来说,有32个寄存器,每个寄存器长 32 b i t 32bit 32bit,它们:
约定符号:
设计原则二——更小就会更快
前面的代码,根据约定符号进行矫正:
add $t0, $s1, $s2
add $t1, $s3, $s4
sub $s0, $t0, $t1
可以看到,在 M I P S 32 MIPS32 MIPS32指令集中,只有 32 ∗ 32 32*32 32∗32个存储空间,这并不足以支持所有运算,所以很多的数据是存储在内存中的,我们将这些被储存在内存中的数据称为_Memory Operands。内存的操作数是用来保存复杂的操作数,这是由寄存器的空间过小导致的。
算数逻辑运算不能直接对内存中的数据进行运算,这就需要我们先从内存中读取(load)数据,然后再进行处理,最终再将操作的结果写入(write)内存。在对内存的操作中,都是通过字节寻址的(每八位分配一个地址)。值得注意的是,每个字节由 8 b i t 8bit 8bit组成,而寄存器中每一位长度是 32 b i t 32bit 32bit,这就表明,我们在知道首地址的情况下,想要寻找第 i i i个元素时,需要将偏移量 ∗ 4 *4 ∗4。
MIPS中数据的对齐方式是:大端对齐。什么是大端对齐呢?
Memory Operand 小例子
C code
g = h + A[8]
其中,$s1 is g,$s2 is h,$s3 is the base address of A
MIPS code
lw $t0, 32($s3) # 读取A[8],32来源于偏移量*4 = 8*4 = 32
add $s1, $s2, $t0
Memory Operand 小例子2
C code
A[12] = h + A[8]
其中,$s2 is h,$s3 is the base address of A
MIPS code
lw $t0, 32($s3)
add $t0, $s2, $t0
sw $t0, 48($s3)#写入
可以看出,寄存器和内存的交互主要通过 l w lw lw , s w sw sw两条语句,进行数据的读取、写入,对于编译器而言,选择哪些变量放到寄存器中,哪些放到内存中,是非常关键的问题,同时也是非常困难的问题。
所谓立即数,即所使用的变量是一个常数,他是包含在指令中的。如:
addi $s3, $s3, 4
这就相当于C语言中的
s3 += 4;
立即数操作中没有减法,因为减掉一个数就相当于加上这个数的相反数,即:
addi $s3, $s3, -4
就可以完成C语言中如下功能
s3 -= 4;
**支持的数字范围:**立即数操作的常亮仅支持有符号的16位整数,即: [ − 32768 , 32767 ] \left[ -32768, 32767\right] [−32768,32767]这个区间。如果需要一个32位整数,那么这个整数就只能被先放到内存中,然后再被寄存器读取。
这里就引出了第三个设计原则:加快高概率事件(Make the Common Case Fast)
小的常数是常用的
使用常用的数,不需要从内存中读取
无符号整数(unsigned int)
有符号整数
浮点数
有符号数的表示
补码和源码之间有+0和-0的区别。
MIPS中特殊的取值——0
MIPS中0号寄存器永远为0,只能读取,不能写入。我们常用$zero来代表上述的零号计算器,利用这个寄存器,我们可以进行下面的操作:
add $t2, $s1, $zero
addi $t2, $zero, 100
事实上,MIPS中是没有初始化的方法的,在上面的代码中,我们用$s1的值初始化了$t2,然后将100赋值给$t2,这种语法的设计减少了很多冗余的语句。
无符号二进制数计算公式:
x = Σ ( x i 2 i ) x = \Sigma \left( x_i 2^i\right) x=Σ(xi2i)
这个老掉牙了,没啥好说的,一个长度为n的无符号二进制数,能表示 [ 0 , 2 n − 1 ] \left[0,2^n-1\right] [0,2n−1]这个区间内的整数。
有符号数的表示:
补码的表示:这一部分我之前看的时候基本上都是硬背的,现在听到这种讲法才恍然大悟(菜是原罪)
假设补码表示的二进制数有n位,标号为 0 , n − 1 0 ,n-1 0,n−1,那么补码到十进制数的计算公式为:
x = − x n − 1 ∗ 2 n − 1 + Σ i = 0 n − 2 ( x i ∗ 2 i ) x = -x_{n-1}*2^{n-1} + \Sigma_{i=0}^{n-2} \left(x_{i}*2^{i}\right) x=−xn−1∗2n−1+Σi=0n−2(xi∗2i)
它对应的取值范围是: [ − 2 n − 1 , + 2 n − 1 − 1 ] \left[ -2^{n-1},+2^{n-1}-1\right] [−2n−1,+2n−1−1]
在整数运算中,大多数情况下,使用的是补码的形式。下面有一些特殊的补码数字(帮助理解用的,不用想哪里特殊):
十进制 | 二进制 |
---|---|
0 | 0000 0000 … 0000 |
-1 | 1111 1111 … 1111 |
最小的数 | 1000 0000 … 0000 |
最大的数 | 0111 1111 … 1111 |
一般情况下,MIPS指令集下的运算都是对有符号数进行运算,除非你显式的告诉计算机要进行无符号数运算,需使用addu操作。
常见操作:
还记得我们第一章的时候讲过,汇编语言多数时间执行着将高级语言翻译成机器语言(machine code) 工作。下面是一些寄存器名称和用途的的对应表:
其中,1号寄存器被称为$at,它是为汇编程序预留的;26-27号寄存器被称为$k0,$k1,他们是保留给操作系统的。
指令集分为六大类:
0-31号寄存器是我们(程序员)能够访问的到的寄存器,还有我们访问不到的寄存器:
主要用途:用于表示算数逻辑运算,分为:
他们分别代表:
例子:
add $t0, $s1, $s2
对应的指令为:
special | $s1 | $s2 | $t0 | 0 | add |
---|---|---|---|---|---|
0 | 17 | 18 | 8 | 0 | 32 |
000000 | 10001 | 10010 | 01000 | 00000 | 100000 |
I-format可以用于之前的 a d d i addi addi操作、 l o a d / s t o r e load/store load/store或是条件跳转等指令。
其中
设计规则四——Good design demands good compromises
这种设计方法看似折中、不利于译码,但将长度固定在了32位,为了统一,增加了一部分译码的复杂性,达到了整体的统一,同时,这种设计方法,降低了后期电路设计的复杂性。
后面再讲
存储程序相关的基本概念:
程序可以被保存为二进制文件,这样的特性使得一个程序可以从一个电脑搬到另一个电脑上使用,这一个叫做“二进制的兼容性”(deepin-wine)
为了保证对已经编译好了的软件的继承,指令集架构应当围绕着少数几个大的指令集架构发展。
special | rs | rt | rd | shamt | funct |
---|---|---|---|---|---|
6 bits | 5 bits | 5 bits | 5 bits | 5 bits | 6 bits |
C语言中
$s1 = $s2 << 10;
$s1 = $s2 >> 10;
与MIPS中:
sll $1,$2,10
srl $1,$2,10
相同
其中: shamt-用于记录移动多少位
逻辑左移运算:
逻辑右移运算:
右移,并将空出来的部分用零填充
右移 i i i位,相当于除以上 2 i 2^i 2i
and $t0, $t1, $t2#t0 = t1 and t2
or $t0, $t1, $t2#t0 = t1 or t2
将$t1 & $t2的值赋给$t0,与运算可以运用在屏蔽寄存器中。
NOR就是not,or,这是一个三目运算符,使用方法如下:
nor $t0, $t1, $t2
这个运算表示 n o t ( t 2 o r t 1 ) not\ \left( t2\ or\ t1\right) not (t2 or t1)
MIPS中没有单独的取反操作,如果想要对$t1取反存入到$t0中,方法如下:
nor $t0, $t1, $zero
在上述代码中,进行的是 n o t ( t 1 o r z e r o ) not\ \left( t1\ or\ zero\right) not (t1 or zero)的操作,其中,任何一个数和零进行或运算,还是他本身,再对它本身取反就可以得到 n o t t 1 not\ t1 not t1的结果。
常见的控制指令:
beq re, rt, L1
bne rs, rt, L1
j L1
他们对应的C语言语法是:
#beq re, rt, L1
if(re == rt){
goto L1;
}
#bne rs, rt, L1
if(re != rt){
goto L1;
}
#j L1,无条件跳转
goto L1;
跳转的目标指令,与当前的beq/bne之间,不能超过正负 2 15 2^{15} 215,同时,在写C和C++的时候除特殊情况外应当避免/减少goto语句的使用,因为使用goto语句不当可能会让程序表意不明,更加混乱。
老规矩,先看C/C++
if(i==j){
f = g + h;
}else {
f = g - h;
}
对应的MIPS
#s3 is i, s4 is j, s0 is f, s1 is g, s2 is h.
bne $s3, $s4, Else#判断i,j是否相等,若不等就直接跳过下面两行
add $s0, $s1, $s2#若相等,就继续执行到这一行,完成f=g+h
j Exit#执行完f = g - h之后跳过下一条语句直接退出
Else: sub $s0, $s1, $s2#执行f = g - h
Exit:
从上面的代码中可以看出,MIPS的标记本身并没有改变代码的执行顺序,仅仅是对某行代码做了标记而已,如果没有 j , b n e , b e q j,bne,beq j,bne,beq三条语句的话,就算程序中有标记,MIPS还是会顺序执行的。
Ccode
while(save[i] == k) i += 1;
MIPS Code
#i is $s3, k is $s5, 地址被保存在$s6中
Loop: sll $t1, $s3, 2 #偏移量 = i * 4
add $t1, $t1, $s6# 当前地址= 起始地址 + 偏移量
lw $t0, 0($t1)#读取数据到t0
bne $t0, $s5, Exit
addi $s3, $s3, 1# s3 = s3 + 1
j Loop
Exit:
没啥好讲的了,自行体会。
MIPS Code
slt rd, rs, rt
slti rt, rs, constant
C Code
#slt rd, rs, rt
if(rs < rt ){
rd = 1;
}else {
rd = 0;
}
#slti rt, rs, constant
if(re < constant){
rd = 1;
}else {
rd = 0;
}
这个语句可以和beq一起用,来进行在大于、小于等情况下的选择结构。
if(i>=j){
i = i + 1;
}else {
j = j + 1;
}
将上述代码转化成mips指令集中的code,i,j–>$s1,$s2
我的答案:
slt $t0, i, j # if i>=j 0
beq $t0, $zero, Else #if t0==zero --> i>=j goto Else:j=j+1,else go on
addi $s1, $s1, 1
j Exit
Else: addi $s2, $s2, 1
Exit: ...
无符号数进行比较:
sltu,sltui,其实说白了就是后面加个u就是无符号数操作,再加个i就是常数操作。
基本块,是一个指令序列,在这个序列中是没有分支指令的,也没有其他分支指令的跳转指令。
函数调用的基本步骤:
jal ProcedureLabel
将寄存器控制权交给相关的进程:
将ProcedureLabel的下一个地址放到$ra中,然后跳转到目标地址。(这里$ra用于记录返回值返回到哪里)
jr $ra
将$ra复制到程序计数器中,这条语句也可以用于其他的跳转用途。
这段他讲的好抽象,我有点没听懂。
下面有一个实例。
C Code
int leaf_example(int g, int h, int i, int j){
int f;
f = (g + h) - (i + j);
return f;
}
我就直接贴最后的代码了:
leaf_example:
addi $sp, $sp, -4#压栈,往下压四个字节
sw $s0, 0($sp)#将s0放入压好的栈中
add $t0, $a0, $a1
add $t1, $a2, $a3
sub $s0, $t0, $t1
add $v0, $s0, $zero#将结果放入用于返回的参数$v0中
lw $s0, 0($sp)#恢复$s0
addi $sp, $sp, 4#恢复堆栈位置
jr $ra#返回
说实话,除了jr都看懂了,那个确实没看懂。
调用者需要将一些信息存储到堆栈中:
if fact(int n){
if(n < 1){
return 1;
}
return n*fact(n-1);
}
对应的mips,参数n放到$a0中,结果放到$v0
fact:
addi $sp, $sp, -8
sw $ra, 4($sp)
sw $a0, 0($sp)
slti $t0, $a0, 1#判断是否小于1
beq $t0, $zero, L1#如果大于等于1,那么跳到后面去
addi $v0, $zero, 1#如果小于1,那么将1付给v0
addi $sp, $sp, 8#从栈空间中出栈两个
jr $ra#返回
L1:
addi $a0, $a0, -1#减一
jal fact# 从新跳转到fact部分去执行
lw $a0, 0($sp)#将堆栈中把两个参数取出
lw $ra, 4($sp)
addi $sp, $sp, 8#恢复堆栈
mul $v0, $a0, $v0#相乘
jr $ra#返回
堆栈用于存储临时变量。
图中$sp指向堆栈可用地址,申请栈的时候,向下申请。$fp的存在使得便于恢复栈。
内存中一个程序分为不同的段:
思考: