引言
上一章我們已經着重討論了數據傳送(或者說復制)指令,相信各位猿友現在都已經對此有一些了解了。說真的,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的博文,都未嘗不可。