1. 程式人生 > >從簡單的演算法初探過程彙編 棧幀指標

從簡單的演算法初探過程彙編 棧幀指標

從簡單的演算法初探過程彙編 棧幀指標

作者:zcabcd123

 

 

 

從簡單的演算法初探過程彙編

轉載自 搗亂小子

 

趁年輕,用程式碼實現夢想 — daoluan.net

不忽視彙編

  較於我們日常接觸的高階語言,諸如c語言,c++,java等等,組合語言是更接近機器的語言,它的常用操作簡單到把一個數值(立即數,暫存器數或者儲存器資料)載入到暫存器,正是這樣,所以讓彙編完成一個程式任務,過程會比較晦澀;高階語言隱藏了很多的機器細節(比如過程(函式)棧幀的初始化,以及過程結束時棧幀的恢復),程式碼清晰易懂。

  真佩服六七十年代那些大牛們,都是怎麼過來的...膜拜膜拜。寫一個100以內整數的和,即使有充分的彙編文件,這也足夠折騰我一陣子,太噁心了。但是瞭解彙編的行為方式和其中的一些重要細節,有助於理解計算機軟體和硬體的工作方式。我就一個簡單的演算法來認識一下彙編。

過程彙編前奏

  過程可以理解為c中的函式,當呼叫者(caller)呼叫被呼叫者(be caller)的時候,系統會為被呼叫者在棧內分配空間,這個空間就稱為棧幀。棧的結構大概如下:

image

  程式棧是向低地址生長的棧,與資料結構當中的棧結構類似,有後進先出的性質,暫存器%esp(stack pointer)儲存棧頂指標的地址,暫存器%ebp(** pointer)儲存幀指標的地址。 程式執行的時候,棧指標可以移動,以便增大或者縮小程式棧的空間,而幀指標是固定的,因為大多數程式棧中儲存的資料都是相對於幀指標的(幀指標+偏移量)。

當呼叫者呼叫另一個過程的時候:

  • 首先,如果這個被呼叫過程如果有引數的話,呼叫的棧幀中會構造這些引數,並存入到呼叫者的棧幀中(所以上面的圖引數n...引數1,就是這個原因了);
  • 將返回地址入棧。返回地址是當被呼叫過程執行完畢之後,呼叫者應該繼續執行的指令地址;它屬於呼叫者棧幀的部分,形成了呼叫者棧幀的末尾
  • 到這一步就進入了被呼叫者的棧幀了,所謂當前棧幀。儲存呼叫者的幀指標,以便在之後找回呼叫者的程式棧;
  • 最後進入程式執行,一般過程會sub 0xNh %esp來分配當前程式棧的大小,用來存取臨時變數啊,暫存暫存器的值啊等等。
  • 如果被呼叫者又要呼叫另一個過程,回到第一步即可;
  • 當過程結束之時,會將棧指標,幀指標恢復,經常會在反彙編中看到如下: 同時,返回地址會被恢復到PC。
  • 這時回到了打呼叫者應該繼續執行的地方。

  上面的文字可以更概括,反彙編一個過程(函式)會有建立(初始化),主體(執行),結束(返回)。之前很容易把棧和堆搞混(不是資料結構裡面),找到一個好文章與大家分享:棧和堆的區別。據說被轉了無數次了,說明寫的不錯。 過程呼叫和返回在組合語言中分別用call和ret(return)來實現。call和ret的做法不是很透明,

  • call將返回地址入棧,並將PC跳轉到被呼叫過程的起始地址;
  • ret與call相反,從棧中彈出返回地址,並跳轉PC。 

具體看圖:

image

關於彙編程式碼格式

  彙編程式碼最為常見的是ATT和intel彙編程式碼格式,ATT應該較為古老,但卻是GCC,OBJDUMP的預設格式。需要注意的是在帶有多個運算元的指令的情況下,列出運算元順序兩者是相反的,所以在思路上很容易混淆。例如實現%esp→%eax,有如下區別。

#intel  
mov eax,esp  
#ATT  
movl %esp,%eax

因為受到書本的影響,所以我習慣在暫存器前加上“%”,並且我更偏好ATT格式的彙編程式碼。

反彙編具體分析

(下面的程式棧圖,我把引數入棧我在標明“引數i=?”,這可能會有點疑惑,如果“引數x=?”這樣會更好,:))

  有一個簡單程式,先不管它實現了什麼功能,看下去,絕對會有收穫的。給出的c程式碼是:

#include <iostream>  
using namespace std;  

int fun(unsigned int x)  
{  
    if(x == 0)  
        return 0;  
    unsigned int nx = x>>1;  
    int rv = fun(nx);  
    return (x & 0x01)+rv;  
}  

