计算机系统之系统架构与操作系统的高度集成
计算机系统之系统架构与操作系统的高度集成

2.4 表达式和赋值语句

高级语言

a = b + c;
d = e - f;
x = y & z;

指令

add a, b, c;
sub d, e, f;
and x, y, z;

上面的指令称为 双操作数 (binary)指令,因为它们都利用两个操作数进行工作来产生一个结果。也被称为 三操作数 (three-operand)指令,因为有三个操作数(两个源操作数和一个目的操作数)。

但是,并不是每一个双操作数指令都需要三个操作数。

2.4.1 操作数放在哪里

处理器内部是一个算术 / 逻辑单元(ALU),它执行ADD、SUB、AND和OR等运算。

对于这些算术 / 逻辑运算来说,所有的操作数都在寄存器中。我们引入寻址模式的概念,寻址模式指的是在一条指令中如何制定某个操作数。

这里使用的寻址模式,所有的操作数都在寄存器中,因此被称为寄存器寻址

我们还需要另外一些指令来将数据在内存和寄存器之间来回搬运。

  • 加载(LD) 从内存加载到寄存器
  • 存储(ST) 从寄存器存储会内存

问:为什么不单纯使用内存操作数来避免寄存器的使用呢?

答:因为从寄存器取操作数比从内存取操作数快得多。

2.4.2 在指令中如何指定内存地址

如果我们有一个PB级(大约 2 ^ {50} 字节)的内存,则在指令中需要 50 位来表示一个内存操作数,这显然并不合理。

所以,我们引入一种寻址模式来缓解每条指令都需要将整个内存地址操作数放入其中的情况。这样的寻址模式为基址加偏移量(base + offset)模式。

在这种寻址模式中,指令中的内存地址为一个寄存器(基址寄存器)的内容与上一个偏移量(以立即值形式包含于指令中)的和。通常表示我为

ld r2, offset(rb); r2 <- MEMORY(rb + offset)

基址加偏移量寻址模式的威力在于它可以用来加载和存储简单的变量,此外它还可以用于复合变量(比如数组和结构体)。

2.4.3 每个操作数应该有多宽

操作数的宽度与其粒度或者说精度有关。C 语言中的基本数据类型有short、int、long、char。这些数据类型的宽度与实现相关。一般来说,short 是 16位, int 是 32 位,char 是 8 位。当时,在 C 语言诞生的年代,流行的指令集都是用 8 位宽的操作数,所以对于 C 语言来说,使用 8 位的 char 类型非常方便。类似地,int 数据类型为 32 位宽的原因是 32 位的处理器体系结构非常流行。

我们约定一个字是 32 位,半字是 16 位, 字节是 8 位。当体系结构出现支持多种精度后,就出现一个内存操作系统的可寻址性问题。这里的可寻址性指的是能够在内存中单独制定的最低精度的操作数。比如,如果一台机器是字节可寻址的,那么能够单独寻址的最低精度是字节。

MSB 指最高有效字节(most significant byte);LSB 指最低有效字节(least significant byte)。

2.4.4 字节序

如上图,4个字节组合起来构成了地址 100 处的一个字。

假设 100 处的这个字的值为 0x11223344,那么这 4 个字节在字中有两种可能的组织方式。

大端模式
小端模式

如果该字的MSB位于地址即 100 上。这种组织方式称为大端模式;如果该字的 LSB 位于字的地址即 100 上,这种组织方式称为小端模式

如果你声明了某种精度的数据类型却用别的精度取访问它,因为字节序的问题,这可能会成为灾难的根源。比如,下面这段代码,我们声明了一个 int 类型的数据,却用 char 类型去访问它,那么在不同的字节序的机器上,读到的内容不同。

int i = 0x11223344;
char *c;

c = (char *) $i;
printf("endian: i = %x; c = %x\n", i, *c);

2.4.5 操作数打包以及字操作数的对齐

现有如下的数据结构

struct {
    char a;
    char b[3];
}

