cs107 编程范式(八)

492 阅读2分钟

汇编语言

注:目前的汇编语言是老师自定义的,具体参照编程范式(七)中所示

int i;
int j;

i = 10;
j = i + 7;
j++;

这些代码会在栈区组成一个8字节大小的活动记录,i和j是紧挨在一起的,如下图所示:

1.PNG

其中R1是通用的4字节位模式存储器,存储对应活动记录的基地址。将上述代码翻译成汇编代码:

M[R1+4] = 10         // store
R2 = M[R1+4]         // load
R3 = R2 + 7          // ALU
M[R1] = R3           // store

R2 = M[R1]           // load
R2 = R2 + 1          // ALU       
M[R1] = R2           // store

注意,我们这里store, load和ALU操作都是处理4字节的数据。或许有人会有问题,为什么不直接使用如下操作

R3 = 10 + 7

这是因为,我们希望汇编代码和c语言代码可以一一对应,上下文无关,这样就算代码赋值改变,也不会产生较大的影响。

接下来,我们在来看一个非4字节大小的例子

int i;
short s1;
short s2;

2.PNG

翻译成汇编代码

int i;
short s1;
short s2;

i = 200;  // M[R1+4] = 200
s1 = i;   

这里需要注意一下,在c语言中我们将4字节的i赋值给s1时,会自动去低地址的2个字节进行赋值,但是在汇编语言中不会发生这种操作。如果直接使用如下操作,会覆盖i中的字节。

R2 = M[R1+4]
M[R1+2] = R2

正确的做法是在R2前加入限定

R2 = M[R1+4]
M[R1+2] = .2R2
s2 = s1 + 1;
R2 = .2M[R1+2]
R2 = R2 + 1
M[R1] = .2R2 

接下来,我们来看看新的代码

int array[4];
int i;

for(i = 0; i < 4; i++) {
    array[i] = 0;
}

i--;

3.PNG

M[R1] = 0
R2 = M[R1]
BGE R2, 4, PC+40         // BGE大于或等于跳转
R3 = M[R1]
R4 = R3 * 4
R5 = R1 + 4
R6 = R4 + R5
M[R6] = 0
R2 = M[R1]
R2 = R2 + 1
M[R1] = R2
JMP PC - 40

R2 = M[R1]
R2 = R2 - 1
M[R1] = R2

PC是一种特殊的寄存器, 存储着当前指令的地址

当我们对汇编指令有一定了解之后,我们来看看计算机中每条汇编指令是什么样子的。

R1 = M[R2 + 4]        // 000000
R1 = 1000             // 000001
R3 = R6 * R10         // 010011
M[R1 - 20] = R19      // 111111

课程中老师设计的汇编指令大概有59种,我们需要将这59中指令进行编码,所以我们需要前6位作为操作码部分。如图中所示,

4.PNG

对于指令R1 = M[R2 + 4]来说,因为寄存器有32个,所以需要5位去表示寄存,这样剩余16位来表示有符号的偏移。

5.PNG

当然,这种划分只有在前6位都是000000的时候才起作用。

对于指令R1 = 1000来说,操作码之后只需要留出5位表示寄存器即可,剩余21位表示立即数。

额外说明一下,对于操作码来说,我们可以使用变长编码来进行处理,例如霍夫曼编码。但是本门课程中使用定长编码。

接下来,我们从汇编的角度来看一下指针和强制类型转换。

struct fraction {
    int num;
    int denom;
};

struct fraction pi;

内存中的布局如下所示

6.PNG

pi.num = 22; // M[R1] = 22
pi.denom = 7; // M[R1+4] = 7

从前面的课程中我们知道,在内存中是没有结构体的概念的,结构体中的变量是紧密排布在一起的。

接下来,我们来看一下这条指令

((struct fraction*)&pi.denom)->denom = 451; // M[R1+8] = 451

你可能会说,这是不是有问题,实际上,在编译器看来&pi.denom操作会产生R1+4的效果,(struct fraction*)&pi.denom会使得编译器将R1+4认为成结构体的基地址,因此在执行上述语句时,就相当于在R1寄存器地址的基础上偏移8位,然后进行赋值。注意,没有与强制类型转换对应的汇编指令,强制类型转换的作用是让编译器绕过类型检查机制进而通过编译生成代码。