3.3 x86指令简介

计算机组成

3 指令系统体系结构

3.3 x86指令简介

Screen Shot 2018-06-02 at 18.51.06

x86指令种类繁多,数量庞大。在这一节我们将会学习x86指令的分类,并分析其中最为基础的一部分指令。

Screen Shot 2018-06-02 at 18.51.17

通常一个指令系统主要包括这几类指令。运算类指令,比如加、减、乘、除这样的算术运算,以及与、或、非这样的逻辑运算。

还有传送类指令,比如把数据从存储器送到通用寄存器,或者从通用寄存器送到I/O接口等等。

有了这两类指令,计算机就可以从外界获取数据,并在内部完成运算,最后将结果输出到外界。

但是如果你想编制比较复杂的程序,例如像高级语言当中 if else 这样的语句,或者是for, while这样的循环语句,那就需要用到转移类指令。

另外还需要有一些对CPU进行控制的指令。

Screen Shot 2018-06-02 at 18.51.26

那无论是哪一类指令,我们首先要关心的就是它究竟改变了什么。例如一条加法指令,它会改变通用寄存器的内容,或者有可能改变标志位,再有是改变存储器单元的内容,或者改变外设端口的内容,还有可能改变指令指针以及其它的情况。那我们在学习到新的指令的时候,一定要认真地想清楚这条指令究竟改变了哪些地方,又对后续的指令会产生什么样的影响。

Screen Shot 2018-06-02 at 18.51.36

我们分不同的类型进行简单的介绍,首先要说明的是x86指令数量非常多,在每个类别中我们只能挑个别的指令作为示例进行说明。大部分指令都适用于保护模式和实模式,只有在涉及到分段这种很复杂的情况下,为了说明的简便,采用实模式下的形式,但其原理都是类似的。

Screen Shot 2018-06-02 at 18.51.45

首先来看传送类指令。这里列出的是主要的传送类指令,我们首先来看最具代表性的MOV指令。

Screen Shot 2018-06-02 at 18.51.56

MOV指令的作用是将源操作数传送到目的操作数。MOV指令有很多不同的形式,我们结合不同MOV指令的示例,同时说一说操作数的寻址方式。

Screen Shot 2018-06-02 at 18.52.05

第一条MOV指令,是将立即数40传送到EBX寄存器当中,这里的源操作数就是在指令编码中直接给出的。

第二条指令是将BL寄存器的内容传送到AL寄存器中,这里的源操作数是给出了存放操作数的寄存器的名称。

第三条指令,是将1000这个存储器单元的内容传送到ECX寄存器中,这种情况则是给出了存放操作数的存储器地址。

第四条指令是将AX寄存器的内容传送到DI所指向的存储器的单元中,这里的传送目标是一个存储器单元,这个存储器单元的地址存放在DI这个寄存器中。因此在指令执行时,需要先访问DI寄存器,取出其中的内容作为地址进行存储器的访问。

最后这条指令只是将一个立即数送了一个存储器的单元,在指令中给出的是这个存储单元地址的计算方法。

所以在x86体系结构中,寻址方式是非常复杂的。而对于这条MOV指令,虽然它的助记符都是MOV这个词,但实际对应了不同类型的指令。

Screen Shot 2018-06-02 at 18.52.24

这张表就说明了MOV指令的不同的编码形式。在这张表中最长可能有六个字节,最短的则是三个字节。指令长度不同也是x86的一个重要特征。但是CPU怎么知道当前要取得这条指令是多长呢?我们看第一个字节,红色的部分标了0或1的就是指令编码中固定的部分,用字母表示的代表这个位根据指令的具体功能不同,可能是0或者是1。但是,如果我们仔细看就会发现,对于第一个字节,无论这些不确定的位设为1还是0,这里七条指令的第一个字节都不会有重复的可能,因此CPU只需要分析第一个字节,就知道这是一条什么类型的指令,并知道这条指令的长度,从而确定了下一条指令的第一个字节。我们再来看一个具体的例子。

Screen Shot 2018-06-02 at 18.52.32

