不只有 int a = 1 总是要用到 if…else 、while 和 for 还有函数或者过程调用。
CPU 执行的也不只是一条指令,因为有 if…else、for 这样的条件和循环存在,指令不会一路平铺直叙地执行下去。程序是怎么被分解成一条条指令来执行的?
CPU是如何执行指令的?
程序员只要知道,代码变指令后,一条一条顺序执行就可以。
CPU 是一堆寄存器组成,寄存器由多个触发器(Flip-Flop)或者锁存器(Latches)组成简单电路,两种不同原理的数字电路组成的逻辑门。
N 个触发器或者锁存器,就可组成一个 N 位(Bit)的寄存器,能保存 N 位数据。 64 位 Intel 服务器,寄存器就是 64 位的。
一个 CPU 会有很多种不同功能的寄存器。三种特殊的:
(1)PC 寄存器(Program Counter Register),也叫指令地址寄存器(Instruction Address Register)。存放下一条要执行的计算机指令的内存地址。
(2)指令寄存器(Instruction Register),存放当前正在执行指令。
(3)条件码寄存器(Status Register),一个个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果。
(4)其他:整数、浮点数、向量、地址寄存器等等。有些可以存放数据,又能存放地址,就叫通用寄存器。
程序执行时,CPU 根据 PC 寄存器里的地址,从内存里面把需要执行的指令(连续保存)读取到指令寄存器里面执行,根据指令长度自增,顺序读下一条。
特殊指令,上一讲 J 类指令(跳转指令),会修改 PC 寄存器里面的地址值。下一条要执行的指令就不是从内存里面顺序加载的了。使用 if…else 条件语句和 while/for 循环语句的原因。
二、从 if…else 来看程序的执行和跳转
包含 if…else 的简单程序。
rand 生成一个随机数 r,r 是 0/ 1。 r 是 0时,把变量 a 设成 1,不然就设成 2。
$ gcc -g -c test.c
$ objdump -d -M intel -S test.o
编译成汇编代码。忽略前后无关的代码,关注于这里的 if…else 条件判断语句:
r == 0 的条件判断,被编译成了 cmp 和 jne 两条指令
(一)cmp 指令:比较两个操作数值,比较结果,存入条件码寄存器
(1)DWORD PTR :操作数据类型是 32 位整数,
(2)[rbp-0x4] :寄存器的地址。
(3)第一个操作数:从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。
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 (对应汇编代码行号, else 条件里的第一条指令)。跳转时(mov 指令),PC 寄存器:不再是自增变成下一条指令的地址,而是被直接设置成 4a 地址,指令寄存器执行4a 地址。
mov 指令:(1)和 cmp 一样, 32 位整型寄存器地址,对应的 2 的 16 进制值 0x2。
(2)4a这条指令:把 2 设置到对应的寄存器里(赋值),PC 寄存器里的值自增,执行下一条 mov 指令。
(3)51这条指令:占位符,没实际作用,第一个操作数 eax:寄存器, 0x0:16 进制的 0 。
if 条件,如满足,
赋值的 mov 指令(41)执行完, jmp无条件跳转 51。 main 函数没有设定返回值,而 mov eax, 0x0其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后(和 else 一样)跳转到这里,。
读取打孔卡机器顺序一段一段读取指令,执行。有跳转地址,比如往后跳 10 个指令,机器自动将卡片带往后移动 10 个指令的位置,再执行指令。也能向前移动, while/for 循环实现原理。
如何通过 if…else 和 goto 来实现循环?
for 循环的程序。三次之后,i>=3,就会跳出循环。
循环用 1e 地址上 cmp 比较指令,和紧接着的 jle 条件跳转指令来实现的。差别: jle 跳转的地址,在这条指令之前的地址 14,而非 if…else 编译出来的跳转指令之后。条件满足,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行,直到条件不满足,执行 jle 之后的指令,循环才结束。
如果你看一长条打孔卡的话,就会看到卡片往后移动一段,执行了之后,又反向移动,去重新执行前面的指令。
jle 和 jmp 指令,有点像 goto 命令,指定跳转位置。虽反对使用 goto,但实际机器指令层面, if…else, for/while ,都是用和 goto 相同方式实现。
总结延伸
这一节,在单条指令的基础上,学习了多条指令,怎么样一条条被执行。 PC 寄存器自增的方式顺序执行,条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。类似 goto 语句。
硬件层面实现goto 语句,要保存下一条指令地址,以及当前正要执行指令的 PC 寄存器、指令寄存器,条件码寄存器,保留条件判断的状态。三个寄存器就可以实现。