空指針調用成員函數


class A
{
public:
void fun() {
cout << "fun()" << endl;
}
virtual void fun1(){
cout << "virtual fun()" << endl;
}
};
void mytest()
{
A* pa = NULL;
pa->fun();//調用成功
pa->fun1();//程序崩潰,報錯:引發一場,讀取訪問權限沖突
}
為什么調用fun可以成功,但是調用虛函數fun1卻不可以呢?

 

對於函數fun(),因為其是一個no-vritual函數,它是靜態綁定的,編譯器會根據對象的靜態類型來選擇函數,pa的靜態類型是A*,那么編譯器在處理pa->fun()的時候會將它指向A::fun();pa的首地址為NULL,雖然編譯器會給該函數傳遞this指針,this指向pa的首地址,但是由於該函數中沒有通過this指針來訪問類的成員變量,即沒有對this解引用,因此該函數可以正常調用。

對於函數fun1(),因為其是一個virtual函數,它是動態綁定的,綁定的是對象的動態類型,主要是靠虛表(V-Table)來實現的,該表中存放類的虛函數的地址,通過對象調用該虛函數時,會通過虛表查找真正要調用的函數的入口地址,如果對象pa為NULL,則無法找到虛函數表,此時會報錯。
 

 

c++的成員函數根據其調用的不同,大致可以分為4類:內聯成員函數,靜態成員函數,虛成員函數和上述3種以外的普通成員函數。從本質來說類成員函數和全局函數在調用上並沒有差別,非內聯函數的在調用時,基本上都包括如下的過程:函數的參數入棧,eip指針值入棧,然后跳到函數體的地址,執行函數體對應的代碼,執行完畢調整棧幀。下面就按照上述4個分類進行分析,先來說一下普通的成員函數:

 

普通的成員函數在被調用時有兩大特征:

1 普通的成員函數是靜態綁定的, 
2 普通的成員函數調用時編譯器隱式傳入this指針的值。

 

