深入浅出计算机组成原理学习笔记:第六讲

一、CPU 是如何执行指令的?

1、软件程序员的理解

写好的代码变成了指令之后、是一条条顺序执行的就可以了

2、CPU的逻辑组成

3、寄存器

N 个触发器或者锁存器,就可以组成一个 N 位(Bit)的寄存器,能够保存 N 位的数据。比方说,我们用的 64 位 Intel 服务器,寄存器就是 64 位的。

 

 

 4、特殊寄存器

5、CPU执行指令流程

1、CPU会根据PC寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面直面执行

2、然后根据指令长度自增、开始顺序读取下一条指令。可以看到一个程序的一条条指令在内存里面是连续保存的。也会一条条顺序加载

3、而有些特殊指令(J类跳转指令)、会修改寄存器里面的地址

4、这样下一条要执行的指令就不是从内存里面顺序加载的

5、事实上、这些跳转指令存在,也就是我们在写程序的时候,使用了 if…else 条件语句和 while/for 循环语句的原因

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@luoahong c]# cat 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;
  }

我们用 rand 生成了一个随机数 r,r 要么是 0,要么是 1。当 r 是 0 的时候,我们把之前定义的变量 a 设成 1,不然就设成 2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@luoahong c]# gcc -g -c test.c
test.c: In function ‘main’:
test.c:15:3: error: expected declaration or statement at end of input
   }
   ^
[root@luoahong c]# cat -n test.c
     #include <time.h>
     #include <stdlib.h>
     3
     4
     5  int main()
     6  {
     7    srand(time(NULL));
     8    int r = rand() % 2;
     9    int a = 10;
    10    if (r == 0)
    11    {
    12      a = 1;
    13    } else {
    14      a = 2;
    15    }

执行报错,是因为少了一个}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
[root@luoahong c]# cat 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;
  }
}
 
[root@luoahong c]# gcc -g -c test.c
[root@luoahong c]# objdump -d -M intel -S test.o
 
test.o:     file format elf64-x86-64
 
 
Disassembly of section .text:
 
0000000000000000 <main>:
#include <time.h>
#include <stdlib.h>
 
 
int main()
{
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
  srand(time(NULL));
   8:   bf 00 00 00 00          mov    edi,0x0
   d:   e8 00 00 00 00          call   12 <main+0x12>
  12:   89 c7                   mov    edi,eax
  14:   e8 00 00 00 00          call   19 <main+0x19>
  int r = rand() % 2;
  19:   e8 00 00 00 00          call   1e <main+0x1e>
  1e:   99                      cdq
  1f:   c1 ea 1f                shr    edx,0x1f
  22:   01 d0                   add    eax,edx
  24:   83 e0 01                and    eax,0x1
  27:   29 d0                   sub    eax,edx
  29:   89 45 fc                mov    DWORD PTR [rbp-0x4],eax
  int a = 10;
  2c:   c7 45 f8 0a 00 00 00    mov    DWORD PTR [rbp-0x8],0xa
  if (r == 0)
  33:   83 7d fc 00             cmp    DWORD PTR [rbp-0x4],0x0
  37:   75 09                   jne    42 <main+0x42>
  {
    a = 1;
  39:   c7 45 f8 01 00 00 00    mov    DWORD PTR [rbp-0x8],0x1
  40:   eb 07                   jmp    49 <main+0x49>
  } else {
    a = 2;
  42:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
  }
}
  49:   c9                      leave
  4a:   c3                      ret

可以看到,这里对于 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 Flah)
符号标志(SF,Sign Flag)以及溢出标志(OF,Overflow Flag),用在不同的判断条件下。


cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。

 

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@luoahong c]# cat test.c
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        a += i;
    }
}
 
[root@luoahong c]# gcc -g -c test.c
test.c: In function ‘main’:
test.c:4:5: error: ‘for’ loop initial declarations are only allowed in C99 mode
     for (int i = 0; i < 3; i++)
     ^
test.c:4:5: note: use option -std=c99 or -std=gnu99 to compile your code

错误:使用gcc编译代码会报错:

原因:这是因为gcc是基于c89标准,不能直接在for循环中初始化增量。而C99标准可以在for循环内定义变量。

解决方法:

 我们再看一段简单的利用 for 循环的程序。我们循环自增变量i 三次,三次之后,i>=3,就会跳出循环。整个程序,对应的 Intel 汇编代码就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[root@luoahong c]# objdump -d -M intel -S test.o
 
test.o:     file format elf64-x86-64
 
 
Disassembly of section .text:
 
0000000000000000 <main>:
int main()
{
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
    int a = 0;
   4:   c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
    int i;
    for (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 (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:   5d                      pop    rbp
  25:   c3                      ret

可以看到,对应的循环也是用 1e 这个地址上的 cmp 比较指令,和紧接着的 jle 条件跳转指令来、实现的。主要的差别在于,这里的 jle 跳转的地址,在这条指令之前的地址 14,而非 if…else 编
译出来的跳转指令之后。往前跳转使得条件满足的时候,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行之前执行过的指令,直到条件不满足,顺序往下执行 jle 之后的指令,整个循环才结束。

其实,你有没有觉得,jle和jmp指令,有点像逻辑程序里面的goto命令,直接指定了一个特定条件下的跳转位置

posted @   活的潇洒80  阅读(2154)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2018-05-16 pycharm的一些快捷键
点击右上角即可分享
微信分享提示