深入理解计算机系统(3.4)---算数与逻辑运算指令详解

引言

 

  上一章我们已经着重讨论了数据传送(或者说复制)指令,相信各位猿友现在都已经对此有一些了解了。说真的,LZ在看第三章的过程中,不断的被汇编的魅力深深的震撼,这些看似简单的汇编指令,却可以将复杂的程序井然有序的执行完毕,实在是让人惊叹。时至今日,这本看似枯燥无比却实则魅力十足的书,已经深深的将LZ吸引了。

  希望各位猿友也有这样的感觉,这是一种非常好的感觉,接下来,各位就一起和LZ来认识认识新的指令吧。

 

算术与逻辑运算指令

 

  算术与逻辑运算包括很多种,估计各位猿友也能很快的想出来,比如最常见的加减乘除、与或非、左移右移等等。这里可能还有一个各位猿友不太容易想到的,就是取地址运算符,不过这个运算指令却是LZ看过这一部分之后,觉得最精妙的一个指令。

  接下来LZ将书中的一个表格贴上来,各位猿友可以先大致浏览下里面的指令。

  这里面比较特别的指令就是leal(取地址指令),其余的指令都是比较常规的算术和逻辑运算,相比之下还比较好理解,因此LZ这里重点介绍leal指令,对于其余的指令LZ不会一一介绍,接下来我们就认识一下这个特别的leal指令吧。

 

leal指令

 

  leal指令是非常神奇的一个指令,它可以取一个存储器操作数的地址,并且将其赋给目的操作数。如果用C语言当中来对应的话,它就相当于&运算。

  比如对于leal 4(%edx,%edx,4),%eax这条指令来讲,我们假设%edx寄存器的值为x的话,那么这条指令的作用就是将 4 + x + 4x = 5x + 4赋给%eax寄存器。它和mov指令的区别就在于,假设是movl 4(%edx,%edx,4),%eax这个指令,它的作用是将内存地址为5x+4的内存区域的值赋给%eax寄存器,而leal指令只是将5x+4这个地址赋给目的操作数%eax而已,它并不对存储器进行引用的值的计算。

  为了更好的表示这条指令的效果,LZ这里简单的画个图来表示这一过程。我们假设下图是执行指令之前,寄存器和存储器的状态。

  可以看到,此时在存储器中,地址为5x+4的区域的值为1000。那么此时若是进行movl 4(%edx,%edx,4),%eax操作,很显然,%eax的值应该为1000,也就是下图。

  但是如果进行leal 4(%edx,%edx,4),%eax操作的话,%eax的值就不是1000了,因为leal指令不会去取存储器当中的值,因此寄存器%eax的值应该是5x+4。

  试想一下,倘若在地址为5x+4的位置存储的是变量i,那么其实这条指令就相当于&i操作,这也就是C语言当中的&取地址操作的汇编级做法。各位猿友感觉如何,是否很神奇呢。

 

一个示例

 

  由于其它的指令都相对比较简单,因此LZ这里就不一一介绍了,这里我们用一个小程序来做一个示例,顺便也去看一下上面的算术与逻辑运算指令都是被如何使用的。我们就考虑书上的一个小例子,其中的C程序代码如下。

int arith(int x, int y , int z){
    int t1 = x+y;
    int t2 = z*48;
    int t3 = t1&0xFFFF;
    int t4 = t2*t3;
    return t4;
}

  这里面包含了加、乘、与运算,我们使用-O1和-S参数编译sum.c这个文件,使用cat sum.s查看它,会得到如下的汇编代码。

    .file    "sum.c"
    .text
.globl arith
    .type    arith, @function
arith:
    pushl    %ebp
    movl    %esp, %ebp
  //以上为栈帧建立 movl
16(%ebp), %eax leal (%eax,%eax,2), %edx sall $4, %edx movl 12(%ebp), %eax addl 8(%ebp), %eax andl $65535, %eax imull %edx, %eax
  //以下为栈帧完成 popl
