程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> 匯編語言學習指南(四)

匯編語言學習指南(四)

編輯:匯編語言

高級語言程序的匯編解析

在高級語言中,如C和PASCAL等等,我們不再直接對硬件資源進行操作,而是面向於問題的解決,這主要體現在數據抽象化和程序的結構化。例如我們用變量名來存取數據,而不再關心這個數據究竟在內存的什麼地方。這樣,對硬件資源的使用方式完全交給了編譯器去處理。不過,一些基本的規則還是存在的,而且大多數編譯器都遵循一些規范,這使得我們在閱讀反匯編代碼的時候日子好過一點。這裡主要講講匯編代碼中一些和高級語言對應的地方。

1. 普通變量。通常聲明的變量是存放在內存中的。編譯器把變量名和一個內存地址聯系起來(這裡要注意的是,所謂的“確定的地址”是對編譯器而言在編譯階段算出的一個臨時的地址。在連接成可執行文件並加載到內存中執行的時候要進行重定位等一系列調整,才生成一個實時的內存地址,不過這並不影響程序的邏輯,所以先不必太在意這些細節,只要知道所有的函數名字和變量名字都對應一個內存的地址就行了),所以變量名在匯編代碼中就表現為一個有效地址,就是放在方括號中的操作數。例如,在C文件中聲明:

int my_age;

這個整型的變量就存在一個特定的內存位置。語句 my_age= 32; 在反匯編代碼中可能表現為:

mov word ptr [007E85DA], 20

所以在方括號中的有效地址對應的是變量名。又如:

char my_name[11] = "lianzi2000";

這樣的說明也確定了一個地址,對應於my_name. 假設地址是007E85DC,則內存中[007E85DC]='l',[007E85DD]='i', etc. 對my_name的訪問也就是對這地址處的數據訪問。

指針變量其本身也同樣對應一個地址,因為它本身也是一個變量。如:

char *your_name;

這時也確定變量"your_name"對應一個內存地址,假設為007E85F0. 語句your_name=my_name;很可能表現為:

mov [007E85F0], 007E85DC ;your_name的內容是my_name的地址。

2. 寄存器變量

在C和C++中允許說明寄存器變量。register int i; 指明i是寄存器存放的整型變量。通常,編譯器都把寄存器變量放在esi和edi中。寄存器是在cpu內部的結構,對它的訪問要比內存快得多,所以把頻繁使用的變量放在寄存器中可以提高程序執行速度。

3. 數組

不管是多少維的數組,在內存中總是把所有的元素都連續存放,所以在內存中總是一維的。例如,int i_array[2][3]; 在內存確定了一個地址,從該地址開始的12個字節用來存貯該數組的元素。所以變量名i_array對應著該數組的起始地址,也即是指向數組的第一個元素。存放的順序一般是i_array[0][0],[0][1],[0][2],[1][0],[1][1],[1][2] 即最右邊的下標變化最快。當需要訪問某個元素時,程序就會從多維索引值換算成一維索引,如訪問i_array[1][1],換算成內存中的一維索引值就是1*3+1=4.這種換算可能在編譯的時候就可以確定,也可能要到運行時才可以確定。無論如何,如果我們把i_array對應的地址裝入一個通用寄存器作為基址,則對數組元素的訪問就是一個計算有效地址的問題:

; i_array[1][1]=0x16

lea ebx,xxxxxxxx ;i_array 對應的地址裝入ebx
mov edx,04 ;訪問i_array[1][1],編譯時就已經確定
mov word ptr [ebx+edx*2], 16 ;

當然,取決於不同的編譯器和程序上下文,具體實現可能不同,但這種基本的形式是確定的。從這裡也可以看到比例因子的作用(還記得比例因子的取值為1,2,4或8嗎?),因為在目前的系統中簡單變量總是占據1,2,4或者8個字節的長度,所以比例因子的存在為在內存中的查表操作提供了極大方便。

4. 結構和對象

結構和對象的成員在內存中也都連續存放,但有時為了在字邊界或雙字邊界對齊,可能有些微調整,所以要確定對象的大小應該用sizeof操作符而不應該把成員的大小相加來計算。當我們聲明一個結構變量或初始化一個對象時,這個結構變量和對象的名字也對應一個內存地址。舉例說明:

