指令跳转:原来if...else就是goto
目录
CPU 是如何执行指令的?
从 if…else 来看程序的执行和跳转
如何通过 if…else 和 goto 来实现循环?
小结
你平时写的程序中,肯定不只有 int a = 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和 for 这样的循环语句,还有函数或者过程调用。
对应的,CPU 执行的也不只是一条指令,一般一个程序包含很多条指令。因为有 if…else、for 这样的条件和循环存在,这些指令也不会一路平铺直叙地执行下去。
今天我们就来看看,一个计算机程序是怎么被分解成一条条指令来执行的。
CPU 是如何执行指令的?
拿我们用的 Intel CPU 来说,里面差不多有几百亿个晶体管。实际上,一条条计算机指令执行起来非常复杂。好在 CPU 在软件层面已经为我们做好了封装。对于我们这些做软件的程序员来说,我们只要知道,写好的代码变成了指令之后,是一条一条顺序执行的就可以了。
我们先不管几百亿的晶体管的背后是怎么通过电路运转起来的,逻辑上,我们可以认为,CPU 其实就是由一堆寄存器组成的。而寄存器就是 CPU 内部,由多个触发器(Flip-Flop)或者锁存器(Latches)组成的简单电路。
触发器和锁存器,其实就是两种不同原理的数字电路组成的逻辑门。这块内容并不是我们这节课的重点,所以你只要了解就好。如果想要深入学习的话,你可以学习数字电路的相关课程,这里我们不深入探讨。
好了,现在我们接着前面说。N 个触发器或者锁存器,就可以组成一个 N 位(Bit)的寄存器,能够保存 N 位的数据。比方说,我们用的 64 位 Intel 服务器,寄存器就是 64 位的。
一个 CPU 里面会有很多种不同功能的寄存器。我这里给你介绍三种比较特殊的。
一个是PC 寄存器(Program Counter Register),我们也叫指令地址寄存器(Instruction Address Register)。顾名思义,它就是用来存放下一条需要执行的计算机指令的内存地址。
第二个是指令寄存器(Instruction Register),用来存放当前正在执行的指令。
第三个是条件码寄存器(Status Register),用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果。
除了这些特殊的寄存器,CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据,又能存放地址,我们就叫它通用寄存器。
实际上,一个程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
而有些特殊指令,比如上一讲我们讲到 J 类指令,也就是跳转指令,会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,也是我们可以在写程序的时候,使用 if…else 条件语句和 while/for 循环语句的原因。
从 if…else 来看程序的执行和跳转
我们现在就来看一个包含 if…else 的简单程序。
// 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。
$ 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],0x03f: 75 09 jne 4a <main+0x4a>{a = 1;41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x148: eb 07 jmp 51 <main+0x51>}else{a = 2;4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x251: b8 00 00 00 00 mov eax,0x0}
可以看到,这里对于 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 Flag)、符号标志(SF,Sign Flag)以及溢出标志(OF,Overflow Flag),用在不同的判断条件下。
cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。
跟着的 jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 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 里的内容结束之后的位置是一样的。
我们讲打孔卡的时候说到,读取打孔卡的机器会顺序地一段一段地读取指令,然后执行。执行完一条指令,它会自动地顺序读取下一条指令。如果执行的当前指令带有跳转的地址,比如往后跳 10 个指令,那么机器会自动将卡片带往后移动 10 个指令的位置,再来执行指令。同样的,机器也能向前移动,去读取之前已经执行过的指令。这也就是我们的 while/for 循环实现的原理。
如何通过 if…else 和 goto 来实现循环?
int main()
{int a = 0;for (int i = 0; i < 3; i++){a += i;}
}
我们再看一段简单的利用 for 循环的程序。我们循环自增变量 i 三次,三次之后,i>=3,就会跳出循环。整个程序,对应的 Intel 汇编代码就是这样的:
for (int i = 0; i < 3; i++)b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x012: 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],eaxfor (int i = 0; i < 3; i++)1a: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x11e: 83 7d f8 02 cmp DWORD PTR [rbp-0x8],0x222: 7e f0 jle 14 <main+0x14>24: b8 00 00 00 00 mov eax,0x0}
可以看到,对应的循环也是用 1e 这个地址上的 cmp 比较指令,和紧接着的 jle 条件跳转指令来实现的。主要的差别在于,这里的 jle 跳转的地址,在这条指令之前的地址 14,而非 if…else 编译出来的跳转指令之后。往前跳转使得条件满足的时候,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行之前执行过的指令,直到条件不满足,顺序往下执行 jle 之后的指令,整个循环才结束。
如果你看一长条打孔卡的话,就会看到卡片往后移动一段,执行了之后,又反向移动,去重新执行前面的指令。
其实,你有没有觉得,jle 和 jmp 指令,有点像程序语言里面的 goto 命令,直接指定了一个特定条件下的跳转位置。虽然我们在用高级语言开发程序的时候反对使用 goto,但是实际在机器指令层面,无论是 if…else…也好,还是 for/while 也好,都是用和 goto 相同的跳转到特定指令位置的方式来实现的。
小结
我们在单条指令的基础上,学习了程序里的多条指令,究竟是怎么样一条一条被执行的。除了简单地通过 PC 寄存器自增的方式顺序执行外,条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。
你会发现,虽然我们可以用高级语言,可以用不同的语法,比如 if…else 这样的条件分支,或者 while/for 这样的循环方式,来实现不用的程序运行流程,但是回归到计算机可以识别的机器指令级别,其实都只是一个简单的地址跳转而已,也就是一个类似于 goto 的语句。
想要在硬件层面实现这个 goto 语句,除了本身需要用来保存下一条指令地址,以及当前正要执行指令的 PC 寄存器、指令寄存器外,我们只需要再增加一个条件码寄存器,来保留条件判断的状态。这样简简单单的三个寄存器,就可以实现条件判断和循环重复执行代码的功能。
相关文章:

指令跳转:原来if...else就是goto
目录 CPU 是如何执行指令的? 从 if…else 来看程序的执行和跳转 如何通过 if…else 和 goto 来实现循环? 小结 你平时写的程序中,肯定不只有 int a 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和…...

【数据库系统概论】第四章数据库安全性
数据库的安全性:保护数据库以防止不合法使用所造成的数据泄露、更改或破坏 grant和revoke语法...

如何正确的关闭Redis服务器
Redis官方原生版本是在Linux平台上开发和测试的,但是大多数初学者都是使用Windows系统来学习如何开发的。因此,官方提供了一个叫做“Microsoft Open Tech Redis”的项目,该项目专门为Windows平台提供了一个官方支持的Redis版本,但…...

MySQL日志管理和权限管理(重点)
目录 一、日志管理1.错误日志2.二进制日志3.慢查询日志 二、权限管理(重点)1.用户登录管理2.创建用户及授权3.刷新权限4.权限简介5.查看权限7.修改密码8、删除用户9、查看密码复杂度 一、日志管理 日志类型 1、错误日志:启动,停止,关闭失败报…...

Maven 使用教程(二)
一、如何创建JAR并将其安装在本地存储库中? 制作JAR文件非常简单,可以通过执行以下命令来完成: mvn package现在可以查看${project.basedir}/target目录,您将看到生成的JAR文件。 现在,您需要将生成的工件࿰…...

[Swift]同一个工程管理多个Target
1.准备 先创建一个测试工程“ADemo”,右键其Target选择Duplicate,再复制一个Target为“ADemo2”。 再选择TARGETS下方的“”,添加一个APP到项目中,这个命名为“BDemo”。 2、管理多个Target 可以对三个target分别导入不同的框…...

DevExpress Reporting中文教程 - 如何在macOS等系统中生成导出报表文档
DevExpress Reporting是.NET Framework下功能完善的报表平台,它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集,包括数据透视表、图表,因此您可以构建无与伦比、信息清晰的报表。 在本文中,我们将讨论如何在.NET MA…...

1967作为子字符串出现在单词中的字符串数目
java解法,程度:简单 说明 给你一个字符串数组 patterns 和一个字符串 word ,统计 patterns 中有多少个字符串是 word 的子字符串。返回字符串数目。 子字符串 是字符串中的一个连续字符序列。 示例 1: 输入:patter…...

CocosCreator 面试题(二)JavaScript中的prototype的理解
1、原型(prototype)的作用 在JavaScript中,每个函数都有一个特殊的属性叫做"prototype",它是一个对象。 原型(prototype)在JavaScript中用于实现对象之间的继承和共享属性。当创建一个函数时&am…...

python:从Excel或者CSV中读取因变量与多个自变量,用于训练机器学习回归模型,并输出预测结果
作者:CSDN @ _养乐多_ 本文详细记录了从Excel读取用于训练机器学习模型的数据,包括独立变量和因变量数据,以供用于机器学习模型的训练。这些机器学习模型包括但不限于随机森林回归模型(RF)和支持向量机回归模型(SVM)。随后,我们将测试数据集应用于这些模型,进行预测和…...

pycharm连接gitlab
1、下载安装gitlab 下载地址:Git - Downloading Package 下载后傻瓜式安装,注意勾选配置环境变量 未配置自己配置,电脑-属性-高级系统配置-环境变量 系统变量path:添加git安装目录下bin目录 2、检验安装完成 桌面右键git-open…...

【C/C++数据结构 - 2】:稳定性与优化揭秘,揭开插入排序、希尔排序和快速排序的神秘面纱!
文章目录 排序的稳定性插入排序插入排序的优化 希尔排序快速排序 排序的稳定性 稳定排序:排序前2个相等的数在序列中的前后位置顺序和排序后它们2个的前后位置顺序相同。(比如:冒泡、插入、基数、归并) 非稳定排序:排…...

PCL点云处理之基于强度特征的SIFT关键点提取法 (二百一十五)
PCL点云处理之基于强度特征的SIFT关键点提取法 (二百一十五) 一、算法介绍二、具体实现1.代码2.效果一、算法介绍 继续SIFT关键点的提取介绍,之前已经基于高程和颜色分别提取了关键点,这里是基于强度信息,若遇到文件无法读取强度问题,请参考上一篇博文,下面是具体的实现…...

uniapp打包配置
安卓: 首先不管是什么打包都需要证书,安卓的证书一般都是公司提供或者自己去申请。然后把包名等下图框住的信息填上,点击打包即可。 ios:ios需要使用mac到苹果开发者平台去申请证书,流程可以参考下边的链接 参考链接…...

人大金仓分析型数据库最大量限制
数据库支持的最大量限制: 维度 限制 数据库大小 无限 表大小 无限 行大小 1.6 TB 域大小 1 GB 每个表的行数 281474976710656 (2^48) 每个表 / 视图的列数 1600 每个表的索引数 无限 每个索引的列数 32 每个表的表级约束 无限 表名长度 63 字节 列出的“无限”…...

centos 里面的service自启动app.jar,出现两个java进程,app是同一个端口
当使用jps -lv查看java虚拟机进程 app.jar启动后,居然出现两个启动进程,而且他们的端口都一样,同一端口,是不允许启动两个相同app的。 使用进程ps查看进程工具 #ps -aux 参数说明: a: 显示跟当前终端关联的所有进…...

【算法|双指针系列No.7】leetcodeLCR 007. 三数之和
个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 🍔本专栏旨在提高自己算法能力的同时,记录一下自己的学习过程,希望…...

ubuntu修改IP地址
参考:ubuntu修改配置IP地址和DNS的方法总结(4种)_ubuntu设置ip地址-CSDN博客 面对ubuntu18以上的版本,主要有两种界面:图形化界面和纯命令行界面。 图形化界面配置比较简单,命令行配置稍许复杂,…...

