從簡單的算法初探過程匯編


不忽視匯編

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

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

過程匯編前奏

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

image

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

當調用者調用另一個過程的時候:

  • 首先,如果這個被調用過程如果有參數的話,調用的棧幀中會構造這些參數,並存入到調用者的棧幀中(所以上面的圖參數n...參數1,就是這個原因了);
  • 將返回地址入棧。返回地址是當被調用過程執行完畢之后,調用者應該繼續執行的指令地址;它屬於調用者棧幀的部分,形成了調用者棧幀的末尾
  • 到這一步就進入了被調用者的棧幀了,所謂當前棧幀。保存調用者的幀指針,以便在之后找回調用者的程序棧;
  • 最后進入程序執行,一般過程會sub 0xNh %esp來分配當前程序棧的大小,用來存取臨時變量啊,暫存寄存器的值啊等等。
  • 如果被調用者又要調用另一個過程,回到第一步即可;
  • 當過程結束之時,會將棧指針,幀指針恢復,經常會在反匯編中看到如下:
    mov         %esp,%ebp  
    pop         %ebp 
    同時,返回地址會被恢復到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代碼是:

View Code
#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查看匯編代碼有如下反匯編代碼,因為晦澀,所以摘抄了如下:

View Code
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之前會有一段准備:

View Code
    fun(i) ;  
00411445  mov         eax,dword ptr [i]  
00411448  push        eax   
00411449  call        fun (4110E6h)  
0041144E  add         esp, 4 

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

image

借助jmp跳至004113A0h,正式進入fun函數。fun內首先保存了幀指針和被調用者保存寄存器和其他相關數據,只有當參數x==0的時候才會終止函數的運行,故在遞歸調用(注意,是遞歸調用,而不是調用)fun之前(即call fun之前),有如下:

image

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

image

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

View Code
    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,執行返回。

View Code
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

程序繼續執行:

View Code
#    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的返回值。接下來:

View Code
    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參數。該是恢復棧的時候了:

View Code
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又成功返回了,程序繼續:

View Code
#    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退出:

View Code
    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的時候了,恢復棧:

View Code
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

還差一次,返回之后程序繼續執行:

View Code
#    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退出(不累贅了):

View Code
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 

View Code
#    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中位的和。

收獲

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

 

文章有點長,歡迎討論。

搗亂小子 2012年2月8日星期三


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM