Masimaro's blog - 反汇编
type: channel
link: https://www.masimaro.top/tag/%E5%8F%8D%E6%B1%87%E7%BC%96/
items: VC 在调用main函数之前的操作
items: C 堆内存管理
items: 用call和ret实现子程序
items: 汇编转移指令jmp原理
items: C语言中处理结构体的原理
items: 汇编debug与masm命令
items: C++继承分析
items: C++多态
items: C++类的构造函数与析构函数
items: 结构体和类

VC 在调用main函数之前的操作
type: item
date: 2018-09-16T15:20:00+08:00
description: 在C/C++语言中规定，程序是从main函数开始，也就是C/C++语言中以main函数作为程序的入口，但是操作系统是如何加载这个main函数的呢，程序真正的入口是否是main函数呢？本文主要围绕这个主题，通过逆向的方式来探讨这个问题。本文的所有环境都是在xp上的，IDE主要使用IDA 与 VC++ 6.0。为何不选更高版本的编译器，为何不在Windows 7或者更高版本的Windows上实验呢？我觉得主要是VC6更能体现程序的原始行为，想一些更高版本的VS 它可能会做一些优化与检查，从而造成反汇编生成的代码过于复杂不利于学习，当逆向的功力更深之后肯定得去分析新版本VS 生成的代码，至于现在，我的水平不够只能看看VC6 生成的代码首先通过VC 6编写这么一个简单的程序#include <stdio.h> #include <windows.h> #include <tchar.h> int main() { wchar_t str[] = L"hello world"; size_t s = wcslen(str); return 0; }通过单步调试，打开VC6 的调用堆栈界面，发现在调用main函数之前还调用了mainCRTStartup 函数:在VC6 的反汇编窗口中好像不太好找到mainCRTStartup函数的代码，因此在这里改用IDA pro来打开生成的exe，在IDA的 export窗口中双击 mainCRTStartup 函数，代码就会跳转到函数对应的位置。它的代码比较长，刚开始也是进行函数的堆栈初始化操作，这个初始化主要是保存原始的ebp，保存重要寄存器的值，并且改变ESP的指针值初始化函数堆栈，这些就不详细说明了，感兴趣的可以去看看我之前写的关于函数反汇编分析的内容：C函数原理在初始化完成之后，它有这样的汇编代码.text:004010EA push offset __except_handler3 .text:004010EF mov eax, large fs:0 .text:004010F5 push eax .text:004010F6 mov large fs:0, esp这段代码主要是用来注册主线程的的异常处理函数的，为什么它这里的4行代码就可以设置线程的异常处理函数呢？这得从SEH的结构说起。每个线程都有自己的SEH链，当发生异常的时候会调用链中存储的处理函数，然后根据处理函数的返回来确定是继续运行原先的代码，还是停止程序还是继续将异常传递下去。这个链表信息保存在每个线程的NT_TIB结构中，这个结构每个线程都有，用来记录当前线程的相关内容，以便在进行线程切换的时候做数据备份和恢复。当然不是所有的线程数据都保存在这个结构中，它只保留部分。该结构的定义如下:typedef struct _NT_TIB { PEXCEPTION_REGISTRATION_RECORD ExceptionList; PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; union { PVOID FiberData; ULONG Version; }; PVOID ArbitraryUserPointer; PNT_TIB Self; } NT_TIB, *PNT_TIB;这个结构的第一个参数是一个异常处理链的链表头指针，链表结构的定义如下:typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;这个结构很简单的定义了一个链表，第一个成员是指向下一个节点的指针，第二个参数是一个异常处理函数的指针，当发生异常的时候会去调用这个函数。而这个链表的头指针被存到fs寄存器中知道了这点之后再来看这段代码，首先将异常函数入栈，然后将之前的链表头指针入栈，这样就组成了一个EXCEPTION_REGISTRATION_RECORD结构的节点而这个节点的指针现在就是ESP中保存的值，之后再将链表的头指针更新，也就是最后一句对fs的重新赋值，这是一个典型的使用头插法新增链表节点的操作。通过这样的几句代码就向主线程中注入了一个新的异常处理函数。之后就是进行各种初始化的操作，调用GetVersion 获取版本号，调用 __heap_init 函数初始化C运行时的堆栈，这个函数后面有一个 esp + 4的操作，这里可以看出这个函数是由调用者来做堆栈平衡的，也就是说它并不是Windows…

