204-复杂的 x86 指令举例
# 204-复杂的 x86 指令举例
x86 作为复杂指令系统的代表,自然会有不少相当复杂的指令。 在这一节我们将会看到其中有代表性的一些例子。
串操作指令是将存储器中的数据串进行每次一个元素的操作。 所谓一个元素可以是字节或者是字。 这个串可以很长,能够达到 64KB, 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,然后才是这条串传送指令。 前面加上了重复前缀,这样的配置就相当于连续执行了三次这条串传送指令。 当执行第一次传送之后,第一个字节被传送到了目的串的位置, 传送完成后,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。 从低地址开始传送,原因也是一样的。
除了串传送指令,还有其他类型的串操作。例如在一个数据 串种,查找特定的数据,或者比较两个数据串是否相同。 这样程序员有了很便利的手段,对一大块数据进行操作。 因此串操作指令是功能非常强大的指令,不过由于数据串当中的 元素数量有可能很多,因此串操作指令的执行时间也可能很长, 这是需要注意的。
最后我们从 一个有趣的例子来看一看 x86 指令的复杂程度。 这张图是 x86 指令的通用格式。 每一个小格都是指令格式中特定的位域。 那么我们可以人为写出一条指令来,这条指令是一个加法,而且 有一个前缀 LOCK,这和我们刚才学到的 REP 一样,都是指令的前缀。 这个加法,其中一个源操作数是 32 位的立即数。另一个源操作数以及目的操作数,是内存当中的一个 32 位的存储单元。 这个存储单元本应默认在数据段,但这里强制指定为在附加段, 这个存储单元的地址由 EAX 寄存器,ECX 寄存器 和一个立即数计算而得。要计算这个内存地址需要一次乘法,两次加法得到偏移地址,再和段机制 进行移位并相加的操作,然后访问这个存储单元得到 32 位数。 在与 1 2 3 4 5 6 7 8 这个立即数相加, 然后再访问这个存储单元,将这个数存进去, 这条指令的编码一共有 15 个字节, 可以认为是一条最长的 x86 指令 ,x86 指令的复杂程度由此可见一斑。
编程人员只用给出一条简短的指令, 计算机就可以完成非常复杂的工作,这自然是一件很好的事情。 计算机似乎就应该这么设计,可惜世界没有这么简单, 有人提出了完全相反的做法,我们下一节再说。