深入浅出计算机组成原理
徐文浩
bothub创始人
立即订阅
13019 人已学习
课程目录
已完结 62 讲
0/4登录后,你可以任选4讲全文学习。
入门篇 (5讲)
开篇词 | 为什么你需要学习计算机组成原理?
免费
01 | 冯·诺依曼体系结构:计算机组成的金字塔
02 | 给你一张知识地图,计算机组成原理应该这么学
03 | 通过你的CPU主频,我们来谈谈“性能”究竟是什么?
04 | 穿越功耗墙,我们该从哪些方面提升“性能”?
原理篇:指令和运算 (12讲)
05 | 计算机指令:让我们试试用纸带编程
06 | 指令跳转:原来if...else就是goto
07 | 函数调用:为什么会发生stack overflow?
08 | ELF和静态链接:为什么程序无法同时在Linux和Windows下运行?
09 | 程序装载:“640K内存”真的不够用么?
10 | 动态链接:程序内部的“共享单车”
11 | 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”?
12 | 理解电路:从电报机到门电路,我们如何做到“千里传信”?
13 | 加法器:如何像搭乐高一样搭电路(上)?
14 | 乘法器:如何像搭乐高一样搭电路(下)?
15 | 浮点数和定点数(上):怎么用有限的Bit表示尽可能多的信息?
16 | 浮点数和定点数(下):深入理解浮点数到底有什么用?
原理篇:处理器 (18讲)
17 | 建立数据通路(上):指令+运算=CPU
18 | 建立数据通路(中):指令+运算=CPU
19 | 建立数据通路(下):指令+运算=CPU
20 | 面向流水线的指令设计(上):一心多用的现代CPU
21 | 面向流水线的指令设计(下):奔腾4是怎么失败的?
22 | 冒险和预测(一):hazard是“危”也是“机”
23 | 冒险和预测(二):流水线里的接力赛
24 | 冒险和预测(三):CPU里的“线程池”
25 | 冒险和预测(四):今天下雨了,明天还会下雨么?
26 | Superscalar和VLIW:如何让CPU的吞吐率超过1?
27 | SIMD:如何加速矩阵乘法?
28 | 异常和中断:程序出错了怎么办?
29 | CISC和RISC:为什么手机芯片都是ARM?
30 | GPU(上):为什么玩游戏需要使用GPU?
31 | GPU(下):为什么深度学习需要使用GPU?
32 | FPGA和ASIC:计算机体系结构的黄金时代
33 | 解读TPU:设计和拆解一块ASIC芯片
34 | 理解虚拟机:你在云上拿到的计算机是什么样的?
原理篇:存储与I/O系统 (17讲)
35 | 存储器层次结构全景:数据存储的大金字塔长什么样?
36 | 局部性原理:数据库性能跟不上,加个缓存就好了?
37 | 高速缓存(上):“4毫秒”究竟值多少钱?
38 | 高速缓存(下):你确定你的数据更新了么?
39 | MESI协议:如何让多核CPU的高速缓存保持一致?
40 | 理解内存(上):虚拟内存和内存保护是什么?
41 | 理解内存(下):解析TLB和内存保护
42 | 总线:计算机内部的高速公路
43 | 输入输出设备:我们并不是只能用灯泡显示“0”和“1”
44 | 理解IO_WAIT:I/O性能到底是怎么回事儿?
45 | 机械硬盘:Google早期用过的“黑科技”
46 | SSD硬盘(上):如何完成性能优化的KPI?
47 | SSD硬盘(下):如何完成性能优化的KPI?
48 | DMA:为什么Kafka这么快?
49 | 数据完整性(上):硬件坏了怎么办?
50 | 数据完整性(下):如何还原犯罪现场?
51 | 分布式计算:如果所有人的大脑都联网会怎样?
应用篇 (5讲)
52 | 设计大型DMP系统(上):MongoDB并不是什么灵丹妙药
53 | 设计大型DMP系统(下):SSD拯救了所有的DBA
54 | 理解Disruptor(上):带你体会CPU高速缓存的风驰电掣
55 | 理解Disruptor(下):不需要换挡和踩刹车的CPU,有多快?
结束语 | 知也无涯,愿你也享受发现的乐趣
免费
答疑与加餐 (5讲)
特别加餐 | 我在2019年F8大会的两日见闻录
FAQ第一期 | 学与不学,知识就在那里,不如就先学好了
用户故事 | 赵文海:怕什么真理无穷,进一寸有一寸的欢喜
FAQ第二期 | 世界上第一个编程语言是怎么来的?
特别加餐 | 我的一天怎么过?
深入浅出计算机组成原理
登录|注册