C 堆内存管理
type: item
date: 2018-09-01T15:51:00+08:00
description: 在Win32 程序中每个进程都占有4GB的虚拟地址空间，这4G的地址空间内部又被分为代码段，全局变量段堆段和栈段，栈内存由函数使用，用来存储函数内部的局部变量，而堆是由程序员自己申请与释放的，系统在管理堆内存的时候采用的双向链表的方式，接下来将通过调试代码来分析堆内存的管理。堆内存的双向链表管理下面是一段测试代码#include <iostream> using namespace std; int main() { int *p = NULL; __int64 *q = NULL; int *m = NULL; p = new int; if (NULL == p) { return -1; } *p = 0x11223344; q = new __int64; if (NULL == q) { return -1; } *q = 0x1122334455667788; m = new int; if (NULL == m) { return -1; } *m = 0x11223344; delete p; delete q; delete m; return 0; }我们对这段代码进行调试，当代码执行到delete p;位置的时候(此时还没有执行delete语句)查看变量的值如下:p q m变量的地址比较接近，这三个指针变量本身保存在函数的栈中。从图中看存储这三个变量内存的地址好像不像栈结构，这是由于在高版本的VS中默认开启了地址随机化，所以这里看不出来这些地址的关系，但是如果在VC6里面可以很明显的看到它们在一个栈结构中。我们将p, q, m这三者所指向的内存都减去 0x20 得到p - 0x20 = 0x00035cc8 - 0x20 = 0x00035ca8 q - 0x20 = 0x00035d08 - 0x20 = 0x00035ce8 m - 0x20 = 0x00035d50 - 0x20 = 0x00035d30在内存窗口中分别查看p - 0x20, q- 0x20, m- 0x20 位置的内存如下通过观察发现p - 0x20处前8个字节存储了两个地址分别是 0x00035c38、0x00035ce8。是不是对0x00035ce8 这个地址感到很熟悉呢，它就是q - 0x20 处的地址，按照这个思路我们观察这些内存发现内存地址前四个字节后四个字节0x00035ca80x00035c380x00035ce80x00035ce80x00035ca80x00035d300x00035d300x00035ce80x00000000看到这些地址有没有发现什么呢？没错，这个结构有两个指针域，第一个指针域指向前一个节点，后一个指针域指向后一个节点，这是一个典型的双向链表结构，你没有发现？没关系，我们将这个地址整理一下得到下面这个图表既然知道了它的管理方式，那么接着往后执行delete语句，这个时候再看这些地址对应的内存中保存的值内存地址前四个字节后四个字节0x00035CA80x00035d700x000300c40x00035ce80x00035c380x00035d300x00035d300x00035ce80x00000000系统已经改变了后面两个节点中next和pre指针域的内容，将p节点从双向链表中除去了。而这个时候仔细观察p节点中存储内容发现里面得值已经变为 0xfeee 了。我们在delete的时候并没有传入对应的参数告知系统该回收多大的内存，那么它是怎么知道该如何回收内存的呢。我们回到之前的那个p - 0x20 内存的图上看，是不是在里面发现了一个0x00000004的值，其实这个值就是当前节点占了多少个字节，如果不相信，可以看看q- 0x20 和m - 0x20 内存处保存的值看看，在对应的偏移处是不是有 8和4。系统根据这个值来回收对应的内存。

