系統棧的工作原理

BattleHeart發表於2015-04-15

1.開篇

本篇文章著重寫的是系統中棧的工作原理,以及函式呼叫過程中棧幀的產生與釋放的過程,有可能名字過大,如果不合適我可以換一個名字,希望大家能夠指正,小丁虛心求教!如果有哪裡寫的不清楚的或者錯誤的地方請及時更正,小丁再次謝過了。文章裡面有錯別字,也可能會有好友說暫存器的32、16位的區別其實我感覺這裡主要講的還是些原理性的東西,後續會將文章圖片錯別字進行調整.

2.記憶體的不同用途

根據不同的作業系統,一個程式可能被分配到不同的記憶體區域去執行。但是不管什麼樣的作業系統、什麼樣的計算機架構,程式使用的記憶體都可以按照功能大致分為以下4個部分:

(1)程式碼區:這個區域儲存著被裝入執行的二進位制機器程式碼,處理器會到這個區域取指並執行。

(2)資料區:用於儲存全域性變數等。

(3)堆區:程式可以在堆區動態地請求一定大小的記憶體,並在用完之後歸還給堆區。動態分配和回收是堆區的特點。

(4)棧區:用於動態地儲存函式之間的關係,以保證被呼叫函式在返回時恢復到母函式中繼續執行。

在Windows平臺下,高階語言寫出的程式經過編譯連結,最終會變成PE檔案。當PE檔案被裝載執行後,就成了所謂的程式。

PE檔案程式碼段中包含的二進位制級別的機器程式碼會被裝入記憶體的程式碼區(.text),處理器將到記憶體的這個區域一條一條地取出指令和運算元,並送入運算邏輯單元進行運算;如果程式碼中請求開闢動態記憶體,則會在記憶體的堆區分配一塊大小合適的區域返回給程式碼區的程式碼使用;當函式呼叫發生時,函式的呼叫關係等資訊會動態地儲存在記憶體的棧區,以供處理器在執行完被呼叫函式的程式碼時,返回母函式。

如果把計算機看成一個有條不紊的工廠,我們可以得到如下類比:

< CPU是完成工作的工人。

< 資料區、堆區、棧區等則是用來存放原料、半成品、成品等各種東西的場所。

< 存放在程式碼區的指令則告訴CPU要做什麼,怎麼做,到哪裡去領原材料,用什麼工具來做,做完以後把成品放到哪個貨倉去。

< 值得一提的是,棧除了扮演存放原料、半成品的倉庫之外,它還是車間排程主任的辦公室。

3.棧與系統棧

從電腦科學的角度來看,棧指的是一種資料結構,是一種先進後出的資料表。棧的最常見操作有兩種:壓棧(PUSH)、彈棧(POP);

用於標識棧的屬性也有兩個:棧頂(TOP)、棧底(BASE)。

棧在記憶體中的存放是高地址是棧底(Base),低地址是棧頂(Top)。

下面來演示下棧的工作原理:

首先我們先以這段彙編指令來進行操作:

mov ax,0123H

push ax

mov bx 2244H

push bx

pop ax

pop bx

首先我們先將10000H-1000FH這段記憶體空間來當做棧來使用,首先執行的操作是push ax,會將0123H壓入到棧中,SP=SP-2,SS:SP指向當前棧頂當前的單元,以當前的單元為新的棧頂,將ax的資料送到SS:SP指向的記憶體單元中,SS:SP此時指向新的棧頂。此時ax的數值是0123H;詳細請見下圖

接來下進行第二部操作:push bx,操作同上;

接下來我們要演示的是pop操作,請注意pop操作的細節,比如到了棧底的時候指標是在哪裡?這些都是要進行關注的。

CPU執行pop ax時,SP=SP+2,SS:SP指向1000EH,pop操作棧頂元素,1000CH處的2266H依然存在,但是它在棧中不存在了,當再次push等入棧指令後,SS:SP移至1000CH,並在裡面寫入新的資料,將其覆蓋。詳細看下圖操作:

當再次進行pop給bx時,這是SP=SP+2,這時候指標就超出了棧底,就變成了SP=10H,所以我們得出一個結論就是當棧為空時,SS=1000H,SP=10H。詳細看下面操作:

記憶體的棧區實際上指的就是系統棧。系統棧由系統自動維護,它用於實現高階語言中函式的呼叫。對於類似C語言這樣的高階語言,系統棧的PUSH、POP等堆疊平衡細節是透明的。一般說來,只有在使用匯編語言開發程式的時候,才需要和它直接打交道。

4.函式呼叫約定與相關指令

函式呼叫約定描述了函式傳遞引數方式和棧幀同工作的技術細節。不同的作業系統、不同的語言、不同的編譯器在實現函式呼叫時的原理雖然基本相同,但具體的呼叫約定還是有差別的。這包括引數傳遞方式,引數入棧順序是從右向左還是從左向右,函式返回時恢復堆疊平衡的操作在子函式中進行還是在母函式中進行。
呼叫方式之間的差異

具體的,對於Visual C++來說,可支援以下3種函式呼叫約定:

