编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 06.计算机总线系统设计
  • 07.计算机指令编程的原理
  • 08.计算机程序如何执行
  • 09.计算机内存设计和原理
  • 10.计算机二进制和字节设计

07.计算机指令编程的原理

  • 01.指令编程的历史
    • 1.1 来看一个案例
    • 1.2 高级语言和机器码
    • 1.3 编译解释过程
  • 02.从编译到汇编
    • 2.1 代码案例分析
    • 2.2 打印程序汇编代码
    • 2.3 解析指令
    • 2.4 如何翻译成机器码
  • 03.CPU执行指令
    • 3.1 思考一个问题
    • 3.2 CPU如何执行指令
    • 3.3 看if-else执行和跳转
    • 3.4 程序控制流程

01.指令编程的历史

1.1 来看一个案例

  • 一种古老的物理设备,叫作“打孔卡(Punched Card)”。
    • 用这种设备写程序,可没法像今天这样,掏出键盘就能打字,而是要先在脑海里或者在纸上写出程序,然后在纸带或者卡片上打洞。
    • 这样,要写的程序、要处理的数据,就变成一条条纸带或者一张张卡片,之后再交给当时的计算机去处理。
  • 人们在特定的位置上打洞或者不打洞,来代表“0”或者“1”。
    • 为什么早期的计算机程序要使用打孔卡,而不能像我们现在一样,用 C 或者 Python 这样的高级语言来写呢?
    • 原因很简单,因为计算机或者说 CPU 本身,并没有能力理解这些高级语言。即使在 2019 年的今天,我们使用的现代个人计算机,仍然只能处理所谓的“机器码”,也就是一连串的“0”和“1”这样的数字。

1.2 高级语言和机器码

  • 我们每天用高级语言的程序,最终是怎么变成一串串“0”和“1”的?
    • 这一串串“0”和“1”又是怎么在 CPU 中处理的?今天,我们就来仔细介绍一下,“机器码”和“计算机指令”到底是怎么回事。
  • 在软硬件接口中,CPU 帮我们做了什么事?
    • 从硬件的角度来看,CPU 就是一个超大规模集成电路,通过电路实现了加法、乘法乃至各种各样的处理逻辑。
    • 从软件工程师的角度来讲,CPU 就是一个执行各种计算机指令(Instruction Code)的逻辑机器。这里的计算机指令,就好比一门 CPU 能够听得懂的语言,我们也可以把它叫作机器语言(Machine Language)。
  • 不同的 CPU 能够听懂的语言不太一样。
    • 比如,我们的个人电脑用的是 Intel 的 CPU,苹果手机用的是 ARM 的 CPU。这两者能听懂的语言就不太一样。
    • 类似这样两种 CPU 各自支持的语言,就是两组不同的计算机指令集,英文叫 Instruction Set。
    • 如果我们在自己电脑上写一个程序,然后把这个程序复制一下,装到自己的手机上,肯定是没办法正常运行的,因为这两者语言不通。
    • 而一台电脑上的程序,简单复制一下到另外一台电脑上,通常就能正常运行,因为这两台 CPU 有着相同的指令集,也就是说,它们的语言相通的。

1.3 编译解释过程

  • 高级语言和机器码之间存在一个编译或解释的过程。
    • 高级语言的代码需要通过编译器或解释器将其转换为机器码,以便计算机可以执行。
    • 编译器将高级语言代码转换为机器码的可执行文件,而解释器则逐行解释高级语言代码并将其转换为机器码执行。
  • 大多数开发人员使用高级语言来编写程序
    • 机器码通常由编译器或解释器生成和执行。高级语言提供了更高层次的抽象和易用性,使得编程更加方便和可靠。

02.从编译到汇编

2.1 代码案例分析

  • 了解了计算机指令和计算机指令集,接下来我们来看看,平时编写的代码,到底是怎么变成一条条计算机指令,最后被 CPU 执行的呢?我们拿一小段真实的 C 语言程序来看看。
    //test.c
    int main(){
        int a = 1;
        int b = 2;
        a = a + b;
        return 0;
    }
  • 这是一段再简单不过的 C 语言程序。
    • 我们给两个变量 a、b 分别赋值 1、2,然后再将 a、b 两个变量中的值加在一起,重新赋值给了 a 这个变量。
    • 要让这段程序在一个 Linux 操作系统上跑起来,需要把整个程序翻译成一个汇编语言(ASM,Assembly Language)的程序,这个过程我们一般叫编译(Compile)成汇编代码。
    • 针对汇编代码,可以再用汇编器(Assembler)翻译成机器码(Machine Code)。这些机器码由“0”和“1”组成的机器语言表示。这一条条机器码,就是一条条的计算机指令。这样一串串的 16 进制数字,就是我们 CPU 能够真正认识的计算机指令。

