深入理解計算機系統-第三章-程序的機器級表示-3.5

3.5 算術和邏輯操作

下圖列出了一些雙字整數操作,分爲四類。二元操作有兩個操作數,而一元操作只有一個操作數。描述這些操作數的符號與3.4節中使用的符號完全相同,除了leal以外,每條指令都有對應的對字和對字節操作的指令。把後綴l換成w就是對字的操作,換成b就是對字節的操作。例如,addl對應有addw和addb。
在這裏插入圖片描述
這裏面比較特別的指令就是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語言當中的&取地址操作的彙編級做法。

一個示例

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這裏爲了簡化理解。

3.5.2 一元和二元操作

第二類操作是一元操作,只有一個操作數,既做源,也做目的。這個操作數可以是一個寄存器,也可以是一個儲存器位置,比如說,incl(%esp)會使得棧頂元素加一。
第三類是二元操作,第二個操作數既是源又是目的,這種語法想c中的+=的賦值運算。例如,指令subl %eax,%edx是的寄存器%edx的值減去%eax的值。第一個操作數可以是立即數,寄存器或是存儲器位置。第二個操作數可以是寄存器或是儲存器位置。不過同movl指令一樣,兩個操作數不能同時都是存儲器位置

在這裏插入圖片描述

3.5.3 移位操作

最後一類是移位操作,先給出移位量,然後是待移位的值。可以進行算術和邏輯右移。移位量用單個字節編碼。移位量可以是一個立即數,或者放在單字節jicunq元素%cl中。左移指令有兩個名字,sall和shll,兩者的效果都一樣,都是將右邊天上0.左移指令不用,sarl執行算術移位(t填上符號位),而shrl執行邏輯移位。

示例

假設我們生成這個C函數的彙編代碼
int shift_left2_rightn(int x,int n)
{
  x<<=2;
  x>>=n;
  return x;
}
下面這段代碼執行實際的移位,並將最後的結果放在寄存器%eax中,參數x和n分別存放在儲存器中相對於寄存器%ebp中地址偏移8和12的地方
1   movl 12(%ebp),%ecx
2  movl 8(%ebp),%eax
3  ____________
4  ____________

3.5.4討論

看書

3.5.5 特殊的算術操作

我們先來看看這些指令的大致介紹,如果各位看過上一章的話,會發現這裏的指令有的會有些眼熟,但是它們的作用卻截然不同。以下是書中的一張概圖。

在這裏插入圖片描述

第一個指令有些眼熟吧,它就是我們上一章當中的imul乘法指令的雙字形式。不過可以看出,這裏的imull指令已經完全變了味道,它將結果存入兩個寄存器。接下來,我們來仔細看看這些指令。

imull、mull指令

這兩個指令一看就是雙胞胎,它們一個負責有符號全64位乘法,一個負責無符號全64位乘法。細心的猿友會發現,imull這個指令好像是負責乘法的指令,而且在之前的乘法並沒有區分有符號和無符號,現在怎麼又成雙胞胎指令了。

我們上一章當中出現的指令是imul指令,當它操作雙字的時候,也就是imull指令。不過不同的是,它的一般形式是imul S D,這裏有兩個操作數,它將計算S和D的乘積並截斷爲雙字,然後存儲在D當中。由於在截斷時,無符號以及有符號的二進制序列是一樣的,因此此處的乘法指令並不區分有符號和無符號。

本次我們討論的imull指令,則與上面的普通乘法指令稍有不同,它只有一個操作數,也就是說,它的一般形式爲imull S,這點在書中的表格中也能看出來,而另外一個操作數默認爲%eax寄存器。最終的結果,會將高32位存入%edx寄存器,而低32位存入%eax寄存器。

試想一下,如果我們只取%eax寄存器當中的32位結果,那其實這裏計算的結果就是S*%eax,此時imull S的作用就與imull S D是一樣的,只是目的操作數被固定爲%eax罷了。

接下來我們看一個簡單的示例,我們去看下指令imull $0x3的結果,我們假設此時%eax寄存器的值爲0x82345600。也就是我們需要計算0x30x82345600的值,這裏LZ直接給出兩者相乘的16進製表示,各位有興趣的可以私下乘一下,爲0xFFFF FFFE 869D 0200。這個結果爲64位的,因此我們寄存器的前後狀態如下所示
  在這裏插入圖片描述
  以看到,%eax保存着低32位的結果,單說這32位的話,它的有符號數值爲-2036530688,正是我們直接計算0x3
0x82345600的32位截斷後的有符號值,顯然這個結果溢出了。如果組合上高32位,則結果爲-6331497984,將它加上或者取模4294967296(2的32次方)將得到我們32位的結果。這裏的有符號乘法採取的是先符號擴展被乘數,然後兩者相乘,將結果再截斷爲64位所得。

對於mull的單操作數指令來講,就比較簡單了,它採用的是無符號乘法,因此就和我們平時的十進制乘法運算類似,只是同樣的,它也會將結果的高32位存入%edx,將低32位存入%eax。

cltd指令

這個指令相對來說就非常簡單了,它就是簡單的將%eax寄存器的值符號擴展32位到%edx寄存器,也就是說,如果%eax寄存器的二進制序列的最高位爲0,則cltd指令將把%edx置爲32個0,相反,如果%eax寄存器的二進制序列最高位爲1,則cltd指令將會自從填充%edx寄存器爲32個1。

idivl、divl指令

這兩個指令與前面的imull以及mull類似,它也將計算結果存放在兩個寄存器當中,其中餘數存放在%edx寄存器,商存放在%eax寄存器。如果各位理解了前面的imull以及mull,那麼這裏idivl和divl理解起來會非常簡單。

這裏LZ舉一個簡單的例子,考慮指令idivl $0x3的結果,我們假設此時%eax寄存器的值爲0x82345600。也就是我們需要計算0x82345600/0x3的值,這裏LZ直接給出兩者相除的16進製表示,各位有興趣的也可以私下除一下,商爲0xD6117200,餘數爲0x0。因此我們寄存器的前後狀態如下所示。

在這裏插入圖片描述

可以看到,在idivl這個指令執行的過程中,其實對被除數進行了符號擴展,類似於cltd指令,或者有時也會將%eax移動到%edx,然後對%edx進行算術右移31位的運算。這兩種方式的結果是一樣的,都是將%eax符號擴展32位並存儲在%edx當中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章