%ebp ret .size arith, .-arith .ident "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3" .section .note.GNU-stack,"",@progbits

  这里面还有leal指令,可以看到程序当中并没有取地址&操作,所以这里的leal指令不是用来取地址的,LZ使用图示来给各位演示这个程序的运行过程。首先便是栈帧的建立过程,栈帧建立好以后,寄存器和存储器的状态如下所示。

  以上便是建立好的栈帧,同上一次一样,帧指针和栈指针都指向一个新的位置,在帧指针偏移量为8、12、16的地方存储着传递进来的参数x、y、z。接下来我们就开始分析,在汇编代码层次,是如何完成上述C语言程序当中的一系列动作的。

  首先是一个mov指令,它的作用很简单,就是将参数z取入寄存器,下面是它的汇编代码以及图示。

movl    16(%ebp), %eax

  上面的指令比较简单,接下来的这条指令就比较特别了,是一条leal指令。这里的leal指令不是用来取地址的,而是用来进行乘法运算的,它的目的是将%eax寄存器当中的值乘以3,然后发送至%edx寄存器。而采用的方式则是2*x + x的方式,这正是我们之前讲过的乘法优化算法,使用移位和加法来计算乘法。接下来看看它的指令与图示。

    leal    (%eax,%eax,2), %edx

  上面计算3z的目的,在接下来这一条指令就看出来了。接下来的一条指令是sal左移操作,位数为4,左移4位其实就相当于乘以16,因此接下来的一条指令其实就相当于将寄存器%edx当中的值乘以16,这其实刚好是在计算48*z。从这里也可以看出来,在执行C程序的时候,并不一定会按照程序当中的顺序去计算。以下是sal指令的内容与图示。

    sall    $4, %edx

  接下来的指令依然是简单的取参数y,因此LZ这里就不再多解释了,直接上内容和图示。

    movl    12(%ebp), %eax

  下面的一条指令是add加法指令,它是将左边操作数的值加到右边的目的操作数。也就是将内存地址为8(%ebp)的值加到%eax寄存器,而8(%ebp)这个位置存的刚好是x,因此这里计算的便是x+y的值,而结果会存入%eax寄存器。以下是指令的内容和图示。

    addl    8(%ebp), %eax

  接下来是一条与运算指令and,它计算的则是t1与0xFFFF(十进制就是65535)的与运算,t1的值为x+y,此时就存在%eax寄存器。我们来看下这条指令的内容与图示。

    andl    $65535, %eax

  接下来是最后一个计算过程的指令imul乘法指令,它的作用也是将左边操作数的值乘到右边的目的操作数上。也就是将%edx寄存器的值乘到%eax寄存器上面去,而%edx此时的值为48*z(也就是t2),而%eax的值为(x+y)&0xFFFF(也就是t3),两者相乘则得到t4的值,结果将存在%eax寄存器,并且作为返回值返回。以下为内容与图示。

    imull    %edx, %eax

  到此,我们整个计算过程就结束了,其中用到了一些算术与逻辑运算指令,其实它们并没有什么难度,相信各位在LZ的图示解释下,应该也不难明白。最后则是栈帧的完成部分,以下为当前帧释放后的状态。

  在这里LZ提一点,各位猿友估计也注意到了,每次在%ebp偏移量为4的位置都是空着的,而参数都在8、12、16这样的位置,难道偏移量为4的位置是空的吗。这里其实不是空的,它存储的是返回地址,只是LZ这里为了简化理解,因此没有考虑这些。这一点在后面的过程实现一章中会有详细的讲解。

 

文章小结

 

  本章的主要内容是认识一些常见的算术与逻辑运算指令,它们其实并不难掌握,接下来的一章,我们将会认识一些不太常用的算术操作指令。总的来说,第三章的内容还是非常有趣的,希望各位猿友要坚持看下去,无论是看书还是看LZ的博文,都未尝不可。

posted @ 2013-10-23 23:23  左潇龙  阅读(4005)  评论(0编辑  收藏  举报