高端营销网站定制,青岛网页建站模板,网页制作与设计元素是什么,seo入口目录
CPU 是如何执行指令的#xff1f;
从 if…else 来看程序的执行和跳转
如何通过 if…else 和 goto 来实现循环#xff1f;
小结 你平时写的程序中#xff0c;肯定不只有 int a 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和…
目录
CPU 是如何执行指令的
从 if…else 来看程序的执行和跳转
如何通过 if…else 和 goto 来实现循环
小结 你平时写的程序中肯定不只有 int a 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和 for 这样的循环语句还有函数或者过程调用。
对应的CPU 执行的也不只是一条指令一般一个程序包含很多条指令。因为有 if…else、for 这样的条件和循环存在这些指令也不会一路平铺直叙地执行下去。
今天我们就来看看一个计算机程序是怎么被分解成一条条指令来执行的。
CPU 是如何执行指令的
拿我们用的 Intel CPU 来说里面差不多有几百亿个晶体管。实际上一条条计算机指令执行起来非常复杂。好在 CPU 在软件层面已经为我们做好了封装。对于我们这些做软件的程序员来说我们只要知道写好的代码变成了指令之后是一条一条顺序执行的就可以了。
我们先不管几百亿的晶体管的背后是怎么通过电路运转起来的逻辑上我们可以认为CPU 其实就是由一堆寄存器组成的。而寄存器就是 CPU 内部由多个触发器Flip-Flop或者锁存器Latches组成的简单电路。
触发器和锁存器其实就是两种不同原理的数字电路组成的逻辑门。这块内容并不是我们这节课的重点所以你只要了解就好。如果想要深入学习的话你可以学习数字电路的相关课程这里我们不深入探讨。
好了现在我们接着前面说。N 个触发器或者锁存器就可以组成一个 N 位Bit的寄存器能够保存 N 位的数据。比方说我们用的 64 位 Intel 服务器寄存器就是 64 位的。 一个 CPU 里面会有很多种不同功能的寄存器。我这里给你介绍三种比较特殊的。
一个是PC 寄存器Program Counter Register我们也叫指令地址寄存器Instruction Address Register。顾名思义它就是用来存放下一条需要执行的计算机指令的内存地址。
第二个是指令寄存器Instruction Register用来存放当前正在执行的指令。
第三个是条件码寄存器Status Register用里面的一个一个标记位Flag存放 CPU 进行算术或者逻辑计算的结果。
除了这些特殊的寄存器CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据又能存放地址我们就叫它通用寄存器。 实际上一个程序执行的时候CPU 会根据 PC 寄存器里的地址从内存里面把需要执行的指令读取到指令寄存器里面执行然后根据指令长度自增开始顺序读取下一条指令。可以看到一个程序的一条条指令在内存里面是连续保存的也会一条条顺序加载。
而有些特殊指令比如上一讲我们讲到 J 类指令也就是跳转指令会修改 PC 寄存器里面的地址值。这样下一条要执行的指令就不是从内存里面顺序加载的了。事实上这些跳转指令的存在也是我们可以在写程序的时候使用 if…else 条件语句和 while/for 循环语句的原因。
从 if…else 来看程序的执行和跳转
我们现在就来看一个包含 if…else 的简单程序。
// test.c#include time.h
#include stdlib.hint main()
{srand(time(NULL));int r rand() % 2;int a 10;if (r 0){a 1;} else {a 2;}
我们用 rand 生成了一个随机数 rr 要么是 0要么是 1。当 r 是 0 的时候我们把之前定义的变量 a 设成 1不然就设成 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],0x03f: 75 09 jne 4a main0x4a{a 1;41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x148: eb 07 jmp 51 main0x51}else{a 2;4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x251: 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就把零标志条件码对应的条件码是 ZFZero Flag设置为 1。除了零标志之外Intel 的 CPU 下还有进位标志CFCarry Flag、符号标志SFSign Flag以及溢出标志OFOverflow 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 循环的程序。我们循环自增变量 i 三次三次之后i3就会跳出循环。整个程序对应的 Intel 汇编代码就是这样的 for (int i 0; i 3; i)b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x012: eb 0a jmp 1e main0x1e{a i;14: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]17: 01 45 fc add DWORD PTR [rbp-0x4],eaxfor (int i 0; i 3; i)1a: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x11e: 83 7d f8 02 cmp DWORD PTR [rbp-0x8],0x222: 7e f0 jle 14 main0x1424: 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 寄存器、指令寄存器外我们只需要再增加一个条件码寄存器来保留条件判断的状态。这样简简单单的三个寄存器就可以实现条件判断和循环重复执行代码的功能。