指令和运算 - 指令执行之指令跳转:条件分支和循环语句是怎么实现的
指令和运算 - 指令执行之指令跳转:条件分支和循环语句是怎么实现的
计算机组成原理目录:https://www.cnblogs.com/binarylei/p/12585607.html
在上一节中,我们分析了高级语言是如何翻译成计算机指令,本节内容则是分析 CPU 是如何执行这些计算机指令的。除了最简单的顺序执行外,指令跳转是如何实现的呢?
1. CPU 是如何执行计算机指令
现代 Intel CPU 里面差不多有几百亿个晶体管。实际上,一条条计算机指令执行起来非常复杂。但对于我们程序员来说,只要知道写好的代码变成指令之后,是一条一条顺序执行的就可以了。
1.1 寄存器
CPU 其实就是由一堆寄存器组成的,这些寄存器有很多种不同的功能。对于 64 位 Intel 服务器,寄存器就是 64 位的。其中有三种比较特殊的寄存器需要重点关注。
- PC 寄存器(Program Counter Register):也叫指令地址寄存器(Instruction Address Register)。顾名思义,它就是用来存放下一条需要执行的计算机指令的内存地址。
- 指令寄存器(Instruction Register):用来存放当前正在执行的指令。
- 条件码寄存器(Status Register):用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果。
除了这些特殊的寄存器,CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据,又能存放地址,我们就叫它通用寄存器。
1.2 指令的执行过程
实际上,一个程序执行的时候过程如下:
- 首先,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,
- 然后,根据指令长度自增,开始顺序读取下一条指令。可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
- 其次,有些特殊指令(eg: J 类跳转指令)会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,也是使用 if…else 条件语句和 while/for 循环语句的原因。
2. 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;
}
}
同样编译一下 test.c 文件
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 这两条指令。
(1)cmp 指令
比较了前后两个操作数的值。cmp 指令的比较结果,会存入到条件码寄存器当中去。cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。
在这里,如果比较的结果是 True,也就是 r == 0,就把零标志条件码(对应的条件码是 ZF,Zero Flag)设置为 1。除了零标志之外,Intel 的 CPU 下还有进位标志(CF,Carry Flag)、符号标志(SF,Sign Flag)以及溢出标志(OF,Overflow Flag),用在不同的判断条件下。
补充:此处 cmp 指令的第一个操作数为 DWORD PTR [rbp-0x4](DWORD PTR 代表操作的数据类型是 32 位的整数,而[rbp-0x4]则是一个寄存器的地址),也就是从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。
(2)jne 指令
jne 指令是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 4a 的位置。这个地址为 4a 的指令,对应代码中 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的 4a 这个地址。这个时候,CPU 再把 4a 地址里的指令加载到指令寄存器中来执行。
地址为 4a 的指令是一条 mov 指令。第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的寄存器地址,以及对应的 2 的 16 进制值 0x2。mov 指令把 2 设置到对应的寄存器里去,相当于一个赋值操作。
(3)mov eax,0x0 指令
然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。这条 mov 指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,它的作用是一个占位符。我们回过头去看前面的 if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令,跳转到 51 行。我们的 main 函数没有设定返回值,而 mov eax, 0x0 其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后也会跳转到这里,和 else 里的内容结束之后的位置是一样的。
3. while/for 循环语句
int main()
{
int a = 0;
for (int i = 0; i < 3; i++)
{
a += i;
}
}
整个程序,对应的 Intel 汇编代码就是这样的:
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 条件跳转指令(jump if less or equal,也就是小于等于)来实现的。条件满足的时候,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行之前执行过的指令,直到条件不满足,顺序往下执行 jle 之后的指令,整个循环才结束。
4. 总结延伸
在单条指令的基础上,学习了程序里的多条指令,究竟是怎么样一条一条被执行的。
- 顺序执行:简单地通过 PC 寄存器自增的方式顺序执行。
- 指令跳转:条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。
无论是 if…else 条件语句,或者 while/for 循环语句,还是 goto 语句,在计算机机器指令上都只是一个简单的地址跳转而已。想要在硬件层面实现指令跳转,除了最基本的 PC 寄存器、指令寄存器外,只需要再增加一个条件码寄存器,来保留条件判断的状态。这样简简单单的三个寄存器,就可以实现条件判断和循环重复执行代码的功能。
推荐阅读:
- 《深入理解计算机系统》的第 3 章:详细讲解了 C 语言和 Intel CPU 的汇编语言以及指令的对应关系,以及 Intel CPU 的各种寄存器和指令集。
每天用心记录一点点。内容也许不重要,但习惯很重要!
posted on 2020-03-31 10:45 binarylei 阅读(2002) 评论(0) 编辑 收藏 举报