本人刚学汇编,对于逆向工程深感兴趣,为了加入这个学习的大家园,特花余力著陋文一篇。
题记:<<inside C++ object>>是一本很强大的C++书籍,里面关于C++底层的语义描述得淋漓尽致,今天,小弟不才,也试图站在底层的角度和众位共同学习的朋友一起探讨。
我们的工作是相当基本的,主要是把C++的类对象构建和析构的过程呈现出来。
编译器:VC6
首先我们定义一个类
class CLASSA
{
public:
  virtual ~CLASSA()
  {
    
  }
  char m;
  char n;
};
把析构函数设为虚函数,这样可以在类里加入虚表指针。
然后我们在主函数中声明CLASSA的对象,并访问其中一个数据成员。
void main()
{
  CLASSA b;
  char c;
  c = b.n;
}
接下来,通过对这个例子的分析,我们来探讨C++的底层语义。
首先,在main()中,我们必要做的两件事是,把栈底指针压栈,当然,在此之前,根据不同的子系统,MS控制台或者是WINDOWS子系统,根据函数的约定已经让参数顺序入栈了,但这不是我们关心的终点。我们所要做的仅仅是很普遍的工作。
Push ebp
Mov ebp, esp
然后,当然要一个合适的栈大小,栈顶方向应该是低位,所以要让栈顶指针减去要分配的栈大小。
Sub esp, 76
然后保存几个关键寄存器的现场。
  push  ebx
  push  esi
  push  edi
然后是惯性得初始化
  lea  edi, DWORD PTR [ebp-76]
  mov  ecx, 19          ; 00000013H
  mov  eax, -858993460        ; ccccccccH
  rep stosd
下面这句就是关键了
; 17   :   CLASSA b;

  lea  ecx, DWORD PTR _b$[ebp]
  call  ??0CLASSA@@QAE@XZ      ; CLASSA::CLASSA
这两句就是声明了CLASSA的对象b。一共执行了两步,第一步用ecx装载了b对象在栈中的地址,这个地方就是C++跟汇编关联的重点,直接说明了C++对于对象的操作完全是基于这个对象的指针。第二步就是调用CLASSA的构造函数,下面我们进入构造函数的地址。
一开始还是相当平凡的函数结构:
  push  ebp
  mov  ebp, esp
  sub  esp, 68          ; 00000044H
  push   ebx
  push   esi
  push      edi
  push     ecx
  lea  edi, DWORD PTR [ebp-68]
  mov  ecx, 17          ; 00000011H
  mov  eax, -858993460        ; ccccccccH
  rep stosd
  pop  ecx
需要注意的是最后一句,把ecx的值弹出栈,嗯,这个就充分体现了栈结构对于保护现场的重要性。上一个压入栈的东西是什么呢,看看上一个入栈动作, push ecx,很好,那是之前我们存放在ecx中的b对象地址。在接下来的工作中,编译器会用到这个地址。
  mov  DWORD PTR _this$[ebp], ecx
  mov  eax, DWORD PTR _this$[ebp]
  mov  DWORD PTR [eax], OFFSET FLAT:??_7CLASSA@@6B@ ; CLASSA::`vftable'
  mov  eax, DWORD PTR _this$[ebp]
我们看了一个令人振奋的地方了,C++中果然有个隐含的this指针。有个需要理解的是,在C++ PRIMER里说明了隐含的this指针是存在于形参中的,然后编译器以类似重载的方式来编译出不同的函数地址。但是从这里我们可以看出,this并不是作为形参压栈的,this是以局部对象存在于栈中的,真正压栈的其实就是ecx寄存器值。为什么不直接给出地址,而要通过ecx传递呢,根据我查到的资料,原因是80X86汇编的两个操作数无法都使用寄存器间接寻址,所以要有个寄存器来过度。
那么再看第三句
mov  DWORD PTR [eax], OFFSET FLAT:??_7CLASSA@@6B@ ; CLASSA::`vftable'
这个就是C++春风得意的关键所在了,取出虚表的偏移,然后把它给eax寄存器的指向指针,而这时eax中正存放着this指针,OH,我们又得出个结论,虚表指针存在于类对象中的第一个位置。第四句又重新让this指针赋值给eax,因为如果你在构造函数中初始化数据成员eax的值很有可能被修改。 
构造函数结束后,我们又回到主函数中。
接下来就是耐人寻味的两句话
; 18   :   char c;
; 19   :   c = b.n;

  mov  al, BYTE PTR _b$[ebp+5]
  mov  BYTE PTR _c$[ebp], al
我们把b对象的第二个成员赋值给c,我们所看到的寻址方式是变加址方式。而不像一般的临时成员是使用普通的寄存器间接寻址。这个说明了类把他的成员都完整得封装了,要访问,只能通过对象的指针进行。
再接着两句就是跟构造很相似的析构了
  lea  ecx, DWORD PTR _b$[ebp]
  call  ??1CLASSA@@UAE@XZ      ; CLASSA::~CLASSA
在析构函数中的语句和构造函数中的语句几乎是一样的,这里不重复了。
最后,我们来看看虚表是如何声明的;
CONST  SEGMENT
??_7CLASSA@@6B@ DD FLAT:??_ECLASSA@@UAEPAXI@Z    ; CLASSA::`vftable'
  DD  FLAT:?fun@CLASSA@@UAEHXZ
CONST  ENDS
虚表在汇编中利用了常量段来定义第一个参数是虚表的地址,然后是内存模式,然后是虚函数地址。有几个虚函数就声明几次。而且一定要按顺序声明。
而虚函数的调用其实也就是根据虚函数地址在虚表指针的偏移之后得到的。
  mov  edx, DWORD PTR _b$[ebp]
  mov  eax, DWORD PTR [edx]
  mov  esi, esp
  mov  ecx, DWORD PTR _b$[ebp]
  call  DWORD PTR [eax+4]
上面五句很清楚得表示了访问虚表第二个虚函数的情况。
上述就是大概的C++对象形成过程,本人也是初学者,正在积极学习中,希望能在看雪与大家有个和谐的学习气氛,谢谢!