Chp2 LAB Comparison of Instruction Sets
P0
0.1 安装虚拟机 Win Xp (32 bit)
- 首先打开VMware workstation 安装 雨林木风 WinXP SP3 安装版 YS8.0
安装过程截图如下:
- 安装VMwareTools,方便再虚拟机和真机之间传输文件,安装完成后重新启动。
0.2 安装和使用WinDXL
0.2.1 安装和配置
- 将WinDLX 文件夹拖动到桌面上,然后点击箭头所指示的图标
- 双击WinDLX图标后,会出现带有六个图标的主窗口。双击其中的一个图标,将显示其子窗口。
-
为了初始化模拟器,点击
File
菜单中的Reset all
菜单项,弹出一个“Reset DLX”对话框。然后点击窗口中的确认
按钮即可。
-
点击
Configuration
打开菜单,然后分贝点击Floating Point Stages
和Memory Size
菜单项,选择如下标准配置
0.2.2 程序的装载和模拟
-
选择
File
Load Code or Data
,窗口中会列出目录中所有汇编程序。然后选择prim.s
, 先点击select
再点击load
。 这是会出现弹窗,选择是(Y)
-
点击主窗口中的
Execution
开始模拟。在出现的下拉式菜单中,点击Single Cycle
或按F7键。这时可以分别打开子窗口进行查看
-
Pipeline
窗口中用图表形示显示了DLX的五段流水段和浮点操作(加/ 减,乘和除)的单元。
-
Code
可以看到代表存储器内容的三栏信息,从左到右依次为:
地址
(符号或数字)、命令的十六进制机器代码
和汇编命令
。这些不同颜色指明命令处于流水线的哪一段,与pipeline中的信息相对应。其他方框中带有一个“X”标志,表明没有处理有效信息。 -
Clock Cycle Diagram
显示流水线的时空图,当前的模拟正在第二时钟周期。 -
Register
显示寄存器的相关信息
P1
Write a prim.c with the same function as prim.s provided by WinDLX, analyze the assembly code for prim.c.
用与WinDLX提供的prim.s相同的函数编写prim.c,分析prim.c的汇编代码
1.1 分析WinDLX提供的prim.s
1.1.1 WinDLX 汇编语言简介
(1)伪指令:
.data [address]
标识下面的数据存放在数据区,address指示数据区的起始地址;
.text [address]
标识下面的代码存放在代码区,address指示代码区的起始地址;
.global label
使得标有label的代码可以被全局访问;
.word word1,word2,...
在存储器中顺序存放列出的字;
.byte byte 1 ,byte2,...
在存储器中顺序存放列出的字节;
.double number1,...
在存储器中顺序存放列出的双精度数;
.ascii "stringl ",...
在存储器中顺序存放列出的字符串,每个字符串均不会被自动加零结尾;
.asciiz "stringl ",...
在存储器中顺序存放列出的字符串,每个字符串会被自动加零结尾;
.space size
在存储器中空出size大小的区域;
.align n
使得后面的数据/代码地址低n位为О对齐。
(2)关键指令:
addi
: R[regb] <-- R[rega] + imm16 将寄存器a中的数值和立即数相加,结果保存在寄存器b中
seq
: seq R[regc], R[rega], R[regb] 就是比较寄存器a和寄存器b的数值,如果相等那么寄存器c中的值为1否则为0。
bnez
操作:bnez R[rega], label 如果R[rega]如果不为0,则跳转到lable处
1.1.2 prim.s源程序注释
该程序的主要功能是生成Count个质数,并存放在Table起始的地址空间中,其中每个质数占用4个字节单元。
找到质数的方法是枚举。value从第一个质数2开始,不断递增,判断value的值是否能被已经保存在table中的质数整除。如果可以,则不是质数,反之是质数,将value保存在table中,直到找到count个质数。
; *********** WINDLX Exp.2: Generate prime number table *************
;*********** (c) 1991 G¸nther Raidl *************
;*********** Modified 1992 Maziar Khosravipour *************
;-------------------------------------------------------------------
; 程序功能:生成Count个质数,并存放在Table表中。
;-------------------------------------------------------------------
.data ; 数据段标识
.global Count ;定义全局变量Count
Count: .word 10 ; count=10, 类型是字
.global Table ; 定义全局变量Table
Table: .space Count*4 ;空出4*count个单位地址空间,空间的首地址是Table (每个存放单元分配4个字节)
.text; 代码段标识
.global main; 标识main可以被全局引用
main:
; 初始化
addi r1,r0,0 ;将r0的值与0相加,结果赋给r1,r1保存的是索引index
addi r2,r0,2 ;同理r2的值为2,r2中保存的是当前的待检测的值value
;判断R2能不能被“Table中的数值”整除
NextValue: addi r3,r0,0 ;同理r3的值为0,保存的是遍历当前Table的索引i。
Loop: seq r4,r1,r3 ; seq操作:这里用来判断是否已经遍历到Table的末尾,结果布尔值存在R4中
bnez r4,IsPrim ;如果r4不等于0,跳转到IsPrim;即遍历完数组都没有可以把r2整除的,说明r2是素数
lw r5,Table(R3);从Table(R3)中读取到R5中;
divu r6,r2,r5; 无符号除法 R6 = R2/R5(其实整除)
multu r7,r6,r5; 无符号乘法 R7 = R6*R5
subu r8,r2,r7; 无符号减法 R8 = R2–R7
beqz r8,IsNoPrim; 如果r8等于0,说明可以被整除,不是素数。跳转到IsNoPrim处
addi r3,r3,4 ; r3+=4 继续增加索引i数值,遍历table
j Loop ;无条件跳转到Loop处,继续循环
IsPrim: ; 如果当前值是素数,
sw Table(r1),r2; 那么将 R2写入到table[r1]中
addi r1,r1,4 ;并且R1后移一个单位:r1+=4 指向下一个table单元
lw r9,Count; 将Count(10)放入R9
srli r10,r1,2; 逻辑右移2位;相当于r1除以4;因为R1每次循环增加4,因此与count比较时要除以4
sge r11,r10,r9; 判断R10是不是大于R9(count),如果是那么R11为1;
bnez r11,Finish; 如果是1(不为0),说明访问已经搜索完count个数,任务结束,跳转到Finish
IsNoPrim: ;如果当前值是不是素数,则检查下一个值
addi r2,r2,1 ; R2++
j NextValue ; 跳转到NextValue处,检查下一个值
Finish: ;结束
trap 0 ; : trap 0通知WINDLX模拟器程序结束
1.2 实现相同功能的prim.c
1.2.1 算法流程
算法流程图如下:
1.2.2 C语言实现prim.c
具体的prim.c 实现如下:
#include <stdio.h>
int Count = 10; // 最多保存的质数是10个
int Table[10]; //存储查找到的质数
int main() {
int index = 0; // table的索引从0开始
int value = 2; // 待检测的值
while (index < Count) { // 判断是否已经找到了count个质数
int i;
for (i = 0; i < index; i ++) { // 判断table中是否存在一个值,可以整除当前待检测的值
if (value % Table[i] == 0) { // 如果可以被整除,则不是素数
// Is not Prime
value++;
break;
}
}
// Is Prime
Table[index] = value; // 反之,是素数, 将其保存在table中
index ++;
value++;
}
return 0;
}
P2
Observe the memory areas in bytes for the prime numbers given by the computer where prim.c running. Analyze what you have seen.
观察prim.c运行的计算机给出的质数的内存区域的字节数。分析你所看到的。
2.1 本地电脑结果分析
- 在visual Studio 上打开prim.c 代码,打上断点,选择【调试】,【开始调试】
- 选择【调试】,【窗口】,【反汇编】
- 打开内存
Ctrl+Alt+M,1
- 然后打开内存,查看Table为起始地址的内存空间。可以看到int型的整数分配了4个字节单元(32bits), 从小到大依次存放对应质数的16进制数。
大端存储模式:数据的低位保存在内存中的高地址中,数据的高位保存在内存中的低地址中;
小端存储模式:数据的低位保存在内存中的低地址中,数据的高位保存在内存中的高地址中;
如下图所示,反应了第一个质数2在不同的存储模式下的内存情况。很显然,我们的实验结果表明,采用的是后者——小端存储模式。
2.2 WinDLX结果分析
- 首先点击
Memory
,Symbols
找到Table的地址为0x00001004
-
然后
F7
单步执行指令,同时观察时钟周期,指令流水,和代码的运行情况
-
点击
Memory
,display
查看内存空间地址。 然后查询第一部得到的Table地址(即0x00001004)
- 观察存储的质数,一个质数占4个字节。其中低字节存储在地地址,高字节存储在高地址,因此为小端存储方式。
P3
交叉编译针对RISC-V目标的prim.c,或者用RISC-V汇编语言编写prim.c的汇编代码,并在RISC-V在线模拟器(www.kvakil.me/venus/)中运行。观察由RISC-V模拟器给出的素数的内存区域的字节数
3.1 RISC-V 简介
- RISC-V 寄存器集介绍
- 常见汇编提示符(assemble directives)
.text:进入代码段。
.align 2:后续代码按22字节对齐。
.globl main:声明全局符号“main”。
.section .rodata:进入只读数据段
.balign 4:数据段按4字节对齐。
.string “Hello, %s!\n”:创建空字符结尾的字符串。
.string “world”:创建空字符结尾的字符串。
-
内存分配
RV32I为程序和数据分配内存。图中的顶部是高地址,底部是低地址。在RISC-V软件规范中,栈指针(sp)从0xbffffff0开始向下增长;程序代码段从0x00010000开始,包括静态链接库;程序代码段结束后是静态数据区,在这个例子中假设从0x10000000开始;然后是动态数据区,由C语言中的malloc()函数分配,向上增长,其中包含动态链接库。
-
常用指令
下面列举了编写prim.s代码需要用到的所有指令格式功能和用法。
3.2 RISC-V 实现prim.s
.data
Count: .word 10 # 定义全局变量Count=10
Table:
.word 0 # 存放结果的地址空间
.text
main:
lw a0,Table # a0存放index的值,初始时a0为table的首地址
addi a1,zero,2 # a1存放的是当前待检测的Value值
addi a4,zero,0 # 当前table中存放的元素
lw a5,Count # a5=Count
NextValue:
addi a3,zero,0 # a3存放的是遍历当前Table的索引i
Loop:
beq a0,a3,IsPrim # 当前已经遍历到了末尾也被被整除,说明是素数
lw a6,0(a3) # a6存放当前遍历的值 table[i]
rem a7,a1,a6 # a7= a1 % a6
beq a7,zero, IsNotPrim # 如果能被整除,说明不是素数
addi a3,a3,4 # 否则遍历下一个 i+=4
j Loop # 跳转到Loop
IsPrim: # 如果当前值是素数
sw a1,0(a0) # 则要把a1存放到table[a0]
addi a0,a0,4 # 索引向后移动4个字节
addi a4,a4,1 # table中的元素个数加1:a4++
beq a4,a5,Finish # 如果a4==a5,说明已经找到了count个素数,结束程序
IsNotPrim: # 如果当前值不是素数
addi a1,a1,1 # 当前值加1, 检查下一个数
j NextValue # 跳转到NextValue处,检查下一个值
Finish:
jalr zero, 0(ra) # 返回main
3.3 结果分析
将上述代码在RISC-V在线模拟器(www.kvakil.me/venus/)中运行。观察由RISC-V模拟器给出的素数的内存区域的字节数,每一个质数占用4个字节。低字节存放在低地址处,高字节存放在高地址处,因此可以判定为小端存储。
P4
交叉编译MIPS目标的prim.c,或者用MIPS汇编语言编写prim.c的汇编代码,在Mars模拟器上运行。观察Mars模拟器给出的质数的存储区域的字节数。
4.1 安装Mars 模拟器
-
然后一路下一步安装即可
-
初步熟悉Mars模拟器的使用
4.2 MIP简介
-
寄存器
(1)两个特殊寄存器:
$0
:不管你存放什么值,其返回值永远是零。$31
:永远存放着正常函数调用指令(jal)的返回地址。(2)
$at
:由编译器生成的复合指令使用,
(3)
$v0, $v1
:用来存放一个子程序 (函数) 的非浮点 运算的结果或返回值。如果这两个寄存器不够存放 需要返回的值,编译器将会通过内存来完成。
(4)
$ a0-a3
:用来传递子函数调用时前4个非 浮点参数。
(5)
$ t0-t9
:依照约定,一个子函数可以不用保 存并随便的使用这些寄存器。在作表达式计算时,这些寄存器是非常好的暂时变量。当调用一个子函数时,这些寄存器中的 值有可能被子函数破坏掉。所以也是最不安全的。
(6)
$ s0-s8
:依照约定,子函数必须保证当函数返回时这些寄存器的内 容必须恢复到函数调用以前的值, 或者在子函数里不用这些寄存器或把它们保存 在堆栈上并在函数退出时恢复。 这种约定使得这些寄存器非常适合作为寄存器变量、 或存放一些在函数调用期间必须保存的原来的值。(类比:x86汇编中的函数序言和函数尾声)
(7)
$ k0, k1
:被OS的异常或中断处理程序使 用。被使用后将不会恢复原来的值。因此它 们很少在别的地方被使用。
(8)
$gp
:如果存在一个全局指针,它将指向运行时决定 的静态数据(static data)区域的一个位置。这意味 着,利用gp作基指针,在gp指针32K左右的数 据存取,系统只需要一条指令就可完成。如果没有全局指针,存取一个静态数据区域 的值需要两条指令:一条是获取有编译器和loader决定好的32位的地 址常量。另外一条是对数据的真正存取。为了使用
$ gp
, 编译器在编译时刻必须知道 一个数据是否在$ gp
的64K(上下32k)范围之内。并不是所有的编译和运行系统支持gp的使用。(9)
$ sp
:堆栈指针的上下需要显 式的通过指令来实现。因此 MIPS通常只在子函数进入和 退出的时刻才调整堆栈的指针。 这通过被调用的子函数来实现。SP通常被调整到这个被调用 的子函数需要的堆栈的最低的 地方,从而编译器可以通过相 对sp的偏移量来存取堆栈上 的堆栈变量。
-
指令
MIP的指令和上文提到的RISC-V的指令几乎一样,它们采用的都是精简指令系统计算结构(RISC), 在下面的编程实例中可以看到,MIP编写的汇编程序可以非常方便的由RISC-V实现的程序改写(只需要改写寄存器的写法)
4.3 MIP实现prim.s
.data # 数据段
count: .word 10 # 定义count=10
table: .space 40 # 定义table,存放素数的地址空间
.text
main:
la $t0,table # t0存放index的值,初始时t0=0
la $s0,table # s0存放table的地址
addi $t1,$zero,2 # t1存放的是当前待检测的Value值,初始值为2
addi $t4,$zero,0 # t4 记录当前table中已经存放的元素个数
lw $t5,count # t5=count,最多保存的素数个数
Nextvalue:
addi $t3,$s0,0 # t3存放的是遍历当前Table的索引i
Loop:
beq $t0,$t3,IsPrim # 当前已经遍历到了末尾也被被整除,说明是素数
lw $t6,0($t3) # t6存放当前遍历的值 table[i]
rem $t7,$t1,$t6 # t7= t1 % t6
beq $t7,$zero, IsNotPrim # 如果能被整除,说明不是素数
addi $t3,$t3,4 # 否则遍历下一个 i+=4
j Loop # 跳转到Loop
IsPrim:
sw $t1,0($t0) # 则要把a1存放到table[a0]
addi $t0,$t0,4 # 索引向后移动4个字节
addi $t1,$t1,1 # 检查下一个数
addi $t4,$t4,1 # table中的元素个数加1:a4++
beq $t4,$t5,Finish # 如果a4==a5,说明已经找到了count个素数,结束程序
j Nextvalue
IsNotPrim:
addi $t1,$t1,1 # 当前值加1, 检查下一个数
j Nextvalue # 跳转到NextValue处,检查下一个值
Finish:
li $v0, 10
syscall
4.3 结果分析
由下图的结果可以看到,一个质数占用4个字节的空间。比如说第一个质数2,它的存储格式为0x00000002
, 在连续的地址空间共存储了10个质数,验证了程序的正确性。
为了进一步探究存储模式,在数据段添加如下的一行字符串
msg: .asciiz "Hello world"
由下图可以看出,其中低字节在低地址处,高字节在高地址处,因此属于小端存储的方式。
P5
不同指令集的对比和总结
-
存储模式
上述三种指令集均为小端存储模式。
-
整数计算
RISC-V 中没有字节或半字宽度的整数计算操作。操作始终是以完整的寄存器宽度。内存访问需要的能量比算术运算高几个数量级。因此低宽度的数据访问可以节省大量的能量,但低宽度的运算不会。ARM-32 具有一个不寻常的功能,对于大多数算术逻辑运算中的一个操作数,你可以选择对它进行移位。尽管这些指令的使用频率很低,但它使数据路径和数据通路更加复杂。与此相对的是,RV32I 提供了单独的移位指令。RV32I 也不包含乘法和除法,它们包含在可选的 RV32M 扩展中。与x86-32 不同,即使处理器没有添加乘除法扩展,完整的 RISC-V 软件栈也可以运行,这可以缩小嵌入式芯片的面积。MIPS-32 汇编程序可能用一系列移位以及加法指令来替换乘法,以提高性能,这可能会使程序员看到处理器执行了汇编程序中没有的指令,进而造成混淆。RV32I 可以忽略了这些特性:循环移位指令和整数算术溢出检测,这两个特性都可以用若干条 RV32I 指令来实现。
-
寻址方式
与 x86-32 不同,RISC-V 没有特殊的堆栈指令。将 31 个寄存器中的某一个作为堆栈指针,标准寻址模式使用起来和压栈(push)和出栈(pop)类似,并且不增加 ISA 的复杂性。
与 MIPS-32 不同,RISC-V 不支持延迟加载(delayed load)。与延迟分支的设计相似,为了更好的适应五级流水线,MIPS-32 重新定义了 load 指令的语义,load 上来的数据在 load 指令两个指令后才可用。
-
条件分支
RISC-V 去掉了 MIPS-32,Oracle SPARC 等指令集中的延迟分支特性等。对RISCV省略了 x86-32 中的循环指令:loop,loope,loopz,loopne,loopnz。