先分享一個案例:
1 #include <stdio.h> 2 3 __declspec(naked) void Test() 4 { 5 int x; 6 x = 3; 7 __asm ret; 8 } 9 10 int main(int argc, char* argv[]) 11 { 12 int x = 1; 13 Test(); 14 printf("%d\n",x); 15 return 0; 16 }
猜猜輸出什么?輸出3,而不是1。
看下反匯編代碼:
有疑問先留着。下面講解下naked:
MSDN中關於naked關鍵字的介紹:
For functions declared with the naked attribute, the compiler generates code without prolog and epilog code. You can use this feature to write your own prolog/epilog code sequences using inline assembler code. Naked functions are particularly useful in writing virtual device drivers. Note that thenaked attribute is only valid on x86, and is not available on x64 or Itanium.
我們知道VC++和gcc都支持naked函數,即所謂的“裸函數”,對於使用 naked 特性聲明的函數,編譯器將生成編碼,而無需 prolog 和 epilog 代碼。而一般性函數,編譯器會主動加上很多prilog和epilog代碼,還會做一些優化,有些是贅余的。而使用內聯匯編,可以寫完全按自己意願跑的函數。可以使用此功能來編寫使用匯編程序代碼的自己的 prolog/epilog 代碼順序。 裸函數尤為用在編寫虛擬設備驅動程序。請注意 naked 特性僅適用於 x86和ARM,並不用於 x64 。關於自己編寫prolog 和 epilog 代碼,在后面有講到。
VC++的聲明語法:__declspec(naked)
gcc的聲明語法:__attribute__((naked))
因為編譯器不會生成入口代碼和退出代碼,所以寫naked函數的時候要分外小心。進入函數代碼時,父函數僅僅會將參數和返回地址壓棧,亦即只有esp寄存器和eip寄存器會發生變化。
一般來說,使用naked函數時需要注意以下問題:(以VC++編譯器為例)
1、函數必須顯式返回。
一般通過__asm ret
的內嵌匯編指令返回。
2、不可以通過任何方式使用局部變量。
若聲明一個局部變量,並在代碼中為其賦值,則會更改父函數中相應位置的局部函數的值。
3、只能通過esp引用參數。
因為子函數繼承了父函數的ebp寄存器,所以只能通過esp引用參數。
4、naked 屬性僅與函數的定義相關,不能在函數原型中指定。不能用於函數指針,不能用於數據定義。
官方給出的naked的規則限制:
-
不允許使用 return 語句。
-
不允許結構化異常處理和 C++ 異常處理構造,因為它們必須在堆棧幀中展開。
-
出於同一原因,禁止任何形式的 setjmp。
-
禁止使用 _alloca 函數。
-
若要確保局部變量的初始化代碼不在 prolog 序列之前出現,函數范圍中不允許存在初始化的局部變量。 具體而言,函數范圍中不允許有 C++ 對象的聲明。 但是,嵌套的范圍中可能有初始化的數據。
-
不建議使用幀指針優化(/Oy 編譯器選項),但會自動為裸函數將其取消。
-
不能在函數詞法范圍中聲明 C++ 類對象。 但是,可以在嵌套的塊中聲明對象。
-
在使用 /clr 進行編譯時,將忽略 naked 關鍵字。
-
對於 __fastcall 裸函數,只要 C/C++ 代碼中存在對某個寄存器參數的引用,prolog 代碼就應將該寄存器的值存儲到該變量的堆棧位置中。 例如:
1 // nkdfastcl.cpp 2 // compile with: /c 3 // processor: x86 4 __declspec(naked) int __fastcall power(int i, int j) { 5 // calculates i^j, assumes that j >= 0 6 7 // prolog 8 __asm { 9 push ebp 10 mov ebp, esp 11 sub esp, __LOCAL_SIZE 12 // store ECX and EDX into stack locations allocated for i and j 13 mov i, ecx 14 mov j, edx 15 } 16 17 { 18 int k = 1; // return value 19 while (j-- > 0) 20 k *= i; 21 __asm { 22 mov eax, k 23 }; 24 } 25 26 // epilog 27 __asm { 28 mov esp, ebp 29 pop ebp 30 ret 31 } 32 }
編寫 Prolog/Epilog 代碼的注意事項
堆棧幀布局:
此示例顯示了可能出現在 32 位函數中的標准 prolog 代碼:
push ebp ; Save ebp mov ebp, esp ; Set stack frame pointer sub esp, localbytes ; Allocate space for locals push <registers> ; Save registers
localbytes 變量表示局部變量堆棧上所需的字節數,<registers> 變量是表示要保存在堆棧上的寄存器列表的占位符。 推入寄存器后,您可以將任何其他適當的數據放置在堆棧上。 下面是相應的 epilog 代碼:
pop <registers> ; Restore registers mov esp, ebp ; Restore stack pointer pop ebp ; Restore ebp ret ; Return from function
堆棧始終向下增長(從高內存地址到低內存地址)。 基指針 (ebp) 指向 ebp 的推入值。 本地區域開始於 ebp-4。 若要訪問局部變量,可通過從 ebp 中減去適當的值來計算ebp 的偏移量。
__LOCAL_SIZE :
編譯器提供符號 __LOCAL_SIZE 以用於函數 prolog 代碼的內聯匯編程序塊中。 此符號用於在自定義 prolog 代碼中的堆棧幀上為局部變量分配空間。
編譯器確定 __LOCAL_SIZE 的值。 其值是所有用戶定義的局部變量和編譯器生成的臨時變量的總字節數。 __LOCAL_SIZE 只能用作即時操作數;它不能在表達式中使用。 不得更改或重新定義此符號的值。 例如:
mov eax, __LOCAL_SIZE ;Immediate operand--Okay
mov eax, [ebp - __LOCAL_SIZE] ;Error
以下包含自定義 prolog 和 epilog 序列的裸函數的示例在 prolog 序列中使用 __LOCAL_SIZE 符號:
// the__local_size_symbol.cpp // processor: x86 __declspec ( naked ) int main() { int i; int j; __asm { /* prolog */ push ebp mov ebp, esp sub esp, __LOCAL_SIZE } /* Function body */ __asm { /* epilog */ mov esp, ebp pop ebp ret } }
最初的案例現在看起來簡單多了,編譯器不主動加prolog 和 epilog 代碼,子函數用父函數的ebp,所以這里修改了main函數的棧,后面結果出錯了。后面的ret是必須寫蛤,要自己維持棧平衡。