2.2 打印程序汇编代码

  • 在一个 Linux 操作系统上,我们可以简单地使用 gcc 和 objdump 这样两条命令,把对应的汇编代码和机器码都打印出来。
    yangchongdeMBP:test yangchong$ gcc -g -c test.c
    yangchongdeMBP:test yangchong$ objdump -d -M intel -S test.o
  • 可以看到,左侧有一堆数字,这些就是一条条机器码;右边有一系列的 push、mov、add、pop 等,这些就是对应的汇编代码。
    • 一行 C 语言代码,有时候只对应一条机器码和汇编代码,有时候则是对应两条机器码和汇编代码。汇编代码和机器码之间是一一对应的。
    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 = 1;
    4:   c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
    int b = 2;
    b:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
    a = a + b;
    12:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
    15:   01 45 fc                add    DWORD PTR [rbp-0x4],eax
    }
    18:   5d                      pop    rbp
    19:   c3                      ret
  • 这个时候你可能又要问了,我们实际在用 GCC 编译器的时候,可以直接把代码编译成机器码呀,为什么还需要汇编代码呢?
    • 原因很简单,你看着那一串数字表示的机器码,是不是摸不着头脑?但是即使你没有学过汇编代码,看的时候多少也能“猜”出一些这些代码的含义。
    • 因为汇编代码其实就是“给程序员看的机器码”,也正因为这样,机器码和汇编代码是一一对应的。
    • 我们人类很容易记住 add、mov 这些用英文表示的指令,而 8b 45 f8 这样的指令,由于很难一下子看明白是在干什么,所以会非常难以记忆。
    • image
      image

2.3 解析指令

  • 了解了这个过程,下面我们放大局部,来看看这一行行的汇编代码和机器指令,到底是什么意思。
    • 我们就从平时用的电脑、手机这些设备来说起。这些设备的 CPU 到底有哪些指令呢?这个还真有不少,我们日常用的 Intel CPU,有 2000 条左右的 CPU 指令,实在是太多了,所以我没法一一来给你讲解。
  • 不过一般来说,常见的指令可以分成五大类。
    • 第一类是算术类指令。我们的加减乘除,在 CPU 层面,都会变成一条条算术类指令。
    • 第二类是数据传输类指令。给变量赋值、在内存里读写数据,用的都是数据传输类指令。
    • 第三类是逻辑类指令。逻辑上的与或非,都是这一类指令。
    • 第四类是条件分支类指令。日常我们写的“if/else”,其实都是条件分支类指令。
    • 最后一类是无条件跳转指令。写一些大一点的程序,我们常常需要写一些函数或者方法。在调用函数的时候,其实就是发起了一个无条件跳转指令。
  • 举例子说明一下,帮助理解、记忆。
    • image
      image

2.4 如何翻译成机器码

  • 下面我们来看看,汇编器是怎么把对应的汇编代码,翻译成为机器码的。
    • 不同的 CPU 有不同的指令集,也就对应着不同的汇编语言和不同的机器码。为了方便你快速理解这个机器码的计算方式,我们选用最简单的 MIPS 指令集,来看看机器码是如何生成的。
  • MIPS 是一组由 MIPS 技术公司在 80 年代中期设计出来的 CPU 指令集。
    • MIPS 的指令是一个 32 位的整数,高 6 位叫操作码(Opcode),也就是代表这条指令具体是一条什么样的指令,剩下的 26 位有三种格式,分别是 R、I 和 J。
    • R 指令是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。
    • I 指令,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。
    • J 指令就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。

03.CPU执行指令

3.1 思考一个问题

  • 思考一个问题,一行代码是怎么变成计算机指令的。
    • 你平时写的程序中,肯定不只有 int a = 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和 for 这样的循环语句,还有函数或者过程调用。
    • 对应的,CPU 执行的也不只是一条指令,一般一个程序包含很多条指令。因为有 if…else、for 这样的条件和循环存在,这些指令也不会一路平铺直叙地执行下去。

3.2 CPU如何执行指令

  • CPU 是如何执行指令的?
    • 实际上,一条条计算机指令执行起来非常复杂。好在 CPU 在软件层面已经为我们做好了封装。对于软件的程序员来说,我们只要知道,写好的代码变成了指令之后,是一条一条顺序执行的就可以了。
  • CPU 其实就是由一堆寄存器组成的。
    • 寄存器就是 CPU 内部,由多个触发器(Flip-Flop)或者锁存器(Latches)组成的简单电路。触发器和锁存器,其实就是两种不同原理的数字电路组成的逻辑门。
  • 64位寄存器是怎么来的
    • N 个触发器或者锁存器,就可以组成一个 N 位(Bit)的寄存器,能够保存 N 位的数据。比方说,我们用的 64 位 Intel 服务器,寄存器就是 64 位的。
  • 一个 CPU 里面会有很多种不同功能的寄存器。介绍三种比较特殊的。
    • 7_2_4_image.png
      7_2_4_image.png
    • 第一个是 PC 寄存器(Program Counter Register),我们也叫指令地址寄存器。顾名思义,它就是用来存放下一条需要执行的计算机指令的内存地址。
    • 第二个是指令寄存器(Instruction Register),用来存放当前正在执行的指令。
    • 第三个是条件码寄存器(Status Register),用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果。
    • 除了这些特殊的寄存器,CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据,又能存放地址,我们就叫它通用寄存器。
    • 7_2_5_image
      7_2_5_image
  • 那么现在看一下CPU如何执行程序指令
    • 一个程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。
    • 可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
  • 什么是跳转指令,如何理解?
    • 有些特殊指令,比如跳转指令,会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,也是我们可以在写程序的时候,使用 if…else 条件语句和 while/for 循环语句的原因。

3.3 看if-else执行和跳转

  • 现在就来看一个包含 if…else 的简单程序。
    #include <printf.h>
    #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;
        }
        printf("a %d", a);
        return 0;
    }
  • 我们用 rand 生成了一个随机数 r,r 要么是 0,要么是 1。当 r 是 0 的时候,我们把之前定义的变量 a 设成 1,不然就设成 2。
    $ 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 指令比较了前后两个操作数的值,这里的 DWORD PTR 代表操作的数据类型是 32 位的整数,而[rbp-0x4]则是变量 r 的内存地址。所以,第一个操作数就是从内存里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。cmp 指令的比较结果,会存入到条件码寄存器当中去。
    • 2.在这里,如果比较的结果是 True,也就是 r == 0,就把零标志条件码(对应的条件码是 ZF,Zero Flag)设置为 1。除了零标志之外,Intel 的 CPU 下还有进位标志(CF,Carry Flag)、符号标志(SF,Sign Flag)以及溢出标志(OF,Overflow Flag),用在不同的判断条件下。
    • 3.cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。跟着的 jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果 ZF 为 1,说明上面的比较结果是 TRUE,如果是 ZF 是 0,也就是上面的比较结果是 False,会跳转到后面跟着的操作数 4a 的位置。这个 4a,对应这里汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的 4a 这个地址。这个时候,CPU 再把 4a 地址里的指令加载到指令寄存器中来执行。
    • 4.跳转到执行地址为 4a 的指令,实际是一条 mov 指令,第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的内存地址,以及 2 的对应的 16 进制值 0x2。mov 指令把 2 设置到对应的内存里去,相当于一个赋值操作。然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。
    • 5.这条 mov 指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,它的作用是一个占位符。我们回过头去看前面的 if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令。跳转的地址就是这一行的地址 51。我们的 main 函数没有设定返回值,而 mov eax, 0x0 其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后也会跳转到这里,和 else 里的内容结束之后的位置是一样的。
    • 7_2_6_image
      7_2_6_image

3.4 程序控制流程

  • 程序里的多条指令,究竟是怎么样一条一条被执行的
    • 通过 PC 寄存器自增的方式顺序执行外,条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。
  • 用高级语言,可以用不同的语法
    • 比如 if…else 这样的条件分支,或者 while/for 这样的循环方式,来实现不同的程序运行流程,但是回归到计算机可以识别的机器指令级别,其实都只是一个简单的地址跳转而已。

3.5

更多内容推荐

  • GitHub:https://github.com/yangchong211
  • 博客:https://juejin.cn/user/1978776659695784
  • 博客汇总:https://github.com/yangchong211/YCBlogs
  • 设计模式专栏:https://github.com/yangchong211/YCDesignBlog
  • Java高级进阶专栏:https://github.com/yangchong211/YCJavaBlog
  • 网络协议专栏:https://github.com/yangchong211/YCNetwork
  • 计算机基础原理专栏:https://github.com/yangchong211/YCComputerBlog
  • 系统性学习C编程:https://github.com/yangchong211/YCStudyC
  • C++学习案例:https://github.com/yangchong211/YCStudyCpp
  • Leetcode算法专栏:https://github.com/yangchong211/YCLeetcode
  • Android技术专栏:https://github.com/yangchong211/YCAndroidBlog
贡献者: yangchong211
上一篇
06.计算机总线系统设计
下一篇
08.计算机程序如何执行