天天看點

從彙編角度分析C語言的過程調用

基本術語定義

1.系統棧(system stack)是一個記憶體區,位于程序位址空間的末端。

2.在将資料壓棧時,棧是自頂向下增長的,該記憶體區用于函數的局部變量提供記憶體。它也支援在調用函數時傳遞參數。

3.如果調用了嵌套的過程,棧會自上而下增長,并接受新的活動記錄(activation record)來儲存一個過程所需的所有資料。

4.目前執行過程的活動記錄,由标記頂部位置的幀指針(frame point)和标記底部位置的棧指針(stack point)定義。

5.在過程執行時,雖然其頂部的限制是固定的,但底部的限制是可以擴充的(在需要更多記憶體空間時)。

分析棧幀(分析如下)

從彙編角度分析C語言的過程調用

上圖第2個棧幀的分析如下: 

1、在棧幀頂部是傳回位址,以及儲存的舊的幀指針。傳回位址指定了目前過程結束時代碼的控制流轉向的記憶體位址,而儲存的舊的幀指針則是前一個活動記錄的幀指針。在目前過程結束後,該幀指針的值可用于重建調用過程的棧幀,在試圖調試調用棧回溯時,這一點很重要。

2、活動記錄的主要部分是為過程調用局部變量配置設定的記憶體空間。在c中,這種變量也稱為自動變量(automatic variable)。

3、在函數調用時,以參數形式傳遞到函數的值,存儲在棧的底部。

4、所有常見的計算機體系結構都提供了以下兩個棧操作指令:

push指令将一個值放置在棧上,并将棧指針esp減去該值所占用的記憶體位元組數。棧的末端下移到更低的位址;

pop指令從棧中彈出一個值,并相應增加棧指針esp的值,也就是說,棧的末端上移了。

5、一般體系結構另外提供兩個指令,用于調用和退出函數(自動傳回到調用過程),它們也會自動操作棧:

call指令将指令指針的目前值壓棧,跳轉到被調用函數的起始位址。  call 指令 :在at&t彙編中,call foo(foo是一個标号)等效于以下彙編指令: pushl %eip ,movl f, %eip  ;   

return指令從棧上彈出傳回位址,并跳轉到該位址。過程的實作必須将rerurn作為最後一條指令,由call放置在棧上的傳回位址位于棧的底部(實際上是上一個活動記錄的底部,目前活動記錄的頂部)。  ret指令: 在at&t彙編中,ret等效于以下彙編指令: popl %eip

過程調用兩個組成步驟

1、在棧中建立參數清單。傳遞到被調用函數的第一個參數最後入棧(從右到左)。這使得c中可以傳遞可變數目的參數,然後将其從棧上逐一彈出(pop)。

2、調用call,這将指令指針的目前值(call之後的下一條指令)壓棧,代碼的控制流轉向被調用的函數。被調用的過程負責管理幀指針ebp,需要執行下列步驟: 

前一個幀指針壓棧,因而棧指針下移。

将棧指針的目前值copy給幀指針,标記目前執行函數的棧區的起始位置。

執行目前函數的代碼。

在函數結束時,存儲的舊幀指針位于棧的底部。其值從棧彈出到幀指針寄存器(ebp),使之指向前一個函數的棧區起始位置。現在,對目前函數執行call指令時壓棧的傳回位址位于棧低。

調用return,将傳回位址從棧彈出。cpu轉移到傳回位址,代碼的控制流也傳回到調用函數。

具體c 語言例子分析

初看起來,這種方法似乎有些混亂,是以,我們先看一個簡單的c語言例子:

從彙編角度分析C語言的過程調用

在ia-32系統上,彙編代碼本身必須是at&t表示法給出。 

at&t彙編文法總結為以下5條規則,就足夠了。 

1.寄存器通過在名稱前加百分号(%)字首引用。example:為使用eax寄存器,彙編代碼中将使用%eax。(如果在c中内聯彙編的話,c代碼必須指定兩個百分号,才能在轉給彙編器的輸出中形成一個百分号)。

2.源寄存器總是在目的寄存器之前指定。 example,在mov語句中,這意味着 mov a,b 将 寄存器a中的值 内容copy到寄存器b中。

3.操作數的長度由彙編語句的字尾指定。b代步byte,w代表word,l代表long。在ia-32上,将一個長整型從eax寄存器移動到ebx寄存器中,需要指定movl %eax,%ebx。

4.間接記憶體引用(指針反引用)需要将寄存器包含在括号中,example:movl(%eax),%ebx 将寄存器eax的值指向的記憶體位址中的長整型copy到ebx寄存器中。

5.offset(register)指定寄存器值與一個偏移量聯用,将偏移量加到寄存器的實際值上。example: 8(%eax)指定将eax+8用作一個操作數。該表示法主要用于記憶體通路,例如指定與棧指針或幀指針的偏移量,以通路某些局部變量。

從彙編角度分析C語言的過程調用
從彙編角度分析C語言的過程調用
從彙編角度分析C語言的過程調用

我們來分析一下 main.s 彙編代碼: 

