在春節前,我曾經參與在《神奇的C語言》一文中的例子(5)的討論,但限於評論內容的有限,如今本文再次對這個問題單獨討論。(此問題原貌,詳見《神奇的C語言》,這裏我將原文中的代碼稍作輕微改動,並從新給出以下)html
原問題給出以下代碼:數組
#include <stdio.h> void func1(char a[]) {
//這裏的參數 a 爲指向數組的指針,所以 &a 和 a 的意義不一樣(前者爲指針變量的地址,後者爲指針變量的值)
//&a 表示指針變量的地址。
//&a[0] 等效爲 a ,即指針變量的值。
_tprintf(_T("func1: &a = 0x%08X; &a[0] = 0x%08X;\n"), &a, &a[0]); } int _tmain(int argc, _TCHAR* argv[]) { char a[10];
//這裏的 a 是數組名,至關於字面地址,因此 &a 至關於直接寫成 a 。 _tprintf(_T("wmain: &a = 0x%08X; &a[0] = 0x%08X;\n"), &a, &a[0]);
//數組名做爲參數傳遞給其餘函數時,退化爲指針 func1(a);
return 0; }
以 VS2005 編譯,採用默認項目配置(Unicode 編碼),在 Release 版本的輸出結果以下(可見 func1 中的 &a 和其餘輸出不一樣,且相差 4 ,在 debug 版本下此差值是一個較大的數值):cookie
----------------------------------------------------函數
Output:post
----------------------------------------------------優化
wmain: &a = 0x0018FF38; &a[0] = 0x0018FF38;編碼
func1 : &a = 0x0018FF34; &a[0] = 0x0018FF38;url
----------------------------------------------------spa
以 IDA 反彙編 Release 版本的可執行文件,獲得 wmain 函數的彙編代碼以下:debug
wmain proc near var_14 = dword ptr -14h ; func1 的實際參數(char* a) var_10 = dword ptr -10h ; a 的起始地址 var_4 = dword ptr -4 ; 用於 ESP 校驗 sub esp, 14h ; 爲臨時變量分配空間 mov eax, __security_cookie xor eax, esp mov [esp+14h+var_4], eax ; 保存 ( ESP ^ _security_cookie ) 到 var_4 push esi mov esi, ds:__imp__wprintf lea eax, [esp+18h+var_10] ; wmain: &a[0] (0018FF38) push eax mov ecx, eax ; wmain: &a (0018FF38) push ecx push offset pStr1 ; 字符串 "wmain: &a = 0x%08X; &a[0] = 0x%08X;\n" call esi ; __imp__wprintf 打印輸出 lea edx, [esp+24h+var_10] ; func1: &a[0] (0018FF38) mov eax, edx push eax lea ecx, [esp+28h+var_14] ; func1: &a (0018FF34), 參數的地址 push ecx push offset pStr2 ; 字符串 "func1: &a = 0x%08X; &a[0] = 0x%08X;\n" mov [esp+30h+var_14], edx ; 爲實際參數賦值 call esi ; __imp__wprintf 打印輸出 mov ecx, [esp+30h+var_4] add esp, 18h ; 爲以上兩次 _tprintf 函數調用復原棧指針 pop esi xor ecx, esp xor eax, eax call __security_check_cookie ; 檢查 ESP 是否被意外破壞 add esp, 14h ; 釋放棧上的臨時變量空間 retn
從以上彙編代碼,能夠獲得關於 Release 版本代碼(以優化運行效率爲主要目標)的以下結論:
(1)直接使用 ESP 尋址函數內的臨時變量或參數。
(2)func1 函數調用被編譯器直接內聯到 wmain 函數體內。在內聯 func1 時,編譯器對代碼作了等效性變換,代碼和棧上數據的順序,與一般函數調用相比有細微差異,但運行結果是等效的。
(3)在寄存器保護環節,保存了 ESI (目標索引)寄存器,其用意是以 ESI 加載 __imp__wprintf 的運行時(綁定後)地址。(對於默認配置,此函數是來自 VS2005 運行時庫 msvcr80.dll 中的導入函數,函數地址位於導入表中,在加載時被綁定)
下面是根據以上彙編代碼獲得的 wmain 函數的棧上數據示意圖(圖中棧的增加方向爲從下向上,並已經根據 輸出結果 推算出了棧上的虛擬地址):
上面的表格中包括了兩次對 __imp__wprintf 調用時的參數,其中 __imp__wprintf 的棧幀,除了參數以外的其他部分在表中沒有顯示,便可以認爲上表是第二次 __imp_wprintf 已返回到 wmain 函數時的棧上數據快照,兩次函數調用的復原棧指針(即釋放參數佔用的空間)被合併爲一條指令(add esp, 18h)。表格中的紅色數據,即爲代碼中交由 _tprintf 打印輸出的值。其中 pStr1 和 pStr2 指向位於 .rdata section(只讀數據段)上的字符串(根據項目選項,爲 Unicode 編碼)。
其中 ESP 校驗過程爲,在 wmain 函數的起始位置,爲臨時變量分配空間後,將此時的 ESP 和一個特定常數(_security_cookie)異或,結果保存到 wmain 的第一個臨時變量(var1)中,以後調用了 __imp__wprintf 等其餘函數後,把 ESP 和 var1 異或的結果保存到 ECX 中(此時 ECX 的期待值爲 _security_cookie),而後檢測 ECX 和 _security_cookie 是否相等便可。
【注】:表格中的棧指針校驗值,根據彙編代碼能夠看出,至關於首個出現的函數臨時變量,它的值的意義是,爲臨時變量分配空間後 (T1 時刻),將此時的 ESP 和一個常量值異或,存儲於該臨時變量。在復原棧指針以前(T2 時刻),校驗 ESP 是否吻合 T1 時刻的值。 -- hoodlum1980,2014-4-11
綜合以上圖表,對代碼輸出則能夠比較容易作出解釋:
第一行輸出結果爲 wmain 函數中的 &a 和 a (a 爲數組名)在寫法上等效的體現,在 wmain 裏 a 爲本地數組的數組名(這裏」本地「的含義指的是對其聲明的可見性),若是把 a 理解爲數組,&a 表示數組的存儲地址,若是把 a 理解爲至關於數組元素指針,則 &a 不具備實際物理意義,所以 &a 和 &a[0] 都等效於 a,即數組的起始地址。
第二行輸出結果爲 func1 函數中的 &a 和 a (a 爲指針變量)在乎義上不一樣的體現,a 是一個指向數組的指針變量(以及 func1 的實際參數),&a 表示此指針變量的地址,&a[0] 表示被指向數組的起始地址,即 &a[0] = a + 0 * sizeof (char) = a (這裏爲數學計算含義), 即指針變量 a 的值。在本例輸出中,func1 的實際參數 a 與」數組起始地址「緊鄰,a 的地址爲 0018FF34h,a 的值爲 0018FF38h(指向 wmain 中的數組)。
所以,本範例的代碼,能夠認爲在原理上即至關於以下代碼:
int _tmain(int argc, _TCHAR* argv[]) { //main 中的結果: char a[10]; _tprintf(_T("main_: &a = 0x%08X; &a[0] = 0x%08X;\n"), a, a); //func1 中的結果 char *p = a; _tprintf(_T("func1: &a = 0x%08X; &a[0] = 0x%08X;\n"), &p, p); return 0; }
【附】
本文中引用的範例來自於:《神奇的C語言》 中的例子 5 ,http://www.cnblogs.com/linxr/p/3521788.html。