c++普通成员函数调用的基本过程(强转,直接编译成函数汇编)
(2017-09-25 14:44:07)| 分类: 知识类 |
c++的成员函数根据其调用的不同,大致可以分为4类:内联成员函数,静态成员函数,虚成员函数和上述3种以外的普通成员函数。从本质来说类成员函数和全局函数在调用上并没有差别,非内联函数的在调用时,基本上都包括如下的过程:函数的参数入栈,eip指针值入栈,然后跳到函数体的地址,执行函数体对应的代码,执行完毕调整栈帧。下面就按照上述4个分类进行分析,先来说一下普通的成员函数:
普通的成员函数在被调用时有两大特征:
1
普通的成员函数是静态绑定的,
2 普通的成员函数调用时编译器隐式传入this指针的值。
通过代码分析一下:
-
#include
-
using
namespace std; -
class
Test -
{
-
public:
-
void Print( inti); -
};
-
void
Test::Print( inti) -
{
-
cout<<i<<endl; -
}
-
int
main() -
{
-
Test *p=new Test(); -
p->Print(2); -
system("pause"); -
}
-
int
main() -
{
-
013F1470
55 push ebp -
013F1471
8B EC mov ebp,esp -
013F1473
81 EC E8 00 00 00 sub esp,0E8h -
013F1479
53 push ebx -
013F147A
56 push esi -
013F147B
57 push edi -
013F147C
8D BD 18 FF FF FF lea edi,[ebp-0E8h] -
013F1482
B9 3A 00 00 00 mov ecx,3Ah ;出现这几句汇编则说明开启了堆栈帧(/RTCs)编译选项, -
;使未初始化的局部变量内存里值为cc,一个int 3指令。 -
013F1487
B8 CC CC CC CC mov eax,0CCCCCCCCh -
013F148C
F3 AB rep stos dword ptr es:[edi] -
Test *p=new Test(); -
013F148E
C7 85 20 FF FF FF 01 00 00 00 mov dword ptr [ebp-0E0h],1 ;new 运算符对应的代码, -
;由于Test中没有成员变量,所以size -
;为1,确保不同对象有不同的地址。 -
013F1498
8B 85 20 FF FF FF mov eax,dword ptr [ebp-0E0h] ;new 运算符对应两个操作:先分配空间 -
;,再调对象的构造函数,如果有必要的话。 -
013F149E
50 push eax ;参数入栈,eax=1 -
013F149F
E8 F6 FC FF FF call operator new (13F119Ah) new函数分配空间,operator;调用 operator new -
;行为和mallo函数相近,但operator new函数抛出异常。 -
013F14A4
83 C4 04 add esp,4 ;函数调用完毕调整栈帧 -
013F14A7
89 85 2C FF FF FF mov dword ptr [ebp-0D4h],eax ;将operator new函数返回的地址值,放到ebp-0D4h~ -
;ebp-0D0四个字节内存里 -
013F14AD
83 BD 2C FF FF FF 00 cmp dword ptr [ebp-0D4h],0 ;测试返回值是否为0 -
013F14B4
74 26 je main+6Ch (13F14DCh) ;如果相等话跳转 -
013F14B6
8B 8D 20 FF FF FF mov ecx,dword ptr [ebp-0E0h] ;013F148E处指令设置dword ptr[ebp-0E0h]为1,ecx=1 -
013F14BC
51 push ecx -
013F14BD
6A 00 push 0 -
013F14BF
8B 95 2C FF FF FF mov edx,dword ptr [ebp-0D4h] ;将对象地址值存入edx -
013F14C5
52 push edx -
013F14C6
E8 B2 FB FF FF call @ILT+120(_memset) (13F107Dh) ;调用memset函数将test对象对应的存储空间清0 -
013F14CB
83 C4 0C add esp,0Ch ;调整栈帧 -
013F14CE
8B 85 2C FF FF FF mov eax,dword ptr [ebp-0D4h] -
013F14D4
89 85 18 FF FF FF mov dword ptr [ebp-0E8h],eax ;将test对象地址值存入[ebp-0E8h]~[ebp-0E4h] -
;这段空间内 -
013F14DA
EB 0A jmp main+76h (13F14E6h) -
013F14DC
C7 85 18 FF FF FF 00 00 00 00 mov dword ptr [ebp-0E8h],0 ;如果走这条指令说明是013F14B4 je main+6Ch -
;(13F14DCh)跳转过来的,说明内存分配失败,这条指令的作用就 -
;是将p值设为0,也就是this值设为0,以期望this+偏移访问数 -
;据时触发一个异常。 -
013F14E6
8B 8D 18 FF FF FF mov ecx,dword ptr [ebp-0E8h] ;this 指针的值存入ecx -
013F14EC
89 4D F8 mov dword ptr [p],ecx ;给指针变量p赋值,如果operator new分配内存失败,则p为0 -
p->Print(2); -
013F14EF
6A 02 push 2 ;参数入栈 -
013F14F1
8B 4D F8 mov ecx,dword ptr [p] ;this 指针的值存入ecx,这就是普通成员和全局函数的区别, -
;参数入栈后,this指针存入ecx,或者最后入栈。 -
013F14F4
E8 D9 FB FF FF call Test::Print (13F10D2h) ;调用函数,说明是静态绑定,如果是动态绑定,则会有 -
;一个查表的过程 -
system("pause"); -
013F14F9
8B F4 mov esi,esp ;保存esp -
013F14FB
68 00 58 3F 01 push offset string "pause" (13F5800h) -
013F1500
FF 15 58 83 3F 01 call dword ptr [__imp__system (13F8358h)] -
013F1506
83 C4 04 add esp,4 -
013F1509
3B F4 cmp esi,esp ;测试堆栈是否平衡 -
013F150B
E8 49 FC FF FF call @ILT+340() (13F1159h) ;对测试结果进行处理 -
}
-
013F1510
33 C0 xor eax,eax -
013F1512
5F pop edi -
013F1513
5E pop esi -
013F1514
5B pop ebx -
013F1515
81 C4 E8 00 00 00 add esp,0E8h -
013F151B
3B EC cmp ebp,esp -
013F151D
E8 37 FC FF FF call @ILT+340(__RTC_CheckEsp) (13F1159h) -
013F1522
8B E5 mov esp,ebp -
013F1524
5D pop ebp -
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函数部分反汇编代码:
-
void
Test::Print( inti) -
{
-
cout<<i<<endl; -
00161403
8B F4 mov esi,esp -
00161405
A1 AC 82 16 00 mov eax,dword ptr [__imp_std::endl (1682ACh)] -
0016140A
50 push eax -
0016140B
8B FC mov edi,esp -
0016140D
8B 4D 08 mov ecx,dword ptr [i] ;只是打印这个形参,所以没有用到this指针,所以调用这个 -
;成员函数不会因为实际的对象不是Test而崩溃。 -
00161410
51 push ecx ;参数入栈 -
00161411
8B 0D A0 82 16 00 mov ecx,dword ptr [__imp_std::cout (1682A0h)] ;将cout对象地址存入ecx,其实就是隐式传人this -
00161417
FF ,std::char_traits<</span>char>15 A4 82 16 00 call dword ptr [__imp_std::basic_ostream<<span class="datatypes" style="margin: 0px; padding: 0px; border: none; color: rgb(46, 139, 87); background-color: inherit; font-weight: bold;">char >::operator<< (1682A4h)] -
0016141D
3B FC cmp edi,esp -
0016141F
E8 35 FD FF FF call @ILT+340(__RTC_CheckEsp) (161159h)
<<<说的形象些如果Print是某一个山寨,山寨一般都有一个暗号(天王盖地虎?),而p的类型则是一个暗号,在这指的是Test类型,编译器此时就是一个守山寨入口的喽啰(纯打比喻),守山寨的喽啰(编译器)看见有人(p这个对象)进山寨(调用Test类的函数),喽啰喊了一句
如下的代码也是没有错误的:
((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类中装一下数据:
-
#include
-
using
namespace std; -
class
Test -
{
-
public:
-
void Print(); -
int j; -
int i; -
};
-
void
Test::Print() -
{
-
cout<<i<<endl; -
}
-
-
int
main() -
{
-
Test *p=new Test(); -
p->Print(); -
((Test*)0)->Print(); -
system("pause"); -
}
现在主要看看现在的Print函数 汇编代码:
-
void
Test::Print() -
{
-
00F613E0
55 push ebp -
00F613E1
8B EC mov ebp,esp -
00F613E3
81 EC CC 00 00 00 sub esp,0CCh -
00F613E9
53 push ebx -
00F613EA
56 push esi -
00F613EB
57 push edi -
00F613EC
51 push ecx ;ecx入栈保存 -
00F613ED
8D BD 34 FF FF FF lea edi,[ebp-0CCh] -
00F613F3
B9 33 00 00 00 mov ecx,33h -
00F613F8
B8 CC CC CC CC mov eax,0CCCCCCCCh -
00F613FD
F3 AB rep stos dword ptr es:[edi] -
00F613FF
59 pop ecx ;恢复了ecx的值 -
00F61400
89 4D F8 mov dword ptr [ebp-8],ecx ;将ecx值存在ebp-8~ebp-5这段空间里,以后要取this值, -
;编译器就会从这段空间里取。 -
cout<<i<<endl; -
00F61403
8B F4 mov esi,esp -
00F61405
A1 AC 82 F6 00 mov eax,dword ptr [__imp_std::endl (0F682ACh)] -
00F6140A
50 push eax -
00F6140B
8B FC mov edi,esp -
00F6140D
8B 4D F8 mov ecx,dword ptr [this] ;此处dword this]实际上就是上面dwordptr [ ptr [ebp-8] -
;这段空间的,这里this想当于汇编编译器定义的一个变量this=ebp-8,这些汇编是debug时, -
;alt+8看到的,通过配置项目属性->c/c++->输出文件->汇编输出的汇编文件应该 -
;比较清楚一些,输出的汇编对应是mov ecx, DWORD PTR _this$[ebp],_this$ = -8的。 -
00F61410
8B 51 04 mov edx,dword ptr [ecx+4] ;此时ecx的值即是this指针的值,由于j在Test中偏移为0, -
;而i偏移为4,所以dword ptr [ecx+4]的意思是,从this值 -
;开始,也就是从对象的内存起始地址开始,往下数4个字节的内存,即从地址 -
;值为ecx+4内存开始,往高地址涵盖双字的空间,也就是4个字节的空间, -
;取出赋给edx,edx值也就是i的值了。 -
00F61413
52 push edx ;i的值入栈 -
00F61414
8B 0D A0 82 F6 00 mov ecx,dword ptr [__imp_std::cout (0F682A0h)] ;cout对象存入ecx -
00F6141A
FF ,std::char_traits<</span>char>15 A4 82 F6 00 call dword ptr [__imp_std::basic_ostream<<span class="keyword" style="margin: 0px; padding: 0px; border: none; color: rgb(0, 102, 153); background-color: inherit; font-weight: bold;">char >:: operator<<(0F682A4h)] -
;operator<<是cout的成员函数的 -
00F61420
3B FC cmp edi,esp -
00F61422
E8 32 FD FF FF call @ILT+340(__RTC_CheckEsp) (0F61159h) -
00F61427
8B C8 mov ecx,eax -
00F61429
FF ,std::char_traits<</span>char>15 A8 82 F6 00 call dword ptr [__imp_std::basic_ostream<<span class="keyword" style="margin: 0px; padding: 0px; border: none; color: rgb(0, 102, 153); background-color: inherit; font-weight: bold;">char >:: operator<<(0F682A8h)] -
00F6142F
3B F4 cmp esi,esp -
00F61431
E8 23 FD FF FF call @ILT+340(__RTC_CheckEsp) (0F61159h) -
}
最后要强调一点,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),虽然上述
O(∩_∩)O

加载中…