2006年7月17日

10:04:20

论调用约定

在C语言中,假设我们有这样的一个函数:

int function(int a,int b)

调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。

栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。

在参数传递中,有两个很重要的问题必须得到明确说明:

  • 当参数个数多于一个时,按照什么顺序把参数压入堆栈
  • 函数调用后,由谁来把堆栈恢复原装

在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

  • stdcall
  • cdecl
  • fastcall
  • thiscall
  • naked call

stdcall调用约定

stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。

stdcall调用约定声明的语法为(以前文的那个函数为例):

int __stdcall function(int a,int b)

stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成:

push 2 第二个参数入栈 push 1 第一个参数入栈 call function 调用参数,注意此时自动把cs:eip入栈

而对于函数自身,则可以翻译为:

push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复esp pop ebp ret 8

而在编译时,这个函数的名字被翻译成_function@8

注意不同编译器会插入自己的汇编代码以提供编译的通用性,但是大体代码如此。其中在函数开始处保留esp到ebp中,在函数结束恢复是编译器常用的方法。

从函数调用看,2和1依次被push进堆栈,而在函数中又通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数。函数结束后,ret 8表示清理8个字节的堆栈,函数自己恢复了堆栈。

cdecl调用约定

cdecl调用约定又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int function (int a ,int b) //不加修饰就是C调用约定 int __cdecl function(int a,int b)//明确指出C调用约定

在写本文时,出乎我的意料,发现cdecl调用约定的参数压栈顺序是和stdcall是一样的,参数首先由有向左压入堆栈。所不同的是,函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的,这也是C语言的一大特色。对于前面的function函数,使用cdecl后的汇编码变成:

调用处 push 1 push 2 call function add esp,8 注意:这里调用者在恢复堆栈被调用函数_function处 push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复esp pop ebp ret 注意,这里没有修改堆栈

MSDN中说,该修饰自动在函数名前加前导的下划线,因此函数名在符号表中被记录为_function,但是我在编译时似乎没有看到这种变化。

由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数,例如对于CRT中的sprintf函数,定义为:

int sprintf(char* buffer,const char* format,...)

由于所有的不定参数都可以通过format确定,因此使用不定个数的参数是没有问题的。

fastcall

fastcall调用约定和stdcall类似,它意味着:

  • 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
  • 被调用函数清理堆栈
  • 函数名修改规则同stdcall

其声明语法为:int fastcall function(int a,int b)

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:

  • 参数从右向左入栈
  • 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。
  • 对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈

为了说明这个调用约定,定义如下类和使用代码:

class A { public:    int function1(int a,int b);    int function2(int a,...); }; int A::function1 (int a,int b) {    return a+b; } #include  int A::function2(int a,...) {    va_list ap;    va_start(ap,a);    int i;    int result = 0;    for(i = 0 ; i < a ; i ++)    {      result += va_arg(ap,int);    }    return result; } void callee() {    A a;    a.function1 (1,2);    a.function2(3,1,2,3); } 

callee函数被翻译成汇编后就变成:

//函数function1调用 0401C1D push 2 00401C1F push 1 00401C21 lea ecx,[ebp-8] 00401C24 call function1 注意,这里this没有被入栈 //函数function2调用 00401C29 push 3 00401C2B push 2 00401C2D push 1 00401C2F push 3 00401C31 lea eax,[ebp-8] 这里引入this指针 00401C34 push eax 00401C35 call function2 00401C3A add esp,14h

可见,对于参数个数固定情况下,它类似于stdcall,不定时则类似cdecl

naked call

这是一个很少见的调用约定,一般程序设计者建议不要使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计,假设定义一个求和的加法程序,可以定义为:

__declspec(naked) int add(int a,int b) { __asm mov eax,a __asm add eax,b __asm ret } 

注意,这个函数没有显式的return返回值,返回通过修改eax寄存器实现,而且连退出函数的ret指令都必须显式插入。上面代码被翻译成汇编以后变成:

