3.4 复杂的x86指令举例

计算机组成

3 指令系统体系结构

3.4 复杂的x86指令举例

Screen Shot 2018-06-02 at 10.25.34

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

Screen Shot 2018-06-02 at 10.41.21

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

Screen Shot 2018-06-02 at 10.25.48

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

Screen Shot 2018-06-02 at 10.25.58

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

Screen Shot 2018-06-02 at 10.26.28

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

Screen Shot 2018-06-02 at 10.26.46

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

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

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

Screen Shot 2018-06-02 at 10.26.56

我们来看一个例子。假设我们在存储器中要进行一次数据串的传送。源串的位置在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,所以不再继续执行。

Screen Shot 2018-06-02 at 10.27.10

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

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

Screen Shot 2018-06-02 at 10.27.22

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

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

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

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

Screen Shot 2018-06-02 at 11.51.20

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

Screen Shot 2018-06-02 at 11.52.21

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

Screen Shot 2018-06-02 at 15.41.34

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

Screen Shot 2018-06-02 at 16.17.19

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

Screen Shot 2018-06-02 at 16.30.55

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

Screen Shot 2018-06-02 at 16.32.32

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

Screen Shot 2018-06-02 at 16.37.21

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

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

Screen Shot 2018-06-02 at 16.48.14

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

Screen Shot 2018-06-02 at 16.48.59

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

Screen Shot 2018-06-02 at 16.50.39

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

Screen Shot 2018-06-02 at 17.50.39

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。这样的指令有什么用呢?实际上在一些非常简单的设备上,它需要进行很简单的算术运算,又不想要有很多的转换,就可以采用这样的方式。当然现在真正这样用的已经越来越少了。

Screen Shot 2018-06-02 at 10.27.32

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

Screen Shot 2018-06-02 at 10.27.41

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

猜你喜欢

转载自www.cnblogs.com/narisu/p/9126250.html
3.4