目录
1.计算机的基本结构
计算机的经典结构如下:
要计算机服从指挥,就要遵循计算机的语言来与计算机交流。
指令:计算机语言的基本单词
指令集:一台计算机中全部的指令称为计算机的指令集
计算机指令集有两种形式:人们编程书写的形式和计算机所能识别的形式
目前使用范围最广的指令集有较早的MIPS ,后来使用比较广泛的ARM 和X86指令集
2. 计算机硬件操作
2.2 计算机基本硬件操作
硬件必须有算术功能
硬件设计4条基本原则:
1. 简单源于规整:汇编语言一行最多一条指令,
2.3 计算机硬件的操作数
计算机的算术运算的操作数是受限的,必须来自于寄存器。寄存器是由硬件直接构成,数量有限,是计算机硬件设计的基本元素。大量的寄存器会使时钟周期变长,需要更远的电信号传输距离。因此硬件设计基本原则2:越少越快:寄存器数量越少,执行速度越快
2.3.1 存储器操作数
由于计算机程序中有的数据结构有很多数据变量,比如说数组,这些数据结构的数据变量远大于寄存器数量,计算机要如何访问和存储这些数据呢?
在文章开始的第一个计算机结构图里面,处理器内部只保存少量的待处理的数据,大部分的数据还是需要从存储器中输入,因此计算机是如何把数据从存储器中读取到寄存器中的呢?这里就要用到数据传输指令。数据传输指令会将数据从存储器读取到寄存器中。
由于寄存器数量有限,因此编译器会将常用的数据存储到寄存器中,将不常用的变量存在存储器汇中,这样可以增加指令的执行速度。
2.3.2 常数或立即数操作数
程序会中经常使用常数,按上面已知的操作方法,就需要把常数从存储器中读取到寄存器中,这样需要使用到取数指令,效率会有些低。另外一种不需要使用取数指令的方法是使用快速加法指令,叫做加立即数(add immediate或者addi)。
这里出现了硬件设计的第3条原则:加速执行常用操作。这样可以加快指令执行速度,并且能耗较低。
2.4 有符号数和无符号数
计算机中数值是采用二进制来表示的,比如十进制的123用二级制表示是1111011。
计算机程序对正数和负数都要进行操作,因此需要区分出正负数,有一种方案是增加符号位来表示正和负,但是是这个符号位的位置存在争议,是放数值的最左边还是最右边?最终采用了一个硬件简单的表达方式,前导位为0表示正数,前导位为1表示负数,这种表示有符号的二进制的方法叫做二进制补码。二级制补码转换成十进制的方法为:符号位乘以,其他位数按照二级制换算十进制的方式来计算:
上图中有个错误,红线部分应该是, 这是一个比较常规的换算方式。对于二级制补码有个便捷的处理方式:取反,详细来说就是对补码的每一位都取反,0变1,1变0。这个捷径是基于这个事实一个数与它按位取反的数字相加,一定是1111111.....11111,即十进制中的-1。因此
,所以
。所以取反的捷径是按位取反再加1。
2.5 计算机中的指令表示
计算机中的指令和人操作计算机的指令是有一定差别的,指令在计算机中就是一串高低电平序列,可以对应于二进制的0101这种,但是由于指令较长,MIPS 指令有32位,又由于计算机数据大部分都是4位二进制符号,因此可以用16进制来表示计算机指令。
因为计算机指令大部分都是寄存器有关系,因此需要将寄存器映射成数字,方便编程时使用。比如MIPS 汇编语言中寄存器$s0~$s7映射到寄存器16~23,$t0~$t7映射到寄存器8~15,。这样就可以直接使用$s0来表示寄存器16,而不用在程序中写寄存器的地址。
举例:
将MIPS 指令add $t0,$s1,$s2 翻译成机器指令
其十进制表示为:
0 | 17 | 18 | 8 | 0 | 32 |
机器指令分若干段,第一个字段和最后一个字段组合起来的作用是告诉计算机要执行加法运算,而中间的第2,3,4字段表示要访问的三个寄存器,第5个字段缺省。
MIPS 字段命名如下:
2.6 逻辑操作
- 移位。将一个二级制的数值全部左移或者右移n位,然后把空出来的全部填0,这样就完成了将二级制数值左移或者右移n位。
- 按位与(AND):该操作是仅当两个操作数均为1时结果才为1
- 按位或(OR):该操作是当有一个操作数为1的时候,结果就为1
- 按位取反(NOT):该操作只有一个操作数,0变1,1变0,但是为了保持三操作数的格式,MIPS的设计者引入或非NOR(NOT OR)指令来取代OR,如果一个操作数为0,那么对于另外一个操作数而言,结果等价于:A NOR 0 = NOT (A OR 0) =NOT(A),这样可以完成取反操作。
2.7 决策指令
高级语言的编程中有if-else来完成决策选择。汇编中也有两种指令来完成决策选择:beq(branch if euqal), bne(branch if not equal)。
- beq register1,register2, L1: 如果register1,register2相等,则跳转到L1来执行
- bne register1,register2, L1:如果register1,register2不相等,则跳转到L1来执行
2.8 计算机硬件对函数的支持
函数是编程过程中常用的一种方法,可以增加代码的可读性和复用性。程序在执行过程中如果调用到了函数,那么程序就会陷入到函数中去执行函数代码,那么在执行函数代码之前就需要先保存程序的当先的状态,待函数执行完成之后返回执行之前的状态。
在函数运行期间,程序需要遵循以下六个步骤:
- 将参数放在可访问的位置
- 将控制权转移给函数
- 获得过程所需的存储资源
- 执行请求的任务
- 将结果保存在调用程序所能访问的位置
- 将控制返回调用的初始点,因为一个程序可能会多次调用同一个函数
由于寄存器是访问数据最快的方法,因此我们希望尽可能的使用寄存器,因此MIPS会将寄存器分为三类:
- 用于保存传递参数的寄存器
- 用于保存函数返回值的寄存器
- 用于函数返回初始点的寄存器
除了分配寄存器,MIPS中还有跳转指令,可以直接跳转到指定的寄存器,主要用于函数的返回地址。
2.8.1 使用更多的寄存器
由于寄存器的数量是有限的,有的函数有时需要用到多与寄存器数量的参数。而调用函数的程序必须要把寄存器恢复到调用之前的存储值的状态。有一种方法是将寄存器中的值换出存储在存储器中,当需要使用时再从存储器加载到寄存器中。换出寄存器中数据的最佳的数据结构是——栈(先进后出)。栈需要一个指针来指向栈中最新的分配地址,以指示下一个入栈数据的位置。栈指针是按照一个寄存器的大小来调整的。向栈中存储数据叫做压栈,从栈中移除数据叫出栈。
参变量g,h,i,j分别对应参数寄存器$a0,$a1,$a2,$a3,f 对应$s0。
编译过程为:
- 先将旧参压栈,也就是在栈中建立三个字的存储空间,并将数据存入
调用之前,之中,之后的栈里情况是:
2. 编译函数体的内容
3. 返回值存储到寄存器中
4. 在函数结束执行返回前,区中寄存器中的旧值
5. 最后根据跳转寄存器中的地址跳转返回
2.8.2 在栈中为新数据分配空间
栈中还需要存储函数的局部变量,但是这些变量不能存储在寄存器中,例如局部的数组或者结构体。栈中包含函数所保存的寄存器和局部变量的片段称为函数帧或者活动记录。下图显示了调用函数前,中,后的栈中的分配情况。
2.8.3 在堆中为新数据分配空间
除了动态变量对函数来说是局部有效之外,C语言程序员还需要对静态变量和动态数据结构提供空间。下图给出了MIPS中内存分配规则。栈是从高端地址开始向下分配空间,内存低端是的第一部分是保留地址,然后是MIPS机器码的第一部分,也就是正文段。正文段之上的代码为静态数据段,是存储常量和其他静态数据变量的地方。虽然数组的长度一般是固定的,但是链表的长度会随着需求来增加或者减少,所以不能很好的匹配静态数据段,那么这类的数据段称为堆(heap),一般在存储器中存放在静态数据段之后。
C语言通过显示调用来分配和释放内存空间,因此一定要注意释放内存,不然会造成内存溢出。malloc()在堆上分配空间并返回指向它的指针,free()释放分配的堆空间。
2.9 并行与指令:同步
当任务之间相互独立的执行互不干扰的时候产生的问题的概率比较小。但是当任务之间需要协作的时候,比如说程序1处理完的数据是程序2需要输入的数据,那么程序2就需要等到程序1完成之后开始运行,那么问题就出现了,程序2如何才能得知程序1运行完成了呢?这个就需要引入概念同步(synchronization),否则会出现数据竞争的危险,进而导致程序读取数据错误而得到错误的结果。
在计算机中同步机制要依赖于硬件提供的同步指令。这个指令可由用户软件调用。贴一段原文参考: