计算机组成

3 指令系统体系结构

3.4 复杂的x86指令举例

x86作为复杂指令系统的代表,自然会有不少相当复杂的指令。在这一节我们将会看到其中有代表性的一些例子。

关于复杂的x86指令,我们这里举四个例子。第一个是串操作指令。

串操作指令是将存储器中的数据串进行每次一个元素的操作。所谓一个元素可以是字节或者是字。这个串可以很长,能够达到64K个Byte。x86提供了5种不同的串操作指令,并且还有3种重复前缀,可以与串操作指令配合使用。

这张表就展示了这5种串操作指令和3种重复前缀。

我们来选择其中一组进行介绍。这就是字节串传送指令,这个指令的格式非常简单,没有任何的操作数。它的功能就是在存储器中将指定位置的一个字节单元传送到存储器的另一个指定的位置。与它配合的经常是这个重复前缀REP,x86的体系结构中有很多种的前缀,这个前缀的涵义是,当CX寄存器的值不等于0时,就重复执行这个串操作指令。那么很奇怪的是这个指令没有任何操作数。其实大家要注意x86当中有很多这样的没有操作数的指令,但这并不意味着它们比那些有操作数的指令要简单。因为它们不写操作数,不是因为没有操作数,很可能是因为操作数太多了,实在在指令中写不下。因此它们实际上是有一些隐含的操作数。

对于这条串传送指令,它要传送的数据串称为源串。源串的地址默认放在 DS:SI 这组寄存器指向的位置。而要传送的目的,我们称为目的串地址,默认放在 ES:DI 这组寄存器指向的位置。而要传送的串的长度则放在 CX 寄存器当中。

我们可以看到,虽然没有写操作数,但是它实际有5个寄存器作为它的操作数。不仅它有隐含的操作数,还有一些隐含的操作。除了进行串的传送之外,在完成这个操作之后,硬件上还会自动完成这些操作:

第一修改 SI 和 DI 寄存器,以指向下一个串元素。然后再判断是否使用了重复前缀,如果是,则将 CX 寄存器的内容减 1。需要注意的是这些操作都是硬件自动完成的,不需要程序员在软件中特别指定。

我们来看一个例子。假设我们在存储器中要进行一次数据串的传送。源串的位置在12040这个地址开始,一共三个字节,我们希望传送到12060开始的地方。那我们编写的程序是这样的,假设事先已配置好了数据段寄存器DS为1000。

这个程序的前两条指令实际是将数据段寄存器的内容传送到附加段寄存器当中,只不过段寄存器之间不能直接传送,所以借用了AX。

然后在 SI 寄存器当中保存源串的偏移地址, 在DI寄存器当中放入目的串的偏移地址,这样DS和SI这组寄存器就指向了源串。而ES和DI这组寄存器就指向了目的串。

下一条指令CLD,这是确定传送的方向,一会儿再进行解释。

然后在CX寄存器当中存入3。

然后才是 REP MOVSB 这条串传送指令。前面加上了重复前缀,这样的配置就相当于连续执行了三次这条串传送指令。

当执行第一次传送之后,第一个字节被传送到了目的串的位置,传送完成后,SI和DI自动被增加,CX 自动被减1。这些操作都是由CPU完成的。

同时我还要说明,所谓的传送这个字节实际上是被CPU发起的向12040地址的读操作,读入到CPU中,再发起一次向12060地址的存储器写操作,写入到对应的字节单元。在第二次传送后,SI和DI又被加1,CX又被减1。第三次传送完之后,虽然SI和DI继续加1,但CX已经减为0,所以不再继续执行。

还需要说明一点的是串传送的方向也是可以设置的。如果设置DF=0,则是从源串的低地址开始传送,在传送过程中,SI和DI是自动增量的修改。如果设置DF=1,则是从源串的高地址开始传送,传送过程中,SI和DI自动减量的修改。

这个表格就说明了SI和DI的修改方法。那如何修改DF标志位呢?其实x86提供了两条控制指令,对标志位进行操作。STD就是把DF标志置1。CLD就是我们刚才的例子中的那条指令,是把DF清0。这就可以确定串传送的方向。设置这样的方向实际上是为了应对源串和目的串有可能重叠的问题。

我们简单来看一个释意。如果源串和目的串在内存中是互相不重叠的,那这时候设置DF为0,或者为1,都没有关系。

但是如果你的源串和目的串有一个重叠,那必须设置DF为1,从高地址依次向低地址开始传送,不然图中绿色的重叠部分,就会在传送的一开始被覆盖,从而导致结果的错误。

那如果源串和目的串是靠右这张图的重叠的形式,则必须设置DF为0。从低地址开始传送,原因也是一样的。

除了串传送指令,还有其他类型的串操作。例如在一个数据串种,查找特定的数据,或者比较两个数据串是否相同。这样程序员有了很便利的手段,对一大块数据进行操作。因此串操作指令是功能非常强大的指令,不过由于数据串当中的元素数量有可能很多,因此串操作指令的执行时间也可能很长,这是需要注意的。

第二个我们来看循环控制指令。

循环控制指令主要有这几类。我们也选其中的一个来进行介绍。

这就是LOOPNE或者是LOOPNZ指令。这两个指令的写法不同,但其实它们的含义和指令的编码其实是一样的。它的操作就是每次将CX寄存器的内容减1,并且判断CX是否为零,如果CX不等于0,而且标志位ZF等于0,则转移到指定的目标地址处继续执行。否则,结束当前的循环,顺序执行下一条指令。我们也来看一个例子。

如果我们要在一百个字符的字符串中,寻找第一个 $ 字符,我们可以这样写,先向CX寄存器中存入100。在这个循环体内部,最重要的是将SI所指向的内存字节与$ 字符进行比较,如果比较结果为相等,则标志寄存器当中的Z位会被置为有效,然后 LOOPNZ 指令会进行判断,如果Z位无效,则转移到NEXT标号处继续执行,也就是继续进行循环,如果不是则退出循环。同时它还会检查CX的内容,所以这个循环要么执行完100次,要么在循环的过程中就发现有比较相等的情况,从而退出循环。当然在循环的出口,还需要进行一些分析,以判断是否找到以及在什么位置找到的。其实我们也可以用更加简单的条件转移指令来完成循环语句的书写,我们可以设想一下如何用条件转移指令,例如 JNZ 来改写这段程序,但是有了这样的循环控制指令,就会给编程带来很大的便利。这也是x86指令系统的一个很大的特点,就是虽然可以用其它指令的组合来进行替代,但是x86中宁愿提供新的指令,从而更加方便的完成这个功能。

然后我们来看一个查表的指令。

查表指令 XLAT,它也是一个没有操作数的指令,我们现在看到没有操作数的指令,都应该保持警惕,这个指令其实相当的复杂。它首先需要在数据段中定义一个字节型的数据表,然后再执行这条指令时的操作是这样的,它会从BX寄存器中取得数据表的起始地址的偏移量,然后从AL寄存器中取得数据表的索引值,然后根据这两个值从数据表中查得表项的内容,并将查得的表项内容存入AL中。所以它的隐含操作数是BX、AL,而且还需要提前定义数据表,并且它还会修改AL的内容。我们也来看一个例子。

这是一段汇编语言程序。首先定义一个字节型的数据表,我们也可以简单的把它理解为一个数组。这条指令是将这个数据表起始地址的偏移量放到BX寄存器中。然后在AL中存入4,再执行XLAT的指令,这时候会发生什么呢?按照刚才的定义,XLAT指令会根据BX找到这个数据表,然后根据AL的内容找到这个数据表中对应的那个元素,并把这个元素的内容放到AL寄存器当中。因此,在执行完这条MOV指令之后,AL的内容应该是4,而执行完XLAT指令之后,它就成为了这个数据表中第4个元素,也就是66。

那如果在AL中又存入了6,再一次执行XLAT指令,这时AL当中的数应该是多少呢?实际执行完以后,应该是7D,就是这个数据表的第6个元素。

最后我们来看一看十进制的调整指令。

十进制的调整指令主要有这几个。我们也来看其中的一个例子。

DAA指令,被称为加法十进制调整指令。它的格式也是没有操作数。它的操作有这样的要求,首先要跟在二进制加法指令之后,它是将AL中的“和”,也就是刚才这条加法指令运算的结果,调整为压缩BCD数的格式,并将调整的结果再送回到AL当中。

BCD数,就是指用二进制编码的十进制数。二进制编码,可以用计算机进行保存,而十进制则便于人的识别。所以BCD数的设计目的,就是为人与计算机的联系提供一个便利的中间表示。这个便利在哪里呢?我们看一个例子,例如一个十进制数42。如果用二进制来表示,那就是0010 1010,即使写成16进制的形式2A,看起来也和42差别很大。如果人想用计算机保存这样的数,又想很直观的能看到十进制这样的显示效果,那就可以采用BCD数的形式。在这个BCD数中,我们用四个比特来代表一个十进制数。对于42,高四个比特(bit)记录了4,低四个比特记录了2,这样这个字节就可以看作记录了一个十进制数42。那这个指令怎么用呢?我们再看一个例子。

这段程序的第一条指令把27,注意是16进制的27H,放在AL寄存器当中,这个数相当于十进制的39,然后将AL的数与15H这个数相加。这条指令运算完后,AL中应该保存的是3CH,这个数实际上是十进制的60。然后我们再运行DAA这条指令,运行完之后,AL当中的数就变成了42H。那这个有什么用呢?

十六进制 二进制 十进制
27 0010 0111 39
15 0001 0101 21
3C 0011 1100 60

实际上是这样的,如果我们希望进行BCD数的运算,也就是我们想做27+15这个操作,如果按照正常的计算机编程的思路,我们应该将十进制的27和15都先转换成二进制,然后再用这两条指令进行运算,运算结束后,再将运算结果由二进制转化为十进制,从而得到了结果42。但是如果想非常简便的进行十进制的运算,而且这些数的范围也不是很大,要么直接就用这样的表示形式,直接用27H代表27,15H代表15,然后我们期望运算的结果是27+15的结果,也就是42。但是这条加法指令并不会领会到这一点,它加完的结果仍然是3CH,所以我们额外增加一条DAA指令,这条指令就是按我们刚才说的十进制相加的思路,把这个结果进行转换,所以它就会得到了42H。这样的指令有什么用呢?实际上在一些非常简单的设备上,它需要进行很简单的算术运算,又不想要有很多的转换,就可以采用这样的方式。当然现在真正这样用的已经越来越少了。

最后我们从一个有趣的例子来看一看x86指令的复杂程度。这张图是x86指令的通用格式,每一个小格都是指令格式中特定的位域。那我们可以人为的写出一条指令来,这条指令是一个加法,而且有一个前缀LOCK,这和我们刚才学到的REP一样,都是指令的前缀。这个加法其中一个源操作数是32位的立即数。另一个源操作数以及目的操作数是内存当中的一个32位的存储单元,这个存储单元本应默认在数据段,但这里强制指定为在附加段,这个存储单元的地址由EAX寄存器,ECX寄存器和一个立即数计算而得。要计算这个内存地址,我们看到需要一次乘法,两次加法得到偏移地址,再和段基址进行移位并相加的操作,然后访问这个存储单元得到32位数,再与12345678这个立即数相加。然后再访问这个存储单元,将这个数存进去。这条指令的编码一共有15个字节,可以认为是一条最长的x86指令。x86指令的复杂程度,由此可见一斑。

编程人员只用给出一条简短的指令,计算机就可以完成非常复杂的工作,这自然是一件很好的事情,计算机似乎就应该这么设计。可惜世界没有这么简单, 有人提出了完全相反的做法,我们下一节再说。

