空指针调用成员函数
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却不可以呢?
对于函数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