int main()  
{  
    unsigned int i = 12;  
    fun(i);  
    return 0;  
}

在vs2008下debug檢視彙編程式碼有如下反彙編程式碼,因為晦澀,所以摘抄瞭如下:

004110E6  jmp         fun (4113A0h)   

int fun(unsigned int x)  
{  
004113A0  push        ebp    
004113A1  mov         ebp,esp   
004113A3  sub         esp,0D8h   
004113A9  push        ebx    
004113AA  push        esi    
004113AB  push        edi    
004113AC  lea         edi,[ebp-0D8h]   
004113B2  mov         ecx,36h   
004113B7  mov         eax,0CCCCCCCCh   
004113BC  rep stos    dword ptr es:[edi]   
    if(x == 0)  
004113BE  cmp         dword ptr [x],0   
004113C2  jne         fun+28h (4113C8h)   
        return 0;  
004113C4  xor         eax,eax   
004113C6  jmp         fun+48h (4113E8h)   
    unsigned int nx = x>>1;  
004113C8  mov         eax,dword ptr [x]   
004113CB  shr         eax,1   
004113CD  mov         dword ptr [nx],eax   
    int rv = fun(nx);  
004113D0  mov         eax,dword ptr [nx]   
004113D3  push        eax    
004113D4  call        fun (4110E6h)   
004113D9  add         esp,4   
004113DC  mov         dword ptr [rv],eax   
    return (x & 0x01)+rv;  
004113DF  mov         eax,dword ptr [x]   
004113E2  and         eax,1   
004113E5  add         eax,dword ptr [rv]   
}  
004113E8  pop         edi    
004113E9  pop         esi    
004113EA  pop         ebx    
004113EB  add         esp,0D8h   
004113F1  cmp         ebp,esp   
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)   
004113F8  mov         esp,ebp   
004113FA  pop         ebp    
004113FB  ret                

int main()  
{  
00411420  push        ebp    
00411421  mov         ebp,esp   
00411423  sub         esp,0CCh   
00411429  push        ebx    
0041142A  push        esi    
0041142B  push        edi    
0041142C  lea         edi,[ebp-0CCh]   
00411432  mov         ecx,33h   
00411437  mov         eax,0CCCCCCCCh   
0041143C  rep stos    dword ptr es:[edi]   
    unsigned int i = 12;  
0041143E  mov         dword ptr [i],0Ch   
    fun(i);  
00411445  mov         eax,dword ptr [i]   
00411448  push        eax    
00411449  call        fun (4110E6h)   
0041144E  add         esp,4   
    return 0;  
00411451  xor         eax,eax   
}  
00411453  pop         edi    
00411454  pop         esi    
00411455  pop         ebx    
00411456  add         esp,0CCh   
0041145C  cmp         ebp,esp   
0041145E  call        @ILT+315(__RTC_CheckEsp) (411140h)   
00411463  mov         esp,ebp   
00411465  pop         ebp    
00411466  ret             

複製程式碼

上面的程式碼,在第一句就間接道明瞭fun的地址。可以看到在call  fun之前會有一段準備:

00411445h的指令就將fun的引數(此時i=6,還記得上面的圖嗎,引數n-引數1)和返回地址入棧,然後PC跳至004110E6h,此時main的棧幀如下:

所以,一直遞迴下去的話:

image

直到x==0,此時會進入if的分支執行步驟。

  if(x == 0)  
004113BE  cmp         dword ptr [x],0   
004113C2  jne         fun+28h (4113C8h)   
        return 0;  
004113C4  xor         eax,eax   
004113C6  jmp         fun+48h (4113E8h)

複製程式碼

在彙編中,會用到異或xor邏輯運算來對一個暫存器清零(004113C4h地址的指令),由於x==0,PC跳至004113E8h,執行返回。

004113E8  pop         edi    
004113E9  pop         esi    
004113EA  pop         ebx    
004113EB  add         esp,0D8h   
004113F1  cmp         ebp,esp   
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)   
004113F8  mov         esp,ebp   
004113FA  pop         ebp    
004113FB  ret             

複製程式碼

在這裡把被儲存的暫存器值都彈出來,恢復棧歸位,留意其中針對%esp和%ebp的操作;執行ret操作,返回,

image

程式繼續執行:

#    int rv = fun(nx);  
#004113D0  mov         eax,dword ptr [nx]   
#004113D3  push        eax    
#004113D4  call        fun (4110E6h)   
004113D9  add         esp,4   
004113DC  mov         dword ptr [rv],eax

複製程式碼

