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

上一篇博客我們講了在彙編語言中,如下的幾個處理器狀態是可見的:

一、程序計數器(在 IA32 中通常稱爲 PC,用 %eip 表示):指示將要執行的下一條指令在存儲器中的地址。

二、整數寄存器文件:包含8個命名的位置,可以存儲一些地址或者整數的數據。有的用來記錄某些重要的程序狀態,有的則用來保存臨時數據。

三、條件碼寄存器:保存最近執行的算數或邏輯指令的狀態信息,它們用來實現控制或數據流中的條件變化,比如用來實現 if 和 while 語句。

四、浮點寄存器:存儲浮點數。

這裏我們要講的就是第三個整數寄存器,在 32 位 CPU 中包含一組 8 個存儲 32 位值的寄存器。這些寄存器用來存儲整數數據或指針。下圖是 IA32 的整數寄存器:

在這裏插入圖片描述

上述八個寄存器主要功能如下:

%eax,可存放一般數據,而且可作爲累加器使用;
%ebx,可存放一般數據,而且可用來存放數據的指針(偏移地址);
%ecx,可存放一般數據,而且可用來做計數器,常常將循環次數用它來存放;
%edx,可存放一般數據,而且可用來存放乘法運算產生的部分積,或用來存放輸入輸出的端口地址(指針);
%esi,可存放一般數據,還可用於串操作中,存放源地址,對一串數據訪問;
%edi,可存放一般數據,還可用於串操作中,存放目的地址,對一串數據訪問;
%esp,用於尋址一個稱爲堆棧的存儲區,通過它來訪問堆棧數據;
%ebp,可存放一般數據,用來存放訪問堆棧段的一個數據區,作爲基地址;
  在大多數情況下,%eax、%ecx、%edx、%ebx、%esi、%edi等6個寄存器可以看做通用寄存器,對它們的使用沒有限制;%esp、%ebp兩個寄存器保存着指向程序棧中重要位置的指針,只有根據棧管理的標準慣例才能修改這兩個寄存器中的值。

這8個寄存器都可以作爲16位(字)或32位(雙字)來訪問。字節操作指令可以獨立的讀或者寫%eax、%ecx、%edx、%ebx等4個寄存器的2個低位字節,因爲%ax、%cx、%dx、%bx這4個16位寄存器又可分別分成ah,al ;bh,bl;ch,cl;dh,dl的8位寄存器。

這裏大家也只需要有個眼熟就好了,後面我們將對這個8個寄存器進行詳細講解。

操作數指示符

我們知道大多數指令都有一個或多個操作數(operand),指示出執行一個操作中要引用的源數據值,以及放置結果的目標位置。下圖是 IA32 支持的多種操作數格式:

在這裏插入圖片描述

上圖我們可以看出源數據值可以是常數形式給出,或者是從寄存器或存儲器中讀出。而結果可以存放在寄存器或存儲器中。我們將不同的操作數分爲如下三種類型:

①、立即數(immediate):書寫方式是$符號後跟一個標準C表示的整數,比如$52,$0x1F等等。任何能放進一個32位的字裏面的數值都可以做立即數。

②、寄存器(register):它表示某個寄存器的內容,可以是8個32位寄存器中的一個(比如%eax),也可以是8個16位寄存器中的一個(比如%ax),還可以是8個單字節寄存器寄存器(比如%al)。上圖是用Ea來表示任意寄存器a,用引用 R[Ea]來表示它的值。

③、存儲器(memory):它會根據計算出來的地址(通常稱爲有效地址)來訪問某個存儲器位置。我們將存儲器看成一個很大的字節數組,用符號Mb[Addr] 表示對存儲在存儲器中從地址 Addr 開始的 b 個字節值的引用。上圖省略了下方的 b.

從上圖我們知道,第一行是立即數,第二行則是寄存器,剩下的全部是存儲器。其中最後一行存儲器語法 Imm(Eb,Ei,s),表示的是最常用的形式,分爲四個部分,

一、Imm 是立即偏移數

二、Eb 是基址寄存器

三、Ei 是變址寄存器

四、s 是比例因子,必須是 1、2、4或8

然後有效地址計算公式爲: Imm + R[Eb]+R[Ei]s。比如對於2(%esp,%eax,4)這個操作數來講,它代表的是內存地址爲2+%esp+4%eax的存儲器區域的值。

在這裏插入圖片描述

數據傳送指令

操作數符號的通用性使得一條簡單的傳送指令能夠完成許多機器中要好幾條指令才能完成的功能。
源操作數制定一個數,它可以是立即數,可以存放在寄存器中,也可以存放在存儲器中。
目的操作數制定一個位置,可以是寄存器,也可以是存儲求地址。
IA32中傳送指令的兩個操作數不能都指向存儲器的位置,就是將一個值從存儲器的位置拷到另一個存儲器的位置需要兩條指令。第一條指令從源值加載到寄存器中,第二條將該寄存器寫入目的位置