通過代碼分析一下:

 1 #include <iostream>  
 2 using namespace  std;  
 3 class Test  
 4 {  
 5 public:  
 6     void Print(int i);  
 7 };  
 8 void Test::Print(int i)  
 9 {  
10     cout<<i<<endl;  
11 }  
12 int main()  
13 {  
14     Test *p=new Test();  
15     p->Print(2);  
16     system("pause");  
17 }  

 上面Print函數符合上面所說4類的中的普通成員,所謂的靜態綁定實質是c++源代碼編譯時,編編譯器在p->Print();處翻譯成直接調用Test類中Print()的匯編代碼,也就是編譯期編譯器就確定了被調函數的相對地址。而所謂的動態綁定實質是,源碼在編譯的時候,編譯器不是翻譯成直接調用Test類中Print()的匯編代碼,而是翻譯成一個查找虛表,得到到函數的相對地址的過程。看一下上面生成的匯編代碼:

 

 1 int main()  
 2 {  
 3 013F1470 55               push        ebp    
 4 013F1471 8B EC            mov         ebp,esp   
 5 013F1473 81 EC E8 00 00 00 sub         esp,0E8h   
 6 013F1479 53               push        ebx    
 7 013F147A 56               push        esi    
 8 013F147B 57               push        edi    
 9 013F147C 8D BD 18 FF FF FF lea         edi,[ebp-0E8h]   
10 013F1482 B9 3A 00 00 00   mov         ecx,3Ah ;出現這幾句匯編則說明開啟了堆棧幀(/RTCs)編譯選項,  
11                                                     ;使未初始化的局部變量內存里值為cc,一個int 3指令。  
12 013F1487 B8 CC CC CC CC   mov         eax,0CCCCCCCCh   
13 013F148C F3 AB            rep stos    dword ptr es:[edi]   
14     Test *p=new Test();  
15 013F148E C7 85 20 FF FF FF 01 00 00 00 mov         dword ptr [ebp-0E0h],1 ;new 運算符對應的代碼,  
16                                                                       ;由於Test中沒有成員變量,所以size  
17                                                                   ;為1,確保不同對象有不同的地址。  
18 013F1498 8B 85 20 FF FF FF mov         eax,dword ptr [ebp-0E0h] ;new 運算符對應兩個操作:先分配空間  
19                                                                     ;,再調對象的構造函數,如果有必要的話。  
20 013F149E 50               push        eax                    ;參數入棧,eax=1  
21 013F149F E8 F6 FC FF FF   call        operator new (13F119Ah) ;調用 operator new函數分配空間,operator new  
22                                                               ;行為和mallo函數相近,但operator new函數拋出異常。  
23 013F14A4 83 C4 04         add         esp,4 ;函數調用完畢調整棧幀  
24 013F14A7 89 85 2C FF FF FF mov         dword ptr [ebp-0D4h],eax ;將operator new函數返回的地址值,放到ebp-0D4h~  
25                                                                 ;ebp-0D0四個字節內存里  
26 013F14AD 83 BD 2C FF FF FF 00 cmp         dword ptr [ebp-0D4h],0 ;測試返回值是否為0  
27 013F14B4 74 26            je          main+6Ch (13F14DCh) ;如果相等話跳轉  
28 013F14B6 8B 8D 20 FF FF FF mov         ecx,dword ptr [ebp-0E0h] ;013F148E處指令設置dword ptr[ebp-0E0h]為1,ecx=1  
29 013F14BC 51               push        ecx    
30 013F14BD 6A 00            push        0      
31 013F14BF 8B 95 2C FF FF FF mov         edx,dword ptr [ebp-0D4h] ;將對象地址值存入edx  
32 013F14C5 52               push        edx    
33 013F14C6 E8 B2 FB FF FF   call        @ILT+120(_memset) (13F107Dh) ;調用memset函數將test對象對應的存儲空間清0  
34 013F14CB 83 C4 0C         add         esp,0Ch ;調整棧幀  
35 013F14CE 8B 85 2C FF FF FF mov         eax,dword ptr [ebp-0D4h]   
36 013F14D4 89 85 18 FF FF FF mov         dword ptr [ebp-0E8h],eax ;將test對象地址值存入[ebp-0E8h]~[ebp-0E4h]  
37                                                                 ;這段空間內  
38 013F14DA EB 0A            jmp         main+76h (13F14E6h)   
39 013F14DC C7 85 18 FF FF FF 00 00 00 00 mov dword ptr [ebp-0E8h],0 ;如果走這條指令說明是013F14B4 je main+6Ch   
40                                                       ;(13F14DCh)跳轉過來的,說明內存分配失敗,這條指令的作用就  
41                                                          ;是將p值設為0,也就是this值設為0,以期望this+偏移訪問數  
42                                                              ;據時觸發一個異常。  
43 013F14E6 8B 8D 18 FF FF FF mov         ecx,dword ptr [ebp-0E8h] ;this 指針的值存入ecx  
44 013F14EC 89 4D F8         mov         dword ptr [p],ecx ;給指針變量p賦值,如果operator new分配內存失敗,則p為0  
45     p->Print(2);  
46 013F14EF 6A 02            push        2                 ;參數入棧  
47 013F14F1 8B 4D F8         mov         ecx,dword ptr [p] ;this 指針的值存入ecx,這就是普通成員和全局函數的區別,  
48                                                                ;參數入棧后,this指針存入ecx,或者最后入棧。  
49 013F14F4 E8 D9 FB FF FF   call        Test::Print (13F10D2h) ;調用函數,說明是靜態綁定,如果是動態綁定,則會有  
50                                                                  ;一個查表的過程  
51     system("pause");  
52 013F14F9 8B F4            mov         esi,esp              ;保存esp  
53 013F14FB 68 00 58 3F 01   push        offset string "pause" (13F5800h)   
54 013F1500 FF 15 58 83 3F 01 call        dword ptr [__imp__system (13F8358h)]   
55 013F1506 83 C4 04         add         esp,4   
56 013F1509 3B F4            cmp         esi,esp ;測試堆棧是否平衡  
57 013F150B E8 49 FC FF FF   call        @ILT+340() (13F1159h) ;對測試結果進行處理  
58 }  
59 013F1510 33 C0            xor         eax,eax   
60 013F1512 5F               pop         edi    
61 013F1513 5E               pop         esi    
62 013F1514 5B               pop         ebx    
63 013F1515 81 C4 E8 00 00 00 add         esp,0E8h   
64 013F151B 3B EC            cmp         ebp,esp   
65 013F151D E8 37 FC FF FF   call        @ILT+340(__RTC_CheckEsp) (13F1159h)   
66 013F1522 8B E5            mov         esp,ebp   
67 013F1524 5D               pop         ebp    
68 013F1525 C3               ret          

編譯器調用Print()時是根據p類型來確定調用哪個類的Print()函數時,也就是說根據->(或者.)左邊對象的類型來確定調用的函數,同時編譯器也是根據對象的類型來確定該成員函數是否能夠被合法的調用,而這個校驗是發生在編譯期的類型靜態檢查的,也就是只是一個代碼級的檢查的。不管對象的真正類型是什么,只要被強制轉化成了Test類型,編譯器就會接受p->Print(2);的調用,從而翻譯成調用Print的代碼。

 