用call和ret实现子程序
type: item
date: 2016-10-23T21:33:00+08:00
description: ret和call是另外两种转移指令，它们与jmp的主要区别是，它们还包含入栈和出栈的操作。具体的原理如下：ret操作相当于：pop ip（直接将栈顶元素赋值给ip寄存器）call s的操作相当于：push ip jmp s（先将ip的值压栈，再跳转）retf的操作相当于：pop ip pop cscall dword ptr s相当于：push cs push ip这两组指令为我们编写含子函数的程序提供了便利，一般的格式如下：main： ......... call s ........ a s: ........ call s1 .......... b ret s1: .......... call s2 ......... c ret s2: ......... d call s3 ret s3: ........ ret分析以上的程序，假设call的下一条指令的偏移地址分别为：a、b、c、d随着程序的执行，ip指向call指令，CPU将这条指令放入指令缓冲器，执行上一条指令，然后ip指向下一条指令，ip = a。执行call指令，根据call的原理先执行a入栈，此时栈中的情况如下然后跳转到s，执行到call指令处时，ip = b，b首先入栈，然后跳转到s1执行到s1处的call指令时，ip = c，c入栈，然后跳转到s2执行到s2处的call指令时，ip = d，d入栈，然后跳转到s3执行到s3处的ret指令时，栈顶元素出栈，ip = d，程序返回到s2中，到ret时，ip = c，程序返回到s1，再次执行ret，ip = b，程序返回到s，执行ret，ip = a，程序返回到main中，接下来正常执行main中的代码，知道整个程序结束。

汇编转移指令jmp原理
type: item
date: 2016-10-23T21:29:00+08:00
description: 在计算机中存储的都是二进制数，计算机将内存中的某些数当做代码，某些数当做数据。在根本上，将cs，ip寄存器所指向的内存当做代码，指令转移就是修改cs，ip寄存器的指向，汇编中提供了一种修改它们的指令——jmp。jmp指令可以修改IP或cs和IP的值来实现指令转移，指令格式为：”jmp 标号“将指令转移到标号处，例如:CODES SEGMENT ASSUME CS:CODES START: MOV AX,0 jmp s inc ax s: mov ax,3 MOV AH,4CH INT 21H CODES ENDS END START通过单步调试可以看出在执行jmp后直接执行s标号后面的代码，此时ax为3,。jmp s所对应的机器码为"EB01"，在“Inc ax”后面再加其他的指令（加两个 nop指令）此时jmp所对应的机器码为"EB03",每一个nop指令占一个字节，在添加或删除它们之间的代码可以看到jmp指令所对应的机器码占两个字节，第一个字节的机器码并不发生改变，改变的是第二个字节的内容，并且第二个字节的内容存储的是跳转目标指令所在内存与jmp指令所在内存之间的位移。其实cup在执行jmp指令时并不会记录标号所在的内存地址，而是通过标号与jmp指令之间的位移，假设jmp指令的下一条指令的地址为org，位移为idata，则目标的内存地址为dec = org + idata。（idata有正负之分）在CPU中有指令累加器称之为CA寄存器， 程序每执行一条，CA的值加1，jmp指令后可以有4中形式“jmp short s、jmp、 s jmp near ptr s、jmp far ptr s”编译器在翻译时，位移所对应的内粗大小为1、2、2、4（分别是cs和ip所对应的位移）。都是带符号的整型。jmp指令的跳转分为两种情况：向前跳转和向后跳转。向后跳转：jmp （.....）s ...... ...... s:......这种情况下，编译器将jmp指令读完后，读下一条指令，并将CA加1，一直读到相应的标号处，此时CA的值就是位移，根据具体的伪指令来分配内存的大小（此时的数应该为正数）向前跳转 ：s:....... ........ jmp (......) s编译器在遇到标号时会在标号后添加几个nop指令（"jmp short s、jmp、 s jmp near ptr s、jmp far ptr s"分别添加1,2,2,4个），读下一条指令时将CA寄存器的值加1，得到对应的位移，生成机器码（此时为负数）.这两种方式分别得到位移后，在执行过程中，利用上述公式计算出对应的地址，实现指令的转移下面的一段代码充分说明了jmp的这种实现跳转的机制：assume cs:code code segment mov ax,4c00h int 21h start: mov ax,0 s: nop nop mov di,offset s mov si,offset s2 mov ax,cs:[si] mov cs:[di],ax s0: jmp short s s1: mov ax,0 int 21h mov ax,0 s2: jmp short s1 nop code ends end start通过以上的分析可以得出，几个jmp指令所占的空间为2个字节，一个保存jmp本省的机器码，EB，另一个保存位移。因此两个nop指令后面的四句是将s2处的“jmp short s1”所对应的机器码拷贝到s处，利用debug下的-u命令可以看出该处的机器码为“EB F6” f6转化为十进制是-10.执行到s0处时，jmp指令使CPU下一次执行s处的代码，“EB F6”对应的操作利用公式可以得出IP = A - A = 0，下一步执行的代码是“MOV AX,4C00H”，也就是说该程序在此处结束。用-t命令单步调试：

