1. CPU中的特殊功能寄存器
1)PC寄存器(Program Counter Register):又称为指令地址寄存器(Instruction Address Register),用来存放下一条需要执行的计算机指令的内存地址。
2)指令寄存器(Instruction Register):用来存放当前正在执行的指令。
3)状态寄存器(Status Register):用来存放CPU算术或逻辑运算之后CPU状态(即标志位Flag)的寄存器,例如:是否有进位/借位、结果为零。
其他特殊功能寄存器这里先不再赘述。有些寄存器既可以存放数据、又能存放地址,被称为通用寄存器。
2. PC寄存器指定程序执行的顺序
一段程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行。然后根据指令长度自增,开始顺序读取下一条指令。
可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
跳转指令(J指令)会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,是用 if…else 条件语句和 while/for 循环语句的原因。
3. 从if…else来看程序的执行和跳转
// test.c
#include <time.h>
#include <stdlib.h>
int main()
{
srand(time(NULL));
int r = rand() % 2; //产生一个随机数r。r要么为0,要么为1。
int a = 10;
if (r == 0)
{
a = 1;
} else {
a = 2;
}
执行程序:
$ gcc -g -c test.c
$ objdump -d -M intel -S test.o
执行结果
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 指令比较了前后两个操作数的值。
2)DWORD PTR 代表操作的数据类型是 32 位的整数。
3)[rbp-0x4]则是变量 r 的寄存器地址。
综上,第一个操作数就是从寄存器中拿到的变量 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 的意思,它会查看对应的零标志位。如果 ZF 为 1,说明上面的比较结果是 TRUE,如果是 ZF 是 0,也就是上面的比较结果是 False,会跳转到后面跟着的操作数 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 里的内容结束之后的位置是一样的。
4. 通过if…else和goto来实现循环
如下C语言的for循环程序。
int main() {
int a = 0;
for (int i = 0; i < 3; i++)
{
a += i;
} }
对应的Intel汇编代码为:
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 (jump if less or equal,or not greater)条件跳转指令来实现的。指令jle 跳转的地址,在这条指令之前的地址 14。
往前跳转使得条件满足的时候,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行之前执行过的指令,直到条件不满足,顺序往下执行 jle 之后的指令,整个循环才结束。
如果使用一长条打孔卡存储程序的话,就会看到卡片往后移动一段,执行了之后,又反向移动,去重新执行前面的指令。
这里的jle和jmp就像goto命令一样,直接指定了个特定条件下的跳转位置。在C语言这样的高级语言程序开发中,太多的goto命令会使程序可读性很差,不利于维护,所以不要使用。
但在机器指令层面,无论是 if…else…,还是 for/while ,都是用和 goto 相同的跳转到特定指令位置的方式来实现的。
想要在硬件层面实现goto 语句,除了本身需要用来保存下一条指令地址,以及当前正要执行指令的 PC 寄存器、指令寄存器外,还需要再增加一个状态寄存器,来保留条件判断的状态。这三个寄存器就可以实现条件判断和循环重复执行代码的功能。
问:
还有switch…case…条件跳转语句,它的汇编程序是怎么样的?
```c
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int i = 0;
4: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
int a = 0;
b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0
switch(i)
12: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
15: 83 f8 01 cmp eax,0x1
18: 74 07 je 21 <main+0x21>
1a: 83 f8 02 cmp eax,0x2
1d: 74 0b je 2a <main+0x2a>
1f: eb 12 jmp 33 <main+0x33>
{
case 1:
a = 1;
21: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
break;
28: eb 11 jmp 3b <main+0x3b>
case 2:
a = 2;
2a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
break;
31: eb 08 jmp 3b <main+0x3b>
default:
a = 3;
33: c7 45 f8 03 00 00 00 mov DWORD PTR [rbp-0x8],0x3
break;
3a: 90 nop
}
return 1;
3b: b8 01 00 00 00 mov eax,0x1
}
40: 5d pop rbp
41: c3 ret