深入理解计算机系统(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的博文,都未尝不可。