複習
-
PUSH & POP
SP 指向當前棧頂最後壓入的一個字節
例子:- PUSH EAX (32bits)
假設壓棧前SP = 0x0012ff40,則PUSH執行後,
0x0012ff3c~3f將用於存放EAX的值,
SP=SP-4,即SP修改爲 0x0012ff3c - POP AX (16bits)
假設壓棧前SP = 0x0012ff40,則POP執行後,
0x0012ff40~41的值拷貝入AX
SP=SP+2,即SP修改爲 0x0012ff42
Tips: SP的修改意味着以前數據理論上不能訪問了,但是該字節的內容並沒有被擦除。
- PUSH EAX (32bits)
-
CALL & RET
例子:- CALL (導致ESP 修改-4/-8)
PUSH IP( a near or short call) / PUSH CS PUSH IP (far call)
JMP 跳轉至被調用函數 - RET (導致ESP 修改+4,近調用)
POP IP - RETF(導致ESP 修改+8,對應far call)
POP IP POP CS
注:近轉移 near/short call是同一代碼段內轉移,只需要變IP
遠轉移 far call是不同代碼段間(調用另一個文件的代碼段)轉移,除了IP,還要改變CS
- CALL (導致ESP 修改-4/-8)
1 參數和局部變量
作用域 Scope:
變量具有作用域,即同一變量名稱可能引用不同的值,具體激活哪個取決於其使用位置
example:局部變量是定義它的塊的私有變量;全局變量可以由其他塊訪問
mainlocal?
編譯器如何實現作用域
編譯器通過操作不同作用域中的不同內存地址來實現範圍,具有相同名稱的變量由編譯器映射到不同的地址。
硬件不知道作用域
參數
- 形參 formal parameter:編譯器按照被調用函數局部參數處理形參
- 實參 actual parameter:
編譯器實現
每一次調用,距離0X58
- 問題 Problem:遞歸調用中,需要即使版本的變量,但無法在編譯時決定如何分配
- 解決 Solution:
- 全局變量靜態分配,即絕對地址
- 局部變量動態分配,因爲不知道需要多少個,即相對地址
- where:棧幀 stack frame(在談到內存分配,稱之爲棧幀)
- how:激活記錄activation record(與棧幀實際上是一個東西,在談到如何運行時,習慣稱之爲激活記錄)
- ebp & esp
2 激活記錄activation record / 棧幀 stack frame
2.1 基礎
局部變量和實際參數:
- 動態分配
- 減慢程序的執行速度
具體實現:爲了最大程度地降低成本,編譯器計算函數局部變量所需的總空間量,並將該空間分配到一個塊中(激活記錄active record)
硬件支持
- 硬件編譯器和硬件在內部維護兩個重要值,這些值有助於以簡單而優雅的方式分隔和操作激活記錄
堆棧指針 R --esp
幀指針 R --ebp - 激活記錄以非常簡單的方式組織,以適應硬件
規則:只能訪問堆棧頂部的激活記錄
- 只有當該函數調用的所有子函數返回,該函數纔可能返回,得到控制權。
- 變量的scope:我們只能訪問當前正在執行的函數的局部變量(和全局變量—不在stack上)。
激活記錄 / 棧幀的定義:
爲每個函數調用分配的內存塊
生命週期:從函數調用到返回
2.2 CALL / RET 過程
Call(1. push IP; 2. Jmp)
調用函數時,編譯器和硬件:
- 調用方 :保存上下文 context
- 被調用方:構造自己的棧幀 stack frame
Return
彙編代碼例子:
3 函數的調用約定
why?
- C 級函數調用
參數
局部變量
返回值 - 彙編/機器級別的功能調用
call和 ret 指令
What?
調用約定 描述了 被調用代碼的接口:
- 參數的分配順序
- 放置參數的位置(推送在堆棧上或放置在寄存器中)
- 函數可以使用哪些寄存器
eax 的返回值 - 調用方還是被調用方負責在返回時展開堆棧
類型:
- _cdecl(上面的例子)
C Declaration 表示C語言默認的函數調用方法- 參數按"從右向左"的順序推送到堆棧
- 調用方負責清除堆棧上的參數,手動清棧
- 被調用函數不要求參數,所有參數數量不對,不會有編譯階段錯誤
- _stdcall / WINAPI
- 參數按"從右向左"的順序推送到堆棧
- 被調用方負責清除堆棧上的參數
- Pascal
- 參數按"從左向右"的順序推送到堆棧
- 被調用方負責清除堆棧上的參數
完整的函數調用