0%

53:Y86-64处理器实现(1)

最近在看CSAPP的第四章,因为要记的东西比较多,所以整理一些东西帮助理解

Y86-64指令集体系结构

对于程序员可见的状态

Y86程序的每条指令都会对我们的处理器进行一些改变,我们把这个过程称之为状态的改变。这里我们需要能够知道对应的行为,使我们的状态发生了哪些变化。这些需要被观测的状态就是“对于程序员可见的状态”

image.png

程序计寄存器(RF)

Y86有15个程序寄存器,每个寄存器存储着一个64位的字,分别是:

1
%rax %rcx %rdx %rbx %rsp %rbp %rsi %rdi %r8~%r14

不同的程序寄存器做不同的用处,之后再细说

程序寄存器(PC)

程序寄存器用于存放当前正在执行的指令的地址,通过修改PC值,可以控制处理器执行指令

条件码(CC)

由三个1位的条件码组成:ZF SF OF,它们保存着最近的算数或逻辑指令造成影响的有关信息

程序状态(Stat)

它表明程序执行的总体状态,用于指示程序是在正常运行还是出现了某种异常状态

内存(DMEM)

内存实际上可以理解为一个很大的字节数组,保存着程序和数据。这里我们的Y86程序只考虑用虚拟地址来引用内存位置。我们只认为虚拟内存系统想Y86提供了一个单一的字节数组映像

Y86-64指令

这里的汇编代码格式采用ATT

image.png

指令细节:

  • x86的movq在这里被拆分成了:rrmovq irmovq rmmovq mrmovq,显式的指明了指令的源和目的。其中对应立即数(i),内存(m),寄存器(r)。指令的第一个字母指定了源,第二个字母指定了目的
  • OPq对应着四个整数指令:addq andq subq xorq,它们只对寄存器进行操作。这些操作会设置三个条件码ZF(零),SF(符号),OF(溢出)
  • jXX对应着七个跳转指令:jmp(无条件跳转),jle(小于等于跳转),jl(小于跳转),je(等于跳转),jne(不等于跳转),jge(大于等于跳转),jg(大于跳转)。跳转指令会根据条件码进行分支判断跳转
  • cmovXX对应了六个条件传送指令:cmovle(小于等于传送),cmovl(小于传送),cmove(等于传送),cmovne(不等于传送),cmovge(大于等于传送),cmovg(大于传送)。条件传送只能用于满足条件时的传送,且源和目的只能是寄存器。
  • call将返回地址入栈,然后跳转到目标地址。ret从这样的调用中返回
  • pushq和popq实现入栈与出栈
  • halt指令用于停止指令的执行,并将状态码设置成HLT状态

指令编码

现在讨论一下程序的指令编码,我们可以在上面的图看到大致的,每个指令的编码结构略有不同但还是由以下部分组成:

1
指令类型 | 源 | 目的

指令类型

指令类型通常在第一个字节给出,第一个字节分为高四位和第四位。其中:

  • 高四位是代码(code)部分,用来决定操作类型
  • 第四位是功能(function)部分,用决定操作所使用的功能。不过功能值只有在i相关指令共用一个操作的时候才有用

我们可以看到Y86带功能值的具体操作

image.png

源和目的

源和目的可能是寄存器或者内存地址,我们分开讨论:

  • 寄存器:15个程序寄存器每个都有一个相对应的寄存器标识符(register ID),这些程序寄存器存在CPU的一个寄存器文件中,这样我们可以把寄存器文件视作一个小的,以寄存器ID为地址的随机访问存储器。如果ID值为0xF意味着不访问任何寄存器。ID值如下:
image.png
  • 内存地址:这里需要分情况讨论,可能存在三种用法:其一是将内存地址作为一个目的地址;其二是将内存地址作为rmmovq和mrmovq的地址指示符的偏移地址;其三是将其作为irmovq的立即数。内存地址在指令中是一个8字节的长数字,使用小端序编码。

现在我们可以把源和目的划分为三个部分:

1
2
|   寄存器字段   | 附加地址字段 |
| rA | rB | Dest |

寄存器字段占一个字节,附加地址字段占用八个字节

指令编码

通过将这几部分拼接组成就可以得到一条指令的编码,其中最重要的是每个字节编码一定要是唯一的解释。任意一个字节序列要么就是一个唯一的指令序列的编码,要么就不是一个合法的字节序列。

Y86-64异常

对于Y86,状态码包括以下情况,它描述程序执行的总体状态:

image.png

对于Y86,当遇到这些异常的时候我们就让处理器停止执行指令。不过更完整的设计中,处理器会调用一个异常处理程序,这个过程用来处理在遇到的某种类型的异常。

Y86-64程序

我们尝试将这个递归求和的程序翻译成Y86的汇编形式:

1
2
3
4
5
int rsun(int *start,int count){
if(count <= 0)
return 0;
return *start + rsum(start+1,count-1);
}

Y86:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# int rsun(int *start,int count)
# start in %rdi count in %rsi
rsum:
xorq %rax %rax # sum = 0
andq %rsi %rsi # set CC -> if %rsi != 0 , ZF = 0
je return # if count == 0 , return 0 -> if ZF == 1 , jmp
pushq %rbx
mrmovq (%rdi) %rbx
irmovq $-1 %r10
addq %r10 %rsi
irmovq $8 %r10
addq %r10 %rdi
call rsum
addq %rbx %rax
popq %rbx
return:
ret

Y86-64指令详情

大多数的Y86指令是容易理解且稳定的,不过我们需要注意两个特别的指令的组合。

pushq

pushq将栈指针rsp-8,并且将一个寄存器的值写入内存中。因此,当执行pushq %rsp时,指令的结果是不确定的,我们可能遇到两种情况:

  • 压入%rsp的原始值
  • 压入减去8的%rsp的值

实际上这里会压入%rsp的原始值,具体的原因,我们会在后面进行解释。

popq

同样的popq %rsp也是这么一个问题,可能会出现两种结果:

  • %rsp置为先前压入的值
  • %rsp置为+8后的%rsp的值

是将这里是将%rsp置为先前压入的值,也就是等价于mrmovq (%rsp) %rsp。具体的原因会在之后进行解释