mov eax,[ebp+8] add eax,[ebp+12] ret 8

注意这个修饰是和__stdcall及cdecl结合使用的,前面是它和cdecl结合使用的代码,对于和stdcall结合的代码,则变成:

__declspec(naked) int __stdcall function(int a,int b) { __asm mov eax,a __asm add eax,b __asm ret 8 //注意后面的8 } 

至于这种函数被调用,则和普通的cdecl及stdcall调用函数一致。

函数调用约定导致的常见问题

如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:

  1. 函数原型声明和函数体定义不一致
  2. DLL导入函数时声明了不同的函数约定

以后者为例,假设我们在dll种声明了一种函数为:

__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是cdecl

使用时代码为:

 typedef int (*WINAPI DLLFUNC)func(int a,int b); hLib = LoadLibrary(...); DLLFUNC func = (DLLFUNC)GetProcAddress(...)//这里修改了调用约定 result = func(1,2);//导致错误 

由于调用者没有理解WINAPI的含义错误的增加了这个修饰,上述代码必然导致堆栈被破坏,MFC在编译时插入的checkesp函数将告诉你,堆栈被破坏了。

posted @ 2006-07-17 10:06 逃逃 阅读(298) | 评论 (1)编辑 收藏


2006年5月22日

1、文件操作的方法

   使用Visual C++编程,有如下方法进行文件操作:

(1)使用标准C运行库函数,包括fopen、fclose、fseek等。

   (2)使用Win16下的文件和目录操作函数,如lopen、lclose、lseek等。不过,在Win32下,这些函数主要是为了和Win16向后兼容。

   (3)使用Win32下的文件和目录操作函数,如CreateFile,CopyFile,DeleteFile,FindNextFile,等等。

      Win32下,打开和创建文件都由CreateFile完成,成功的话,得到一个Win32下的句柄,这不同于“C”的fopen返回的句柄。在Win16下,该句柄和C运行库文件操作函数相容。但在Win32下,“C”的文件操作函数不能使用该句柄,如果需要的话,可以使用函数_open_osfhandle从Win32句柄得到一个“C”文件函数可以使用的文件句柄。 关闭文件使用Win32的CloseHandle。 在Win32下,CreateFile可以操作的对象除了磁盘文件外,还包括设备文件如通讯端口、管道、控制台输入、邮件槽等等。

(4)使用CFile和其派生类进行文件操作。CFile从CObject派生,其派生类包括操作文本文件的CStdioFile,操作内存文件的CmemFile,等等。CFile是建立在Win32的文件操作体系的基础上,它封装了部分Win32文件操作函数。最好是使用CFile类(或派生类)的对象来操作文件,必要的话,可以从这些类派生自己的文件操作类。统一使用CFile的界面可以得到好的移植性。


2、文件操作的方法
MFC用一些类来封装文件访问的Win32 API。以CFile为基础,从CFile派生出几个类,如CStdioFile,CMemFile,MFC内部使用的CMiororFile,等等。

2.1、CFile的结构

     2.1.1、CFile定义的枚举类型

               CFile类定义了一些和文件操作相关的枚举类型,主要有四种:OpenFlags,Attribute,SeekPosition,hFileNull。下面,分别解释这些枚举类型。

  1. OpenFlags

    OpenFlags定义了13种文件访问和共享模式:

    enum OpenFlags {

    //第一(从右,下同)至第二位,打开文件时访问模式,读/写/读写

    modeRead = 0x0000,

    modeWrite = 0x0001,

    modeReadWrite = 0x0002,

    shareCompat = 0x0000, //32位MFC中没用

    //第五到第七位,打开文件时的共享模式

    shareExclusive = 0x0010,//独占方式,禁止其他进程读写

    shareDenyWrite = 0x0020,//禁止其他进程写

    shareDenyRead = 0x0030,//禁止其他进程读

    shareDenyNone = 0x0040,//允许其他进程写

    //第八位,打开文件时的文件继承方式

    modeNoInherit = 0x0080,//不允许子进程继承

    //第十三、十四位,是否创建新文件和创建方式

    modeCreate = 0x1000,//创建新文件,文件长度0

    modeNoTruncate = 0x2000,//创建新文件时如文件已存在则打开

    //第十五、十六位,文件以二进制或者文本方式打开,在派生类CStdioFile中用

    typeText = 0x4000,

    typeBinary = (int)0x8000

    };

  2. Attribute

    Attribute定义了文件属性:正常、只读、隐含、系统文件,文件或者目录等。

    enum Attribute {

    normal = 0x00,

    readOnly = 0x01,

    hidden = 0x02,

    system = 0x04,

    volume = 0x08,

    directory = 0x10,

    archive = 0x20

    }

  3. SeekPosition

    SeekPosition定义了三种文件位置:头、尾、当前:

    enum SeekPosition{

    begin = 0x0,

    current = 0x1,

    end = 0x2

    };

  4. hFileNull

hFileNull定义了空文件句柄

enum { hFileNull = -1 };

2.1.2、CFile的其他一些成员变量

CFile除了定义枚举类型,还定义了一些成员变量。例如:

UINT m_hFile

该成员变量是public访问属性,保存::CreateFile返回的操作系统的文件句柄。MFC重载了运算符号HFILE来返回m_hFile,这样在使用HFILE类型变量的地方可以使用CFile对象。

BOOL m_bCloseOnDelete;

CString m_strFileName;

这两个成员变量是protected访问属性。m_bCloseOnDelete用来指示是否在关闭文件时删除CFile对象;m_strFileName用来保存文件名。

2.1.3、CFile的成员函数

CFile的成员函数实现了对Win32文件操作函数的封装,完成以下动作:打开、创建、关闭文件,文件指针定位,文件的锁定与解锁,文件状态的读取和修改,等等。其中,用到了m_hFile文件句柄的一般是虚拟函数,和此无关的一般是静态成员函数。一般地,成员函数被映射到对应的Win32函数,如表11-1所示。

表11-1 CFile函数对Win32文件函数的封装

虚拟

静态

成员函数

对应的Win32函数

文件的创建、打开、关闭

 

Abort

CloseHandle

 

Duplicate

DuplicateHandle

 

Open

CreateFile

 

Close

CloseHandle

文件的读写

 

Read

ReadFile

   

ReadHuge(向后兼容)

调用Read成员函数

 

Write

WriteFile

   

WriteHuage(向后兼容)

调用Write成员函数

 

Flush

FlushFileBuffers

文件定位

 

Seek

SetFilePointer

   

SeekToBegin

调用Seek成员函数

   

SeekToEnd

调用Seek成员函数

 

GetLength

调用Seek成员函数

 

SetLength

SetEndOfFile

文件的锁定/解锁

 

LockRange

LockFile

 

UnlockRange

UnlockFile

文件状态操作函数

 

GetPosition

SetFilePointer

   

GetStatus(CFileStatus&)

GetFileTime,GetFileSize等

 

GetStatus(LPSTR lpszFileName CFileStatus&)

FindFirstFile

 

GetFileName

不是简单地映射到某个函数

 

GetFileTitle

 

 

GetFilePath

 

 

SetFilePath

 
 

SetStatus

 

改名和删除

 

Rename

MoveFile

 

Remove

DeleteFile


2.1.4、CFile的部分实现


这里主要讨论CFile对象的构造函数和文件的打开/创建的过程。

  1. 构造函数

CFile有如下几个构造函数:

  • CFile()

缺省构造函数,仅仅构造一个CFile对象,还必须使用Open成员函数来打开文件。

  • CFile(int hFile)

已经打开了一个文件hFile,在此基础上构造一个CFile对象来给它打包。HFile将被赋值给CFile的成员变量m_hFile。

  • CFile(LPCTSTR lpszFileName, UINT nOpenFlags)

指定一个文件名和文件打开方式,构造CFile对象,调用Open打开/创建文件,把文件句柄保存到m_hFile。

  1. 打开/创建文件

Open的原型如下:

BOOL CFile::Open(LPCTSTR lpszFileName, UINT nOpenFlags,

CFileException* pException)

Open调用Win32函数::CreateFile打开文件,并把文件句柄保存到成员变量m_hFile中。

CreateFile函数的原型如下:

HANDLE CreateFile(

LPCTSTR lpFileName,// pointer to name of the file

DWORD dwDesiredAccess,// access (read-write) mode

DWORD dwShareMode,// share mode

LPSECURITY_ATTRIBUTES lpSecurityAttributes, //pointer to security descriptor

DWORD dwCreationDistribution,// how to create

DWORD dwFlagsAndAttributes,// file attributes

HANDLE hTemplateFile// handle to file with attributes to copy

);

显然,Open必须把自己的两个参数lpszFileName和nOpenFlags映射到CreateFile的七个参数上。

从OpenFlags的定义可以看出,(nOpenFlags & 3)表示了读写标识,映射成变量dwAccess,可以取值为Win32的GENERIC_READ、GENERIC_WRITE、GENERIC_READ|GENERIC_WRITE。

(nOpenFlags & 0x70)表示了共享模式,映射成变量dwShareMode,可以取值为Win32的FILE_SHARE_READ、FILE_SHARE_WRITE、FILE_SHARE_WRITE|FILE_SHARE_READ。

Open定义了一个局部的SECURITY_ATTRIBUTES变量sa,(nOpenFlags & 0x80)被赋值给sa.bInheritHandle。

(nOpenFlags & modeCreate)表示了创建方式,映射成变量dwCreateFlag,可以取值为Win32的OPEN_ALWAYS、CREATE_ALWAYS、OPEN_EXISTING。

在生成了上述参数之后,先调用::CreateFile:

HANDLE hFile =::CreateFile(lpszFileName,

dwAccess, dwShareMode, &sa,

dwCreateFlag, FILE_ATTRIBUTE_NORMAL, NULL);

然后,hFile被赋值给成员变量m_hFile,m_bCloseOnDelete被设置为TRUE。

由上可以看出,CFile打开(创建)一个文件时大大简化了:: CreateFile函数的复杂性,即只需要指定一个文件名、一个打开文件的参数即可。若该参数指定为0,则表示以只读方式打开一个存在的文件,独占使用,不允许子进程继承。

在CFile对象使用时,如果它是在堆中分配的,则应该销毁它;如果在栈中分配的,则CFile对象将被自动销毁。销毁时析构函数被调用,析构函数是虚拟函数。若m_bCloseOnDelete为真且m_hFile非空,则析构函数调用Close关闭文件。

至于其他CFile成员函数的实现,这里不作分析了。

2.1.5、CFile的派生类

这里主要简要地介绍CStdioFile和CmemFile及CFileFind。

  1. CStdioFile

    CStdioFile对文本文件进行操作。

    CStdioFile定义了新的成员变量m_pStream,类型是FILE*。在打开或者创建文件时,使用_open_osfhandle从m_hFile(Win32文件句柄)得到一个“C”的FILE类型的文件指针,然后,在文件操作中,使用“C”的文件操作函数。例如,读文件使用_fread,而不是::ReadFile,写文件使用了_fwrite,而不是::WriteFile,等等。m_hFile是CFile的成员变量。

    另外,CStdioFile不支持CFile的Dumplicate、LockRange、UnlockRange操作,但是实现了两个新的操作ReadString和WriteString。

  2. CMemFile

    CMemFile把一块内存当作一个文件来操作,所以,它没有打开文件的操作,而是设计了Attach和Detach用来分配或者释放一块内存。相应地,它提供了Alloc、Free虚拟函数来操作内存文件,它覆盖了Read、Write来读写内存文件。

  3. CFileFind

为了方便文件查找,MFC把有关功能归结成为一个类CFileFind。CFileFind派生于CObject类。首先,它使用FindFile和FineNextFile包装了Win32函数::FindFirstFile和::FindNextFile;其次,它提供了许多函数用来获取文件的状态或者属性。

使用CFileStatus结构来描述文件的属性,其定义如下:

struct CFileStatus

{

CTime m_ctime; // 文件创建时间

CTime m_mtime; // 文件最近一次修改时间

CTime m_atime; // 文件最近一次访问时间

LONG m_size; // 文件大小

BYTE m_attribute; // 文件属性

BYTE _m_padding; // 没有实际含义,用来增加一个字节

TCHAR m_szFullName[_MAX_PATH]; //绝对路径

#ifdef _DEBUG

//实现Dump虚拟函数,输出文件属性

void Dump(CDumpContext& dc) const;

#endif

};

例如:

CFileStatus status;

pFile->GetStatus(status);

#ifdef _DEBUG

status.dump(afxDump);

#endif

 

posted @ 2006-05-22 16:13 逃逃 阅读(646) | 评论 (1)编辑 收藏


2006年4月17日

10.1内存分配 
  10.1.1 内存分配函数

MFCWin32或者C语言的内存分配API,有四种内存分配API可供使用。

  1. Win32的堆分配函数

    每一个进程都可以使用堆分配函数创建一个私有的堆──调用进程地址空间的一个或者多个页面。DLL创建的私有堆必定在调用DLL的进程的地址空间内,只能被调用进程访问。

    HeapCreate用来创建堆;HeapAlloc用来从堆中分配一定数量的空间,HeapAlloc分配的内存是不能移动的;HeapSize可以确定从堆中分配的空间的大小;HeapFree用来释放从堆中分配的空间;HeapDestroy销毁创建的堆。

  2. Windows传统的全局或者局部内存分配函数

    由于Win32采用平面内存结构模式,Win32下的全局和局部内存函数除了名字不同外,其他完全相同。任一函数都可以用来分配任意大小的内存(仅仅受可用物理内存的限制)。用法可以和Win16下基本一样。

    Win32下保留这类函数保证了和Win16的兼容。

  3. C语言的标准内存分配函数

    C语言的标准内存分配函数包括以下函数:

    malloc,calloc,realloc,free,等。

    这些函数最后都映射成堆API函数,所以,malloc分配的内存是不能移动的。这些函数的调式版本为

    malloc_dbg,calloc_dbg,realloc_dbg,free_dbg,等。

  4. Win32的虚拟内存分配函数

虚拟内存API是其他API的基础。虚拟内存API以页为最小分配单位,X86上页长度为4KB,可以用GetSystemInfo函数提取页长度。虚拟内存分配函数包括以下函数:

  • LPVOID VirtualAlloc(LPVOID lpvAddress,

DWORD cbSize,

DWORD fdwAllocationType,

DWORD fdwProtect);

该函数用来分配一定范围的虚拟页。参数1指定起始地址;参数2指定分配内存的长度;参数3指定分配方式,取值MEM_COMMINT或者MEM_RESERVE;参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、PAGE_READWRITE或者PAGE_NOACCESS。

  • LPVOID VirtualAllocEx(HANDLE process,

LPVOID lpvAddress,

DWORD cbSize,

DWORD fdwAllocationType,

DWORD fdwProtect);

该函数功能类似于VirtualAlloc,但是允许指定进程process。VirtaulFree、VirtualProtect、VirtualQuery都有对应的扩展函数。

  • BOOL VirtualFree(LPVOID lpvAddress,

DWORD dwSize,

DWORD dwFreeType);

该函数用来回收或者释放分配的虚拟内存。参数1指定希望回收或者释放内存的基地址;如果是回收,参数2可以指向虚拟地址范围内的任何地方,如果是释放,参数2必须是VirtualAlloc返回的地址;参数3指定是否释放或者回收内存,取值为MEM_DECOMMINT或者MEM_RELEASE。

  • BOOL VirtualProtect(LPVOID lpvAddress,

DWORD cbSize,

DWORD fdwNewProtect,

PDWORD pfdwOldProtect);

该函数用来把已经分配的页改变成保护页。参数1指定分配页的基地址;参数2指定保护页的长度;参数3指定页的保护属性,取值PAGE_READ、PAGE_WRITE、PAGE_READWRITE等等;参数4用来返回原来的保护属性。

  • DWORD VirtualQuery(LPCVOID lpAddress,

PMEMORY_BASIC_INFORMATION lpBuffer,

DWORD dwLength

);

该函数用来查询内存中指定页的特性。参数1指向希望查询的虚拟地址;参数2是指向内存基本信息结构的指针;参数3指定查询的长度。

  • BOOL VirtualLock(LPVOID lpAddress,DWORD dwSize);

该函数用来锁定内存,锁定的内存页不能交换到页文件。参数1指定要锁定内存的起始地址;参数2指定锁定的长度。

  • BOOL VirtualUnLock(LPVOID lpAddress,DWORD dwSize);

参数1指定要解锁的内存的起始地址;参数2指定要解锁的内存的长度。


C++的new 和 delete操作符

MFC定义了两种作用范围的new和delete操作符。对于new,不论哪种,参数1类型必须是size_t,且返回void类型指针。

  1. 全局范围内的new和delete操作符

    原型如下:

    void _cdecl ::operator new(size_t nSize);

    void __cdecl operator delete(void* p);

    调试版本:

    void* __cdecl operator new(size_t nSize, int nType,

    LPCSTR lpszFileName, int nLine)

  2. 类定义的new和delete操作符

原型如下:

void* PASCAL classname::operator new(size_t nSize);

void PASCAL classname::operator delete(void* p);

类的operator new操作符是类的静态成员函数,对该类的对象来说将覆盖全局的operator new。全局的operator new用来给内部类型对象(如int)、没有定义operator new操作符的类的对象分配内存。

new操作符被映射成malloc或者malloc_dbg,delete被映射成free或者free_dbg。



10.1.2调试手段

MFC应用程序可以使用C运行库的调试手段,也可以使用MFC提供的调试手段。两种调试手段分别论述如下。

  1. C运行库提供和支持的调试功能

    C运行库提供和支持的调试功能如下:

    1. 调试信息报告函数

      用来报告应用程序的调试版本运行时的警告和出错信息。包括:

      _CrtDbgReport 用来报告调试信息;

      _CrtSetReportMode 设置是否警告、出错或者断言信息;

      _CrtSetReportFile 设置是否把调试信息写入到一个文件。

    2. 条件验证或者断言宏:

      断言宏主要有:

      assert 检验某个条件是否满足,不满足终止程序执行。

      验证函数主要有:

      _CrtIsValidHeapPointer 验证某个指针是否在本地堆中;

      _CrtIsValidPointer 验证指定范围的内存是否可以读写;

      _CrtIsMemoryBlock 验证某个内存块是否在本地堆中。

    3. 内存(堆)调试:

    malloc_dbg 分配内存时保存有关内存分配的信息,如在什么文件、哪一行分配的内存等。有一系列用来提供内存诊断的函数:

    _CrtMemCheckpoint 保存内存快照在一个_CrtMemState结构中;

    _CrtMemDifference 比较两个_CrtMemState;

    _CrtMemDumpStatistics 转储输出一_CrtMemState结构的内容;

    _CrtMemDumpAllObjectsSince 输出上次快照或程序开始执行以来在堆中分配的所有对象的信息;

    _CrtDumpMemoryLeaks 检测程序执行以来的内存漏洞,如果有漏洞则输出所有分配的对象。

    2.      MFC提供的调试手段

    MFC在C运行库提供和支持的调试功能基础上,设计了一些类、函数等来协助调试。

    1. MFC的TRACE、ASSERT

      ASSERT

      使用ASSERT断言判定程序是否可以继续执行。

      TRACE

      使用TRACE宏显示或者打印调试信息。TRACE是通过函数AfxTrace实现的。由于AfxTrace函数使用了cdecl调用约定,故可以接受个数不定的参数,如同printf函数一样。它的定义和实现如下:

      void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...)

      {

      #ifdef _DEBUG // all AfxTrace output is controlled by afxTraceEnabled

      if (!afxTraceEnabled)

      return;

      #endif

      //处理个数不定的参数

      va_list args;

      va_start(args, lpszFormat);

      int nBuf;

      TCHAR szBuffer[512];

      nBuf = _vstprintf(szBuffer, lpszFormat, args);

      ASSERT(nBuf < _countof(szBuffer));

      if ((afxTraceFlags & traceMultiApp) && (AfxGetApp() != NULL))

      afxDump << AfxGetApp()->m_pszExeName << ": ";

      afxDump << szBuffer;

      va_end(args);

      }

      #endif //_DEBUG

      在程序源码中,可以控制是否显示跟踪信息,显示什么跟踪信息。如果全局变量afxTraceEnabled为TRUE,则TRACE宏可以输出;否则,没有TRACE信息被输出。如果通过afxTraceFlags指定了跟踪什么消息,则输出有关跟踪信息,例如为了指定“Multilple Application Debug”,令AfxTraceFlags|=traceMultiApp。可以跟踪的信息有:

      enum AfxTraceFlags

      {

      traceMultiApp = 1, // multi-app debugging

      traceAppMsg = 2, // main message pump trace (includes DDE)

      traceWinMsg = 4, // Windows message tracing

      traceCmdRouting = 8, // Windows command routing trace

      //(set 4+8 for control notifications)

      traceOle = 16, // special OLE callback trace

      traceDatabase = 32, // special database trace

      traceInternet = 64 // special Internet client trace

      };

      这样,应用程序可以在需要的地方指定afxTraceEnabled的值打开或者关闭TRACE开关,指定AfxTraceFlags的值过滤跟踪信息。

      Visual C++提供了一个TRACE工具,也可以用来完成上述功能。

      为了显示消息信息,MFC内部定义了一个AFX_MAP_MESSAG类型的数组allMessages,储存了Windows消息和消息名映射对。例如:

      allMessages[1].nMsg = WM_CREATE,

      allMessages[1].lpszMsg = “WM_CREATE”

      MFC内部还使用函数_AfxTraceMsg显示跟踪消息,它可以接收一个字符串和一个MSG指针,然后,把该字符串和MSG的各个域的信息组合成一个大的字符串并使用AfxTrace显示出来。

      allMessages和函数_AfxTraceMsg的详细实现可以参见AfxTrace.cpp。

    2. MFC对象内容转储

      对象内容转储是CObject类提供的功能,所有从它派生的类都可以通过覆盖虚拟函数DUMP来支持该功能。在讲述CObject类时曾提到过。

      虚拟函数Dump的定义:

      class ClassName : public CObject

      {

      public:

      #ifdef _DEBUG

      virtual void Dump( CDumpContext& dc ) const;

      #endif

      };

      在使用Dump时,必须给它提供一个CDumpContext类型的参数,该参数指定的对象将负责输出调试信息。为此,MFC提供了一个预定义的全局CDumpContext对象afxDump,它把调试信息输送给调试器的调试窗口。从前面AfxTrace的实现可以知道,MFC使用了afxDump输出跟踪信息到调试窗口。

      CDumpContext类没有基类,它提供了以文本形式输出诊断信息的功能。

      例如:

      CPerson* pMyPerson = new CPerson;

      // set some fields of the CPerson object...

      //...

      // now dump the contents

      #ifdef _DEBUG

      pMyPerson->Dump( afxDump );

      #endif

    3. MFC对象有效性检测

    对象有效性检测是CObject类提供的功能,所有从它派生的类都可以通过覆盖虚拟函数AssertValid来支持该功能。在讲述CObject类时曾提到过。

    虚拟函数AssertValid的定义:

    class ClassName : public CObject

    {

    public:

    #ifdef _DEBUG

    virtual void AssertValid( ) const;

    #endif

    };

    使用ASSERT_VALID宏判断一个对象是否有效,该对象的类必须覆盖了AssertValid函数。形式为:ASSERT_VALID(pObject)。

    另外,MFC提供了一些函数来判断地址是否有效,如:

    AfxIsMemoryBlock,AfxIsString,AfxIsValidAddress。

    10.1.3内存诊断

    MFC使用DEBUG_NEW来跟踪内存分配时的执行的源码文件和行数。

    把#define new DEBUG_NEW插入到每一个源文件中,这样,调试版本就使用_malloc_dbg来分配内存。MFC Appwizard在创建框架文件时已经作了这样的处理。

    1. AfxDoForAllObjects

      MFC提供了函数AfxDoForAllObjects来追踪动态分配的内存对象,函数原型如下:

      void AfxDoForAllObjects( void (*pfn)(CObject* pObject,

      void* pContext), void* pContext );

      其中:

      参数1是一个函数指针,AfxDoForAllObjects对每个对象调用该指针表示的函数。

      参数2将传递给参数1指定的函数。

      AfxDoForAllObjects可以检测到所有使用new分配的CObject对象或者CObject类派生的对象,但全局对象、嵌入对象和栈中分配的对象除外。

    10.1.4内存漏洞检测

    仅仅用于new的DEBUG版本分配的内存。

    完成内存漏洞检测,需要如下系列步骤:

    • 调用AfxEnableMemoryTracking(TRUE/FALSE)打开/关闭内存诊断。在调试版本下,缺省是打开的;关闭内存诊断可以加快程序执行速度,减少诊断输出。

    • 使用MFC全局变量afxMemDF更精确地指定诊断输出的特征,缺省值是allocMemDF,可以取如下值或者这些值相或:

    afxMemDF,delayFreeMemDF,checkAlwaysMemDF

    其中:allocMemDF表示可以进行内存诊断输出;delayFreeMemDF表示是否是在应用程序结束时才调用free或者delete,这样导致程序最大可能的分配内存;checkAlwaysMemDF表示每一次分配或者释放内存之后都调用函数AfxCheckMemory进行内存检测(AfxCheckMemory检查堆中所有通过new分配的内存(不含malloc))。

    这一步是可选步骤,非必须。

    • 创建一个CMemState类型的变量oldMemState,调用CMemState的成员函数CheckPoint获得初次内存快照。

    • 执行了系列内存分配或者释放之后,创建另一个CMemState类型变量newMemState,调用CMemState的成员函数CheckPoint获得新的内存快照。

    • 创建第三个CMemState类型变量difMemState,调用CMemState的成员函数Difference比较oldMemState和newMemState,结果保存在变量difMemState中。如果没有不同,则返回FALSE,否则返回TRUE。

    • 如果不同,则调用成员函数DumpStatistics输出比较结果。

    例如:

    // Declare the variables needed

    #ifdef _DEBUG

    CMemoryState oldMemState, newMemState, diffMemState;

    oldMemState.Checkpoint();

    #endif

    // do your memory allocations and deallocations...

    CString s = "This is a frame variable";

    // the next object is a heap object

    CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );

    #ifdef _DEBUG

    newMemState.Checkpoint();

    if( diffMemState.Difference( oldMemState, newMemState ) )

    {

    TRACE( "Memory leaked!\n" );

    diffMemState.DumpStatistics();

    //or diffMemState.DumpAllObjectsSince();

    }

    #endif

    MFC在应用程序(调试版)结束时,自动进行内存漏洞检测,如果存在漏洞,则输出漏洞的有关信息。

posted @ 2006-04-17 16:09 逃逃 阅读(638) | 评论 (2)编辑 收藏


仅列出标题  

posts - 3, comments - 4, trackbacks - 0, articles - 0

Copyright © 逃逃