3.4 复杂的x86指令举例的更多相关文章

  1. 3.3 x86指令简介

    计算机组成 3 指令系统体系结构 3.3 x86指令简介 x86指令种类繁多,数量庞大.在这一节我们将会学习x86指令的分类,并分析其中最为基础的一部分指令. 通常一个指令系统主要包括这几类指令.运算 ...

  2. 反汇编基本原理与x86指令构造

    反汇编基本原理与x86指令构造 概要:旨在讲述程序的二进制代码转换到汇编.即反汇编的基本原理.以及 x86 架构的 CPU 的指令构造,有这个基础后就能够自己编写汇编程序了,也能够将二进制代码数据转换 ...

  3. 从X86指令深扒JVM的位移操作

    概述 之所以会写这个,主要是因为最近做的一个项目碰到了一个移位的问题,因为位移操作溢出导致结果不准确,本来可以点到为止,问题也能很快解决,但是不痛不痒的感觉着实让人不爽,于是深扒了下个中细节,直到看到 ...

  4. 第18章-x86指令集之常用指令

    x86的指令集可分为以下4种: 通用指令 x87 FPU指令,浮点数运算的指令 SIMD指令,就是SSE指令 系统指令,写OS内核时使用的特殊指令 下面介绍一些通用的指令.指令由标识命令种类的助记符( ...

  5. 深入理解计算机系统(4.1)---X86的孪生兄弟,Y86指令体系结构

    引言 各位猿友们好,计算机系统系列很久没更新了,实在是抱歉之极.新的一年,为了给计算机系统系列添加一些新的元素,LZ将其更改为书的原名<深入理解计算机系统>.这本书非常厚,而且难度较高,L ...

  6. 开始逆向objc基础准备(一)简单认识一下arm32,以及与x86汇编指令类比

    ARM32体系中有31或33个通用寄存器,没有特定的某种态下有r0-r15一共16个寄存器,快速中断态下有另一组r8-r12备份寄存器,在用户态和系统态之外其它态下都各自有一组r13-r14备份寄存器 ...

  7. 【PC桌面软件的末日,手机移动端App称王】写在windows11支持安卓,macOS支持ios,龙芯支持x86和arm指令翻译

    面对这场突如其来的变革,作为软件开发者,应该如何选择自己今后的发展方向?桌面软件开发领域还有前景吗? 起源 自从苹果发布m1处理器,让自家Mac支持IOS移动端app运行之后,彻底打破了移动端app和 ...

  8. 关于Android开发中Arm、X86和Mips(草稿)

    一.架构 1.Arm架构 是一个32位精简指令集(RISC)处理器架构,其广泛地使用在许多嵌入式系统设计. 2.X86架构 是一个intel通用计算机系列的标准编号缩写,也标识一套通用的计算机指令集合 ...

  9. 近期业务大量突增微服务性能优化总结-3.针对 x86 云环境改进异步日志等待策略

    最近,业务增长的很迅猛,对于我们后台这块也是一个不小的挑战,这次遇到的核心业务接口的性能瓶颈,并不是单独的一个问题导致的,而是几个问题揉在一起:我们解决一个之后,发上线,之后发现还有另一个的性能瓶颈问 ...

随机推荐

  1. eclipse启动 报错,错误信息为 return exit code=13

    打不开的报错如下图: 解决方法:手工配置Eclipse使用的JDK,在Eclipse的安装目录中找到eclipse.ini文件,增加正确的JDK安装目录,如图 在plugins/ 下一行,增加 -vm ...

  2. quartz-job实现定时任务配置

    使用quartz开源调度框架,写服务实现在一些指定场景发送特定短信,创建一个实现org.quartz.Job接口的java类.Job接口包含唯一的方法: public void execute(Job ...

  3. RocketMQ事务消费和顺序消费详解

    一.RocketMq有3中消息类型 1.普通消费 2. 顺序消费 3.事务消费 顺序消费场景 在网购的时候,我们需要下单,那么下单需要假如有三个顺序,第一.创建订单 ,第二:订单付款,第三:订单完成. ...

  4. 在thinkphp里面执行原生的sql语句

    在thinkphp里面执行原生的sql语句 怎样在thinkphp里面执行原生的sql语句? $Model = new Model();//或者 $Model = D(); 或者 $Model = M ...

  5. selinux配置错误实例介绍

    错误原因 配置关闭SELinux,结果误操作 应修改配置文件/etc/selinux/config中的“SELINUX”参数的值, # SELINUX=enforcing  原始配置 SELINUX= ...

  6. MySQL数据库----数据操作

    注意的几点:1.如果你在cmd中书命令的时候,输入错了就用\c跳出 2.\s查看配置信息 一.操作文件夹(库) 增:create database db1 charset utf8; 删:drop d ...

  7. SNMP学习笔记之SNMP 原理与实战详解

    原文地址:http://freeloda.blog.51cto.com/2033581/1306743 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法 ...

  8. 20145306 张文锦 网络攻防 web基础

    20145306 网络攻防 web基础 实验内容 WebServer:掌握Apache启停配置修改(如监听端口)前端编程:熟悉HTML+JavaScript.了解表单的概念编写不同的HTML网页,放入 ...

  9. STM32.SPI(25Q16)

    1.首先认识下W25Q16DVSIG, SOP8 SPI FLASH 16MBIT  2MB(4096个字节) (里面可以放字库,图片,也可以程序掉电不丢失数据放里面) 例程讲解: ① 1.用到SPI ...

  10. Python3基础 getattr 获取对象的指定属性值

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...