这个数据结构在内存中的一种可能布局以 100 位起始地址,如下图所示:

有效的编译器会进行优化——将上面的数据结构打包,以 100 为起始地址,如下图所示:

编译器进行的打包与数据类型要求的精度和体系结构支持的可寻址性都是对应的。除了节省空间以外,还能减少在处理器寄存器和内存之间来回搬运该数据结构所需的访问次数。因此,打包操作数在空间和时间上都是高效的。

但是,有些情况并不能够采取打包策略。考虑下面数据结构:

struct {
    char a;
    int b;
}

下面是其数据结构的一种可能存在的布局,以 100 为起始地址:

这种布局的问题在于,为了加载 b 变量,不得不从内存中读取两个字。无论由硬件还是软件来实现都是低效的。体系结构通常要求字操作数从字地址开始,这就是所谓的操作数与操作地址的对齐限制

所以,编译器很可能将上面的数据结构按下图的方式布局,起始地址为 100:

尽管这种布局浪费了 37.5 % 的空间,但是从访问操作的时间这个角度来看,变得更加高效了。这种时间 – 空间的权衡,体现在了计算机科学领域中很多地方。

2.5 高级数据结构

2.5.1 结构体

struct {
    int a;
    char c;
    int d;
    long e;
}

高级语言的结构体数据类型可以通过基址加偏移量的寻址模式来提供支持。编译器知道每个数据类型需要多少空间,也知道每个变量在内存中的对齐情况。

2.5.2 数组

通常来说,数组常在循环中使用。

a[i] = a[i] + 1;

在上面的语句中,相对于基址寄存器的偏移量是不固定的。同时,为了计算偏移量还需要额外的计算。所以,一些计算机体系结构提供了一种寻址模式允许有效地址来自两个寄存器内容之和。这被称为基址加索引的寻址模式。

2.6 条件与循环控制语句

我们使用程序计数器(PC)的寄存器来存放程序执行指令的位置。

条件分支指令的格式如下:

beq r1, r2, offset;

这种地址寻址模式称为程序计数器相对寻址

无条件指令的格式如下:

j rtarget

如果有很多连续的、稠密的条件语句,那么将它们编译为嵌套的 if-then-else 语句就会生成低效的代码。另一种选择是,使用一个跳转表记录所有 case 代码段的起始地址,这样就能产生高效的代码。

switch 和 循环语句都能用上述的条件分支和无条件分支指令实现。

2.8 编译函数调用

  • 寄存器 s0 ~ s2 是调用者的寄存器
  • 寄存器 t0 ~ t2 是临时寄存器
  • 寄存器a0 ~ a2 是传参寄存器
  • 寄存器 v0 用于保持返回值
  • 寄存器 ra 用于保持返回地址
  • 寄存器 at 用于保持目标地址
  • 寄存器 sp 用作栈指针
函数调用栈的存储过程 1
函数调用栈的存储过程 2
函数调用栈的存储过程 3
函数调用栈的存储过程 4
函数调用栈的存储过程 5

2.8.6 栈指针

栈指针包含了栈中与被执行过程相关的活动记录的第一个地址,并且在这个过程中的执行过程中都不会改变。

2.9 指令集体系结构选择

2.9.3 体系结构类型

  • 面向栈的体系结构
  • 面向内存的体系结构
  • 面向寄存器的体系结构
  • 混合类型

2.9.4 指令格式

  • 零操作数指令
  • 单操作数指令
  • 双操作数指令
  • 三操作数指令

指令格式指的是指令如何在内存中布局,广义上讲分为两类:

  • 所有指令等长
  • 指令长度边长

本文为原创文章,欢迎分享,勿全文转载,如果内容你实在喜欢,可以加入收藏夹,说不定哪天故事又继续更新了呢。
本文地址:https://qoogle.top/chapter-two-of-computer-systems-an-integrated-approach-to-architecture-and-operating-systems/
最后修改日期:2020年4月6日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。