C语言中处理结构体的原理
type: item
date: 2016-10-23T21:26:00+08:00
description: 汇编中有几种寻址方式，分别是直接寻址：（ds:[idata]）、寄存器间接寻址(ds:[bx])、寄存器相对寻址（ds:[bx + idata]、ds:[bx + si]）基址变址寻址(ds:[bx + si])、相对基址变址寻址([bx + si + idata])。结构体的存储逻辑图如下：（以下数据表示某公司的名称、CEO、CEO的福布斯排行、收入、代表产品）现在假设公司的CEO在富豪榜上的排名为38，收入增加了70，代表产品变为VAX，通过汇编编程修改上述信息，以下是相应的汇编代码：(假设数据段为seg)mov ax,seg mov ds,ax mov bx,0 mov word ptr ds:[bx + 12],38 add [bx + 14],70 mov si,0 mov byte ptr [bx + 10 + si],'V' inc si mov byte ptr [bx + 10 + si],'A' inc si mov byte ptr [bx + 10 + si],'X'对应的C语言代码可以写成：struct company { char cn[3]; char name[9]; int pm; int salary; char product[3]; }; company dec = {"DEC","Ken Olsen",137,40,"PDP"}; int main() { int i; dec.pm = 38; dec.salary += 70; dec.product[i] = 'V'; ++i; dec.product[i] = 'A'; ++i; dec.product[i] = 'X'; return 0; }对比C语言代码和汇编代码，可以看出，对于结构体变量，系统会先根据定义分配相应大小的空间，并将各个变量名与内存关联起来，结构体对象名与系统分配的空间的首地址相对应(定义的结构体对象的首地址在段中的相对地址存储在bx中)，即在使用dec名时实际与汇编代码“mov ax,seg” "mov ds,ax"对应，将数据段段首地址存入ds寄存器中，系统根据对象中的变量名找到对应的偏移地址，偏移地址的大小由对应的数据类型决定，如cn数组前没有变量，cn的偏移地址为0，cn所在的地址为 ds:[bx]，cn为长度为3的字符型数组，在上一个偏移地址的基础上加上上一个变量所占空间的大小即为下一个变量的偏移地址，所以name数组的首地址为ds:[bx + 3]，这样给出了对象名就相当于给定了该对象在段中的相对地址（上述代码中的bx），给定了对象中的成员变量名就相当于给定了某一内存在对象中的偏移地址（ds:[bx + idata]）。根据数组名可以找到数组的首地址，但数组中具体元素的访问则需要给定元素个数，即si的值来定位数组中的具体内存，C语言中的 ++i 相当于汇编中的 （add si ,数组中元素的长度）。根据以上的分析可以看出，构建一个结构体对象时，系统会在代码段中根据结构体的定义开辟相应大小的内存空间，并将该空间在段中的偏移地址与对象名绑定。对象中的变量名与该变量在对象所在内存中的偏移地址相关联，数组中的标号用于定位数组中的元素在数组中的相对位置。（对象名决定bx，变量名决定bx + idata，数组中的元素标号决定bx + idata + si）。

