类反汇编
类的内存地址对齐
参考之前写的一个博客https://www.cnblogs.com/Sna1lGo/p/14088339.html
类的this指针
每个有关于对象里面的调用,其实都用到了this指针,this指针是一个对象的首地址,然后利用首地址的偏移来访问对象中的成员或者方法(函数)
对象中的函数
在对象中调用函数的时候,其实是采用了ecx来保存this指针,然后来调用处理。这样的函数约定也称为thiscall
public: int m_nInt; }; int main() { 00221950 push ebp 00221951 mov ebp,esp 00221953 sub esp,0D0h 00221959 push ebx 0022195A push esi 0022195B push edi 0022195C lea edi,[ebp-0D0h] 00221962 mov ecx,34h 00221967 mov eax,0CCCCCCCCh 0022196C rep stos dword ptr es:[edi] 0022196E mov eax,dword ptr [__security_cookie (022A004h)] 00221973 xor eax,ebp 00221975 mov dword ptr [ebp-4],eax 00221978 mov ecx,offset _36AAE6AE_源@cpp (022C029h) 0022197D call @__CheckForDebuggerJustMyCode@4 (022132Ah) CTest Test; Test.SetNumber(5); 00221982 push 5 00221984 lea ecx,[Test] 00221987 call std::endl<char,std::char_traits<char> > (022116Dh) printf("CTest : %d\r\n", Test.m_nInt); 0022198C mov eax,dword ptr [Test] 0022198F push eax 00221990 push offset string "CTest : %d\r\n" (0227BCCh) 00221995 call _printf (02213CAh) 0022199A add esp,8 return 0; 0022199D xor eax,eax }
类中函数调用的注意点:类中的函数用的是thiscall,就是先用ecx来传入this指针,然后别的和__stdcall是一样的,但是thiscall并不是一个关键字,所以如果在类的成员函数里面添加了关键字,会用关键字的函数约定来调用
CTest Test; Test.SetNumber(5); 00ED1982 push 5 00ED1984 lea eax,[Test] 00ED1987 push eax 00ED1988 call std::endl<char,std::char_traits<char> > (0ED13DEh)
这里的成员函数setNnumber采用__stdcall后就是利用栈来传参了
对象中的数据访问
要访问对象的数据,也要使用this指针然后偏移来访问
类中的静态数据成员
当类中有了static静态数据成员定义后,虽然是在类中定义的,但是它和静态局部变量类似,存放的位置和全局变量一直,只是增加了作用域的检查。都是一个含有作用域的全局变量。当程序被加载时操作系统就把执行文件的数据读到了内存单元中,静态数据成员就存在了。而这个时候还没有实例对象产生,静态数据成员不属于某一对象,与对象是一对多的关系,仅仅和类有关,与对象无关。所以在计算类和对象的大小的时候,并不把静态成员纳入其中
对象作为函数参数
对象作为函数的参数时,会先将对象中的所有数据进行备份(复制),将复制的数据作为形参传递到调用函数中使用。
传参的顺序为从下到上,最先定义的最后入栈。
当对象成员比较多的时候:
这个时候对象成员比较多,利用栈来吧所有数据入栈就很麻烦,所以采用一个直接入栈对象的首地址,然后进行串指令赋值给栈空间
int m_nInt1; int m_nInt2; char mChar1[32] = "test"; }; int main() { 00D84A60 push ebp 00D84A61 mov ebp,esp 00D84A63 sub esp,0F4h 00D84A69 push ebx 00D84A6A push esi 00D84A6B push edi 00D84A6C lea edi,[ebp-0F4h] 00D84A72 mov ecx,3Dh 00D84A77 mov eax,0CCCCCCCCh 00D84A7C rep stos dword ptr es:[edi] 00D84A7E mov eax,dword ptr [__security_cookie (0D8A004h)] 00D84A83 xor eax,ebp 00D84A85 mov dword ptr [ebp-4],eax 00D84A88 mov ecx,offset _36AAE6AE_源@cpp (0D8C029h) 00D84A8D call @__CheckForDebuggerJustMyCode@4 (0D8132Ah) CTest Test1; 00D84A92 lea ecx,[Test1] 00D84A95 call CTest::CTest (0D813E8h) Test1.m_nInt1 = 1; 00D84A9A mov dword ptr [Test1],1 Test1.m_nInt2 = 2; 00D84AA1 mov dword ptr [ebp-2Ch],2 Test1.PrintfCTestNum(Test1); 00D84AA8 sub esp,28h 00D84AAB mov ecx,0Ah 00D84AB0 lea esi,[Test1] 00D84AB3 mov edi,esp 00D84AB5 rep movs dword ptr es:[edi],dword ptr [esi] 这里直接复制对象的数据 00D84AB7 lea ecx,[Test1] 00D84ABA call CTest::PrintfCTestNum (0D813E3h) return 0; 00D84ABF xor eax,eax
对象作为函数参数的释放问题
在把对象作为函数参数传入的时候,其实就相当于创建一个对象赋值为传入对象的值,也就是复制一个传入的对象,这个时候就需要用到了类中的拷贝构造函数,如果没有拷贝构造函数就是默认的类中的拷贝构造函数,就相当于自动进行一个浅拷贝,弄一个和传入对象一模一样的对象,甚至于首地址也是,这个时候如果函数调用完毕会把传入的变量销毁,就会把原来的对象销毁掉。
解决办法:
1 为了不让传入的参数被销毁掉,所以需要自己写拷贝构造函数,有了拷贝构造函数,就相当于开辟了一个新对象来把传入的对象的数值完全赋值给新对象,但是并不是对应的一个空间,所以对这个怎么处理都没关系。
2 利用指针和引入来作为参数处理,这样的话也就根本不需要用到复制一个副本来处理了,就调用指针来通过指针处理传入的的对象的各种数据,而且只需要分配一个临时变量等于指针的值就好了,当函数调用完毕后会自动删除的是临时变量的值,影响不了原来的对象。
对象作为返回值
通常基本的数据类型,双精度(double)类型除外的都是用eax作为返回值直接处理,但是对象是自定义的所有不能用eax来直接处理。
以对象作为返回值和以对象作为函数参数非常类似,做参数时是会在栈空间开辟一个地址来处理,作为返回值的时候,会在堆开辟一个空间来存放对象,然后把这个临时栈空间的首地址作为返回值。但是同样的是返返回值的首地址也是用eax来保存。
int main() { 00AB1950 push ebp 00AB1951 mov ebp,esp 00AB1953 sub esp,15Ch 00AB1959 push ebx 00AB195A push esi 00AB195B push edi 00AB195C lea edi,[ebp-15Ch] 00AB1962 mov ecx,57h 00AB1967 mov eax,0CCCCCCCCh 00AB196C rep stos dword ptr es:[edi] 00AB196E mov ecx,offset _36AAE6AE_源@cpp (0ABC029h) 00AB1973 call @__CheckForDebuggerJustMyCode@4 (0AB132Ah) CReturn objA; objA = GetCReturn(); 00AB1978 lea eax,[ebp-158h] ;传入预留的栈空间的首地址 00AB197E push eax 00AB197F call GetCReturn (0AB13EDh) ;调用GetCReturn函数 00AB1984 add esp,4 ;平衡栈空间 00AB1987 mov ecx,0Bh ;设置循环次数 00AB198C mov esi,eax ;调用对象作为返回值后,会把开辟的对象的空间的首地址给eax 00AB198E lea edi,[ebp-124h] ; 00AB1994 rep movs dword ptr es:[edi],dword ptr [esi] 00AB1996 mov ecx,0Bh 00AB199B lea esi,[ebp-124h] 00AB19A1 lea edi,[objA] 00AB19A4 rep movs dword ptr es:[edi],dword ptr [esi]
CReturn GetCReturn()
{
00AB4F00 push ebp
00AB4F01 mov ebp,esp
00AB4F03 sub esp,100h
00AB4F09 push ebx
00AB4F0A push esi
00AB4F0B push edi
00AB4F0C lea edi,[ebp-100h]
00AB4F12 mov ecx,40h
00AB4F17 mov eax,0CCCCCCCCh
00AB4F1C rep stos dword ptr es:[edi] ;初始化栈
00AB4F1E mov ecx,offset _36AAE6AE_源@cpp (0ABC029h)
00AB4F23 call @__CheckForDebuggerJustMyCode@4 (0AB132Ah) ;预防栈溢出
CReturn RetObj;
RetObj.m_nNumber = 0;
00AB4F28 mov dword ptr [RetObj],0 ;给对象的首地址的第一个元素赋值
for (int i = 0; i < 10; i++)
00AB4F2F mov dword ptr [ebp-3Ch],0
00AB4F36 jmp GetCReturn+41h (0AB4F41h)
00AB4F38 mov eax,dword ptr [ebp-3Ch]
00AB4F3B add eax,1
00AB4F3E mov dword ptr [ebp-3Ch],eax
00AB4F41 cmp dword ptr [ebp-3Ch],0Ah
00AB4F45 jge GetCReturn+56h (0AB4F56h)
{
RetObj.m_nArry[i] = i + 1;
00AB4F47 mov eax,dword ptr [ebp-3Ch]
00AB4F4A add eax,1
00AB4F4D mov ecx,dword ptr [ebp-3Ch]
00AB4F50 mov dword ptr [ebp+ecx*4-2Ch],eax
}
00AB4F54 jmp GetCReturn+38h (0AB4F38h)
return RetObj;
00AB4F56 mov ecx,0Bh ;循环赋值的次数
00AB4F5B lea esi,[RetObj] ;把需要首地址给esi
00AB4F5E mov edi,dword ptr [ebp+8] ;把开辟的空间的首地址给edi
00AB4F61 rep movs dword ptr es:[edi],dword ptr [esi] ;把栈中的数据给堆中开辟的数据
00AB4F63 mov eax,dword ptr [ebp+8] ;返回首地址给eax
}
函数在调用GetCReturn之间,先在main中将申请的返回对象的首地址作为参数入栈,然后再在函数调用完后进行了数据赋值,将GetCReturn中定义的局部对象的数据复制到这个返回对象的空间中,再讲这个返回的对象复制给目标对象obja,从而达成了返回对象的目的。这里的返回对象也可以算是一个临时对象,但是作为临时对象而目标对象是临时对象的复制,那么在释放的时候肯定会有问题,存在 出错的风险,这与对象作为参数意义,如果析构函数有资源释放的操作,就可以造成同一资源多次释放。
和对象做函数参数的区别: 这里不能用对象指针或者引用来处理,因为对临时对象进行指针或者引用会导致返回已经被释放的局部变量的地址
#include<iostream> using namespace std; class CReturn { public: int m_nNumber; int m_nArry[10]; }; CReturn GetCReturn() { CReturn RetObj; RetObj.m_nNumber = 0; for (int i = 0; i < 10; i++) { RetObj.m_nArry[i] = i + 1; } return RetObj; } int main() { CReturn objA; objA = GetCReturn(); printf("%d %d %d \n", objA.m_nNumber, objA.m_nArry[0], objA.m_nArry[9]); return 0; }