【 聲明:版權所有,歡迎轉載,請勿用於商業用途。 聯系信箱:feixiaoxing @163.com】
說到用匯編的眼光看C++語言,那麼怎麼閱讀匯編代碼就成了我們需要解決的一個問題。其實,實話說,匯編其實不難。只是我們需要明白這樣幾個問題:
(1)匯編是什麼語言?
(2)匯編中的主要內容有哪些?
(3)匯編語言是怎麼和實際C/C++語言代碼一一對應的?
(1)匯編是什麼語言
其實匯編語言是CPU指令碼的一種標記符號。不同的CPU具有不同的指令集,普通PC上的CPU一般來自AMD或者是INTEL,使用的也就是我們今天所要說的X86指令集。其他類似的CPU還有POWERPC,主要來自電信企業的交換機、路由器使用;ARM類型,主要是智能終端或者儀器儀表類的設備使用;SUN SPARC類型,主要來供SUN服務器使用。因為CPU指令集和二進制碼幾乎是一一對應的,所以匯編語言不但可以幫助我們快速了解機器的硬件,也方便我們了解程序是怎麼在設備上面運行的。
(2)匯編語言的內容有哪些?
匯編語言的內容有很多,但是真正和我們C/C++語言相關的內容其實並不多。大體上你需要了解的只有寄存器、段地址、堆棧、寄存器之間的基本操作、地址訪問這些就足夠了。
(3)匯編語言是怎麼和實際語言一一對應的?
我們從一個范例開始。一般來說,一條語句都需要拆分成若干條匯編語句。比如說下面這一段話:
int m = 10;
int n = 20;
int p = m + n;
我們這裡假設m、n、p都是處在一個函數之中,所以事實上三個變量都是臨時變量,在進入到函數之前,ebp和esp之間都要騰出空間為這些臨時變量做准備。那麼這三句話應該是這樣解釋的。
43: int m = 10;
004012E8 mov dword ptr [ebp-4],0Ah
44: int n = 20;
004012EF mov dword ptr [ebp-8],14h
45: int p = m + n;
004012F6 mov eax,dword ptr [ebp-4]
004012F9 add eax,dword ptr [ebp-8]
004012FC mov dword ptr [ebp-0Ch],eax
我們可以通過上面的代碼直觀地看到匯編語句和C語言之間的對應關系。第一句,m賦值為10,內存正是ebp向下的內存;第二句和第一句類似;第三句稍微有點復雜,我們可以分析一下。首先我們看到,CPU從堆棧中把m的數據找了出來,也就是[ebp-4]地址處的數據,接著CPU用同樣的方法把n的數據也找了出來,直接加到寄存器eax上,最後一步比較簡單,就是把eax的數據保存在[ebp-0c]處的地址上。只要是函數內部的臨時變量,就會看到這樣的形式。臨時變量就是依靠ebp的偏移地址獲取的。
大家有沒有想過,如果p是全局變量呢?
45: int m = 10;
004012E8 mov dword ptr [ebp-4],0Ah
46: int n = 20;
004012EF mov dword ptr [ebp-8],14h
47: p = m + n;
004012F6 mov eax,dword ptr [ebp-4]
004012F9 add eax,dword ptr [ebp-8]
004012FC mov [p (0042b0b4)],eax
看到上面的代碼,我們發現m、n的賦值方向沒有發生什麼樣的變化。變化的是,最後寄存器eax的數值被賦值給了一個絕對地址0x42b0b4。這說明了一個問題,在程序加載到內存後,全局變量是有獨立地址空間,並不會隨著堆棧的浮動發生變化。
前面我們說過,在函數內部的所有變量都會保存在ebp到esp之間的堆棧空間上,那麼代碼是怎麼做的呢?我們可以看這樣一段匯編代碼?
41: void process()
42: {
004012D0 push ebp
004012D1 mov ebp,esp
004012D3 sub esp,4Ch
004012D6 push ebx
004012D7 push esi
004012D8 push edi
004012D9 lea edi,[ebp-4Ch]
004012DC mov ecx,13h
004012E1 mov eax,0CCCCCCCCh
004012E6 rep stos dword ptr [edi]
43: int m = 10;
004012E8 mov dword ptr [ebp-4],0Ah
44: int n = 20;
004012EF mov dword ptr [ebp-8],14h
45: int p = m + n;
004012F6 mov eax,dword ptr [ebp-4]
004012F9 add eax,dword ptr [ebp-8]
004012FC mov dword ptr [ebp-0Ch],eax
46: }
我們把剛才一段函數的完整代碼打印出來。我們發現,事實上在臨時變量m運算之前,函數做了很多預備操作,其主要目的有兩個:(1)為臨時變量准備空間;(2)對函數運算中使用到的寄存器進行保存處理,這是因為寄存器是所有函數共有的資源,如果不記錄好原來的數據,那麼在函數返回後,寄存器就會忘記原來的數值,不能在原來的狀態下繼續正確地運算了。從地址0x4012D0到地址0x4012E6之間共有10句話,其實意思並不困難。第一句,ebp壓棧;第二句esp復制給ebp;第三句esp自減4C大小,這個大小一般是按照函數內部定義了多少臨時變量決定的;第四句,ebx壓棧;第五句,esi壓棧;第六句,edi壓棧;第七句到第十句,把[ebp-4C]處向上的0x4C個字節全部設置成CC,edi為起始地址,ecx為循環次數0x13次,dword表示每次設置4個字節。
那麼函數在返回前做了一些什麼呢?
46: }
004012FF pop edi
00401300 pop esi
00401301 pop ebx
00401302 mov esp,ebp
00401304 pop ebp
00401305 ret
其實函數返回的時候,做的內容特別簡單。第一句edi出棧;第二句esi出棧;第三句ebx出棧,和前面寄存器進棧的順序是完全相反的。最後三句特別關鍵,我們看到ebp復制給esp,ebp出棧,函數返回,這樣一切都恢復到函數調用之前的狀態了。
那麼函數調用的時候,入參是怎麼處理的呢?
53: process(20);
0040EFA4 push 14h
0040EFA6 call @ILT+40(process) (0040102d)
0040EFAB add esp,4
上面一段代碼就是process函數含有一個參數時候的情形。函數調用後esp+4,堆棧恢復。堆棧+4,主要是因為參數的空間就是4個字節。所以用一幅圖說明一下,函數調用的時候堆棧空間應該是這樣的:
| 函數參數 |
| 返回地址 |
| 臨時變量 | <------------------------ ebp
| 壓棧寄存器 |
| 棧頂 | <-------------------------esp
其他知識:
(1) 全局運算cpu寄存器很多,一般有eax,ebx,ecx,edx等等。我們通常說的ax,bx,cx,dx指的只是他們的低位部分。
(2)段寄存器寄存了程序的代碼段,數據段和堆棧段。代碼段保存了全部的程序代碼,數據段保存了全據變量的代碼,而堆棧則是全部堆棧空間。
(3)目前vc編譯器支持嵌入式匯編,大家有興趣的話可以在函數內部試試身手。下面的代碼只是一個范例:
void process(int* q)
{
_asm {
push eax
push ebx
push ecx
mov eax, 0x10
mov ebx, 0x15
add eax, ebx
mov ecx, q
mov [ecx], eax
pop ecx
pop ebx
pop eax
}
}
(全部完)