汇编debug与masm命令
type: item
date: 2016-10-23T21:22:00+08:00
description: 汇编语言这块是我之前写在网易博客上的，不过那个账号基本已经作废了，所以现在抽个时间把当时的博客搬到CSDN上。汇编命令（编译器masm命令）：找到masm所在的文件夹，我的在d：\MASM中，用cmd打开dos界面，输入“d:”切换到D盘，再输入“d:\MASM\masm”打开编译器中的masm程序得到如下结果：再输入路径+含".asm"的文件（若在当前文件夹中则不必输入路径），这个表示生成了一个“.obj”文件，在第二行若不输入任何内容则默认在当前文件夹下生成一个与“.asm”同名的“.obj”文件。下面几个直接输入空格，不生成这几个文件，知道提示所有工作都完成（0 warning error）再按照上述格式找到MASM文件中的link程序，输入所需的“.obj”文件的相对路径 ".exe"行后不输入任何内容表示在该文件夹下生成一个与“.obj”文件同名的“.exe”文件，到这里汇编程序的编译链接工作就完成了。下面是该程序的调试，输入“debug” + 执行程序的路径进入程序，-u命令：查看汇编代码；-t命令：执行下一条语句-g + 的内存：跳转到该内存所对应的语句（再用t命令执行该条命令）-r命令：查看寄存器的内容（后可直接接寄存器的名称，就只查看该寄存器的内容）-d命令：后接内存地址，查看改地址后面8 * 16个字节空间的地址（每行16个字节，共8行）后面是对应的字符‘.’表示没有该数字对应的字符加上地址范围的话就只查看该地址范围内存储的数据

C++继承分析
type: item
date: 2016-07-09T09:28:00+08:00
description: 面向对象的三大特性之一就是继承，继承运行我么重用基类中已经存在的内容，这样就简化了代码的编写工作。继承中有三种继承方式即:public protected private，这三种方式规定了不同的访问权限，这些权限的检查由编译器在语法检查阶段进行，不参与生成最终的机器码，所以在这里不对这三中权限进行讨论，一下的内容都是采用的共有继承。单继承首先看下面的代码：class CParent { public: CParent(){ printf("CParent()\n"); } ~CParent(){ printf("~CParent()\n"); } void setNumber(int n){ m_nParent = n; } int getNumber(){ return m_nParent; } protected: int m_nParent; }; class CChild : public CParent { public: void ShowNumber(int n){ setNumber(n); m_nChild = 2 *m_nParent; printf("child:%d\n", m_nChild); printf("parent:%d\n", m_nParent); } protected: int m_nChild; }; int main() { CChild cc; cc.ShowNumber(2); return 0; }上面的代码中定义了一个基类，以及一个对应的派生类，在派生类的函数中，调用和成员m_nParent，我们没有在派生类中定义这个变量，很明显这个变量来自于基类，子类会继承基类中的函数成员和数据成员，下面的汇编代码展示了它是如何存储以及如何调用函数的:41: CChild cc; 004012AD lea ecx,[ebp-14h];将类对象的首地址this放入ecx中 004012B0 call @ILT+5(CChild::CChild) (0040100a);调用构造函数 004012B5 mov dword ptr [ebp-4],0 42: cc.ShowNumber(2); 004012BC push 2 004012BE lea ecx,[ebp-14h] 004012C1 call @ILT+10(CChild::ShowNumber) (0040100f);调用自身的函数 43: return 0; 004012C6 mov dword ptr [ebp-18h],0 004012CD mov dword ptr [ebp-4],0FFFFFFFFh 004012D4 lea ecx,[ebp-14h] 004012D7 call @ILT+25(CChild::~CChild) (0040101e);调用析构函数 004012DC mov eax,dword ptr [ebp-18h] ;构造函数 0040140A mov dword ptr [ebp-4],ecx 0040140D mov ecx,dword ptr [ebp-4];到此ecx和ebp - 4位置的值都是对象的首地址 00401410 call @ILT+35(CParent::CParent) (00401028);调用父类的构造 00401415 mov eax,dword ptr [ebp-4] ;ShowNumber函数 00401339 pop ecx;还原this指针 0040133A mov dword ptr [ebp-4],ecx;ebp - 4存储的是this指针 31: setNumber(n); 0040133D mov eax,dword ptr [ebp+8];ebp + 8是showNumber参数 00401340 push eax 00401341 mov ecx,dword ptr [ebp-4] 00401344 call @ILT+0(CParent::setNumber) (00401005) 32: m_nChild = 2 *m_nParent; 00401349 mov ecx,dword ptr [ebp-4] 0040134C mov edx,dword ptr [ecx];取this对象的头4个字节的值到edx中 0040134E shl edx,1;edx左移1位，相当于edx = edx * 2 00401350 mov eax,dword ptr [ebp-4] 00401353 mov dword ptr [eax+4],edx ;将edx的值放入到对象的第4个字节处 33: printf("child:%d\n", m_nChild);…

