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都提供了对CC++运行库的支持。但是VC编译器是将C++编译器使用的C标准库和C编译器所使用的C运行库放在一个DLL中实现的,而将IO流和STL放在另一个DLL中实现。

对于VC6来说,VC6C运行库和在MSVCRT.DLL中实现。VC7C运行库在MSVCRT71.DLL中实现。VC8C运行库在MSVCR80.DLL中实现。

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

为了满足不同情况的需要,编译器的运行库还包括一个调试版本。调试版本的运行库增加了一些发布版本中没有的函数,比如用于调试的函数,还添加了更多的错误检查和辅助调试代码。调试版本的运行库的库名可以由上面介绍的库后加D得到。如MSVCP60D.DLL.

使用运行库的程序在运行时必须能够找到运行库的函数。有两种方法:一种是静态链接,另一种是动态链接。

静态链接就是说在程序链接时,将程序所使用的支持库的函数复制到程序中,这样支持库的函数就位于程序模块中。但是这样会导致程序文件很大。同时,如果一个程序包含多个模块,每个模块都是采用静态链接方式来链接运行库时,此时程序中就会存在多个运行库的副本。

动态链接是在程序运行时,动态的加载包含支持函数的动态链接库DLL,找到需要的函数并调用。

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

运行库的初始化和清理

由于运行库函数和设施是其他代码的基础,如CRT堆。因此必须在用户调用mallocnew之前初始化好,因此必须在程序入口函数之前完成对运行库的初始化。

实际编译器的做法是:编译器自动为每个模块插入编译器编写的入口函数,被称为CRT入口函数。在CRT入口函数中完成初始化工作后,再调用用户的入口函数如:mainWinMain等。用户入口函数结束后返回到CRT入口函数中执行清理工作。

CRT 入口函数()

{

执行CRT初始化工作。

用户入口函数.如:main

执行CRT清理工作。

}

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

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堆。程序调用newmalloc申请的空间就是从CRT堆中分配的。

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

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

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

7:调用_cinit()函数初始化CC++数据。包括初始化浮点计算包、除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函数的任务就是遍历指针表,调用指针表内的每个函数。

CC++分别有一张表来保存初始化函数的指针。_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处设置断点,并开始调试。然后可以发现PreMainAPreMainB都在main之前运行了。而PostMainAPostMainBmain之后运行。要注意:vc项目中一定要设置使用静态库,否则PostMainAPostMainB将不会被调用。还应注意:由于在main之后C运行库已被清理,此时再调用cout输出将会出现错误。这也是没有在PostMainAPostMainB中输出信息的原因。在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框架定义了ASSERTVERIFY宏,ASSERTCRT_ASSERT宏大同小异。VERIFY宏在调试版本时与ASSERT功能相同。但是在发布版本中其表达式仍然被编译进目标代码。所以在发布版本时仍然可以使用VERIFY宏,且在其内部也可以进行表达式的计算。


如有纰漏,请不吝赐教!

2013、3、7于浙江杭州
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值