1. _cdecl: c\c++默认调用方式,调用方平衡栈,不定参数的函数可以使用 ret;
2. _stdcall: 被调用方平衡栈,不定参数的函数不可以使用 ret 4;
3. _fastcall:寄存器方式传参,被调用方平衡栈,不定参数的函数不可以使用, cx,dx分别存储1,2参数,其余参数还是用栈
call 指令 隐含push 函数返回下一条指令地址
_cdecl 函数的结束"}" mov esp,ebp
pop ebp
ret //ret指令 会把当前esp指向值更新到eip,同时esp-4, 此时一个栈帧调用完成。
//如果函数有返回值会先赋值给eax,再执行ret.
int _fastcall fun22(int a, int b, int c,int d)
{ F9 ecx = a, edx = b esp存储函数返回下一条指令地址 esp+4 = c esp+8 = d
008613C0 push ebp
008613C1 mov ebp,esp
008613C3 sub esp,0E4h F9 ecx = a, edx = b ebp+4存储函数返回下一条指令地址 ebp+8 = c ebp+0xc = d
第七章:变量在内存中的位置和访问方式
作用域:全局变量属于进程作用域;静态变量属于文件作用域;局部变量属于函数作用域;
在大多数情况下,在PE文件中的只读数据节中,常量的节属性被修饰为不可写;而全局变量和静态变量则在属性为可读写的数据节中。
全局变量和常量类似,被写入文件中,当用户执行该文件,OS分析并加载各节到虚拟内在地址,这是全局变量已经存在了,加载完才开始执行入口点代码。
全局变量的内在地址在全局数据区中,通过栈指针是无法访问到,访问也与常量类似,都是通过立即数来访问。局部变量使用ebp或esp间接访问。
全局变量在内存中的地址顺序是先定义的变量在低地址,后定义的在高地址,局部变量刚好相反。
静态变量:全局静态变量,局部静态变量
全局静态变量和全局变量类似,只是编译器限制外部源码文件访问的全局变量。
局部静态变量会预先被作为全局变量处理,和全局变量都保存在执行文件中的数据区中,只是被定义在某个作用域内;
局部静态变量的初始化只是在做赋值操作而已,c++语法规定局部静态变量只被初始化一次。
在第一个局部静态变量附近分配一个地址存放标志。
void InNumber(int var)
{
static int nInt = var;
static int nInt2 = var+2;
static int nInt3 = var+3;
printf("%d,%d,%d",nInt, nInt2, nInt3);
}
static int nInt = var;
00AC1003 mov eax,dword ptr ds:[00AC3370h]
00AC1008 and eax,1
00AC100B jne InNumber+25h (0AC1025h)
00AC100D mov ecx,dword ptr ds:[0AC3370h]
00AC1013 or ecx,1
00AC1016 mov dword ptr ds:[0AC3370h],ecx
00AC101C mov edx,dword ptr [var]
00AC101F mov dword ptr ds:[0AC337Ch],edx
static int nInt2 = var+2;
00AC1025 mov eax,dword ptr ds:[00AC3370h]
00AC102A and eax,2
00AC102D jne InNumber+4Ah (0AC104Ah)
00AC102F mov ecx,dword ptr ds:[0AC3370h]
00AC1035 or ecx,2
00AC1038 mov dword ptr ds:[0AC3370h],ecx
00AC103E mov edx,dword ptr [var]
00AC1041 add edx,2
00AC1044 mov dword ptr ds:[0AC3374h],edx
static int nInt3 = var+3;
00AC104A mov eax,dword ptr ds:[00AC3370h]
00AC104F and eax,4
00AC1052 jne InNumber+6Fh (0AC106Fh)
00AC1054 mov ecx,dword ptr ds:[0AC3370h]
00AC105A or ecx,4
00AC105D mov dword ptr ds:[0AC3370h],ecx
00AC1063 mov edx,dword ptr [var]
00AC1066 add edx,3
00AC1069 mov dword ptr ds:[0AC3378h],edx
printf("%d,%d,%d",nInt, nInt2, nInt3);
00AC106F mov eax,dword ptr ds:[00AC3378h]
如果如下
void InNumber(int var)
{
static int nn = 33; //nn将不再使用标志,直接当作全局变量处理,但作用域还是局部。
static int nInt = var;
通过名字粉碎来区别作用域,UE打开.obj文件如下:
nInt3@?1??InNumber@@YAXH@Z@4HA
$?nInt@?1??InNumber@@YAXH@Z@4HA
void Show(char szBuffer[]) //数组传参
{
int wrongSize = sizeof(szBuffer); //此时szBuffer为指针类型,并非数组, 4
int rightSize = strlen(szBuffer); //取决外部传入
strcpy_s(szBuffer, 20, "hello, my sunny!");
}
局部静态数组和局部静态变量有些不同,无论局部静态数组有多少个元素,也只会检查一次初始标志位。
数组的下标寻址比指针寻址高效。数组名本身就是常量地址。而指针需要取其指向的地址值。
第九章:结构体和类
空类实际长度为1字节,如果不占内存,则无法得到空类实例的地址,this指针失效,因此不能被实例化;没有数据成员,还可以有成员函数。
结构体和类中的数据成员分配内存时,结构体中的当前数据成员类型长度为M,指定的对齐值为N,那么实际对齐值为q=min(M,N),其成员的地址安排在q的倍数上。
结构体中数据成员类型最大值为M,指定的对齐值为N,那么实际对齐值为min(M,N). 结构体整体大小为min(M,N)的整数倍。
#pragma pack(N) //调整对齐大小。
当结构体中以数组作为成员时,将根据数组元素的长度计算对齐值,而不是按数组的整体大小去计算。
struct STR{
char cChar;
char cArray[4];
short sShort;
};
STR ss = {'y',"and",4};
//79 61 6e 64 00 cc 04 00
//y a n d 4
void *pp = &((STR*)NULL)->sShort; //取相对地址。
类的成员函数默认是thiscall调用方式,与_stdcall相同,被调用方负责平衡栈,但传参不同,第一参数(this)使用寄存器ecx传递,而非栈顶。
thiscall不是关键字,不能显示调用。
静态数据成员和静态变量原理相同(有作用域的特殊全局变量),初值会写入编译链接后的执行文件后。所以不参与对象size计算。
类对象作为参数,如果只是简单变量,真依序压栈对象的成员变量,最先定义的最后压栈。如果没有构造函数,也不会调系统默认的构造函数。
class CRet
{
public:
int a;
int b[8];
};
CRet GetReturn()
{
CRet temp;
temp.a = 3;
for (int i = 0; i < 8; ++i)
{
temp.b[i] = i + 6;
}
return temp;
}
int _tmain(int argc, _TCHAR* argv[])
{
CRet mobj;
//1.此时main函数已经临时对象temp
mobj = GetReturn(); //2.GetReturn函数结束,栈帧关闭,所以栈帧关闭前会复制到temp,3. 再次temp赋值给mobj,过了这条语句作用域,temp消失。//如果重载了“=”,这里会调用=
如果为如下://定义时赋初值才会调用拷贝构造
CRet mobj = GetReturn(); //不会有临时对象,而是直接把mobj的地址作为隐含的参数传递给GetReturn(),在GetReturn()函数内部完成拷贝构造的过程。
//note:
1. mobj = GetReturn(); ";"后会析构
2. mobj = GetReturn(),
printf("hell"); 则是在这里的";"后才会析构这个temp对象。
3. 当引用这个temp对象时,生命期与引用相同。
//下面两个有错误,不要返回局部变量
CRet* GetAA()
{
CRet temp;
return &temp;
}
CRet& GetBB()
{
CRet temp;
return temp;
}
thiscall 使用ecx传this指针, __fastcall使用ecx用来传第一个参数,所以不能作为唯一识别特征。
局部对象
堆对象 new成功后再会调构造函数
参数对象 调用拷贝构造函数,该构造函数只有一个参数,类型为对象的引用
返回对象
全局对象 //main函数退出后调用析构函数
静态对象 //main函数退出后调用析构函数
定点断点, tools->options->debugging->symbols select Microsoft Symbols Servers to download symbols.
Ctrl+B than put in function name.
Ctrl+Alt+B list and edit all breakpoint, Condition and Hit Count.
编译器在以下两种情况下会提供默认的构造函数:
1.本类、本类中定义的成员对象或者父类中有虚函数存在;
2.父类或本类中定义的成员对象带有构造函数;
如果没有这两种情况,默认构造函数已经没有存在的意义,只会影响效率。
delete p; delete []p; 释放对象类型标志,1为单个对象,3为释放对象数组,0表示仅仅执行析构函数。
申请对象数组时,由于对象都在同一个堆空间中,编译器使用了堆空间的前4字节数据来保存对象的总个数。
析构顺序,调用自身的析构函数->声明倒序调用成员对象的析构函数->调用基类析构函数
基类声明为虚析构
CBase *psub = new CSub;
delete psub;
mov edx,dword ptr [ebp-0ECh] //传递this
0039183E mov eax,dword ptr [edx] //取得虚表指针
00391840 mov ecx,dword ptr [ebp-0ECh] //传递this
00391846 mov edx,dword ptr [eax] //间接调用虚析构函数
00391848 call edx
基类声明为虚析构
CBase *psub = new CSub;
delete psub;
mov ecx,dword ptr [ebp-0ECh] //传递this
012A183C call CBase::`scalar deleting destructor' (012A124Eh) //直接调用基类析构函数
要习惯给析构函数加virtual