C++多态
type: item
date: 2016-07-05T09:35:00+08:00
description: 面向对象的程序设计的三大要素之一就是多态，多态是指基类的指针指向不同的派生类，其行为不同。多态的实现主要是通过虚函数和虚表来完成，虚表保存在对象的头四个字节，要调用虚函数必须存在对象，也就是说虚函数必须作为类的成员函数来使用。编译器为每个拥有虚函数的对象准备了一个虚函数表，表中存储了虚函数的地址，类对象在头四个字节中存储了虚函数表的指针。下面是一个具体的例子class CVirtual { public: virtual void showNumber(){ printf("%d\n", nNum); } virtual void setNumber(int n){ nNum = n; } private: int nNum; }; int main() { CVirtual cv; cv.setNumber(2); cv.showNumber(); return 0; }上述这段代码定义了两个虚函数setNumber和showNumber，并在主函数中调用了他们，下面通过反汇编的方式来展示编译器是如何调用虚函数的26: CVirtual cv; 00401288 lea ecx,[ebp-8];对象中有一个整形数字占4个字节，同时又有虚函数，头四个字节用来存储虚函数表指针，总共占8个字节 0040128B call @ILT+25(CVirtual::CVirtual) (0040101e);调用构造函数 27: cv.setNumber(2); 00401290 push 2 00401292 lea ecx,[ebp-8] 00401295 call @ILT+0(CVirtual::setNumber) (00401005);调用虚函数 28: cv.showNumber(); 0040129A lea ecx,[ebp-8] 0040129D call @ILT+5(CVirtual::showNumber) (0040100a);调用虚函数 29: return 0; 004012A2 xor eax,eax ;构造函数 0401389 pop ecx;还原ecx使得ecx保存对象的首地址 0040138A mov dword ptr [ebp-4],ecx 0040138D mov eax,dword ptr [ebp-4] 00401390 mov dword ptr [eax],offset CVirtual::`vftable' (0042f020);将虚函数表的首地址赋值到对象的头4个字节 00401396 mov eax,dword ptr [ebp-4] 00401399 pop edi 0040139A pop esi 0040139B pop ebx 0040139C mov esp,ebp 0040139E pop ebp 0040139F ret ;setNumber(int n) 0040134A mov dword ptr [ebp-4],ecx 18: nNum = n; 0040134D mov eax,dword ptr [ebp-4];eax = ecx 00401350 mov ecx,dword ptr [ebp+8] 00401353 mov dword ptr [eax+4],ecx 从上面的汇编代码可以看到，当类中有虚函数的时候，编译器会提供一个默认的构造函数，用于初始化对象的头4个字节，这四个字节存储的是一个指针值，我们定位到这个指针所在的内存如下图：这段内存中存储了两个值，分别为0x0040100AH和0x00401005H，我们执行后面的代码发现，这两个地址给定的是虚函数所在的地址。在调用时编译器直接调用对应的虚函数，并没有通过虚表来寻址到对应的函数地址。下面看另外一个例子:class CParent { public: virtual void showClass() { printf("CParent\n"); } }; class CChild:public CParent { public: virtual void showClass() { printf("CChild\n"); } }; int main() { CParent *pClass = NULL; CParent cp; CChild cc; pClass = &cp; pClass->showClass(); pClass = &cc; pClass->showClass(); pClass = NULL; return 0; }上述代码定义了一个基类和派生类，并且在派生类中重写了函数showClass,在调用时用分别利用基类的指针指向基类和派生类的对象来调用这个虚函数，得到的结果自然是…

C++类的构造函数与析构函数
type: item
date: 2016-06-29T09:42:00+08:00
description: C++中每个类都有其构造与析构函数，它们负责对象的创建和对象的清理和回收，即使我们不写这两个，编译器也会默认为我们提供这些构造函数。下面仍然是通过反汇编的方式来说明C++中构造和析构函数是如何工作的。编译器是否真的会默认提供构造与析构函数在一般讲解C++的书籍中都会提及到当我们不为类提供任何构造与析构函数时编译器会默认提供这样六种成员函数：不带参构造，拷贝构造，“=”的重载函数，析构函数，以及带const和不带const的取地址符重载。但是编译器具体是怎么做的，下面来对其中的部分进行说明不带参构造下面是一个例子：class test { private: char szBuf[255]; public: static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }下面是对应的汇编源码：00401400 push ebp 00401401 mov ebp,esp 00401403 sub esp,140h;栈顶向上抬了140h的空间用于存储类对象的数据 00401409 push ebx 0040140A push esi 0040140B push edi 0040140C lea edi,[ebp-140h] 00401412 mov ecx,50h 00401417 mov eax,0CCCCCCCCh 0040141C rep stos dword ptr [edi] 26: test t; 27: printhello(t); 0040141E sub esp,100h从上面可以看到，在定义类的对象时并没有进行任何的函数调用，在进行对象的内存空间分配时仅仅是将栈容量扩大，就好像定义一个普通变量一样，也就是说在默认情况下编译器并不会提供不带参的构造函数，在初始化对象时仅仅将其作为一个普通变量，在编译之前计算出它所占内存的大小，然后分配，并不调用函数。再看下面一个例子：class test { private: char szBuf[255]; public: virtual void sayhello() { cout<<"hello world"; } static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }30: test t; 0040143E lea ecx,[ebp-104h] 00401444 call @ILT+140(test::test) (00401091) ;构造函数 004014A0 push ebp 004014A1 mov ebp,esp 004014A3 sub esp,44h 004014A6 push ebx 004014A7 push esi 004014A8 push edi 004014A9 push ecx 004014AA lea edi,[ebp-44h] 004014AD mov ecx,11h 004014B2 mov eax,0CCCCCCCCh 004014B7 rep stos dword ptr [edi] 004014B9 pop ecx 004014BA mov dword ptr [ebp-4],ecx 004014BD mov eax,dword ptr [ebp-4] 004014C0 mov dword ptr [eax],offset test::`vftable' (0042f02c) 004014C6 mov eax,dword ptr [ebp-4] 004014C9 pop edi 004014CA pop esi 004014CB pop ebx 004014CC mov esp,ebp 004014CE pop ebp这段C++代码与之前的仅仅是多了一个虚函数，这个时候编译器为这个类定义了一个默认的构造函数，从汇编代码中可以看到，这个构造函数主要初始化了类对象的头4个字节，将虚函数表的地址放入到这个4个字节中，因此我们得出结论，一般编译器不会提供不带参的构造函数，除非类中有虚函数。下面请看这样一个例子：class Parent { public: Parent() { cout<<"parent!"<<endl; } }; class ch…

