|
|
作者 陈辉东
介绍
本文以一个简单的C++程序的汇编代码作为分析对象,通过该汇编代码我们可以窥视一下C++的函数,变量等成员的内存安排和编译系统的实现机制.
正文
本文以一个简单的C++程序的汇编代码作为分析对象,通过该汇编代码我们可以窥视一下C++的函数,变量等成员的内存安排和编译系统的实现机制.
下面是本文所用到的C++代码:
下面是该代码编译后生成的汇编代码:
TITLE E:\pracise\Base\CSimple.cpp
.386P
include listing.inc
if @Version gt 510
.model FLAT
else
_TEXT SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT ENDS
_DATA SEGMENT DWORD USE32 PUBLIC 'DATA'
_DATA ENDS
CONST SEGMENT DWORD USE32 PUBLIC 'CONST'
CONST ENDS
_BSS SEGMENT DWORD USE32 PUBLIC 'BSS'
_BSS ENDS
$$SYMBOLS SEGMENT BYTE USE32 'DEBSYM'
$$SYMBOLS ENDS
$$TYPES SEGMENT BYTE USE32 'DEBTYP'
$$TYPES ENDS
_TLS SEGMENT DWORD USE32 PUBLIC 'TLS'
_TLS ENDS
; COMDAT ?set_age@IT@@QAEXH@Z
_TEXT SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT ENDS
; COMDAT ?get_age@IT@@QAEHXZ
_TEXT SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT ENDS
; COMDAT _main
_TEXT SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT ENDS
FLAT GROUP _DATA, CONST, _BSS
ASSUME CS: FLAT, DS: FLAT, SS: FLAT
endif
PUBLIC ?get_age@IT@@QAEHXZ ; IT::get_age
; COMDAT ?get_age@IT@@QAEHXZ
_TEXT SEGMENT
_this$ = -4
?get_age@IT@@QAEHXZ PROC NEAR ; IT::get_age, COMDAT
; 18 : {
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
mov DWORD PTR _this$[ebp], ecx
; 19 : return m_age;
mov eax, DWORD PTR _this$[ebp]
mov eax, DWORD PTR [eax]
; 20 : }
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
?get_age@IT@@QAEHXZ ENDP ; IT::get_age
_TEXT ENDS
PUBLIC ?set_age@IT@@QAEXH@Z ; IT::set_age
PUBLIC _main
EXTRN __chkesp:NEAR
; COMDAT _main
_TEXT SEGMENT
_dongdong$ = -4
_main PROC NEAR ; COMDAT
; 23 : {
push ebp
mov ebp, esp
sub esp, 68 ; 00000044H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-68]
mov ecx, 17 ; 00000011H
mov eax, -858993460 ; ccccccccH
rep stosd
; 24 : IT dongdong;
; 25 : dongdong.set_age(4);
push 4
lea ecx, DWORD PTR _dongdong$[ebp]
call ?set_age@IT@@QAEXH@Z ; IT::set_age
; 26 : dongdong.get_age();
lea ecx, DWORD PTR _dongdong$[ebp]
call ?get_age@IT@@QAEHXZ ; IT::get_age
; 27 : return 0;
xor eax, eax
; 28 : }
pop edi
pop esi
pop ebx
add esp, 68 ; 00000044H
cmp ebp, esp
call __chkesp
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; COMDAT ?set_age@IT@@QAEXH@Z
_TEXT SEGMENT
_age$ = 8
_this$ = -4
?set_age@IT@@QAEXH@Z PROC NEAR ; IT::set_age, COMDAT
; 12 : void set_age(int age){m_age = age;}
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
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR _age$[ebp]
mov DWORD PTR [eax], ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 4
?set_age@IT@@QAEXH@Z ENDP ; IT::set_age
_TEXT ENDS
END
下面,我们来分析该汇编代码。
首先是汇编代码的标题版本等。如下:
第1行指定本汇编程序的标题,此处用该程序全路径的名称作为标题;
第2行”.386”告诉编译器,程序要用到80386的指令集;
第3行是将listing.inc包含进来;
接下来的代码(4-32行)进行编译器版本判断,如果版本大于5.1,则编译模式为FLAT模式,否则得进行段的定义,并指定段_DATA,CONST,_BSS在组FLAT中,同时设置CS,DS等段寄存器的值为FLAT的起始地址。
所谓FLAT模式,即平坦模式,是Win32平台中特有的内存模式。在这种模式下,段的基地址用32位来表示,故一个段的大小为4G。由于Win32平台中只有这种内存模式,因此不存在段的区别,所有的应用程序都在这4G的空间上运行,不过windows将它们放在各自独立的虚拟地址空间上运行,每一个程序都可以拥有4G空间的寻址能力。在平坦模式下,汇编代码中的段只是表明这个大的共享段中的一个内存区域。这些区域包括数据段(_DATA, _BSS),代码段(_TEXT),静态数据段(CONST),堆栈段(STACK)等。
下面我们从程序的入口地址main来开始分析:
" UBLIC _main"指明main是全局标志符,可以被其他外部模块调用;
"EXTRN __chkesp"指明chkesp已经在别的模块中定义;
"_TEXT SEGMENT"指明下面是代码段开始;"_dongdong$ = -4"指对象dongdong的位置为ebp-4;
"push ebp"保存当前环境,
"mov ebp, esp"将当前栈顶指针赋给ebp,
"sub esp, 68"将栈顶指针上移68个字节(中间空出的这些地址是给main里的变量预留的);
"push ebx"…保存当前寄存器的内容;
"lea edi, DWORD PTR [ebp-68]”获取指令"sub esp, 68"执行后esp所指的地址,存入edi中;
"mov ecx,17"..是对几个寄存器进行初始化;
"push 4"将4入栈;
"lea ecx,DWORD PTR _dongdong$ [ebp]",将对象dongdong所在的地址存入ecx中;
"call ?set_age@IT@@QAEXH@Z"调用set_age函数。
现在来看函数set_age的汇编代码:
“_age $ = 8” “_this $ = -4”表明变量age存放在ebp+8处,当前对象指针地址存放在ebp-4处;
行128-138与函数main类似,先保存当前环境,对寄存器赋初值;
“pop ecx”此处ecx就是前面main中对象dongdong的地址;
“mov DWORD PTR _this$[ebp], ecx”将对象dongdong的地址存入_this$[ebp]中(就是前面声明的用来存放对象地址的);
“mov eax, DWORD PTR _this$[ebp]” ” mov ecx, DWORD PTR _age$[ebp]”
将对象的地址存入eax中,age的地址存入ecx中;
“mov DWORD PTR [eax], ecx”将ecx的内容存入eax所指的地址中去,即将age的值赋给m_age.怎么知道是m_age呢?因为m_age是对象dongdong中的第一个成员变量,dongdong的首地址就是m_age的首地址。
接下去是出栈,函数返回。
函数get_age的执行过程和set_age差不多,这里不再赘述。
看了上面的分析,也许你搞晕了。其实,过程并不复杂:
比如在main里,步骤总结如下:
1. 保存ebp(ebp入栈);
2. ebp指向esp所指;
3. esp指向esp-68位置(为变量腾出空间);
4. 几个寄存器(ebx..)入栈;
5. edi指向3中的esp的位置;
6. 初始化几个寄存器(ecx,eax..);
7. 4压入栈;
8. 将对象dongdong的地址存入ecx中
9. 调用函数set_age.
然后系统转入函数set_age中,与上面类似,函数set_age开始也是进行环境地址的保存和寄存器的初始化,之后在获得对象的地址,再进行赋值。
同时,从上面汇编代码可以看出,程序的代码存储在代码段_TEXT中,一般变量在堆栈中。一个类的对象声明后,其地址位于堆栈中,对象的数据成员存放的地址紧随在对象之后。
|
|