首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 操作系统 > windows >

windows软件工程师进阶系列:《软件调试》之七-运行期检查

2013-04-09 
windows程序员进阶系列:《软件调试》之七--运行期检查C/C++运行库 C标准定义了标准C函数,C++标准定义了C++标

windows程序员进阶系列:《软件调试》之七--运行期检查

 

 

C/C++运行库

 

C标准定义了标准C函数,C++标准定义了C++标准类库。这些库通常被称为支持库。

Runtime Library)。对于VC编译器来说,它同时提供了支持C语言的C运行库和C++语言的C++标准库。

 

C运行库

 

C语言标准定义了一系列常用的函数,称为C库函数。但是C标准仅仅定义了C库函数的原型和功能,并没有为如何实现定义标准。因此,每个编译器都会有自己的实现方法。通常,编译器都是实现标准C函数库的一个超集,称它们为C运行库,简称CRT。

 

C支持库中包含了内存分配、错误处理、字符串处理、浮点计算、数据类型转换、文件和输入输出等大量函数。

 

C++的标准库由三大部分组成:第一部分是C标准库、第二部分是IO流。第三部分是STL。

 

Microsoft的各版本的vc都提供了对C和C++运行库的支持。但是VC编译器是将C++编译器使用的C标准库和C编译器所使用的C运行库放在一个DLL中实现的,而将IO流和STL放在另一个DLL中实现。

 

VC6来说,VC6的C运行库和在MSVCRT.DLL中实现。VC7的C运行库在MSVCRT71.DLL中实现。VC8的C运行库在MSVCR80.DLL中实现。

 

C++标准库中的另外两个部分:IO流和STL,在VC6中是在MSVCP60.DLL实现。VC7是在MSVCP71.DLL实现。VC8是在MSVCP80.DLL中实现。

 

D得到。如MSVCP60D.DLL.

 

 

 

DLL,找到需要的函数并调用。

 

vc中可以通过编译器选项来设置链接支持库的方式。/MT代表静态链接,/MD代表动态链接。

 

 

CRT堆。因此必须在用户调用malloc或new之前初始化好,因此必须在程序入口函数之前完成对运行库的初始化。

 

CRT入口函数。在CRT入口函数中完成初始化工作后,再调用用户的入口函数如:main、WinMain等。用户入口函数结束后返回到CRT入口函数中执行清理工作。

 

CRT 入口函数()

{

   执行CRT初始化工作。

   用户入口函数.如:main

CRT清理工作。

}

 

exe模块的各种用户入口函数:

 windows软件工程师进阶系列:《软件调试》之七-运行期检查

DLL模块的用户入口函数是DllMain。它所对应的CRT入口函数为_DllmainCRTStartup函数。

CRT入口函数作为模块入口函数,并注册到PE文件中。这样在模块执行时,首先执行CRT入口函数,它完成运行库的初始化工作后调用用户入口函数。用户入口函数返回后再执行清理工作。

CRT入口函数作为模块入口,但是我们可以通过#pragma comment(linker,"/entry:myFunc")设置其他函数作为入口。

 

#include<iostream>#pragma comment(linker,"/entry:myFunc")int myFunc(){std::cout<<"The entry point is :myFunc!"<<std::endl;return 0;}

 

main函数没有被调用。myFunc函数直接作为模块的入口函数。由于没有调用CRT入口函数也就无所谓CRT的初始化了。CRT运行库的初始化和清理就需要用户自己考虑。以上设置仅仅是为了演示之用,并不推荐使用。

 

CRT初始化

CRT入口函数,在该函数内实际实行了一下几类工作:

1:调用_security_init_cookie()初始化安全cookie。以后的文章会有介绍。

2:初始化全局变量,包括环境变量和标识操作系统版本号的全局变量等。

3:调用_heap_init()函数创建C运行库所使用的堆,简称CRT堆。程序调用new或malloc申请的空间就是从CRT堆中分配的。

4:调用_mtinit()函数初始化多线程支持。

5:调用_RTC_Initialize()执行运行期检查功能。

6:调用_ioinit()函数初始化低级IO。

7:调用_cinit()函数初始化C和C++数据。包括初始化浮点计算包、除0向量,以及调用_initterm(_ix_a,_xi_z)执行注册在PE文件数据区得初始化函数,调用_initterm(_xc_a,_xc_z)调用全局C++对象的构造函数。
    crt0dat.c中包含了_cinit()函数的源代码。其中最主要的代码便是对_initterm()的两次调用:

_initterm(_xi_a,_xi_z);

_initterm(_xc_a,_xc_z);

_xi_a ,_xi_z和_xc_a,_xc_z是两对全局变量 。分别指向两个函数指针表的起始点和终结点。_initterm函数的任务就是遍历指针表,调用指针表内的每个函数。