结构体和类
type: item
date: 2016-06-09T09:44:00+08:00
description: 在C++中类与结构体并没有太大的区别，只是默认的成员访问权限不同，类默认权限为私有，而结构体为公有，所以在这将它们统一处理，在例子中采用类的方式。类对象在内存中的分布在类中只有数据成员占内存空间，而类的函数成员主要分布在代码段中，不占内存空间，一般对象所占的内存空间大小为sizeof(成员1) + sizeof(成员2) + ... + sizeof(成员n)但是有几种情况不符合这个公式，比如虚函数和继承，空类，内存对齐，静态数据成员。只要出现虚函数就会多出4个字节的空间，作为虚函数表，继承时需要考虑基类的大小，另外出现静态成员时静态成员由于存在于数据段中，并不在类对象的空间中，所以静态成员不计算在类对象的大小中这些不在此处讨论，主要说明其余的三种情况：空类按照上述公式，空类应该不占内存，但是实际情况却不是这样，下面来看一个具体的例子：class Test { public: int Print(){printf("Hello world!\n");} }; int main() { Test test; printf("%d\n", sizeof(test)); return 0; }运行程序发现，输出结果为1，这个结果与我们预想的可能有点不一样，按理来说，空类中没有数据成员，应该不占内存空间才对，但是我们知道每个类都有一个this指针指向具体的内存，以便成员函数的调用，即使定义一个类什么都不写，编译器也会提供默认的构造函数用来初始化类，但是如果类的实例不占内存空间，那么该如何初始化？所以编译器为它分配一个1字节的空间以便初始化this指针。所以空类占一个字节。内存对齐下面看这样一个类class Test { public: short s; int n; };当在程序中定义这样一个类，通过sizeof来输出大小得到的是8，上面的公式又不满足了，我们知道为了程序的运行效率，编译器并不会依次申请内存用于存储变量，而会采用内存对齐的方式，以牺牲一定内存空间的代价来换取程序的效率，这个类的大小为8，也是内存对齐的结果，查看类工各个成员的地址我们发现 n的地址为0x0012ff44，而s的地址为0x0012ff40，s本来是占2个字节，但是n并没有出现在其后的42的位置，我所用的VC++6.0默认采用的是8个字节的对齐方式，假设编译器采用的是n个字节的对齐方式，而类中某成员实际所占内存空间的大小为m，那么该成员所在的内存地址必须为p的整数倍，而p = min(m, n)，所以对于s来说，采用的是2个字节的对齐方式，分配到的首地址为40是2的倍数，而其后的整型成员n占4个字节，采用上述公式，得到它的内存地址应该是4的倍数，所以取其后的44作为它的地址，中间有两个字节没有使用，所以这个类占8个字节。下面再来看一个例子:class Test { public: short s; //8 double d; //8 char c; };通过程序得出当前结果体的大小为24，根据上面的分析，首先在为s分配空间的时候采用的是2个字节的对齐方式，假设分配到的地址为0x0012ff40，那么d采用的是8个字节的对齐方式，它的地址应该为0x0012ff48，最后为c分配内存的时候，应该是用1个字节的对齐方式，总共应该占的空间为8 + 8 + 1 = 17但是结果却并不是这样。在内存对齐时编译器实际采用对齐方式是：假设结构体成员的最大成员占n个字节，编译器默认采用m个字节的对齐方式，那么实际对齐大小应该为min(m, n)的整数倍，所以实际采用的是8个字节的对齐方式，而结构体的大小应该是实际对齐方式的整数倍，所以占24个字节。在编写程序时可以使用#pragma pack(n)的方式来改变编译器的默认对齐方式。另外对于嵌套定义的结构体，对齐情况也有少许不同。class One { public: short s; double d; char c; }; class Two { One one; int n; };输出class two的大小为32个字节，嵌套定义的结构体仍然能够满足上述两个法则，首先其中的成员结构体one大小为24，然后另外一个成员n占4个字节，得到总共占28个字节，然后根据第二个对齐的规则在24和8之间取最小值8，可以得到结构体的大小应该为8的整数倍32个字节。类的成员函数类的成员函数在调用时直接利用对象打点调用，在函数中直接使用类中的成员，函数操作的是不同对象的数据成员，能够达到这个目的实际上类的对象在调用类的成员函数时默认传入的第一个参数是一个指向这个对象地址的指针叫做this指针，具体this指针的原理看下面一段代码：class test { private: int i; public: test(){i = 0;} int G…