如果要明確使用某一種呼叫約定,只需要在函式前加上呼叫約定的宣告即可,否則預設情況下,VC會使用_stdcall的呼叫方式。 除了引數入棧方向和恢復棧平衡操作位置的不同之外,引數傳遞有時也會有所不同。例如,每一個C++類成員函式都有一個this指標,在Windows平臺中,這個指標一般是用ECX暫存器來傳遞的,但如果用GCC編譯器來編譯,這個指標會作為最後一個引數壓入棧中。

注意:同一段程式碼用不同的編譯選項、不同的編譯器編譯連結後,得到的可執行檔案會有很多不同。

函式呼叫大概包括以下幾個步驟:

(1)引數入棧:將引數從右向左依次壓入系統棧中。

(2)返回地址入棧:將當前程式碼區呼叫指令的下一條指令地址壓入棧中,供函式返回時繼續執行。

(3)程式碼區跳轉:處理器從當前程式碼區跳轉到被呼叫函式的入口處。

(4)棧幀調整:具體包括:

<1>儲存當前棧幀狀態值,已備後面恢復本棧幀時使用(EBP入棧)。

<2>將當前棧幀切換到新棧幀(將ESP值裝入EBP,更新棧幀底部)。

<3>給新棧幀分配空間(把ESP減去所需空間的大小,抬高棧頂)。

<4>對於_stdcall呼叫約定,函式呼叫時用到的指令序列大致如下:

push 引數3      ;假設該函式有3個引數,將從右向做依次入棧

push 引數2

push 引數1

call 函式地址   ;call指令將同時完成兩項工作:a)向棧中壓入當前指令地址的下一個指令地址,即儲存返回地址。 b)跳轉到所呼叫函式的入口處。

push  ebp        ;儲存舊棧幀的底部

mov  ebp,esp     ;設定新棧幀的底部 (棧幀切換)

sub   esp,xxx     ;設定新棧幀的頂部 (抬高棧頂,為新棧幀開闢空間)

函式返回的步驟如下:

<1>儲存返回值,通常將函式的返回值儲存在暫存器EAX中。

<2>彈出當前幀,恢復上一個棧幀。具體包括:

(1)在堆疊平衡的基礎上,給ESP加上棧幀的大小,降低棧頂,回收當前棧幀的空間。

(2)將當前棧幀底部儲存的前棧幀EBP值彈入EBP暫存器,恢復出上一個棧幀。

(3)將函式返回地址彈給EIP暫存器。

<3>跳轉:按照函式返回地址跳回母函式中繼續執行。

還是以C語言和Win32平臺為例,函式返回時的相關的指令序列如下:

add esp,xxx     ;降低棧頂,回收當前的棧幀

pop ebp         ;將上一個棧幀底部位置恢復到ebp

retn            ;a)彈出當前棧頂元素,即彈出棧幀中的返回地址,至此,棧幀恢復到上一個棧幀工作完成。b)讓處理器跳轉到彈出的返回地址,恢復呼叫前程式碼區

5.暫存器與函式棧幀

每一個函式獨佔自己的棧幀空間。當前正在執行的函式的棧幀總是在棧頂。Win32系統提供兩個特殊的暫存器用於標識位於系統棧頂端的棧幀。

(1)ESP:棧指標暫存器(extended stack pointer),其記憶體放著一個指標,該指標永遠指向系統棧最上面一個棧幀的棧頂。

(2)EBP:基址指標暫存器(extended base pointer),其記憶體放著一個指標,該指標永遠指向系統棧最上面一個棧幀的底部。

【暫存器對棧的標識作用見(圖1)】

函式棧幀:ESP和EBP之間的記憶體空間為當前棧幀,EBP標識了當前棧幀的底部,ESP標識了當前棧幀的頂部。

在函式棧幀中,一般包含以下幾類重要資訊。

(1)區域性變數:為函式區域性變數開闢的記憶體空間。

(2)棧幀狀態值:儲存前棧幀的頂部和底部(實際上只儲存前棧幀的底部,前棧幀的頂部可以通過棧幀平衡計算得到),用於在本棧被彈出後恢復出上一個棧幀。

(3)函式返回地址:儲存當前函式呼叫前的“斷點”資訊,也就是函式呼叫前的指令位置,以便在函式返回時能夠恢復到函式被呼叫前的程式碼區中繼續執行指令。

注:函式棧幀的大小並不固定,一般與其對應函式的區域性變數多少有關。函式執行過程中,其棧幀大小也是在不停變化的。除了與棧相關的暫存器外,我們還需要記住另一個至關重要的暫存器。

EIP:指令暫存器(extended instruction pointer),其記憶體放著一個指標,該指標永遠指向下一條等待執行的指令地址。 可以說如果控制了EIP暫存器的內容,就控制了程式——我們讓EIP指向哪裡,CPU就會去執行哪裡的指令。這裡不多說EIP的作用,我個人認為王爽老是的彙編裡面講EIP講的已經是挺好的了~這裡不想多寫關於EIP的事情。

6.結束語

本文是針對上面兩篇文章的一個基礎性的補充~希望大家能夠喜歡和指正其中的不足之處,小丁虛心學習於請教~不知道名字叫啥~

內容參考:0day安全:軟體漏洞分析技術(第2版)

相關文章