java springboot 通过ConfigurationProperties给第三方bean注入属性
之前我们的文章 java boot将一组yml配置信息装配在一个对象中 讲过了 通过ConfigurationProperties将配置文件中的内容默认装配进属性类 但 这建立在 bean是自己定义的 如果 这是个第三方的类呢? 就比如 我们在 application 中写了一套数据源的加载规则 但需要用第…...

windows系统安装openssl并且转换证书格式
概述 碎碎念,如果你有MAC电脑,就别折腾了,直接用MAC电脑吧,不用安装直接用openssl 本文主要讲到了openssl的基本使用方法,开发环境为windows,开发工具为VS2019.本文主要是说明openssl如何使用,不介绍任何理…...

【GO】基础速成
简单介绍一下go好处 编译语言,可以提前报错同时又有python的一些优点,自带多线程 开始学习 学习网站:学习网站 值 包含:字符串、整型、浮点型、布尔型等等 字符串可以 进行拼接。 需要注意的是布尔型在go里面不自动转化为in…...

五子棋(C语言实现)
目录 构思 1、主程序 2、初始化 3、游戏菜单 4、打印棋盘 6、玩家下棋 7、判断输赢 8、功能整合 人机下棋 完整版: game.h game.c text.c 测试功能代码 构思 五子棋不必多介绍了,大家小时候都玩过哈。 我们要通过程序实现这个小游戏&…...

thymeleaf,bootstrap-fileinput 多文件上传
组件遍历上传 一、前端 <!DOCTYPE html> <html lang"zh" xmlns:th"http://www.thymeleaf.org" > <head><th:block th:include"include :: header(修改固定资产信息)" /><th:block th:include"include :: date…...

爬虫 | 基础模块了解
文章目录 📚http协议📚requests模块📚re模块🐇 re.I 或 re.IGNORECASE🐇re.M或 re.MULTILINE🐇re.S 或 re.DOTALL🐇 re.A 或 re.ASCII🐇 re.X 或 re.VERBOSE🐇特殊字符类…...

CSS复习笔记
CSS 文章目录 CSS1.概念2.CSS 引入方式3.选择器基础选择器:标签选择器类选择器id 选择器通配符选择器 复合选择器:**后代选择器****子代选择器****并集选择器****交集选择器-了解****伪类选择器** 结构伪类选择器:**:nth-child(公式)**伪元素…...

编译linux的设备树
使用make dtbs命令时 在arch/arm 的目录Makefile文件中有 boot : arch/arm/boot prepare 和scripts是空的 在文件scripts/Kbuild.include中 变量build : -f $(srctree)/scripts/Makefile.build obj build变量虽然没有在arch/arm 的目录Makefile文件中定义,但…...

⛳ MyBatis 中 Mapper 接口工作原理实例解析
🎍目录 ⛳ MyBatis 中 Mapper 接口工作原理实例解析🎨 一、Mapper 接口是怎么找到实现类的?🐾 二、从一段代码看起🚜 三、Mapper 接口🏭 四、Mapper 接口的动态代理类的生成🎁 五、总结 ⛳ MyBa…...

Android 音频可视化
Android音频可视化,指的是将音频的频率绘制到屏幕上,达到一种视觉效果,使播放或录制过程更加生动形象。 在Android进行视频可视化涉及的三个主要知识点,其中比较难以理解的傅里叶变换公式。 Android原生的Visualizer使用(获取频…...

刷机与救砖避坑指南
提示:快速进行刷机和救砖学习理解 文章目录 一、刷机1.什么是刷机,需要进行那些准备?2.刷机1.解开bl(bootloader)锁2.刷入TWRP和Magsik3.刷入第三方ROM 二、救砖(9008)1.手机售后一键线刷包&…...

软件建模知识点
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言 提示:这里可以添加本文要记录的大概内容: 例如:…...