[Note: the interpretation of the call of a virtual function depends on the type of the object for which it is called (the dynamic type), whereas the interpretation of a call of a nonvirtual member function depends only on the type of the pointer or reference denoting that object (the static type) (5.2.2). ](ISO/IEC 14882:2003(E)//10.3.6 Virtual functions)

 Print函數部分反匯編代碼: 

 1 void Test::Print(int i)  
 2 {  
 3     cout<<i<<endl;  
 4 00161403 8B F4            mov         esi,esp   
 5 00161405 A1 AC 82 16 00   mov         eax,dword ptr [__imp_std::endl (1682ACh)]   
 6 0016140A 50               push        eax    
 7 0016140B 8B FC            mov         edi,esp   
 8 0016140D 8B 4D 08         mov         ecx,dword ptr [i] ;只是打印這個形參,所以沒有用到this指針,所以調用這個  
 9                                                                 ;成員函數不會因為實際的對象不是Test而崩潰。  
10 00161410 51               push        ecx  ;參數入棧  
11 00161411 8B 0D A0 82 16 00 mov         ecx,dword ptr [__imp_std::cout (1682A0h)] ;將cout對象地址存入ecx,其實就是隱式傳人this  
12 00161417 FF 15 A4 82 16 00 call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1682A4h)]   
13 0016141D 3B FC            cmp         edi,esp   
14 0016141F E8 35 FD FF FF   call        @ILT+340(__RTC_CheckEsp) (161159h)   

說的形象些如果Print是某一個山寨,山寨一般都有一個暗號(天王蓋地虎?),而p的類型則是一個暗號,在這指的是Test類型,編譯器此時就是一個守山寨入口的嘍啰(純打比喻),守山寨的嘍啰(編譯器)看見有人(p這個對象)進山寨(調用Test類的函數),嘍啰喊了一句 :土豆土豆我是地瓜,(進行類型靜態檢查),那人回了句臭魚臭魚我是爛蝦,嘍啰一聽密碼正確(檢查了p的類型是Test型的),登錄中。。,只要暗號正確,嘍啰就會放行,他不管這個人是不是真的寨子里的人(真的Test類型的對象),只要暗號正確(類型正確),哪怕這個人不是山寨的,而是來卧底的(不是Test類型的對象),只要把暗號整正確(強制轉換成Test型),也會進入山寨的,這也給以后山寨留下了悲劇,欲知后事如何,請聽下回分解>>>

如下的代碼也是沒有錯誤的:

 int i=0;//華麗的卧底
 ((Test*)&i)->Print(2);//得到了個暗號,進了山寨

((Test*)0)->Print(2);//進了山寨的不是人,而是寂寞

再說第二點,函數參數入棧后,this指針的值也會入棧或者存入ecx寄存器。而this指針的值可以認為是p的值,也就是->左邊對象的值。傳入this值的目的是為了操作對象里的數據,通過類的聲明,編譯器可以確定對象內成員變量的相對於類對象起始地址的偏移,即相對this值的偏移。而成員函數調用時隱式傳入的this值,編譯器是不對this值進行檢查,編譯器只是簡單生成this+偏移操作對象的匯編代碼,所以->左邊對象的類型正確,編譯器就會找到相應的成員函數,不管傳入this值是否正確,只要this+偏移訪問的地址是合法的,os也不會抱怨,一旦this+偏移不合法,激活os的異常機制,程序才會宕了。

If the function is a nonstatic member function, the “this” parameter of the function (9.3.2)shall be initialized with a pointer to the object of the call, converted as if by an explicit type conversion.[Note: There is no access checking on this conversion; the access checking is done as part of the (possibly implicit) class member access operator. See 11.2. ]