1         movl $0x4050,%eax          立即數--寄存器
2         movl %ebp,%esp             寄存器--寄存器
3         movl (%edi,%ecx),%eax      存儲器--寄存器
4         movl $-17,(%esp)           立即數--存儲器
5         movl %eax.-12(%ebp)        寄存器--存儲器

movb只傳送一個字節,movw指令傳送兩個字節
在這裏插入圖片描述
movsbl和movzbl指令負責拷貝一個字節,並設置目的操作數中其餘的位,movsbl指令的源操作數是單字節的,它執行符號擴展到32位(也就是將高24位設置爲源字節的最高位),然後拷貝到雙字的目的中。類似的,movzbl指令源操作數是單字節的,在前面加24個0擴展到32位,將結果拷貝到雙字的目的中
例子

初始假設 %dh=8,%eax=98765432(32位)
1        movb %dh,%al                 %eax=9876548D
2        movsbl %dh,%eax            %eax=FFFFFF8D
3        movzbl %dh,%eax            %eax=0000008D

movb指令不改變其他三個字節。
根據源字節的最高位,movsbl指令將其他三個字節設置爲全1或者全0.
movzbl指令無論如何都是將其他三個字節設置爲全0

MOV指令

mov指令的作用是將源操作數S中的數據複製到目的操作數D中,mov指令有一個數據格式和兩個操作數,因此一般的形式爲[movx S D]。其中x爲數據格式,S爲源操作數,D爲目的操作數。

這裏舉一個簡單的例子,比如我們有一條指令爲movl %edx %eax。那麼它的執行過程就如下圖所示。

在這裏插入圖片描述

可以看到,在指令執行之後,%edx寄存器當中的內容會被複制到%eax寄存器。需要一提的是,mov指令可以在後面加上任何數據格式,比如上面這一過程中,數據格式則爲四個字節,也就是雙字。因此不難推斷出,我們還可以使用movb和movw去複製一個字節或者兩個字節。

movs指令

movs指令的作用是將源操作數S中的數據做符號擴展後,再複製到目的操作數D中,movs指令有兩個數據格式和兩個操作數,因此一般的形式爲[movsxy S D]。其中x、y爲數據格式,S爲源操作數,D爲目的操作數。其中x、y的組合一共有三種,分別是bw、bl、wl,這三個組合代表的意思分別是單字節到雙字節,單字節到雙字以及雙字節到雙字。

這裏LZ依然舉一個例子,對於指令movswl %dx %eax來講,它的作用如下圖所示。
在這裏插入圖片描述

這裏爲了可以看出符號位的擴展,因此LZ這裏使用了十六進制的整數表示方式。可以看到,movs指令將0x8FFF擴展以後存入%eax寄存器,其中%dx爲寄存器%edx的後16位表示。

movz指令

movz指令的作用是將源操作數S做零擴展後,再複製到目的操作數中。它與movs指令十分相似,也有兩個數據格式和兩個操作數,因此一般的形式爲[movzxy S D]。其中x、y爲數據格式,S爲源操作數,D爲目的操作數。其中x、y的組合一共有三種,分別是bw、bl、wl,這三個組合代表的意思分別是單字節到雙字節,單字節到雙字以及雙字節到雙字。

這裏依然採用相似的示例,我們來看看對於指令movzwl %dx %eax來講,它的作用與上面的movs有何不同。
在這裏插入圖片描述

可以看出,movz與movs指令是十分相似的,只是這裏擴展後,目標寄存器%eax的前16位爲0而不再是1。

push指令

push指令與上面的mov族指令有着不同,它的目的操作數被固定爲棧頂,因此它的指令當中沒有目的操作數。另外有一點需要注意的是,它在進行復制操作之前,需要移動棧頂指針(-4)。push指令的一般形式爲[pushl S],其中l代表數據格式爲雙字,S爲源操作數,目的操作數默認爲棧頂。

這裏LZ舉一個簡單的例子,比如pushl %edx這條命令,它的任務是將%edx寄存器的值複製到棧頂。我們首先來看一下命令執行前,寄存器以及存儲器的狀態。

在這裏插入圖片描述
  可以看到,寄存器%ebp和%esp分別指向幀指針和棧指針,而%esp實際上就是指向的棧頂。由於現在棧頂位於-16的位置,因此若要將%edx壓入棧,則先需要將棧頂移動到-20的位置,然後再進行復制,移動後的狀態如下圖所示。

在這裏插入圖片描述

可以看到,這裏棧指針的位置已經發生了變化,向下移動了四位,並且將%edx寄存器的值放入新的棧頂,因此pushl %edx指令就相當於下面兩條指令。

subl $4,%esp

movl %edx,(%esp)

這裏可以看出來,其實pushl指令做了一個隱藏操作,就是移動棧指針(-4),這一點希望各位猿友們注意。

pop指令

pop指令與push指令是做的相反的操作,一個是入棧一個是出棧。對於pop指令來講,它的源操作數被固定爲棧頂,相反,它會先進行復制操作,然後再移動棧指針。pop指令的一般形式爲[popl D],其中l代表數據格式爲雙字,D爲目的操作數,源操作數默認爲棧頂。

