第二章 指令:计算机的语言
2.1 序言
指令集:一个给定的计算机体系结构所包含的指令集和。
2.2 计算机硬件的操作
MIPS汇编语言的记法:
add a,b,c
将两个变量b和c相加,放入变量a中。
记法表达方式固定:每条MIPS算术指令只执行一个操作,并且有且仅有3个变量。
【例1】
a=b+c;
d=a-e;
由编译器完成将c语言程序转换为MIPS汇编指令。
add a,b,c
sub d,a,e
【例2】
f=(g+h)-(i+j);
汇编指令:
计算g与h的和,需要创建临时变量t0:
add t0,g,h
计算i与j的和:
add t1,i,j
最后减法指令:
sub f,t0,t1
2.3计算机硬件的操作数
MIPS算数运算指令的操作数是严格的,他们必须来自寄存器。MIPS体系结构中寄存器大小为32位
字:计算机中的基本访问单位,通常为32位为一组,在MIPS体系结构中与寄存器大小相同。
一个字在32位系统里是4个字节(byte),在64位系统里是8个字节。1字节(byte)=8位(bit)
寄存器表示:$s0, $s1,……表示与C和Java程序中变量对应的寄存器; $t0, $t1,……表示将程序编译为MIPS指令时的临时寄存器。
【例子】
f=(g+h)-(i+j);
变量f、g、h、i、j依次分配给寄存器$s0、 $s1、 $s2、 $s3、 $s4。
编译后的MIPS代码为:
add $t0, $s1, $s2
add $t1, $s3, $s4
sub $s0, $t0, $t1
2.3.1 存储器操作数
数据传送指令:在存储器和寄存器之间移动数据的命令
地址:在存储器空间中指明某特定数据元素位置的值
存储器:一个下标很大的从0开始的一维数组,地址相当于数组的下标。
下图中,第三个数据元素的地址为2,存放的数据为10.
取数指令(lw,load word的缩写):将数据从存储器复制到寄存器的数据传送指令
格式:操作码 目标寄存器 访问存储器的常数(寄存器)
【例】
g=h+A[8];
编译器将寄存器$s1, $s2分配给变量g、h。设数组A的起始地址(基址(base address))存放在寄存器 $s3中
lw $t0, 8( $s3)
add $s1, $s2, $t0
数据传送指令中的常量为偏移量,存放基址的寄存器称为基址寄存器
图2-3给出了图2-2的实际MIPS地址,第三个字的字节地址是8
MIPS是按字节编址的,所以字的起始地址必须是4的倍数,这叫对齐限制
存数指令(sw (store word)):将数据从寄存器复制到存储器
【例子】
A[12]=h+A[8];
lw $t0, 32( $s3)
add $t0, $s2, $t0
sw $t0, 48( $s3)
2.3.2常数或立即操作数
为避免使用取数指令,提供其中一个操作数是常数的算术运算指令。
例如addi(add immediate)
addi $s3, $s3, 4
$s3= $s3+4
2.4 有符号数和无符号数
计算机中所有信息由二进制数位或位组成。
最低有效位:最右边一位(即第0位)
最高有效位:最左边一位(即第31位)
计算机区分正负数:采用二进制补码表示有符号二进制数
二进制补码:所有负数的最高有效位是1,这个位叫做符号位。
2.5 计算机中指令的表示
MIPS汇编语言中,寄存器$s0 ~ $s7映射到寄存器16~ 23,寄存器 $t0 ~ $t7 映射到寄存器8~15
MIPS字段
R型(用于寄存器)
op | rs | rt | rd | shamt | funct |
---|---|---|---|---|---|
6位 | 5位 | 5位 | 5位 | 5位 | 6位 |
MIPS指令中各字段名称及含义:
- op: 指令的基本操作,通常称为操作码
- rs:第一个源操作数寄存器
- rt:第二个源操作数寄存器
- rd:用于存放操作结果的目的寄存器
- shamt:位移量
- funct:功能。称为功能码,用于指明op字段中操作的特定变式。
I型(用于立即数)
op | rs | rt | constant or address |
---|---|---|---|
6位 | 5位 | 5位 | 16位 |
16位的地址字段意味着取字指令可以取相对于基址寄存器地址偏移 | |||
±215字节范围内的任意数据字。 同样,加立即数指令中常数也被限制不超过±215 |
我们分析2.3.1例子中的取字指令:
lw $t0, 32( $s3)
这里,19(寄存器 $s3)存放于rs字段,8(寄存器 $t0)存放于rt字段,32存放于address字段。要注意,在取字指令中,rt字段用于指明接收取数结果的目的寄存器。
【例题】
给一个例子来描述程序员所编程序到机器执行指令的整个转换过程。设数组A的基址存放在$t1中,h存放在 $s2中
A[300]=h+A[300];
被编译为如下汇编指令:lw $t0,1200( $t1) add $t0, $s2, $t0 sw $t0,1200( $t1)
写出三条MIPS指令的机器语言代码:
先用十进制表示机器指令语言。
op rs rt rd address/shamt funct 35 9 8 1200 0 18 8 8 0 32 43 9 8 1200
再将十进制指令转化为二进制机器指令,注意位数
MIPS机器语言归纳如下:
2.6逻辑操作
指令全集包括异或(XOR),参考序言的图。
2.7决策指令
条件分支指令
beq register1, register2, L1
如果register1和register2数值相等,则跳转到标签为L1的语句行。beq(branch if equal)
bne register1, register2, L1
如果register1和register2数值不相等,则跳转到标签为L1的语句行。beq(branch if not equal)
【例子】f,g,h,i,j五个变量依次对应$s0到 $s4的寄存器
if(i==j)f=g+h;else f=g-h;
MIPS代码:
bne $s3, $s4, Else
add $s0, $s1, $s2
j Exit
Else: sub $s0, $s1, $s2
Exit:
在if语句的结尾部分,需引入另一种分支指令,叫做无条件指令,遇到这种指令时,程序必须分支。MIPS将无条件分支指令命名为jump,简写成j(标签Exit在后面定义)
2.7.1循环
while(save[i]==k)
i+=1;
设i,k存放在寄存器$s3, $s5中,数组save的基址存放在寄存器 $s6中。
首先将save[i]读入一个临时寄存器中。在读入数组时,需要计算地址,即将i乘以4,可以采取左移2位实现乘法。由于是循环,在指令前增加标签Loop,可以在循环末端跳回该指令。
Loop:sll $t1, $s3,2
得到save[i]的地址,需要将 $t1与 $s6相加
add $t1, $t1, $s6
lw $t0, 0( $t1)
bne $t0, $s5, Exit
addi $s3, $s3,1
j Loop
Exit:
2.7.2case/switch语句
实现switch语句最简单方法是借助一系列条件判断,更有效的方法是转移地址表或转移表。
寄存器跳转指令为jr。
2.8计算机硬件对过程的支持
MIPS软件在为过程调用分配32个寄存器时遵循以下约定:
- $a0~ $a3:用于传递参数的4个参数寄存器。
- $v0~ $v1:用于返回值的两个值寄存器
- $ra: 用于返回起始点的返回地址寄存器
还有一条过程调用指令:跳转到某个地址的同时将下一条指令的地址保存在寄存器 $ra中。
跳转和链接指令格式:
jal ProcedureAddress
指令中的链接部分表示指向调用点的地址或链接,以允许过程返回到合适的地址。
跳转和链接指令:跳转到某个地址的同时将下一条指令的地址保存到寄存器$ra中的指令
返回地址:指向调用点的链接,使过程可以返回到合适的地址,在MIPS中它存储在寄存器 $ra中。
类似MIPS的计算机使用了寄存器跳转指令 jr ,用于case语句,表示无条件跳转到寄存器所指定的地址:
jr $ra
- 调用者(caller):调用一个过程并为过程提供必要参数值的程序。
- 被调用者(callee):根据调用者提供的参数执行一系列存储的指令,然后将控制权返回调用者的过程。
- 程序计数器(PC):包含在程序中正在被执行指令地址的寄存器。
2.8.1使用更多的寄存器
- 栈(stack):被组织成后进先出队列形式并用于寄存器换出的数据结构。
- 栈指针(stack pointer):指示栈中最近分配的地址的值,它指示寄存器被换出的位置,或寄存器旧值的存放位置。在MIPS中,栈指针是寄存器$sp。
- 压栈(push):向栈中增加元素。
- 出栈(pop):从栈中移除元素。
按历史惯例,栈“增长”是按地址从高到低的顺序进行的。所以将数据压栈时,栈指针值减少,而数据出栈时,栈指针增大。
int leaf_example(int g,int h,int i,int j)
{
int f;
f=(g+h)-(i+j);
return f;
}
汇编代码:
首先,参数g,h,i,j对应寄存器$a0、 $a1、 $a2, $a3,f对应 $s0。
- 程序从标号开始
- 保存过程中使用的寄存器($s0, $t0, $t1)。使用两个临时寄存器 $t0、 $t1。即将旧值“压栈”
leaf_example:
addi $sp, $sp,-12
sw $t1,8($sp)
sw $t0,4($sp)
sw $s0,0($sp)
add $t0, $a0, $a1
add $t1, $a2, $a3
sub $s0, $t0, $t1
add $v0, $s0, $zero #return f($v0 = $s0 +0)
lw $s0, 0($sp)
lw $t0, 4($sp)
lw $t1, 8($sp)
addi $sp,$sp,12 #弹出数据
jr $ra #跳转寄存器的返回地址
- $t0 ~ $t9:10个临时寄存器,在过程调用中不必被调用者保存
- $s0 ~ $s7: 8个保留寄存器,在过程调用中必须被保存(一旦被使用,由被调用者保存和恢复)
2.8.2嵌套过程
不调用其他过程的过程称为叶过程。
但实际中,主程序往往调用其他过程,其他过程又会调用其他过程。甚至还有递归过程,所以在调用非叶过程时要更加小心。
举例:主程序将参数3存入寄存器$a0,然后使用jal A调用过程A。过程A通过jal B调用过程B,参数为7,同样存入寄存器 $a0。由于A并未完成任务,所以在寄存器 $a0的使用上存在冲突。在寄存器 $ra保存的返回地址上也存在冲突。
解决方法:将其他所有需要保存的寄存器压栈,如将保存寄存器压栈一样。
【例题】演示嵌套过程的链接
int fact(int n)
{
if(n<1)return(1);
else return(n*fact(n-1));
}
参数n对应参数寄存器$a0。栈中需要保存两个寄存器,一个是返回地址,一个是 $a0:
fact:
addi $sp, $sp, -8
sw $ra, 4( $sp)
sw $a0, 0( $sp)
测试n是否小于1,如果n>=1,跳转L1.
slti $t0, $a0, 1 #如果n<1, $t0为1
beq $t0, $zero,L1 #如果 $t0的内容为0,跳转L1
如果n<1,将1置入值寄存器并返回。在0上加1,再将和存入 $v0。将栈中保存的值弹出,并返回地址:
addi $v0, $zero, 1
addi $sp, $sp, 8
jr $ra
如果n>=1,参数n-1,再次调用fact:
L1: addi $a0, $a0, -1
jal fact
接下来返回(n*fact(n-1)),所以我们需要旧参数和旧返回地址:
lw $a0, 0( $sp)
lw $ra, 4( $sp)
addi $sp, $sp, 8
mul $v0, $a0, $v0 #return n*fact(n-1)
jr $ra
2.8.3在栈中为新数据分配空间
栈还需要存储过程的局部变量,但这些变量不适用于寄存器,例如局部的数组或结构体。栈中包含过程所保存的寄存器和局部变量的片段称为过程帧或活动记录。
某些MIPS软件使用帧指针指向过程帧的第一个字。帧指针在一个过程中为局部存储器引用提供一个固定的基址寄存器。
2.8.4在堆中为新数据分配空间
C语言通过显式的函数调用在堆上分配和释放空间。malloc()在堆上分配空间并返回指向它的指针,free()释放指针指向的堆空间。
2.9人机交互(利用字节表示字符)
今天大多数计算机使用8位的字节来表示字符,也即ASCII码。
字节存储sb(store byte)指令:把一个寄存器最右边的八位取出来然后写到内存中。
lb $t0, 0( $sp)
sb $t0, 0( $gb)
字符通常被组合为字符数目可变的字符串。表示一个字符串的方式有三种选择:
- 保留字符串的第一个位置用于给出字符串的长度
- 附加一个带有字符串长度的变量
- 字符串最后的位置用一个字符来标识其结尾。
C语言使用第三种选择,用一个值为0(ASCII码中的null)的字节来结束字符串。
void strcpy(char x[],char y[])
{
int i ;
i = 0
while( ( x[ i ] = y [ i ] ) != ' \0 ' )
i += 1 ;
}
汇编代码:
设数组x,y的基地址在 $a0和 $a1中,i在 $s0中。
strcpy: addi $sp, $sp, -4 sw $s0, 0( $sp) #保留 $s0中的内容到内存 add $s0, $zero, $zero L1: add $t0, $s0, $a1
不用将i乘以4,因为是字符,所以y是字节的数组。
lbu $t2, 0( $t1)
读取y[i]中的字符,将字符放入 $t2中
add $t3, $s0, $a0 #x[i]的地址 sb $t2, 0( $t3) #将 $t2内容即y[i]存入x[i]的地址中 beq $t2, $zero, L2 #if y[i]==0,go to L2 addi $s0, $s0, 1 j L1 L2: lw $s0, 0( $sp) #restore old $s0 还原 $s0 addi $sp, $sp, 4 jr $ra
上例中,保存寄存器 $s0中的内容 ,在最后还原 $s0旧值。以便过程运行完返回调用者时,寄存器 $s0保留原来的数据。
2.10MIPS中32位立即数和寻址
2.10.1 32位立即数
尽管常数往往比较短,且适于16字段,但有时有比较大的常数,MIPS指令集中的读取立即数高位指令lui(load upper immediate)专门用于设置寄存器中常数的高16位。
【例题】
0000 0000 0011 1101 0000 1001 0000 0000
将上数加载到寄存器$s0的MIPS汇编代码:
高16位用十进制表示是61
lui $s0,61
此时,寄存器$s0中的值为:0000 0000 0011 1101 0000 0000 0000 0000
插入低16位,十进制表示为2304
ori $s0, $s0,2304
2.10.2 分支和跳转中的寻址
MIPS 跳转指令寻址采用最简单的寻址方式。称为J型,除6位操作码之外,其余位都是地址字段。
j 10000 #go to location 10000
2 | 10000 |
---|---|
6位 | 26位 |
跳转操作码的值为2,跳转地址为10000 | |
和跳转指令不同,条件分支指令除了规定分支地址之外还必须指定两个操作数。 |
bne $s0, $s1, Exit #go to Exit if $s0 != $s1
5 | 16 | 17 | Exit |
---|---|---|---|
6位 | 5位 | 5 位 | 16位 |
此时,出现一个问题,任何程序的地址不能大于216.解决方法:指定一个总是加到分支地址上的寄存器,这样分支指令的地址可按如下方式计算: | |||
程序计数器=寄存器+分支地址 | |||
程序计数器(Program Counter,PC)包含当前指令的地址,使用PC来作为增加地址的寄存器,我们可转移到离当前指令距离了±215个字的地方。几乎所有循环和if语句都远远小于216个字,这种分支寻址方式称为PC相对寻址。 |
【例】
beq $s0, $s1, L1
用两条指令替换上面的指令,使得L1可以有更远的转移距离。
bne $s0, $s1, L2
j L1
L2:
2.10.3 MIPS寻址模式总结
寻址模式(addressing mode)
寻址模式:
- 立即数寻址(immediate addressing):操作数是位于指令自身中的常数。
- 寄存器寻址(register addressing):操作数是寄存器
- 基址寻址(base addressing)或偏移寻址:操作数在内存中,其地址是指令中基址寄存器和常数的和。
- PC相对寻址(PC-relative addressing),地址是PC和指令中常数的和。
- 伪直接寻址:跳转地址由指令中26位字段和PC高位相连而成。
2.10.4 机器语言解码
2.11 并行与指令:同步
在任务之间需要互相协作,如某些任务写的结果是其他任务需要读取的值。此时,执行读任务的乙方要知道写任务什么时候完成了写操作,才能安全读回数据。
所以任务需要同步,否则就有发生数据竞争的危险。
利用加锁和解锁实现同步机制。采用加锁和解锁直接创立一个仅允许单个处理器操作的区域,叫做互斥区。
用原子交换原语来演示如何建立基本同步机制。这个原语是将寄存器中的一个值和存储器中的一个值互相交换。