所谓指令集,指的就是计算机的全部指令,这章节将以MIPS指令集作为学习对象,如果是x86指令集,还请参考《深入理解计算机系统》。MIPS指令集在嵌入式芯片市场占有相当大的市场份额,应用在包括用户电子,网络/存储设备,相机,打印机等等。学习MIPS指令集,其实最终只需要掌握几张表就足够了,要能看懂学会当成API文档进行查阅。本章节并不会讲解太多指令怎么用,我们的关注点更多的落在如何应用MIPS指令进行计算机的设计。
文档下载地址:MIPS Green Sheet
所有的算术运算指令,格式都是十分规整,以add a, b, c 为例,其表示a = b + c,这种设计符合硬件简单性原则,即规范化的设计使得硬件实现简单,简单让高性能变得低成本。
一般硬件中的操作数,存在于三个地方,在寄存器中,在内存中,还有就是直接包含在指令当中,直接包含在指令当中的操作数,我们把它叫做立即数。
注意:所有的算逻运算,涉及到的读写都是使用寄存器,并不直接使用内存。
对于32位的MIPS机来讲,有32个寄存器,每个寄存器有32位,编号为0~31,32位的数据成为word(字)。对汇编器来讲,为了方便阅读,分为$t0,$t1,…,$t9,作为临时变量存储;$s0,$s1,…,$s7,作为已存储变量存储。MIPS为何不使用更多的寄存器呢?大量的寄存器可能会使得时钟周期变长,因为电信号传输更远的距离必然花费更长的时间。设计者必须在更多寄存器和加快时钟周期之间进行权衡。这就是越小越快的设计原则。
内存用于存储一些当前并不经常使用、容量较大的数据,如数组、结构体、动态数据。内存操作辅助算逻运算,可以将操作数从内存load到寄存器,也可以将操作数从寄存器store到内存。MIPS32中,以8位组成每个byte;操作数取哪一个,由地址标识。此外,MIPS32中,words在内存中是对齐的,地址必须是4的倍数,采用大端方式进行编址。大端和小端的区别,其实已经在x86的学习中涉及。假设现在有数:0x00 00 30 39,下面是大端法和小端法的表示:
对处理器而言,内存相较于寄存器访问速度要慢很多。
对于比较小的立即数,我们可以直接将它放在指令里面访问,这样可以避免到内存中取数。可想而知,一个load指令,肯定要比取立即数,花费更多的时钟周期。如果我们能够根据使用的频率,来定义常数,也是加速大概率事件的一个好方法。
这部分的内容过于基础了,这里简单点一下就可以。不过有关于数制之间的转换和甄别,是一定要掌握的。
无符号整数,实质上就是把最高位当作数值位看待。
无符号整数,实质上就是把最高位当作符号位看待。常见的一些数值范围应该要熟悉:C语言各种数据类型取值范围
符号扩展要做的事情,就是使用更多的位来表示一个数字,同时数值保持不变。在MIPS指令中,有一些指令会进行符号扩展:
对于无符号数扩展来说,我们仅需要补0;对于有符号数,我们仅需要补1。
指令和数据在计算机中的表示并没有什么差别,都是二进制编码表示。MIPS中有两种指令的格式。
所谓R格式指令,可以理解为指令只和寄存器打交道。R格式指令如下:
例如,指令add $t0, $t1, $t2
的指令格式为:
查表,对应翻译到二进制为:
所谓I格式指令,可以理解为指令内包含了立即数(Immediate number)。I格式指令如下:
上面我们已经看到了,MIPS最终都把不同的指令归结为上面两种形式,不同的类型指令采用不同的解码方式,但是都是32位的统一指令长度,且尽可能保证指令格式相似,这体现了优秀的设计需要适宜的折中方案。
分支语句,其实就是在一个语句块的前后加上bne,beq等判断条件,条件成立则往下执行或跳转到某个label,继续执行相应的语句。而循环结构,其实就是在一定的条件内,让语句块形成一个闭环,反复执行这个语句块。这里面不讲太多,到最后综合举例的时候再看看就好了,其实和x86的语句结构是一样的。
在这个部分,我们还需要掌握的一个概念就是“基本块”,所谓基本块,指的是一个指令序列,这个指令序列内部没有跳出的指令,也没有被跳转到的指令。一些高级处理机能够加速基本块的执行。
最后提一点,在以字节寻址的MIPS机中,bne、beq机器指令的立即数(被设计者规定为字地址,非字节地址),都要乘上4才能得到最终的地址。
ISA层面上的过程调用还是值得我们留意的,对MIPS来说,调用过程的步骤,和其他ISA并无太大差别:
对于MIPS32来说,一些寄存器的用途如下:
过程的调用一般用到指令:
过程调用中一个比较有意思的例子是嵌套调用,要实现嵌套调用,一定得用到栈这种结构。举斐波那契数的计算如下:
斐波那契的递推公式是:f(n) = f(n-1) + f(n-2),初始f(0) = 1,f(1) = 1,规定n ≥ 2
其MIPS的汇编代码为:
fact:
addi $sp, $sp, -8 # 栈中开辟8字节空间
sw $ra, 4($sp) # 前4个字节保存返回地址
sw $a0, 0($sp) # 后4个字节保存过程参数,这里就是n
slti $t0, $a0, 1 # 测试一下是否到达了边界
beq $t0, $zero, L1 # 还没到边界,跳转L1,继续递归
addi $v0, $zero, 1 # 若到边界,返回1
addi $sp, $sp, 8 # 出栈
jr $ra # 返回上层调用
L1:
addi $a0, $a0, -1 # 准备新的参数
jal fact # 更深一层调用
lw $a0, 0($sp) # 这里是调用出口,先取得该层的n
lw $ra, 4($sp) # 再取得该层的将来的返回地址
addi $sp, $sp, 8 # 出栈
mul $v0, $a0, $v0 # 将n和递归过程得到的结果相乘
jr $ra # 返回上层调用
了解计算机硬件对过程的支持,还需要了解一下简单的内存布局示意:
32位立即数用的比较少,毕竟大部分常数都比较小。MIPS给我们提供了一些指令可供操作。
构造一个32位立即数分两步,一是构造高16位,二是构造低16位:
lui $s0, 61
ori $s0, $s0, 2304
这个问题是很经典的问题,对一个简单Hello World程序在机器上的执行过程进行探索,我们能够漫游整个计算机系统。一般来说,现代计算机系统上,程序的执行流程是:
前面关于预处理和编译成asm就不用多说了,我们关注一下目标模块如何生成:
首先,一个单独的.o文件包含了本模块的基本信息,一般包含:
一个.o文件单打独斗行不通,需要其他的.o模块进行链接,才可以构造出一个可执行程序,例如输出就还需要用到printf.o文件,链接一般分两个阶段,即符号解析和重定位,这个阶段还涉及到合并相同内存段,虚拟内存空间映射等操作。
构造出来的可执行文件是存在于磁盘上的,我们需要加载它才能运行,加载的一般步骤如下;
以上就是程序运行的一次简单漫游。这里我们还要提点一下Java程序和JVM,实在太重要了。
下图展示了Java翻译和运行步骤:
Java程序首先被编译成Java字节码指令集,JVM执对Java字节码文件进行解释,解释的优势就移植性强,从手机到网络浏览器,都有JVM的身影,劣势就是性能较差,与C程序相比存在10倍的性能差异。
为了保持可移植性,同时提升性能,开发Java下一阶段的目标是实现程序执行的同时可以翻译,叫JIT(Java即时编译器),JIT能够在运行Java时选择性地把一些方法编译成宿主机上地本地机器语言。
之所以了解这些架构,主要还是为了拓宽一下视野(当然,某年腾讯的面试也问到了这些架构相关的话题)
ARM、 MIPS 、X86三大架构对比
strcpy:
addi $sp, $sp, -4 # adjust stack for 1 item
sw $s0, 0($sp) # save $s0
add $s0, $zero, $zero # i = 0
L1:
add $t1, $s0, $a1 # addr of y[i] in $t1
lbu $t2, 0($t1) # $t2 = y[i]
add $t3, $s0, $a0 # addr of x[i] in $t3
sb $t2, 0($t3) # x[i] = y[i]
beq $t2, $zero, L2 # exit loop if y[i] == 0
addi $s0, $s0, 1 # i = i + 1
j L1 # next iteration of loop
L2:
lw $s0, 0($sp) # restore saved $s0
addi $sp, $sp, 4 # pop 1 item from stack
jr $ra # and return
指令集总结:
MIPS指令 | 名称 | 格式 |
---|---|---|
加 | add | R |
减 | sub | R |
加立即数 | addi | I |
取字 | lw | I |
存字 | sw | I |
取半字 | lh | I |
取无符号半字 | lhu | I |
存半字 | sh | I |
取字节 | lb | I |
取无符号字节 | lbu | I |
存字节 | sb | I |
取链接字 | ll | I |
存条件字 | sc | I |
取立即数高位 | lui | I |
与 | and | R |
或 | or | R |
或非 | nor | R |
与立即数 | andi | I |
或立即数 | ori | I |
逻辑左移 | sll | R |
逻辑右移 | srl | R |
相等跳转 | beq | I |
不相等跳转 | bne | I |
小于置位 | slt | R |
小于立即数置位 | slti | I |
小于无符号立即数置位 | sltiu | I |
跳转 | j | J |
跳转至寄存器指定位置 | jr | R |
跳转和链接 | jal | J |
MIPS伪指令 | 名称 | 格式 |
---|---|---|
移位 | move | R |
乘 | mult | R |
乘立即数 | multi | I |
取立即数 | li | I |
小于跳转 | blt | I |
小于等于跳转 | ble | I |
大于跳转 | bgt | I |
大于等于跳转 | bge | I |