06 | 指令跳转:原来if...else就是goto

徐文浩 2019-05-08
上一讲,我们讲解了一行代码是怎么变成计算机指令的。你平时写的程序中,肯定不只有 int a = 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和 for 这样的循环语句,还有函数或者过程调用。
对应的,CPU 执行的也不只是一条指令,一般一个程序包含很多条指令。因为有 if…else、for 这样的条件和循环存在,这些指令也不会一路平铺直叙地执行下去。
今天我们就在上一节的基础上来看看,一个计算机程序是怎么被分解成一条条指令来执行的。

CPU 是如何执行指令的?

拿我们用的 Intel CPU 来说,里面差不多有几百亿个晶体管。实际上,一条条计算机指令执行起来非常复杂。好在 CPU 在软件层面已经为我们做好了封装。对于我们这些做软件的程序员来说,我们只要知道,写好的代码变成了指令之后,是一条一条顺序执行的就可以了。
我们先不管几百亿的晶体管的背后是怎么通过电路运转起来的,逻辑上,我们可以认为,CPU 其实就是由一堆寄存器组成的。而寄存器就是 CPU 内部,由多个触发器(Flip-Flop)或者锁存器(Latches)组成的简单电路。
触发器和锁存器,其实就是两种不同原理的数字电路组成的逻辑门。这块内容并不是我们这节课的重点,所以你只要了解就好。如果想要深入学习的话,你可以学习数字电路的相关课程,这里我们不深入探讨。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《深入浅出计算机组成原理》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(73)

  • 胖胖胖
    个人理解:这一讲的核心在于理解几个寄存器的作用,从而理解cpu运行程序的过程:cpu从PC寄存器中取地址,找到地址对应的内存位子,取出其中指令送入指令寄存器执行,然后指令自增,重复操作。所以只要程序在内存中是连续存储的,就会顺序执行这也是冯诺依曼体系的理念吧。而实际上跳转指令就是当前指令修改了当前PC寄存器中所保存的下一条指令的地址,从而实现了跳转。当然各个寄存器实际上是由数电中的一个一个门电路组合出来的,而各个门电路的具体电路形式也是属于模电的东西。对于我们来说,有个具体概念就行,实在需要的时候再回去翻翻课本捡起来就行。

    作者回复: 👍完全正确。

    2019-05-08
    61
  • L
    非计算机专业 表示看到这一章已经很懵逼了

    作者回复: 那要加油搞清楚啊。

    2019-05-08
    36
  • 不记年
    cpu的在执行指令时还要有个转码的电路来将指令转换成不同的电信号,这些电信号可以控制各个寄存器的动作~

    作者回复: 👍这个关于CPU的控制器的译码器的部分我会在后续讲解CPU的部分讲到。

    2019-05-10
    10
  • Out
    老师您好,在文中您提到:“在这里,如果比较的结果是False,也就是0,就把零标志码设置为1” 这个地方是不是有问题,根据我查到结果,cmp will ZF to 1 when two operands are equal. 所以如果比较的结果是True,才会把零标志码设置为1。

    作者回复: 是得,笔误了。应该是 “如果比较得结果是True,也就是 r == 0,就把零标志码设置为1”
    不然后面得jne跳转和这里也对不上。

    2019-05-09
    10
  • Linuxer
    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

    课后问题验证,这么看如果是单纯的两个分支采用if else更有利,另外 mov eax,0x1从这儿看象是main的返回值

    作者回复: 是的,如果没有提供返回值,很多版本的编译器会隐式地生成一个return 0;的返回值,就会生成 mov eax, 0x0 的多出来的指令。我修改一下让文章更准确一点。

    2019-05-08
    7
  • aiter
    徐老师好~
    C语言我不会,。,努力看了半天,算是懂了大部分,但是for循环那里还是有点问题~汇编语言里,jmp 1e 之后,应该是做比较cmp,但是为什么不是0和3比较,而是和16进制的2(0x2)比较?
    ————-
    因为后面用的jle(jump if less or equal) <=2.如果是使用jl(jump if less) <3.应该是编译器的优化行为?可以自己写汇编代码,使用jl 0x3试试效果是否一样

    作者回复: aiter同学谢谢。我回复了,不过你这里的理解不太对,jle指令并不是和2做比较,而是判断标志位的,jle 和 jl 用的是不同的标志位,具体可以看看这个reference http://www.unixwiz.net/techtips/x86-jumps.html

    2019-05-09
    6
  • rookie
    程序如下:
    int main(){
    int i = 0;
    int a = 0;
    switch(i){
    case 1:
    a = 1;
    break;
    case 2:
    a = 2;
    break;
    default:
    a = 3;
    break;}
    return 1;
    }

    下面是机器码和汇编代码:
    0000000000000000 <main>:
       0: 55 push rbp
       1: 48 89 e5 mov rbp,rsp
       4: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0 #将0复制给[rbp-0x8] 这个内存地址,即 i = 0
       b: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 #将0复制给[rbp-0x4] 这个内存地址,即 a = 0
      12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] #将i的值1复制给寄存器eax
      15: 83 f8 01 cmp eax,0x1 #将eax与1进行比较,如果true则执行下面的je指令,跳转到21
      18: 74 07 je 21 <main+0x21>
      1a: 83 f8 02 cmp eax,0x2 #将eax与2进行比较,如果true则执行下面的je指令,跳转到2a
      1d: 74 0b je 2a <main+0x2a>
      1f: eb 12 jmp 33 <main+0x33> #跳转到33
      21: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 #a = 1
      28: eb 11 jmp 3b <main+0x3b>
      2a: c7 45 fc 02 00 00 00 mov DWORD PTR [rbp-0x4],0x2 #a = 2
      31: eb 08 jmp 3b <main+0x3b>
      33: c7 45 fc 03 00 00 00 mov DWORD PTR [rbp-0x4],0x3 #a = 3
      3a: 90 nop
      3b: b8 01 00 00 00 mov eax,0x1 # eax = 1
      40: 5d pop rbp
      41: c3 ret # 返回eax

    作者回复: 👍

    2019-05-10
    4
  • Linuxer
    51: b8 00 00 00 00 mov eax,0x0
    这个会不会是main的返回值呢?

    作者回复: Linuxer同学你说得对,这个就是main的返回值。

    2019-05-08
    4
  • Amark
    上一节讲的是: 高级语言的代码到汇编语言再到机器指令的过程,而这节其实是: 计算机如何执行机器指令的,分为顺序执行与跳转执行的方式,当然这两种执行方式是通过指令寄存器,PC寄存器,条形码寄存器实现的。
    2019-06-15
    1
    3
  • 大熊
    接testswitch1的那条
    如果条件比较多,3个以上,反汇编出来的代码就是先经过一系列计算,最后跳转。

    -----------testswitch2.c
    #include <stdio.h>

    int main(int argc, char const *argv[])
    {
        int m = 3;
        int a = 0;

        switch (m)
        {
        case 5:
            a = 5;
            break;
        case 4:
            a = 4;
            break;
        case 3:
            a = 3;
            break;
        case 2:
            a = 2;
            break;
        case 1:
            a = 1;
            break;
        default:
            a = 9;
        }
        return 0;
    }
    -----------testswitch2.c

    上面的代码进行的反汇编:

    -----------testswitch2
    #include <stdio.h>

    int main()
    {
       0: 55 push rbp
       1: 48 89 e5 mov rbp,rsp
       4: 89 7d ec mov DWORD PTR [rbp-0x14],edi
       7: 48 89 75 e0 mov QWORD PTR [rbp-0x20],rsi
        int m = 3;
       b: c7 45 f8 03 00 00 00 mov DWORD PTR [rbp-0x8],0x3 # 把3,放在地址[rbp-0x8]
        int a = 0;
      12: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 # 把0,放在地址[rbp-0x4]

        switch (m)
      19: 83 7d f8 05 cmp DWORD PTR [rbp-0x8],0x5 # 把[rbp-0x8]的值(即3)和5比较
      1d: 77 51 ja 70 <main+0x70> # 如果大于5,则跳转到70(即default)执行 a = 9
      1f: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] # 把3,放入eax

      # 从这里开始不做过多解释,这就是经过一系列操作和计算的部分
      22: 48 8d 14 85 00 00 00 lea rdx,[rax*4+0x0]
      29: 00
      2a: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] # 31 <main+0x31>
      31: 8b 04 02 mov eax,DWORD PTR [rdx+rax*1] #
      34: 48 63 d0 movsxd rdx,eax
      37: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] # 3e <main+0x3e>
      3e: 48 01 d0 add rax,rdx
      # 到这里结束
      41: ff e0 jmp rax # 直接执行的是jmp指令,直接跳转到要执行的语句位置
    /*后面代码省略*/
    -----------testswitch2

    所以,switch判断条件多,且最好case之间的差值不要过大的时候最好使用switch

    作者回复: 👍多动手自己体会是王道。

    2019-05-22
    3
  • 免费的人
    switch case 要看编译器有没有生成跳表,没有的话跟if else效率应该是一样的,比如case个数比较少的情况

    作者回复: 👍

    2019-05-09
    3
  • -W.LI-
    switch case 我猜是用jump if equal写的,所有判断顺序写一起,所有处理逻辑顺序写一起,满足条件就跳到对应的处理逻辑,遇见break就跳转到switch块的外面,如果没有就会顺序执行剩下的处理逻辑(case穿透)。

    作者回复: 可以写一些带switch...case的程序试一下,你会发现编译器是很聪明的

    2019-05-26
    2
  • 活的潇洒
    反复阅读、并实践操作、我用的是CentOS实践过程中遇到一些问题我都记录下来了,具体详见我的博客
    https://www.cnblogs.com/luoahong/p/10862085.html

    作者回复: 👍感谢分享给大家

    2019-05-16
    2
  • 越努力,越幸运
    `而 [rbp-0x4] 则是一个寄存器的地址` 这个不应该是栈地址吗,rbp是栈基址,rbp - 0x4是第一个local var的内存地址
    2019-05-15
    1
    2
  • aiter
    徐老师好~
    C语言我不会,。,努力看了半天,算是懂了大部分,但是for循环那里还是有点问题~汇编语言里,jmp 1e 之后,应该是做比较cmp,但是为什么不是0和3比较,而是和16进制的2(0x2)比较?
    ————-
    因为后面用的jle(jump if less or equal) <=2.如果是使用jl(jump if less) <3.应该是编译器的优化行为?可以自己写汇编代码,使用jl 0x3试试效果是否一样
    作者回复: aiter同学谢谢。我回复了,不过你这里的理解不太对,jle指令并不是和2做比较,而是判断标志位的,jle 和 jl 用的是不同的标志位,具体可以看看这个reference http://www.unixwiz.net/techtips/x86-jumps.html
    ———-
    谢谢老师,我再学习一下。我上面想说的是cmpl指令后面的数字,比如把代码改为:for(i =0;i<50;i++),objdump的汇编就是cmpl $0x31,-0x8(rbp). 0x31=49。但是这个cmpl比较指令,和后面的jle这个是什么关系(比如什么情况才会使用jl),再学习理解一下

    作者回复: cmp指令执行完成之后,会更新对应的标志位
    而jle指令读取特定的标志位来决定是否跳转
    使用什么指令是由编译器决定的
    不同的跳转指令有时候可以实现相同的功能
    不同的跳转指令读取的标志位可以参见
    http://marin.jb.free.fr/jumps/

    2019-05-12
    2
  • 二进制
    认真学一遍汇编课程,你会觉得这文章很简单。

    作者回复: 👍

    2019-05-10
    2
  • 小肚腩era
    今年大四,正在实习。在实际工作慢慢发现自己基础知识的薄弱,所以现在也是抓紧时间在补习这些知识。听老师这一讲,又想起了汇编的知识,比起以前,又有了更深的理解。十分期待老师更新专栏~

    作者回复: 👍加油

    2019-05-08
    2
  •        鸟人
    大家不要把“汇编语言”当成是像C一样的一门统一编程语言。

    请问这节转换的汇编是哪里的汇编?

    作者回复: 这一节这里用的是Intel X86体系结构的汇编。对应的代码你可以直接复制到Linux下面通过同样的gcc和objdump命令dump出来看。

    2019-05-08
    2
  • 一步
    老师,我们常说的二进制执行文件,是指高级语言已经编译成一条条cpu 指令组成的文件吗?

    作者回复: 一步同学你好,差不多,但是不完全一样。二进制可执行文件里面除了指令信息之外还有很多别的信息,可以参考稍后的静态链接那一讲,会更具体地讲解我们的二进制可执行文件里面有些什么内容。

    2019-05-08
    2
  • 晴天~
    徐老师好,有一个问题想请教一下:
    执行cmp这样的指令会改变条件码寄存器部分状态码的值,我想请教一下,cpu的条件码寄存器肯定不止一个,后面的jne,jle需要判断条件码的值执行跳转,是不是有一个机制来保证jne这样的指令找到正确的条件码寄存器呢?

    作者回复: 晴天~同学

    你好,为什么不能只有一个呢?x86里一个CPU只有一个条件吗寄存器

    2019-08-19
    1
收起评论
73
返回
顶部