您的位置:首页 > 编程语言 > C语言/C++

通过汇编代码分析C++的动态联编机制

2007-08-12 10:19 423 查看
C++中的一大法宝就是虚函数,简单来说就是加virtual关键字定义的函数。
其特性就是支持动态联编。现在C++开发的大型软件中几乎已经离不开虚函数的
使用,一个典型的例子就是虚函数是MFC的基石之一。

这里有两个概念需要先解释:

静态联编:通俗点来讲就是程序编译时确定调用目标的地址。
动态联编:程序运行阶段确定调用目标的地址。

在C++中通常的函数调用都是静态联编,但如果定义函数时加了virtual关键
字,并且在调用函数时是通过指针或引用调用,那么此时就是采用动态联编。

一个简单例子:
// test.cpp
#include<iostream.h>
class ClassA
{
public:
int num1;
ClassA(){ num1=0xffff; };
virtual void test1(void){};
virtual void test2(void){};
};
ClassA objA,* pobjA;

int main(void)
{
pobjA=&objA;
objA.test1();
objA.test2();
pobjA->test1();
pobjA->test2();
return 0;
}

使用VC编译:
开一个命令行直接在命令行调用cl来编译: (如果你安装vc时没有选择注册环境
变量,那么先在命令行运行VC目录下bin/VCVARS32.BAT )

cl test.cpp /Fa
产生test.asm中间汇编代码

接下来就看看asm里有什么玄虚,分析起来有点长,要有耐心 !

我们来看看:

数据定义:

_BSS SEGMENT
?objA@@3VClassA@@A DQ 01H DUP (?) ;objA 64位
?pobjA@@3PAVClassA@@A DD 01H DUP (?) ;pobjA 一个地址32位
_BSS ENDS

看到objA为64位,里边存放了哪些内容呢? 接着看看构造函数:

_this$ = -4
??0ClassA@@QAE@XZ PROC NEAR ; ClassA::ClassA() 定义了一个变量 _this ?!
; File test.cpp
; Line 6
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx ; ecx 赋值给 _this ?? 不明白??

mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
; ClassA::`vftable'

; 前面的部分都是编译器加的东东,我们的赋值在这里

mov ecx, DWORD PTR _this$[ebp]
mov DWORD PTR [ecx+4], 65535 ;0xffff num1=0xffff;
; 看来 _this+4就是num1的地址

mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
??0ClassA@@QAE@XZ ENDP

那个_this和mov DWORD PTR _this$[ebp], ecx 让人比较郁闷了吧,不急看看何
处调用的构造函数:

_$E9 PROC NEAR
; File test.cpp
; Line 10
push ebp
mov ebp, esp
mov ecx, OFFSET FLAT:?objA@@3VClassA@@A
call ??0ClassA@@QAE@XZ ;call ClassA::ClassA()
pop ebp
ret 0
_$E9 ENDP

看,ecx指向objA的地址,通过赋值,那个_this就是objA的开始地址,其实CLASS中
的非静态方法编译器编译时都会自动添加一个this变量,并且在函数开始处把ecx
赋值给他,指向调用该方法的对象的地址 。

那么构造函数里的这两行又是干什么呢?
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
; ClassA::`vftable'

我们已经知道_this保存的为对象地址: &objA。 那么 eax = &objA
接着就相当于 ( * eax ) = OFFSET FLAT:??_7ClassA@@6B@

来看看 ??_7ClassA@@6B@ 是哪个道上混的:

CONST SEGMENT
??_7ClassA@@6B@
DD FLAT:?test1@ClassA@@UAEXXZ ; ClassA::`vftable'
DD FLAT:?test2@ClassA@@UAEXXZ
CONST ENDS

看来这里存放的就是test1(),test2()函数的入口地址 ! 那么这个赋值:
mov DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
; ClassA::`vftable'
就是在对象的起始地址填入这么一个地址列表的地址。

好了,至此我们已经看到了objA的构造了:

| 低地址 |
+--------+ ---> objA的起始地址 &objA
|pvftable|
+--------+-------------------------+
| num1 | num1变量的空间 |
+--------+ ---> objA的结束地址 +--->+--------------+ 地址表 vftable
| 高地址 | |test1()的地址 |
+--------------+
|test2()的地址 |
+--------------+

来看看main函数:
_main PROC NEAR
; Line 13
push ebp
mov ebp, esp
; Line 14
mov DWORD PTR ?pobjA@@3PAVClassA@@A,
OFFSET FLAT:?objA@@3VClassA@@A ; pobjA = &objA

; Line 15
mov ecx, OFFSET FLAT:?objA@@3VClassA@@A ; ecx = this指针
; 指向调用者的地址
call ?test1@ClassA@@UAEXXZ ; objA.test1()
; objA.test1()直接调用,已经确定了地址
; Line 16
mov ecx, OFFSET FLAT:?objA@@3VClassA@@A
call ?test2@ClassA@@UAEXXZ ; objA.test2()
; Line 17
mov eax, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
mov edx, DWORD PTR [eax] ; edx = vftable
mov ecx, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
call DWORD PTR [edx] ;
; call vftable[0] 即 pobjA->test1() 看地址是动态查找的 ; )

; Line 18
mov eax, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
call DWORD PTR [edx+4] ; pobjA->test2()
; call vftable[1] 而vftable[1]里存放的是test2()的入口地址
; Line 19
xor eax, eax
; Line 20
pop ebp
ret 0
_main ENDP

好了,相信到这里你已经对动态联编有了深刻印象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