计算机组成原理:指令跳转

CPU是如何执行指令的

实际上,计算机指令的物理执行过程非常复杂。好在CPU在软件层面已经为我们做好了封装。我们只要知道,写好的代码变成了指令之后,是一条条顺序执行的就可以了

逻辑上,我们可以认为,CPU其实就是由一堆寄存器组成的。而寄存器就是CPU内部,由多个触发器或者锁存器组成的电路。N个触发器或者锁存器,就可以组成一个N位(Bit)的寄存器,能够保持N位的数据。比如说,64位的Intel服务器,寄存器就是64位的。

在这里插入图片描述

一个CPU里面会有很大不同功能的寄存器。比较特殊的:

  • PC寄存器(Program Counter Register),也叫指令地址寄存器(Instruction Address Register):用来存放下一条需要执行的计算机支持的内存地址
  • 指令寄存器(Instruction Register):用来存放当前正在执行的指令
  • 条件码寄存器(Status Register):用里面的一个个标记位(flag),存放CPU进行算术或者逻辑计算的结果。

除了这些特殊的寄存器,CPU里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放地址,又能存放数据,我们就叫它通用寄存器。
在这里插入图片描述

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

而有些特殊指令,也就是跳转指令,会修改PC寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载了。事实上,这些跳转指令的存在,也是我们可以在写程序的时候,使用 if…else 条件语句和 while/for 循环语句的原因。

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

// 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;
  } 

gcc -g -c test.c
$ objdump -d -M intel -S test.o 

我们把这个程序编译成汇编代码。你可以忽略前后无关的代码,只关注于这里的 if…else 条件判断语句。对应的汇编代码是这样的:

    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
    } 

可以看到,这里对于r==0的条件判断,被编译成了cmp和jne这两条指令

  • cmp指令比较了前后两个操作数的值
    • 这里的 DWORD PTR 代表操作的数据类型是 32 位的整数,而 [rbp-0x4] 则是一个寄存器的地址。
    • 所以,第一个操作数就是从寄存器里拿到的变量r的值。第二个操作数0x0 就是我们设定的常量 0 的 16 进制表示。
    • cmp 指令的比较结果,会存入到条件码寄存器当中去。
    • 在这里,如果比较的结果是 True,也就是 r == 0,就把零标志条件码(对应的条件码是ZF,Zero Flag)设置为 1。除了零标志之外,Intel 的 CPU 下还有进位标志(CF,Carry Flag)、符号标志(SF,Sign Flag)以及溢出标志(OF,Overflow Flag),用在不同的判断条件下。
    • cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。
      • 跟着的jne指令,是 jump if not equal 的意思,它会查看对应的令标志位。如果为0,会跳转到后面跟着的操作数4a的位置。这个4a,对应这里的汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。
      • 当跳转发生时,PC寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的4a这个地址。这个时候,CPU再把4a地址里的指令加载到指令寄存器中来执行
  • 跳转到执行地址为4a的指令,实际是一条 mov 指令
    • 第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的寄存器地址,以及对应的 2 的 16 进制值 0x2。
    • mov 指令把 2设置到对应的寄存器里去,相当于一个赋值操作。
    • 然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。
    • 这条mov指令的第一个操作数eax,代表累加寄存器,第二个操作数0x0则是16进制的0的表示
    • 这条指令其实没有实际的作用,它的作用是一个占位符。
    • 我们回过头去看前面的if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令。跳转的地址就是这一行的地址 51。
    • 我们的main函数没有设定返回值,而mov eax,0x0,其实就是给main函数生成了一个默认的为0的返回值到累加器里面。if条件里面内容执行完成之后也会跳转到这里,和else里的内容结束之后的位置是一样的。

在这里插入图片描述
打孔卡的执行过程:读取打孔器的机器会顺序的一段一段的读取指令,然后执行。执行完一条指令,它会自动的顺序执行下一条指令。如果执行的当前指令带有跳转的地址,比如往后跳10个指令,那么机器会自动将卡片带往后移动10个指令的位置,再来执行指令。同样的,机器也能向前移动,去读取之前已经执行过的指令。这也是我们while/for循环实现的原理。

如何通过 if…else 和 goto 来实现循环?

int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        a += i;
    }
}
    for (int i = 0; i < 3; i++)
   b:   c7 45 f8 00 00 00 00    mov    DWORD PTR [rbp-0x8],0x0
  12:   eb 0a                   jmp    1e <main+0x1e>
    {
        a += i;
  14:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  17:   01 45 fc                add    DWORD PTR [rbp-0x4],eax
    for (int i = 0; i < 3; i++)
  1a:   83 45 f8 01             add    DWORD PTR [rbp-0x8],0x1
  1e:   83 7d f8 02             cmp    DWORD PTR [rbp-0x8],0x2
  22:   7e f0                   jle    14 <main+0x14>
  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 相同的跳转到特定指令位置的方式来实现的。

总结

程序多条指令执行原理:除了简单地通过 PC 寄存器自增的方式顺序执行外,条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。

虽然我们可以用高级语言,可以用不同的语法,比如 if…else 这样的条件分支,或者 while/for 这样的循环方式,来实现不用的程序运行流程,但是回归到计算机可以识别的机器指令级别,其实都只是一个简单的地址跳转而已,也就是一个类似于 goto 的语句。

想要在硬件层面实现这个 goto 语句,除了本身需要用来保存下一条指令地址,以及当前正要执行指令的 PC 寄存器、指令寄存器外,我们只需要再增加一个条件码寄存器,来保留条件判断的状态

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值