在上一篇博客 程序編碼以及數據格式 中我們給出了一個簡單的C程序,然后編譯成了匯編代碼。大家看不懂沒關系,后面的博客我們將逐漸揭開一些匯編指令的神秘面紗。本篇博客我們將對操作數指示符和數據傳送指令進行詳細的介紹。
1、整數寄存器
上一篇博客我們講了在匯編語言中,如下的幾個處理器狀態是可見的:
一、程序計數器(在 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個寄存器進行詳細講解。
2、操作數指示符
我們知道大多數指令都有一個或多個操作數(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的存儲器區域的值。
3、數據傳送指令
數據傳送指令:將數據從一個位置復制到另一個位置的指令。簡單來說就是復制指令。
將源操作數的值復制到目的操作數中並覆蓋。源操作數指定的值是一個立即數,存儲在寄存器或存儲器中。目的操作數指定一個位置,要么是一個寄存器,要么是一個存儲器地址。在 IA32 中還有一條限制,傳送指令的兩個操作數不能都執行存儲器位置。
將一個值從一個存儲器位置復制到另一個存儲器位置需要兩條指令:(就和宋丹丹把大象送進冰箱的步驟一樣)
①、第一條指令將源值加載到寄存器中
②、第二條指令將該寄存器值寫入到目的位置。
下圖是許多不同的指令類:
4、MOV 指令
MOV 類由三條指令組成:movb,movw和 movl。指令格式為 [movx S D],表示將源操作數S中的數據復制到目的操作數D中。三種指令的區別是它們分別是在大小為 1,2和4個字節的數據上進行操作。
這里舉一個簡單的例子,比如我們有一條指令為movl %edx %eax。那么它的執行過程就如下圖所示。
上圖引用至:http://www.cnblogs.com/zuoxiaolong/p/computer15.html
在指令執行之后,%edx寄存器當中的內容會被復制到%eax寄存器。數據格式則為四個字節,也就是雙字。我們還可以使用movb和movw去復制一個字節或者兩個字節。
5、MOVS指令
MOVS指令格式為 [movsxy S D],其中x、y為數據格式,S為源操作數,D為目的操作數。x、y的組合有三種,分別是bw,bl,wl,分別表示字節(8位)傳送到字(16位),字節(16位)傳送到雙字(32位),字(16位)傳送到雙字(32位)。
將較小的源數據復制到一個較大的數據位置。高位用符號位擴展,即目的位置的所有高位用源值的最高位數值進行填充。
比如對於指令movswl %dx %eax來講,它的作用如下圖所示。
上圖引用至:http://www.cnblogs.com/zuoxiaolong/p/computer15.html
這里使用了十六進制的整數表示方式。可以看到,movs指令將0x8FFF擴展以后存入%eax寄存器,其中%dx為寄存器%edx的后16位表示。
6、MOVZ 指令
MOVZ 指令和上面的MOVS 指令十分相似。指令格式為 [movzxy S D],其中x、y為數據格式,S為源操作數,D為目的操作數。x、y的組合有三種,分別是bw,bl,wl,分別表示字節(8位)傳送到字(16位),字節(16位)傳送到雙字(32位),字(16位)傳送到雙字(32位)。
將較小的源數據復制到一個較大的數據位置。高位用0擴展,即目的位置的所有高位用0進行填充。
比如對於指令movzwl %dx %eax來講,它的作用如下圖所示。
上圖引用至:http://www.cnblogs.com/zuoxiaolong/p/computer15.html
擴展后,目標寄存器%eax的前16位為0而不再是1。
7、push 和 pop 指令
我們知道 棧 是一個數據結構,可以添加或刪除值,遵循“后進先出”的原則。
push:把數據壓入棧中,添加數據。
pop:把數據移出棧,刪除數據。注意移出的值總是最近被壓入而仍然在棧中的值。
棧可以實現為一個數組,總是從數組的一端插入或刪除元素。而這一端稱為棧頂,在 IA32 中,程序棧存放在存儲器某個區域,如下圖所示:
注意由於操作數字節的不同,pushl 是將雙字(32位)壓入棧中;popl 是移出雙字。
將一個雙字值壓入棧中,首先要將棧指針減4,然后將值寫到新的棧頂地址。因此指令 pushl %ebp 等價於下面兩條指令:
subl $4,%esp
movl %ebp,(%esp)
上圖所示,當 %esp 為0x108,%eax為0x123時,執行指令 pushl %ebp 的效果。首先 %esp 會減4,得到0x104,然后會將 0x123 存放到存儲器地址 0x104處。
將一個雙字值從棧頂移出,首先要從棧頂位置讀出數據,然后將棧指針加4。因此指令 popl %eax 等價於下面兩條指令:
movl (%esp),%eax
addl $4,%esp
上圖所示,先從存儲器中讀取值 0x123,再寫到寄存器 %edx中,然后寄存器%esp的值將增加回到0x108。
注意值0x123仍然會保存在存儲器0x104中,直到被覆蓋(比如被另一條入棧操作覆蓋)。無論如何,%esp 指向的地址總是棧頂,任何存儲在棧頂之外的數據都認為是無效的。
8、總結
本章主要介紹了操作數指示符,需要我們理解幾種表達式的計算方法。接着介紹了幾種數據傳送指令,包括MOV,MOVS,MOVZ,PUSH和POP等,總體上來看不難理解,我們在了解這些指令后,再回頭看那些匯編代碼,應該會理解很多了。下一篇博客我們將進一步介紹匯編指令——算術和邏輯操作。