系列文章目录
目录
计算机硬件对过程的支持
过程:更具提供的参数执行一定任务,并返回执行结果的存储子程序,是程序员进行结构化编程的工具
遵循步骤
- 将参数放在过程可以访问的寄存器里
- 将控制转移给过程
- 获得过程所需要的存储资源
- 执行过程的操作(请求的任务)
- 将结果的值放在调用程序可以访问到的寄存器
- 将控制返回到调用点
如上所述,寄存器是计算机中保存数据最快的位置,所以我们希望尽可能多地使用寄存器。MIPS在为过程调用分配32个寄存器时遵循以下约定:
- $a0~$a3:用于传递参数的4个参数寄存器
- $v0~$v1:用于返回值的两个值寄存器
- $ra:用于返回起始地点的返回地址寄存器
过程调用指令
过程调用:跳转和链接指令
jal ProcedureLabel
- 跳转到目标地址
- 同时将下一条指令的地址放在寄存器$ra中
过程返回:寄存器跳转
jr $ra
- 拷贝$ra到程序计数器
- 也被用于运算后跳转
- 例如用于case/switc分支语句
调用程序或称为调用者,将参数放在$a0~$a3,然后使用jal X跳转到过程X(被调用者)。被调用者执行运算,将结果放在$v0和$v1,然后使用jr $ra 指令将控制返回给调用者
在存储程序概念中,使用一个寄存器来保存当前运行的指令地址时绝对必要的。这个寄存器叫做程序计数器(program counter),在MIPS体系结构中缩写位PC。
jal指令实际将PC+4 保存在寄存器$ra中,从而将链接指向下一条指令,为返回做好准备(预测思想)
过程使用的寄存器
使用更多寄存器
假设对于一个过程,编译器需要使用多余4个参数寄存器和两个返回值寄存器。由于在任务完成后必须消除踪迹,因此调用者使用任何寄存器都必须恢复到过程调用前所存储的值。这种情况可以看成需要将寄存器换出到存储器的一个例子
换出寄存器最理想的结构是栈。栈需要一个指针指向栈中最新分配的地址,以指示下一个过程放置换出寄存器的位置,或是寄存器旧值的存放位置。在每次寄存器进行保存或恢复时,栈指针以字为单位进行调整。MIPS软件为栈指针准备了第29号寄存器$sp。将数据放入栈叫做压栈,从栈中移除数据称为出栈。
不调用其他过程(叶过程)例子
int leaf_example (int g, h, i, j)
{ int f; f = (g + h) - (i + j);
return f;}
- 参数 g, …, j 在 $a0, …, $a3中
- f 在 $s0 (因此,需要存储 $s0 到堆栈)
- 结果在$v0
嵌套过程
- 叶过程:不调用其他过程
- 嵌套过程:过程调用其他过程
- 对于嵌套调用,调用需要存储到堆栈的信息:
- 它的返回地址
- 调用后还需要用的任何参数寄存器和临时存储器
- 调用后返回。寄存器会从堆栈中恢复
示例C code:
int fact (int n)
{
if (n < 1) return 1;
else return n * fact(n - 1);
}
- 参数 n 放在 $a0
- 结果放在 $v0
C语言包括两种储存方式:动态的(automatic)和静态的(static)。动态变量位于过程中,当过程推出时失效。静态变量在进入和退出过程时始终存在。在所有过程之外声明的C变量,以及声明时使用关键字static的变量都被视为静态的,其余的变量都被视作动态的。为了简化静态数据的访问,MIPS保留了全局指针($gp)
保留 | 不保留 |
保存寄存器:$s0~$s7 | 临时寄存器:$t0~$t9 |
栈指针寄存器:$sp | 参数寄存器:$a0~$a3 |
返回地址寄存器:$ra | 返回值寄存器$v0~$v1 |
栈指针以上栈 | 栈指针一下栈 |
在栈中为新数据分配空间
栈中包含过程所保存的寄存器和局部变量的片段称为过程帧或活动记录
帧指针$fp:指向给定过程中保存的寄存器和局部变量的值
MIPS内存布局
- 正文:程序代码
- 静态数据:全局变量
- eg:C语言静态变量,常数数组和字符串
- $gp初始化地址,允许段内的±偏移
- 动态数据:栈
- 栈:自动存储
字节/半字节操作
- lb(load byte)指令从内存中读出一个字节,并将其放在一个寄存器最右边的8位
- sb(store byte)指令把一个寄存器最右边的8位取出来然后写到内存中
eg:
lb $t0 , 0($sp)
sb $t0 , 0($sp)
- lh(load half)指令从存储器读出半字,然后将其放在寄存器的最右边16位。读取半字指令lh也将半字当作有符号数并进行符号扩展,以填充寄存器左侧的16位
- lhu将半字当作无符号数
MIPS中32位立即数和地址寻址
32位立即数
MIPS指令集中的读取立即数高位指令lui(load upper immediate)专门用于设置寄存器中常数的高16位,允许后续指令设置常数低16位
分支和跳转中的寻址
跳转(j和jal)的目标地址可以在代码段的任何位置
- 指令除op外,指令其他字段都是地址
- 直接跳转到地址,地址为绝对地址
- PC是32位,高四位来自其他地方,低28位来自跳转指令,PC = PC(高4位)|(address*4)(左移两位,低28位)
- 注意设计程序时,跳转界限:256MB(6400万条指令)
条件分支除了规定分支外还必须指定两个操作数
- 只保留了16位用于指定分支地址
- PC相对寻址
- adress只有16位,通过PC + adress 拓展到32位
- 此时PC的增加量是4的倍数
- 目标地址 = PC + offset * 4 (注意PC指向的是目前运行指令的下一条)
eg.
远程分支
如果跳转对象地址太大无法用16位偏移量表示,汇编语言将重写代码
假设$s0,$s1相等时跳转
beq $s0, $s1, L1
...
L1:
代替为
bne $s0, $s1, L2
j L1
L2:
MIPS中的指令类型
地址模式总结
机器语言解码
并行与指令:同步
- 处理器共享存储器同一区域
- P1写,P2读
- 如果P1和P2不同步,将发生数据竞争
- 结果由访问次序决定
- 依赖硬件提供同步指令
- 原子读/写内存操作
- 在读和写之间,不再允许对该空间的其他操作
- 可以是单一的指令
- 例如寄存器之间的原子交换
- 或者指令的原子配对
MIPS中的同步
翻译并执行程序
伪汇编指令
- 大多数汇编指令和机器指令是一对一的
- 特殊的是伪指令
- 伪指令:汇编指令的变种
生成目标模块
-
汇编器(或编译器)把程序翻译成机器语言
-
提供从部分构建完整程序的信息
-
目标文件头:描述目标文件其他部分的大小和位置
-
正文段:翻译后的指令,包含机器语言代码
-
静态数据段:包含在程序生命周期内分配的数据
-
重定位信息,标记了一些程序加载进内存时依赖于绝对地址的指令和数据
-
符号表,全局定义和外部引用
-
调试信息:用于关联源文件
链接目标模块
- 产生一个可执行的映像
- 合并段(代码和数据模块象征性地放入内存)
- 决定数据和指令标签的地址
- 修补内部和外部引用
- 可以留下依靠重定位程序修复的部分
- 但虚拟内存,不需要做这些
- 虚拟内存空间,程序必须以绝对地址装入
- 举例:两个过程目标文件链接
加载程序
- 把待执行的程序从硬盘的镜像文件读入内存
- 1. 读取可执行文件头来确定正文段和数据段的大小
- 2. 为正文和数据创建一个足够大的地址空间
- 3. 把指令和初始数据拷贝到内存或者设置页表项,使它们可用
- 4. 把主程序的参数复制到栈顶
- 5. 初始化寄存器(包括堆栈指针$sp, 帧指针$fp, 全局指针$gp )
- 6. 跳转到启动进程
- 复制参数到寄存器并调用主函数main
- 主函数返回时,通过系统调用exit终止程序
动态链接库DLL
-
调用时,只是连接或装入库文件
-
过程代码重定位;
-
避免所有程序中出现的链接库;但是这些库的信息是一次性代入内存,占用内存空间。只是在用到的时候才链接该库;
-
自动装入最新的编译器中的版本的动态库
-
谬误与陷阱
Fallacies谬误
- 更强大的指令意味着更高的性能
- 需求更少的指令
- 但复杂指令实现困难,可能带慢了所有的指令,包括简单指令
- 编译器更擅长从简单指令译码
- 使用汇编程序获得高性能
- 但现代编译器更适合现代的处理机 代码量多,将会引起更多的错误,编写程序和调试所花的时间多