(ISO/IEC 14882:2003(E)//5.2.2 Function call 4)

現在我們往Test類中裝一下數據:

 1 #include <iostream>  
 2 using namespace  std;  
 3 class Test  
 4 {  
 5 public:  
 6     void Print();  
 7     int j;  
 8     int i;  
 9 };  
10 void Test::Print()  
11 {  
12     cout<<i<<endl;  
13 }  
14   
15 int main()  
16 {  
17     Test *p=new Test();  
18     p->Print();  
19     ((Test*)0)->Print();  
20     system("pause");  
21 }  

現在主要看看現在的Print函數 匯編代碼:

 

 1 void Test::Print()  
 2 {  
 3 00F613E0 55               push        ebp    
 4 00F613E1 8B EC            mov         ebp,esp   
 5 00F613E3 81 EC CC 00 00 00 sub         esp,0CCh   
 6 00F613E9 53               push        ebx    
 7 00F613EA 56               push        esi    
 8 00F613EB 57               push        edi    
 9 00F613EC 51               push        ecx  ;ecx入棧保存  
10 00F613ED 8D BD 34 FF FF FF lea         edi,[ebp-0CCh]   
11 00F613F3 B9 33 00 00 00   mov         ecx,33h   
12 00F613F8 B8 CC CC CC CC   mov         eax,0CCCCCCCCh   
13 00F613FD F3 AB            rep stos    dword ptr es:[edi]   
14 00F613FF 59               pop         ecx  ;恢復了ecx的值  
15 00F61400 89 4D F8         mov         dword ptr [ebp-8],ecx ;將ecx值存在ebp-8~ebp-5這段空間里,以后要取this值,  
16                                                                ;編譯器就會從這段空間里取。  
17     cout<<i<<endl;  
18 00F61403 8B F4            mov         esi,esp   
19 00F61405 A1 AC 82 F6 00   mov         eax,dword ptr [__imp_std::endl (0F682ACh)]   
20 00F6140A 50               push        eax    
21 00F6140B 8B FC            mov         edi,esp   
22 00F6140D 8B 4D F8         mov         ecx,dword ptr [this] ;此處dword ptr [this]實際上就是上面dword ptr [ebp-8]  
23                                    ;這段空間的,這里this想當於匯編編譯器定義的一個變量this=ebp-8,這些匯編是debug時,  
24                                        ;alt+8看到的,通過配置項目屬性->c/c++->輸出文件->匯編輸出的匯編文件應該  
25                       ;比較清楚一些,輸出的匯編對應是mov    ecx, DWORD PTR _this$[ebp],_this$ = -8的。  
26 00F61410 8B 51 04         mov         edx,dword ptr [ecx+4] ;此時ecx的值即是this指針的值,由於j在Test中偏移為0,  
27                                            ;而i偏移為4,所以dword ptr [ecx+4]的意思是,從this值  
28                                            ;開始,也就是從對象的內存起始地址開始,往下數4個字節的內存,即從地址  
29                                            ;值為ecx+4內存開始,往高地址涵蓋雙字的空間,也就是4個字節的空間,  
30                                            ;取出賦給edx,edx值也就是i的值了。  
31 00F61413 52               push        edx  ;i的值入棧  
32 00F61414 8B 0D A0 82 F6 00 mov         ecx,dword ptr [__imp_std::cout (0F682A0h)] ;cout對象存入ecx  
33 00F6141A FF 15 A4 82 F6 00 call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F682A4h)]   
34                                          ;operator<<是cout的成員函數的  
35 00F61420 3B FC            cmp         edi,esp   
36 00F61422 E8 32 FD FF FF   call        @ILT+340(__RTC_CheckEsp) (0F61159h)   
37 00F61427 8B C8            mov         ecx,eax   
38 00F61429 FF 15 A8 82 F6 00 call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F682A8h)]   
39 00F6142F 3B F4            cmp         esi,esp   
40 00F61431 E8 23 FD FF FF   call        @ILT+340(__RTC_CheckEsp) (0F61159h)   
41 }  

從上面Pint函數對應的匯編代碼可以看到,當成員函數在訪問成員變量時,伴隨着一個通過this指針尋址的過程, mov ecx,dword ptr [this];mov edx,dword ptr [ecx+4],就因為這兩句代碼,許多進入Print函數這個山寨的無間道,就有可能被山大王(觸發OS的異常機制)發現,mov edx,dword ptr [ecx+4]這句是一個內存訪問語句,我們都知道對於指針int *p;如果指向一個非法的地址,那么會觸發os的異常機制的,比如p=0;*p=1,同樣的ecx+4值不是一個合法的值,也會觸發os異常的,所以像((Test*)0)->Print();語句觸發異常了,顯然訪問了地址為0x00000004的內存.而win32位每個進程的地址空間里,開始內存地址空間里設置了一個分區,范圍是0x00000000~0x0000ffff,如果進程中有線程試圖讀寫這段區域,cpu就會引發非法訪問的。

 int a[2]={2,1111};((Test*)a)->Print();,根據分析,輸出1111的,華麗的卧底~~~

 

最后要強調一點,c++標准規定,If a nonstatic member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.(ISO/IEC 14882:2003(E)//9.3.1 Nonstatic member functions),雖然上述 int a[2]={2,1111};((Test*)a)->Print()代碼在一些主流的編譯器vc,gcc編譯執行通過,但是並不保證所有平台都沒有問題的。實際編程中無論如何也不要寫類似的代碼。

 

轉自:https://blog.csdn.net/demon__hunter/article/details/5397906

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM