我们知道,类对象是c++中很重要的一部分,那么它的大小以及构造方式在汇编代码中是如何实现的呢?在一个程序的虚拟进程空间中,类对象的分布是怎样的呢?它们的构造方式和普通的内置类型有什么区别?下面我们就一起从汇编的角度来看看编译器是如何实现类对象的构造的,本文的所有系统环境环境为ubuntu16.04,G++编译。
1.让我们来看看最简单的情况:
#include<stdio.h>
#include<iostream>
using namespace std;
class cy{
public:
int a;
int b;
int add(int x,int y){
int sum;
sum=a+b;
return sum;
}
cy(int m,int n):a(m),b(n){}
};
int main(){
int sum;
cy aa(1,2);
printf("%d\n",aa.a);
return 0;
}
让我们来看看这个简单对象构造的汇编代码:
080485fb <main>:
80485fb: 8d 4c 24 04 lea 0x4(%esp),%ecx
80485ff: 83 e4 f0 and $0xfffffff0,%esp
8048602: ff 71 fc pushl -0x4(%ecx)
8048605: 55 push %ebp
8048606: 89 e5 mov %esp,%ebp //main函数的堆栈开始;
8048608: 51 push %ecx //累加寄存器
8048609: 83 ec 14 sub $0x14,%esp //预留20字节空间
804860c: 65 a1 14 00 00 00 mov %gs:0x14,%eax //这条指令暂时没懂
8048612: 89 45 f4 mov %eax,-0xc(%ebp)
8048615: 31 c0 xor %eax,%eax //寄存器清零
8048617: 83 ec 04 sub $0x4,%esp //调整栈顶指针
804861a: 6a 02 push $0x2 //因为要调用构造函数,这里将构造函数的参数2入栈,从右向左
804861c: 6a 01 push $0x1 //参数1入栈
804861e: 8d 45 ec lea -0x14(%ebp),%eax //对象aa的地址,位于ebp寄存器的下方第20字节的地方
8048621: 50 push %eax //将对象aa的地址入栈
8048622: e8 91 00 00 00 call 80486b8 <_ZN2cyC1Eii> //调用类cy的构造函数来初始化对象aa
8048627: 83 c4 10 add $0x10,%esp //函数返回,调整栈顶指针
804862a: 8b 45 ec mov -0x14(%ebp),%eax //将对象aa的地址放入到eax寄存器
804862d: 83 ec 08 sub $0x8,%esp
8048630: 50 push %eax //将对象aa的地址入栈
8048631: 68 50 87 04 08 push $0x8048750 //将字符串常量%d\n的地址入栈
8048636: e8 95 fe ff ff call 80484d0 <printf@plt> //调用printf函数,这里是动态链接
804863b: 83 c4 10 add $0x10,%esp //调整栈顶指针
804863e: b8 00 00 00 00 mov $0x0,%eax
8048643: 8b 55 f4 mov -0xc(%ebp),%edx
8048646: 65 33 15 14 00 00 00 xor %gs:0x14,%edx
804864d: 74 05 je 8048654 <main+0x59>
804864f: e8 8c fe ff ff call 80484e0 <__stack_chk_fail@plt>
8048654: 8b 4d fc mov -0x4(%ebp),%ecx
8048657: c9 leave
8048658: 8d 61 fc lea -0x4(%ecx),%esp
804865b: c3 ret
以及cy对象的构造函数:
080486b8 <_ZN2cyC1Eii>:
80486b8: 55 push %ebp //上一级函数的栈底地址入栈
80486b9: 89 e5 mov %esp,%ebp //将栈底指针移到当前栈顶指针
80486bb: 8b 45 08 mov 0x8(%ebp),%eax //将相对与ebp指针向上偏移8个单位地址的值传送到eax寄存器,因为此时ebp之上的内容
//分别为oldebp(函数的返回地址上一级栈帧的ebp,-0x0(%ebp)),ret(函数的返回地址,-0x04(%ebp),aa对象的地址(-0x08(%ebp)),所以这条指令的意思就是将aa对
//象的地址放到eax寄存器
80486be: 8b 55 0c mov 0xc(%ebp),%edx //将入栈的1的值传送到edx寄存器
80486c1: 89 10 mov %edx,(%eax) //将1存放到aa对象的前4个字节中
80486c3: 8b 45 08 mov 0x8(%ebp),%eax //将aa对象地址再次放到eax寄存器
80486c6: 8b 55 10 mov 0x10(%ebp),%edx //将2的值放到edx寄存器
80486c9: 89 50 04 mov %edx,0x4(%eax) //将寄存器edx的值放到相对域aa地址向高地址方向偏移4个字节的地址中(就是aa对象 //成员b的地址)
80486cc: 90 nop
80486cd: 5d pop %ebp
80486ce: c3 ret
80486cf: 90 nop
2.以对象为参数,返回一个整形值:
class cy{
public:
int a;
int b;
int add(int x,int y){
int sum;
sum=a+b;
return sum;
}
cy(int m,int n):a(m),b(n){}
};
int add(cy tmp){
int sum=0;
sum=tmp.a+tmp.b;
return sum;
}
int main(){
int sum;
cy aa(1,2);
sum=add(aa);
printf("%d\n",sum);
printf("%p\n",&aa);
return 0;
}
主要汇编码如下:
8048622: 55 push %ebp
8048623: 89 e5 mov %esp,%ebp
8048625: 51 push %ecx
8048626: 83 ec 14 sub $0x14,%esp
8048629: 65 a1 14 00 00 00 mov %gs:0x14,%eax
804862f: 89 45 f4 mov %eax,-0xc(%ebp)
8048632: 31 c0 xor %eax,%eax
8048634: 83 ec 04 sub $0x4,%esp
8048637: 6a 02 push $0x2
8048639: 6a 01 push $0x1
804863b: 8d 45 ec lea -0x14(%ebp),%eax //类对象aa的地址
804863e: 50 push %eax
804863f: e8 b8 00 00 00 call 80486fc <_ZN2cyC1Eii> //调用类cy构造函数
8048644: 83 c4 10 add $0x10,%esp
8048647: 83 ec 08 sub $0x8,%esp
804864a: ff 75 f0 pushl -0x10(%ebp) //将2的值入栈
804864d: ff 75 ec pushl -0x14(%ebp) //将1的值入栈,那么相当与对象aa入栈
8048650: e8 a6 ff ff ff call 80485fb <_Z3add2cy>
8048655: 83 c4 10 add $0x10,%esp
8048658: 89 45 e8 mov %eax,-0x18(%ebp) //eax保存返回值,然后将返回值赋值sum,课件变量sum的地址为-0x18(%ebp)
我们可以看到类对象作参数时,将对象的每个变量的值依次入栈。