1.從main 主函數開始分析. 在ia-32系統中,ebp寄存器用于幀指針(棧頂),pushl  %ebp 将該ebp寄存器中的值壓入系統棧上最低位置,這導緻棧頂指針向下移動4byte,這是因為ia-32系統上需要4byte來表示一個指針(pushl中的字尾l,在at&t彙編中表示一個long型)。

2.第3行,movl   %esp, %ebp  将esp(棧指針)寄存器 的值 copy到ebp(幀指針)寄存器中;把目前的棧指針作為本函數的幀指針。

3.第4行,subl $24,%esp 從棧指針減去0x18 byte,使得棧指針下移,将棧的空間增大了0x18=24byte;

調整棧指針,為局部變量保留白間。局部變量必須放置在棧上,在c代碼中,a與b兩個局部變量,兩者都是整型變量,在記憶體中都需要4個byte。

因為棧的前4個byte儲存了 幀指針的舊值(上一個活動記錄),編譯器将接下來的兩個 4byte記憶體配置設定給了這兩個局部變量。

ebp - 0xc 存着局部變量a的值 3    ; ebp - 0x8 存着局部變量b的值 4 (這裡可以看到參數是從右到左 壓入棧的)。

4.第5行 ,第6行 movl $0x3, -0xc(%ebp)    movl $0x4, -0x8(%ebp) : 為了向配置設定的記憶體空間設定初始值(對應c中 局部變量的初始化),編譯器使用了處理器的指針反引用選項。 這兩天指令通知編譯器,引用“幀指針減12”得到的值 在記憶體中指向的位置。使用mov指令将值3 寫入該位置。

編譯器接下來用同樣的方法處理第2個局部變量,其在棧的位置稍低,ebp - 0x8 (ebp - 8byte) 位置 ,值為4。

5.第7行,第8行設定第2個參數(b),第9行,第10行負責設定第1個參數(a)。     movl    -8(%ebp), %eax   ;   movl    %eax, 4(%esp)  ; movl    -12(%ebp), %eax;    movl    %eax, (%esp)

局部變量a和b必須用作即将調用的add過程調用的參數。編譯器通過将适當的值放置在棧的末端來建立參數清單。

如前所述,第一個參數在最低部。棧指針用于查找棧的末尾。

記憶體中對應的位置通過指針反引用确定。将棧上的兩個局部變量的值分别讀入eax寄存器,然後将eax的值寫入參數清單中對應的位置。(一般情況)

6.上圖描述了 add()函數調用前後,棧的狀态。現在可以使用call 指令調用add()函數。call指令 将eip(指令指針寄存器)壓入棧,代碼控制流在add例程的開始處恢複執行。

根據調用約定,例程首先将此前的幀指針(ebp)壓入棧,并将棧指針(esp)指派給 幀指針(ebp)。

過程的參數可以根據幀指針(ebp)查找。編譯器知道參數就在調用函數的活動記錄末尾,而在目前活動記錄開始處又存儲了兩個4byte的值(傳回位址,舊幀指針)。是以參數可以通過反引用ebp+8和ebp+12通路。

add 指令用于 加法,而eax寄存器用作工作空間。結果值就儲存在該寄存器中,使它可以傳遞給調用函數(這裡是main())。

為了傳回到調用函數,需要執行以下兩個操作: <a>使用pop将存儲的幀指針(ebp)從棧彈出到ebp寄存器。棧幀的頂端重新恢複到main()的設定;<b>ret将傳回位址從棧彈出到 eip(指令指針)寄存器,控制流轉向該位址。

7.因為main()中還使用了另一個局部變量(ret)來存儲add()函數的傳回值,傳回後需要将eax寄存器的值 copy 到ret在棧上的位置。

總結

關于at&t彙編

(個人了解)彙編可以用一句話概括:彙編就是在(寄存器和寄存器)或 (寄存器和記憶體)之間來回move 資料;就是指:資料在記憶體和寄存器間來回流動,流動的越頻繁就代表程式越複雜,比如office這樣的大型軟體。

從c語言層面分析: 

ebp-xx     一般 是局部變量

ebp+xx   一般都是參數 

ebp+4  傳回位址 ,制高點,   很多攻擊都是攻擊這裡, 防毒軟體,這裡是重點會掃描。

c函數堆棧中配置設定的空間,并不會清零,是以在寫c代碼的時候,局部變量一定要初始化指派。

參數的傳遞形式、傳遞順序已經棧平衡并不是固定的(不同的函數調用約定)。

關于 寄存器 與記憶體的差別:   

寄存器位于cpu内部,執行速度快,但比較貴。

記憶體速度相對較慢,成本低,是以容量能做很大。

寄存器和記憶體沒有本質差別,都是用于存儲資料的容器,都是定寬的。

寄存器常用的8個通用寄存器 :eax,ecx,edx,ebx,   esp, ebp, esi, edi.

計算機中的幾個常用計量機關:byte, word, dword :byte(位元組) = 8bit ; word (字 ) = 16bit ; dword (雙字)=32bit; 

記憶體的數量特别龐大,無法每個記憶體單元都命名一個名字,是以用編号來替代。

我們稱計算機cpu是32bit或者64bit,有很多書上說之是以叫32bit計算機是因為寄存器的寬度是32bit,這是不準确的,因為還有很多寄存器是大于32bit的。

繼續閱讀