C和C++分别有一张表来保存初始化函数的指针。_xi_a,_xi_z分别指向C初始化函数指针表的起始点和终止点。_xc_a和_xc_z指向C++初始化函数指针表的起始点和终止点。

PE文件的特定区域读取数据并构造上面的两个表。从而完成数据的初始化,调用包括全局类对象的构造函数以及编译器自动生成的用于初始化全局数据的函数。

main函数返回后,还有两张用于执行清理工作函数的函数的指针表。

_initterm会在程序结束后循环调用表内的函数执行清理工作,如调用C++对象的析构函数。_xt_a和_xt_z这对指针用于指向该表的起始点和终止点。

main函数返回之后、析构函数调用之前通过_initterm调用。此时虽然main已经结束,但是仍然可以使用C运行库的各种函数。因为C运行库还未执行清理操作。_xp_a和_xp_t指向该表的起始点和终止点。

//在main之前。

_initterm(_xi_a,_xi_z);

_initterm(_xc_a,_xc_z);

//main函数。

  Mainret=main(...);

//main之后

_initterm(_xp_a,_xp_z);//main之后,析构之前。

_initterm(_xt_a,_xt_z);//清理函数。

main之前或之后执行一些自己的函数代码,可以插入如下代码:

#include<iostream>//#include"afx.h"#pragma comment(linker,"/SubSystem:CONSOLE")int PreMain_A();int PreMain_B();int PostMain_A();int PostMain_B();typedef int cb();//在main之前被调用。#pragma data_seg(".CRT$XCU")cb*start1[]={PreMain_A};#pragma data_seg(".CRT$XCU")cb*start2[]={PreMain_B};//在main之后被调用。#pragma  data_seg(".CRT$XPU")//在main之后,析构函数被调用之前被调用。cb*start3[]={PostMain_A};#pragma  data_seg(".CRT$XTU")//cb*start4[]={PostMain_B};#pragma data_seg()int PreMain_A(){std::cout<<"PreMainA is called!"<<std::endl;return 0;}int PreMain_B(){std::cout<<"PreMainB is called!"<<std::endl;return 0;}int main(int argc,char**argv){std::cout<<"Hello World!!"<<std::endl;return 0;}int PostMain_A(){return 0;}int PostMain_B(){return 0;}


 

return0处设置断点,并开始调试。然后可以发现PreMainA和PreMainB都在main之前运行了。而PostMainA和PostMainB在main之后运行。要注意:vc项目中一定要设置使用静态库,否则PostMainA和PostMainB将不会被调用。还应注意:由于在main之后C运行库已被清理,此时再调用cout输出将会出现错误。这也是没有在PostMainA和PostMainB中输出信息的原因。在main之前之所以可以的调用,是因为我们的自定义函数被放到了C运行库初始化之后被调用。如果放在之前也同样会出现错误。

CRT中初始化中手工在main之前和之后添加自定义函数的内容:请参考翻译博文:在main前和main后的CRT代码。

 

 

 

ASSERT

VC运行库定义了两个宏来提供断言功能:分别为_ASSERT和_ASSERTE。它们定义如下:


 

#define _ASSERT(expr)\   do{if(!exp)&&\      (1==_CtrDbgReport(_CRT_ASSERT,_FILE_,_LINE_,NULL,NULL)))\     _CrtDbgBreak();}while(0)#define _ASSERTE(expr)\   do{if(!exp)&&\      (1==_CtrDbgReport(_CRT_ASSERT,_FILE_,_LINE_,NULL,#expr)))\     _CrtDbgBreak();}while(0)


 

do .while(0)是为了将大括号中的多条语句封装在一起。可以看到这两个宏的区别仅仅是在断言失败调用_CrtDbgReport时最后一个参数不同。_ASSERTE会在断言失败时在断言失败窗口上显示断言表达式而_ASSERT不会显示。

_DEBUG没有被定义时,它们都被定义成了空。不再起任何作用。

 

#define _ASSERT(expr) ((void)0)#define _ASSERTE(expr)((void)0)

 

_ASSERT(*p++!=0);误用的原因仍然是在发布版本上ASSERT被置为0。

C也提供了一个名为assert的宏来实现断言。由于实现自标准C因此无论是linux还是windows都可以使用,因此具有良好的移植性。MFC框架定义了ASSERT和VERIFY宏,ASSERT与CRT的_ASSERT宏大同小异。VERIFY宏在调试版本时与ASSERT功能相同。但是在发布版本中其表达式仍然被编译进目标代码。所以在发布版本时仍然可以使用VERIFY宏,且在其内部也可以进行表达式的计算。


                                  如有纰漏,请不吝赐教!

                                       2013、3、7于浙江杭州

热点排行