将立即数 10EE 送到AX寄存器中。那我们就要选择这条将立即数送到寄存器的指令编码,前四位是固定的。第五位w代表要传送的数据是一个byte还是一个word,设为1表明是一个word,CPU在译码时通过这一位,知道接下来两个字节都是属于这条指令的,这两个字节是我们要传送的这个立即数。而在第一个字节中,最后三个比特则需要指明寄存器的编号,所以需要设为 000,也就是AX寄存器的编号。

第二条指令的编码则对应了一个存储器到寄存器的传输,大家可以自己来分析。

Screen Shot 2018-06-02 at 18.52.42

第二类传送类指令,我们来介绍栈操作指令。包括PUSH指令和POP指令。

Screen Shot 2018-06-02 at 18.52.51

堆栈是存储器中的一个区域,由堆栈段寄存器SS指定这个区域的起始地址,由堆栈指针寄存器SP指向当前访问的存储单元。在执行完

PUSH AX; AX=1234H

PUSH BX; BX=5678H

这两条指令后。1234这就是AX的内容,5678这就是BX的内容,被依次存放到了内存单元中,而且对应的SP寄存器的内容也发生了变化。注意SP寄存器的变化,有两个特点:

  1. 它是从高地址向低地址变化,也就是所谓的向下生长。

  2. 这个变化不需要在指令中明确指出,它是由硬件自动完成的隐含操作。

然后如果我们执行了 POP CX 这条指令,这块存储器空间又会变成怎么样呢?注意SP寄存器向高地址增长了两个字节,同时原先保存在栈中的5678这两个字节会被传送到CX寄存器中,这个动作就被称为弹栈。

压栈,也就是PUSH,就像往箱子里放衣服一样,先放一件再放一件。而弹栈时,就像从箱子里往外拿衣服,一定是后放进去的那一件先被拿出来。栈是一个后进先出的结构,后被压栈的数在弹栈时会先被弹出。

栈这个结构,经常用来进行函数的参数传递,当高级语言转换为机器语言时,实际上在函数调用前,都会先调用PUSH指令,将要传递的参数压到栈中,然后在函数执行时会从栈中取出所需的参数。

Screen Shot 2018-06-02 at 18.53.20

第二类我们来看算术运算指令。

Screen Shot 2018-06-02 at 18.53.29

Screen Shot 2018-06-02 at 18.53.39

算术运算,最基础的是完成加法、减法,乘法和除法,另外还有一些符号扩展和十进制调整的指令。

Screen Shot 2018-06-02 at 18.53.48

最简单的加法类指令,我们已经很熟悉了。这两个操作数可以是立即数,可以是寄存器操作数,也可以是内存操作数。

另外还有ADC指令,就是带进位的加法。在运算时,除了将两个操作数相加外,还会将标志寄存器当中的进位标志,也就是CF一起进行运算。

还有一个比较特殊的加法指令是加1指令。例如 INC CL,就是将CL寄存器的内容加1。当这条指令的操作数是寄存器时(INC Reg),它的指令长度只有一个字节,这也是最短的x86指令。当然INC指令的操作数也可以是存储器单元。

Screen Shot 2018-06-02 at 18.53.57

减法类指令,它和加法类指令很类似。

有普通的减法指令。

也有带借位的减法指令,同样是将借位标志(CF)参与运算。、

对于加1指令也有类似的减1指令。

Screen Shot 2018-06-02 at 18.54.05

另外有一个比较特殊的减法指令,就是比较指令。这条指令和减法指令类似,都是将两个操作数进行相减,但区别在于它不会将运算结果保存到目的操作数中,而只是改变标志寄存器中对应的标志位。例如相减的结果如果是零,则会改变零标志位,也就是ZF。

这条指令经常和条件转移指令配合完成特定的功能。例如在两个数相等的情况下才转移,否则不发生转移。

Screen Shot 2018-06-02 at 18.54.15

我们再来看逻辑运算和移位指令。

Screen Shot 2018-06-02 at 18.54.24

这一类指令是对二进制位进行操作。我们也来看看其中的几个例子。