接下來我們舉一個例子,與上面的例子類似,我們考慮popl %edx這條指令的效果,它會將棧頂的值彈出到寄存器%edx。首先來看執行之前,寄存器以及存儲器的狀態。

在這裏插入圖片描述
  接下來執行pop指令時,會先將棧頂的值複製到%edx,然後再將棧指針移動(+4)。我們來看一下它執行後的狀態。

在這裏插入圖片描述

可以看到,之前棧頂的內容已經被彈出到%edx寄存器,並且當前棧頂已經移動到了-16的位置,也就是進行了+4操作。因此popl %edx指令就相當於下面兩條指令。

movl (%esp),%edx

addl $4,%esp

這裏可以看出來,其實popl指令也同樣做了一個隱藏操作,就是移動棧指針(+4)。

數據複製示例

上面我們已經瞭解了幾乎所有的數據複製指令,接下來我們寫一小段程序,來看下這些數據複製指令,如何完成我們的程序操作。

simple(int *xp,int y){
    int t = *xp;
    *xp=y;
    return t;
}

上面是一個簡單的C程序sum.c,它其中包含了一些賦值操作,我們來看看它的彙編代碼。使用GCC -O1 -S sum.c來獲取我們的彙編代碼,並使用cat sum.s來查看一下。

   .file    "sum.c"
    .text
.globl simple
    .type    simple, @function
simple:
    pushl    %ebp
    movl    %esp, %ebp
    //以上爲棧的建立部分
    movl    8(%ebp), %edx
    movl    (%edx), %eax
    movl    12(%ebp), %ecx
    movl    %ecx, (%edx)
    //以下爲棧的完成部分
    popl    %ebp
    ret
    .size    simple, .-simple
    .ident    "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

分析這段彙編代碼的時候,我們應該分爲三個部分來看待,首先是棧的建立、然後是使用、最後是完成部分。可以看到,裏面幾乎全是數據複製指令,我們先來看看棧的建立部分。

其實對於一開始pushl和movl指令來講,它主要做了兩件事。第一個是將原來的幀指針備份到棧頂,然後再將幀指針和棧指針統一指向這個新的棧頂,也就是完成了一個新棧的建立。它在完成後,棧的狀態如下所示。

在這裏插入圖片描述

可以看到,寄存器%ebp和寄存器%esp都指向當前幀指針的位置,其中變量xp位於+8的位置,而y位於+12的位置。由於xp是一個指針變量,因此它會指向一個內存中的區域,其中的值爲*xp。

瞭解完寄存器和存儲器的狀態,此時棧已經建立完畢,接下來我們看緊接着的一句彙編代碼的作用。

movl    8(%ebp), %edx

這一句將內存地址爲%ebp+8的值複製到%edx,很明顯,從上面的圖中可以看出,%ebp+8這個位置存儲着xp變量。這一句指令做了一個簡單的操作,就是將xp提取到%edx寄存器,如下所示。

在這裏插入圖片描述

此時已經將%edx的值改爲了變量xp,看接下來的一句操作。

movl (%edx), %eax
  這一句將內存地址爲%edx的值賦給寄存器%eax,並準備返回值。此時%edx寄存器的值已經改爲了xp變量,因此(%edx)其實就是*xp,而%eax寄存器一般會作爲函數的返回值,因此它其實替代了臨時變量t。執行後的狀態如下所示。

在這裏插入圖片描述

此時其實已經完成了程序中的int t = *xp以及爲return t準備好了返回值,接下來的一句彙編代碼作用也非常簡單,如下。

movl    12(%ebp), %ecx

它的作用是將地址爲%ebp+12的值複製到寄存器%ecx,從圖中可以看出,%ebp+12就是存儲的變量y。因此它的作用就是將y複製到寄存器%ecx,如下所示。
在這裏插入圖片描述

上面這一步挺簡單,我們來看最後一步操作,如下。

movl    %ecx, (%edx)

它的作用是將%ecx寄存器的值複製到內存中%edx的位置。此時%ecx的值爲y,而%edx中爲xp,因此目的操作數則爲xp指向的位置,也就是*xp。這一句話執行的就是程序代碼當中,*xp=y這個操作,它執行後的狀態如下所示。

在這裏插入圖片描述

可以看到,在執行了*xp=y以後,xp指針所指向的位置,其值已經變爲了y。此時程序其實已經基本運行完畢,剩下的工作也就是棧的完成操作了,也就是popl指令。在棧完成之後,也就是pop指令執行之後,當前幀會恢復到調用者的幀上面去,如下所示。

在這裏插入圖片描述

此時當前幀已經恢復到了調用者的幀,最後ret指令會改變程序計數器(PC)的值,然後跳出子函數,繼續執行調用者當中的代碼。到此,我們的數據複製示例就結束了,儘管這個例子並不難,但是麻雀雖小五臟俱全,如果理解了這個過程,相信就算是再複雜一些的彙編指令,也只是分析的時間長一點罷了。

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