rv = 0;

  可以看到,處理器釋放了棧上的記憶體(%esp+4,還記得嗎,棧是向低地址增長的),因為在call之前,也就是00411448h地址處,呼叫者也就是main函式將%eax引數入棧,接著fun退出之後,引數的記憶體也就理所當然的要釋放掉。聯想一下,如果引數有很多個,那麼call之前就會有多個push,對應的,call之後就會有“add %esp n”的操作將其釋放。接著將%eax(在暫存器是用習慣當中,%eax經常被用作返回值暫存器)的值給了rv,如此一來rv就順理成章地得到了fun的返回值。接下來:

    return (x & 0x01)+rv;  
004113DF  mov         eax,dword ptr [x]   
004113E2  and         eax,1   
004113E5  add         eax,dword ptr [rv]

%eax←(x&0x01)+rv = 0x01&0x01 + 0 = 1;(提示:從這裡開始體會fun的功能)

  簡單的將x&0x01+rv後送入%eax(記得嗎,%eax經常被用作返回值暫存器),此時可能會有疑問,x是從哪裡來的,答案是x存在呼叫者的棧幀內,而非被呼叫者的棧幀,因為x是函式的一個引數,dword ptr [x]應該就是對讀取了呼叫者棧幀中的x引數。該是恢復棧的時候了:

004113E8  pop         edi    
004113E9  pop         esi    
004113EA  pop         ebx    
004113EB  add         esp,0D8h   
004113F1  cmp         ebp,esp   
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)   
004113F8  mov         esp,ebp   
004113FA  pop         ebp    
004113FB  ret  

恢復棧幀,執行ret,如圖:

image

 

fun又成功返回了,程式繼續:

#    int rv = fun(nx);  
#004113D0  mov         eax,dword ptr [nx]   
#004113D3  push        eax    
#004113D4  call        fun (4110E6h)   
004113D9  add         esp,4   
004113DC  mov         dword ptr [rv],eax

rv = %eax = 1;

又回到了剛才走過的地方,但是資料有異。接下來程式執行return退出:

    return (x & 0x01)+rv;  
004113DF  mov         eax,dword ptr [x]   
004113E2  and         eax,1   
004113E5  add         eax,dword ptr [rv]

%eax←(x&0x01)+rv = 0x3&0x01 + 1 = 2;又該是ret的時候了,恢復棧:

004113E8  pop         edi    
004113E9  pop         esi    
004113EA  pop         ebx    
004113EB  add         esp,0D8h   
004113F1  cmp         ebp,esp   
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)   
004113F8  mov         esp,ebp   
004113FA  pop         ebp    
004113FB  ret  

複製程式碼

棧幀結構如圖:

image

還差一次,返回之後程式繼續執行:

#    int rv = fun(nx);  
#004113D0  mov         eax,dword ptr [nx]   
#004113D3  push        eax    
#004113D4  call        fun (4110E6h)   
004113D9  add         esp,4   
004113DC  mov         dword ptr [rv],eax

rv = %eax = 2;

接下來程式return退出(不累贅了):

return (x & 0x01)+rv;  
004113DF  mov         eax,dword ptr [x]   
004113E2  and         eax,1   
004113E5  add         eax,dword ptr [rv]   
004113E8  pop         edi    
004113E9  pop         esi    
004113EA  pop         ebx    
004113EB  add         esp,0D8h   
004113F1  cmp         ebp,esp   
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)   
004113F8  mov         esp,ebp   
004113FA  pop         ebp    
004113FB  ret  

至此,程式完全退出了fun的遞迴過程,回到了主函式main,main也有自己的棧幀,因為main也是一個函式。下圖:

image

#    fun(i);  
#00411445  mov         eax,dword ptr [i]   
#00411448  push        eax    
#00411449  call        fun (4110E6h)   
0041144E  add         esp,4   
    return 0;  
00411451  xor         eax,eax

0x0041144E處,add %esp,4,目的是釋放一開始入棧的fun的引數,而主函式返回0(return 0),也是用到了異或邏輯運算xor來講%eax清零。

到這裡,相信有點明白了,在遞迴呼叫過程中,程式棧是如何變化的,並且上面的函式計算引數i中位的和。

收穫

  發現這樣一個小小的遞迴程式,分析起它反彙編如有一種返璞歸真的感覺,對理解“遞迴呼叫”會更為清晰的思路。縱觀上面的分析,遞迴呼叫雖然是演算法中解決問題常用的方法,但是它對付起龐大遞迴次數的程式來說(上面因為分析所以選取的遞迴次數較少),非常消耗記憶體。 所以在寫程式的時候,在時間和空間的消耗抉擇上,需要謹慎。通過學習彙編和反彙編程式碼的分析,將更瞭解機器的行為,從而寫出更為高效的程式碼。