Screen Shot 2018-06-02 at 18.54.34

例如逻辑非指令,这是一个单操作数指令。我们先将一个数送到AL中,然后执行 NOT AL,执行完之后,AL中的数就变成了 0101 0101,这是对AL中原先的操作数每一个位都进行取反操作。

而逻辑与指令,只是将两个操作数按位进行与操作,结果送到目的操作数中。

Screen Shot 2018-06-02 at 18.54.41

这是移位指令。首先来看左移指令,这条指令的功能是将目的操作数按照指定的位数向左移动,最后移出的一位会保留在CF中,右边的空缺则0补入。DST这个操作数可以是寄存器操作数,也可以是存储器操作数。而移动的位数可以写立即数1,也可以用CL寄存器。

将二进制数向左移动1位相当于乘以2,再向左移动n位,就相当于乘以2的n次方的运算。我们经常会用左移指令代替乘法指令,假设我们有一个数x,我们要计算10x,我们就可以用移位指令代替乘法指令,首先用左移一位得到了两倍的x,SHL AX, CL 这条指令就是在刚才的基础上再左移2位,这样就得到了8倍的x。再将8x与刚才已经保存到的BX相加,就得到了我们最后的结果10倍的x。

当年8086要执行一个乘法指令需要一百多个周期,而执行这样的移位指令只需要十几个周期,或者几十个周期,所以虽然指令数量多了一些,运算的速度还是比乘法指令要快的,因为乘法比移位要复杂的多。我们以后在学习乘法器的实现时也会体会到这一点,但是还要说明的是,随着乘法器技术的进步,乘法运算的速度也越来越快,现在和移位指令之间的差距也就没有那么明显了。

Screen Shot 2018-06-02 at 18.54.50

右移分为两种。

一种叫做逻辑右移,右移后左边空出的位由0补入,这和刚才的左移是类似的。

另一种算术右移,左边空出的位则会用原来的最高位进行填充。在表示有符号数时,用这种方式进行移位,就相当于除以2的n次方的运算。在一些情况下,我们也可以用若干条右移指令来代替除法指令。

Screen Shot 2018-06-02 at 18.54.58

下面我们来看转移指令。

Screen Shot 2018-06-02 at 18.55.06

转移指令的作用就是改变指令的执行顺序。

如果需要根据条件判断是否需要进行转移,那就是条件转移指令,否则就是无条件转移指令。而根据转移目标地址的提供方式,又可以将这些指令分为直接转移和间接转移这两种方式。

Screen Shot 2018-06-02 at 18.55.14

先来看,无条件的直接转移。这种类型的转移,实际上还有三种不同的编码格式,其中短转移由一个字节的操作码和一个字节的位移量组成,执行这条指令时是将第二个字节的八位位移量与当前的指令指针寄存器IP相加,并将结果更新到IP中。所谓的位移量是这个转移的目标地址与当前的指令指针值的差。

第二种近转移。第一个字节也是操作码,注意和短转移是不一样的。然后跟了两个字节的位移量(\(2^{16}=32K\)),这样它就可以在当前指令指针前后32K字节的范围内进行转移,当然这是指实模式下。从80386开始,近转移可以使用32位的位移量,这样就可以在前后2GByte的范围内进行转移。

第三个是远转移。与前两者不同的是,远转移直接给出了目标地址,而不是与当前指令指针的相对位移。我们来看一个远转移的例子。

Screen Shot 2018-06-02 at 18.55.32

这是一条远转移,FAR PTR指明了这是一条远转移,转移的目标地址到PROG2。

远转移,一般是转移到不同的代码段,因此又称段间转移。我们假设当前这条JMP指令是在21000这个地址所在的代码段,而它要转移到43006这个地址,在另一个代码段。当执行这条指令时,CPU会取出整条指令的编码,并将这两个字节放入IP寄存器,另两个字节放入CS段寄存器,当这个动作完成后,下一条指令就会通过CS和IP这两个寄存器中的新的内容计算得到,这里就是43006。所以CPU接下去就会从43006取出下一条指令开始执行。