struct tag_info_struct
{
int age;
int sex;
float height;
float weight;
} marry;

變量marry就對應一個內存地址。在這個地址開始,有足夠多的字節(sizeof(marry))容納所有的成員。每一個成員則對應一個相對於這個地址的偏移量。這裡假設此結構中所有的成員都連續存放,則age的相對地址為0,sex為2, height 為4,weight為8。

; marry.sex=0;

lea ebx,xxxxxxxx ;marry 對應的內存地址
mov word ptr [ebx+2], 0
......

對象的情況基本相同。注意成員函數具體的實現在代碼段中,在對象中存放的是一個指向該函數的指針。

5. 函數調用

一個函數在被定義時,也確定一個內存地址對應於函數名字。如:

long comb(int m, int n)
{
long temp;
.....

return temp;
}

這樣,函數comb就對應一個內存地址。對它的調用表現為:

CALL xxxxxxxx ;comb對應的地址。這個函數需要兩個整型參數,就通過堆棧來傳遞:

;lresult=comb(2,3);

push 3
push 2
call xxxxxxxx
mov dword ptr [yyyyyyyy], eax ;yyyyyyyy是長整型變量lresult的地址

這裡請注意兩點。第一,在C語言中,參數的壓棧順序是和參數順序相反的,即後面的參數先壓棧,所以先執行push 3. 第二,在我們討論的32位系統中,如果不指明參數類型,缺省的情況就是壓入32位雙字。因此,兩個push指令總共壓入了兩個雙字,即8個字節的數據。然後執行call指令。call 指令又把返回地址,即下一條指令(mov dword ptr....)的32位地址壓入,然後跳轉到xxxxxxxx去執行。

在comb子程序入口處(xxxxxxxx),堆棧的狀態是這樣的:

03000000 (請回憶small endian 格式)
02000000
yyyyyyyy <--ESP 指向返回地址

前面講過,子程序的標准起始代碼是這樣的:

push ebp ;保存原先的ebp
mov ebp, esp;建立框架指針
sub esp, XXX;給臨時變量預留空間
.....

執行push ebp之後,堆棧如下:

03000000
02000000
yyyyyyyy
old ebp <---- esp 指向原來的ebp

執行mov ebp,esp之後,ebp 和esp 都指向原來的ebp. 然後sub esp, xxx 給臨時變量留空間。這裡,只有一個臨時變量temp,是一個長整數,需要4個字節,所以xxx=4。這樣就建立了這個子程序的框架:

03000000
02000000
yyyyyyyy
old ebp <---- 當前ebp指向這裡
temp

所以子程序可以用[ebp+8]取得第一參數(m),用[ebp+C]來取得第二參數(n),以此類推。臨時變量則都在ebp下面,如這裡的temp就對應於[ebp-4].

子程序執行到最後,要返回temp的值:

mov eax,[ebp-04]
然後執行相反的操作以撤銷框架:

mov esp,ebp ;這時esp 和ebp都指向old ebp,臨時變量已經被撤銷
pop ebp ;撤銷框架指針,恢復原ebp.

這是esp指向返回地址。緊接的retn指令返回主程序:

retn 4

該指令從堆棧彈出返回地址裝入EIP,從而返回到主程序去執行call後面的指令。同時調整esp(esp=esp+4*2),從而撤銷參數,使堆棧恢復到調用子程序以前的狀態,這就是堆棧的平衡。調用子程序前後總是應該維持堆棧的平衡。從這裡也可以看到,臨時變量temp已經隨著子程序的返回而消失,所以試圖返回一個指向臨時變量的指針是非法的。

為了更好地支持高級語言,INTEL還提供了指令Enter 和Leave 來自動完成框架的建立和撤銷。Enter 接受兩個操作數,第一個指明給臨時變量預留的字節數,第二個是子程序嵌套調用層數,一般都為0。enter xxx,0 相當於:

push ebp
mov ebp,esp
sub esp,xxx

leave 則相當於:

mov esp,ebp
pop ebp

=============================================================
好啦,我的學習心得講完了,謝謝各位的抬舉。教程是不敢當的,因為我也是個大菜鳥。如果這些東東能使你們的學習輕松一些,進步快一些,本菜鳥就很開心了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved