从汇编看c++的虚拟继承以及其内存布局
作者: 发布时间:[ 2013/7/2 10:15:42 ] 推荐标签:
先看第一种简单的情形,所有类中没有任何虚函数的菱形继承。
下面是c++源码:
class Top {//虚基类
public:
int i;
Top(int ii) {
i = ii;
}
};
class Left : public virtual Top {
public:
int j;
Left(int jj, int ii) : Top(ii) {
j = jj;
}
};
class Right : public virtual Top {
public:
int k;
Right(int kk, int ii) : Top(ii) {
k = kk;
}
};
class Bottom : public Left, public Right {
public:
int l;
Bottom(int ll, int jj, int kk, int ii) : Top(ii), Left(jj, ii), Right(kk, ii) {
l = ll;
}
};
int main() {
Bottom b(1, 2, 3, 4);
Bottom* bp = &b;
//访问自身成员变量
b.l = 1;
bp->l = 2;
//访问父类Left的成员变量
Left* lp = bp;
b.j = 1;
bp->j = 2;
lp->j = 3;
//访问父类Right的成员变量
Right* rp = bp;
b.k = 1;
bp->k = 2;
rp->k = 3;
//访问虚基类Top的成员变量
Top* tp = bp;
b.i = 1;
bp->i = 2;
tp->i = 3;
};
让我们来看看,汇编代码里面是怎样的情形,先看main函数里面的汇编码:
; 33 : int main() {
push ebp
mov ebp, esp
sub esp, 48 ; 为对象程序所需变量预留空间,其中对象b只栈24byte
; 34 : Bottom b(1, 2, 3, 4);
push 1;压入标志1,作为判断是否调用虚基类构造函数的依据 1表示调用,0表示不调用
push 4;压栈4,为对象b的构造函数传递参数
push 3;压栈3,为对象b的构造函数传递参数
push 2;压栈2,为对象b的构造函数传递参数
push 1;压栈1,为对象b的构造函数传递参数
lea ecx, DWORD PTR _b$[ebp];获取对象b的首地址,传给寄存器ecx,作为隐含参数传递给对象b的构造函数
call ??0Bottom@@QAE@HHHH@Z ; 调用对象b的构造函数
; 35 : Bottom* bp = &b;
lea eax, DWORD PTR _b$[ebp];将对象b的首地址给寄存器eax
mov DWORD PTR _bp$[ebp], eax;将对象b的首地址给指针变量bp
; 36 :
; 37 : b.l = 1;
mov DWORD PTR _b$[ebp+16], 1;将1写入偏移对象首地址16字节处内存,即为对象b的成员变量l赋值1
; 38 : bp->l = 2;
mov ecx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器ecx
mov DWORD PTR [ecx+16], 2;将2写入偏移对象首地址16字节处内存,即为对象b的成员变量l赋值2
;可以看到,无论是用对象本身,还是对象指针访问对象b的成员变量
;其成员变量的偏移量都在编译期固定了,为16字节
;且两种方式访问没有差别
; 39 :
; 40 : Left* lp = bp;
mov edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
mov DWORD PTR _lp$[ebp], edx;将对象b的首地址给对象指针lp,此时lp指向父对象Left的首地址
;从下面的内存布局图可以看到,父对象Left的首地址和Bottom一样
; 41 : b.j = 1;
mov DWORD PTR _b$[ebp+4], 1;将1赋给偏移对象b首地址4byte处内存,即为
;继承来的成员变量j赋值1
; 42 : bp->j = 2;
mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov DWORD PTR [eax+4], 2;将2写入偏移对象b首地址4byte处内存,即为继承来的成员变量j赋值2
;可以看到,无论使用b对象本身,还是指针访问继承来的成员变量j,
;其成员变量的偏移量都是编译器固定了,都为4byte
;且两种方式访问无差别
; 43 : lp->j = 3;
mov ecx, DWORD PTR _lp$[ebp];将父类Left对象的首地址给寄存器ecx
mov DWORD PTR [ecx+4], 3;将3赋给偏移Left对象首地址4byte处内存,即为父对象Left的成员变量j赋值3
;可以看到,用这种方式访问父对象Left的成员变量,其偏移量也是编译器固定
;为4byte
; 44 :
; 45 : Right* rp = bp;
cmp DWORD PTR _bp$[ebp], 0;比较指针的值是否为0,也是判断bp是否为空指针
je SHORT $LN3@main处执行,否则顺序执行,这里是顺序执行
mov edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
add edx, 8;寄存器edx里面的内容加8,现在edx里面保存的地址偏移了对象b的首地址8byte,即指向了对象Right的首地址
mov DWORD PTR tv90[ebp], edx;将寄存器edx内容存入临时变量tv90
jmp SHORT $LN4@main;跳转到标号$LN4@main处执行
$LN3@main:
mov DWORD PTR tv90[ebp], 0;将临时变量tv90赋值为空指针,这是在上面判断bp指针为空的情况下执行,这里不执行这一句
$LN4@main:
mov eax, DWORD PTR tv90[ebp];将临时变量tv90里面的值赋给寄存器eax,eax保存了对象Right的首地址
mov DWORD PTR _rp$[ebp], eax;将寄存器eax里面的值赋给指针rp
;到这里,完成了从指针bp到指针rp的转化,这里之所以有对bp指针为空的判断
;是因为,rp里面的地址值是由bp里面的地址值加8byte得来,如果不进行判断,一旦bp为空指针
;即bp不指向任何对象,那么rp将指向错误的内存,这种转换有危险,编译器必须避免这种情况
; 46 : b.k = 1;
mov DWORD PTR _b$[ebp+12], 1;将1写入偏移对象b首地址12byte处,即将1赋给继承来的成员变量k
; 47 : bp->k = 2;
mov ecx, DWORD PTR _bp$[ebp];将对象b首地址给寄存器ecx
mov DWORD PTR [ecx+12], 2;将2写入偏移对象b首地址12byte处,即将2赋给继承来的成员变量k
;可以看到,这里其成员变量的偏移量也是编译器固定,为2byte
;且两种方式访问没有差别
; 48 : rp->k = 3;
mov edx, DWORD PTR _rp$[ebp];将父对象Right首地址给寄存器edx
mov DWORD PTR [edx+4], 3;将3写给偏移父对象Right首地址4byte处,即将3赋给成员变量k
; 49 :
; 50 : Top* tp = bp;
cmp DWORD PTR _bp$[ebp], 0;比较bp指针的值是否为0,也是判断bp是否为空,原因同上
jne SHORT $LN5@main;如果不为空,跳转到标号处$LN5@main执行,否则顺序执行,这里跳转到标号处执行
mov DWORD PTR tv145[ebp], 0;如果bp为空指针,将0赋给临时变量tv145,这里不执行这一句
jmp SHORT $LN6@main;跳转到标号处$LN6@main执行
$LN5@main:
mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov ecx, DWORD PTR [eax];将对象b首地址里面的内容给寄存器ecx,对象b首地址处的值是vtable的地址,关于vtable将在下面解释
mov edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
add edx, DWORD PTR [ecx+4];ecx里面存有vtable的首地址,这里取偏移vtable首地址4byte处内存内容(即对象b,或者父对象Left首地址到虚基类首地址的偏移量), 然后加上对象b的首地址
;得到虚基类对象Top的首地址
mov DWORD PTR tv145[ebp], edx;寄存器edx里面保存虚基类对象Top的首地址,保存到临时变量tv145里面
$LN6@main:
mov eax, DWORD PTR tv145[ebp];将临时变量tv145里面的值给寄存器eax
mov DWORD PTR _tp$[ebp], eax;寄存器eax里面含有虚基类对象Top首地址,给指针tp
;这里完成了从指针bp到tp的转换
; 51 : b.i = 1;
mov ecx, DWORD PTR _b$[ebp];将对象b的首地址的内容给寄存器ecx,ecx里面是vtable的首地址
mov edx, DWORD PTR [ecx+4];取偏移vtable首地址4byte处的内容,即对象b首地址到虚基类Top首地址偏移量给寄存器edx
mov DWORD PTR _b$[ebp+edx], 1;将对象首地址加上edx里面的偏移量,得到虚基类Top首地址,将1写入这给地址所指内存,ji
;为继承自虚基类的成员变量i赋值
; 52 : bp->i = 2;
mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov ecx, DWORD PTR [eax];将对象b首地址处内容给寄存器ecx,即将vtable的首地址给寄存器ecx
mov edx, DWORD PTR [ecx+4];将偏移vtable首地址4byte处内存内容给寄存器edx,即将对象b首地址到虚基类Top首地址偏移量给edx
mov eax, DWORD PTR _bp$[ebp];将对象b首地址给寄存器eax
mov DWORD PTR [eax+edx], 2;将对象b首地址加上刚才取出的偏移量,得到虚基类Top的首地址,将2写入改地址所处内存处,
;即为继承自虚基类的成员变量i赋值2
; 53 : tp->i = 3;
mov ecx, DWORD PTR _tp$[ebp];将虚基类对象Top的首地址给寄存器ecx
mov DWORD PTR [ecx], 3;将3赋给虚基类对象Top首地址处内才能,即为成员变量i赋值
; 54 :
; 55 : };
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11