《深入浅出计算机组成原理》原理篇(二)笔记

  这篇的标题是:指令跳转:原来if...else就是goto,收益颇多的就是指令跳转和寄存器之间密不可分的联系。一下是笔记,可随便看看。

  指令的执行在程序员的角度来说:只要了解代码写好变成指令后,是一条一条顺序执行的就可。但真实执行是非常复杂的,这些复杂的过程都是由CPU在软件层面为我们做好封装的。

  逻辑上,我们可以认为,CPU其实就是由一堆寄存器组成的,而寄存器就是CPU内部,由多个触发器或者锁存器组成的简单电路。(注:触发器或者锁存器,其实就是两种不同原理的数字电路组成的逻辑门,有兴趣可专门研究。)

  N个触发器或者锁存器,就可以组成一个N bit(位)的寄存器,可保存N位(bit)的数据。例:64位的Intel服务器,寄存器就是64位的。

  上图可得:一个CPU里面会有很多种不同功能的寄存器,有三种较为特殊的,稍作解释:

  1、PC寄存器(Program Counter Register),也称指令地址寄存器(Instruction Adress Register),顾名思义,它的作用就是存储下一条需要执行的计算机指令的内存地址。

  2、指令寄存器(Instruction Register):用来存储当前正在执行的指令。

  3、条件码寄存器(status Register):用寄存器种的标记位(Flag)来存放CPU进行算术运算或者逻辑计算的结果值。

  除了以上三个特殊的寄存器,CPU还有很多用来存储数据和地址的寄存器。例如:整数寄存器,浮点数寄存器,向量寄存器和地址寄存器等等。有一些寄存器既可以存放数据,又可以存储地址,可称之为:通用寄存器。

  实际上,一个程序执行的时候呢,CPU会根据PC寄存器里的地址,从内存中将需要执行的指令读取到指令寄存器中执行,然后根据指令长度自增,开始顺序读取下一条指令即可。可以看出,一个程序的每条指令,在内存中是连续保存的,也是一条条顺序加载执行的。

  当然,有特殊指令的存在,例如:J类指令,也称之位跳转指令,会修改PC寄存器里面的地址值。如此一来,下一条要执行的指令就不是从内存中顺序加载的了,事实上,这些跳转指令的存在,是因为我们程序使用了if...else条件语句和while/for循环语句导致的。

  从if...else来看程序的执行和跳转:

  e.g:

  

// test.c
#include <time.h>
#include <stdlib.h>
int main()
{
  srand(time(NULL));
  int r = rand() % 2;
  int a = 10;
  if (r == 0)
  {
    a = 1;
  } else {
    a = 2;
  } 

//条件语句对应的汇编代码:

  if (r == 0)
3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
3f: 75 09 jne 4a <main+0x4a>
  {
    a = 1;
41:   c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
48: eb 07 jmp 51 <main+0x51>
  } else {
    a = 2;
4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
51: b8 00 00 00 00 mov eax,0x0
  }

  cmp:比较前后两个操作数的值

  DWORD PTR:代表操作的数据类型是32位的整数

  [rbp-0x4]:一个寄存器的地址

  代码中可以看出:r == 0 被编译成cmp 和 jne这两个指令,所以,第一个操作数就是从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。cmp 指令的比较结果,会存入到条件码寄存器当中去。

  在这里,若比较的结果是 True,也就是 r == 0,就把零标志条件码(对应的条件码是 ZF,Zero Flag)设置为 1。

  除了零标志之外,Intel 的 CPU 下还有:

    1、进位标志(CF,Carry Flag)、

    2、符号标志(SF,Sign Flag)、

    3、溢出标志(OF,Overflow Flag)

  用在不同的判断条件下。

  cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。

  jne指令:jump if not equal,如果 r==0 那么就会把零标志条件码 (Zero Flag) 设置为1 。如果 Zero Flag 是 0 那么就表示if 为 false 所有就会跳转到4a、如果发生了跳转那么PC寄存器的下一条指令地址就变成了4a 然后再把他加载到指令寄存器中来执行。

  

int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        a += i;
    }
}
//汇编代码


  for (int i = 0; i <= 2; i++)
 b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0

12: eb 0a jmp 1e
  {
    a += i;
14:   8b 45 f8 mov eax,DWORD PTR [rbp-0x4]
17:   01 45 fc add DWORD PTR [rbp-0x8],eax


1a:   83 45 f8 01 add DWORD PTR [rbp-0x4],0x1
1e:   83 7d f8 02 cmp DWORD PTR [rbp-0x4],0x2
22:   7e f0 jle 14
24:   b8 00 00 00 00 mov eax,0x0
  }

  可以看到,对应的循环也是用 1e 这个地址上的 cmp 比较指令,和紧接着的 jle 条件跳转指令来实现的。主要的差别在于,这里的 jle 跳转的地址,在这条指令之前的地址 14,而非 if…else 编译出来的跳转指令之后。往前跳转使得条件满足的时候,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行之前执行过的指令,直到条件不满足,顺序往下执行 jle 之后的指令,整个循环才结束。

  其实,jle 和 jmp 指令,有点像程序语言里面的 goto 命令,直接指定了一个特定条件下的跳转位置。虽然我们在用高级语言开发程序的时候反对使用 goto,但是实际在机器指令层面,无论是 if…else…也好,还是 for/while 也好,都是用和 goto 相同的跳转到特定指令位置的方式来实现的。

  en~~~~~

  今天就到这儿了,拜拜

posted @ 2021-05-10 17:35  与疯  阅读(306)  评论(0)    收藏  举报