Screen Shot 2018-06-02 at 18.55.40

我们再来看间接转移,刚才的直接转移都是在指令中直接给出目标地址,无论是目标地址的完整内容,还是目标地址与当前指令指针的偏移量。而间接转移其转移目标地址可以在寄存器中,例如写成:JMP AX 或者 JMP EAX 这样的形式。

转移目标地址还有可能在存储器中,这两条指令都是将SI寄存器的内容作为地址访问存储器,从而获得转移目标的地址,然后从这个目标地址取出下一条指令继续执行。

Screen Shot 2018-06-02 at 18.55.57

至于条件转移指令,则是根据当前的状态标志位决定是否发生转移。

一般会跟在影响标志位的算术和逻辑运算指令之后。

在8086中所有的条件转移都是短转移。从386开始条件转移也可以使用32位的位移量,从而可以在前后2G字节的范围内进行转移。

Screen Shot 2018-06-02 at 18.56.09

Screen Shot 2018-06-02 at 18.56.17

这些都是条件转移指令。譬如 JC 这条指令就是在进位标志CF为1时才会发生转移,而 JNZ 或者 JNE 则是在ZF这个标志位为0时才发生转移。其它指令都是采用类似的方式检查某个标志位,或者某几个标志位。我们来看一个例子。

Screen Shot 2018-06-02 at 18.56.28

如果要计算两个很大的数,这两个数分别存放在存储器中2000H和3000H开始的位置,这两个数很长,有很多个字节构成,它的长度则存放在2500H这个字节单元中。

假设就是左下角这样的,这个区域存放在一个数。而从3000开始的区域存放着另一个数。2500这个字节单元存放着这两个数的长度,12H也就是18个字节。

让我们来看右边这个程序。首先将2500H单元中的内容传送到CL寄存器中。

然后在 SI 和 DI 寄存器中分别放上这两个存储器区域的起始地址。

再用CLC指令将标志位CF清零,以免影响后面的操作。

下面是一个循环体,首先将SI中的数放在AX中。

然后将DI中的数与AX当中的数相加。

注意第一次运算时,由于进位标志已被清零,所以ADC指令不会加上一个多余的进位。

然后将当前的运算结果再传送到SI寄存器指向的内存单元,这就完成了这两个数最低16位的相加。

然后用INC指令连续两次,将SI寄存器加了2。我们这里请大家考虑是否可以用“ADD SI, 2”这样的指令形式呢?

然后将DI加2。

再将循环变量CL减1。

最后判断ZF标志位是否为1。因为如果CL寄存器减到0时,ZF标志位就会变为有效。当ZF标志位无效时,就会跳转到LOOP1的标号,继续取出SI和DI对应的存储单元中的数,也就是下一个16位,然后继续进行相加,不断的循环,直到完成了所指定的次数,在这里是12H也就是18次。

因为最后一次加法有可能产生一个进位,所以我们还需要进行一个0+0的操作,但是是带进位的,这样就把可能存在的一个进位给算出来,再放到当前SI所指向的内存单元。

现在,两个很大的数的运算结果就放在了从2000开始的这个内存单元中了。通过这个例子,我们就熟悉了刚才所介绍的各种指令。

Screen Shot 2018-06-02 at 18.56.37

最后我们来看一看处理器控制指令。

Screen Shot 2018-06-02 at 18.56.48

处理器控制指令用于控制CPU的功能,还有对标志位进行操作。

例如刚才用到的CLC指令,就是对进位标志CF清零。还有一个有趣的指令是NOP,它其实是一个什么都不做的指令,不会对机器的状态产生任何的改变,那它有什么用呢?这个有趣的问题就留给大家思考吧!

Screen Shot 2018-06-02 at 18.56.57

即使是最为基础的x86指令,也很难在短时间内一一介绍,而且也没有那个必要。大部分指令还是非常容易理解和掌握的,能够读懂最基础的代码就可以了,至于那些复杂的变化,用到的时候再查手册也来得及。

猜你喜欢

转载自www.cnblogs.com/narisu/p/9127368.html
3.3