编译器的调试支持
编译器用于将高级语言翻译成cpu可以识别的机器代码。经历了几十年的发展,编译器发生了很大的变化,但是支持调试这一点没有任何改变。检查并报告被编译软件中的错误是编译器设计的一个主要目标。编译器在编译源代码和链接目标代码时,会做很多的检查工作。这包括编译期检查和运行期检查。
编译期检查:编译器在编译过程中,会检查代码中的语法错误,与此同时还会检查可能存在的逻辑错误和设计缺陷,并以编译错误或警告的方式报告出来。
运行期检查:为了帮助程序在运行阶段发现问题,编译器在编译时会加入检查功能,包括内存检查和栈检查。后面将会对编译期检查和运行期检查做详细介绍。本文将介绍编译期检查。
软件是程序和文档的集合。源程序在经过编译期被编译成等价汇编语言模块,然后经过汇编器产生出与目标平台cpu一致的机器码模块,最后经过链接器解决机器码模块中的名称和地址引用,产生出符合目标平台上操作系统所要求格式的可执行模块。
以目标代码为界,可以把程序的构建过程分为翻译和链接两个阶段。
链接器
链接器的主要职责是将编译器产生的多个目标文件合成一个可以在目标平台执行的映像。它需要解决以下任务:
1:解决目标文件中的外部符号,包括函数调用和变量引用。建立IDT表和IAT表。
2:为已经解决外部引用的目标代码生成代码段。
3:生成包含只读数据的代码段。
4:生成包含资源数据的资源段。
5:生成基地址重定位表。该表中包含所有需要重定位的项。
编译错误和警告
在编译的过程中,编译器如果发现被编译代码中的问题会以警告或错误的形式报告出来。为了标注每个警告或错误的来源和含义,编译器会为它们赋予一个唯一的标识符。
错误ID和来源
每个标识符是以1个或多个大写字母开头,后面为4位阿拉伯数字。字符用来表示报告信息的组件,数字用来表示错误或警告的原因。
下图为VisualC++的错误ID的格式和来源:
对于同一来源的错误,处于不同范围的ID通常对应不同的严重级别,分为致命错误、错误和警告。
下图为各类错误ID范围划分方法:
编译警告
为了区分不同程度的警告,vc将警告信息分为4个级别:1--4级。1级程度最高,4级程度最低。可以通过配置编译器的编译选项来配置如何显示编译警告。
下图为设置编译警告的命令行开关:
除了使用编译选项来设置编译警告,还可以在源代码中通过编译器的#pragmawarning指令来控制编译警告。如:下面的语句分别为禁止4705号警告和将其恢复为默认设置。
#pragmawarning(disable:4705)
#prgamawarning(default:4705)
下面的语句会禁止4705号和4034号警告,并只报告一次4385号警告,将4164号警告当做错误处理:
#pragmawarning(disable:47054034;once4385;error:41164)
可以使用#pragmawarning(push)将所有编译警告的当前设置保存起来,然后使用#pragmawarning(pop)恢复保存起来的设置。
编译期检查
编译器在编译时会将发现的问题以编译警告和错误的形式报告出来。我们将把在编译阶段所做的检查称作编译期检查,与此相对应的还有运行期检查,在下一篇文章中我们会介绍运行期检查。编译期检查可以发现代码中的词法、语法及少量的语义方面的问题。两种常见的编译期检查为:未初始化的局部变量和类型不匹配。
未初始化的局部变量
局部变量是指定义在函数内的变量。一种是在栈上分配的,另一种是分配在寄存器上。由于大多数局部变量是分配在栈上的,所以此处只讨论此种情况。在栈上分配的局部变量,是通过调整栈指针来分配和释放的。当编译器编译调试版本时,会自动将所分配的局部变量区域初始化为一个固定的0xcc(x86系统)。由于0xcc就是INT3指令的机器码,因此当访程序问到这些未在程序中显式初始化的区域时,就会导致INT3软件中断。在发布版本中,考虑到初始化工作会导致不小的空间和时间开销,编译器没有在发布版本中编译器不会做上述工作。调试版本和发布版本的这个重要差异也是导致调试版本和发布版本运行行为不一致的一个原因。
类型不匹配
编译器在进行语义分析时会检查变量比较、赋值等操作,以便发现潜在的问题。如当符号整数和无符号整数比较时,以及将双精度浮点数赋值给单精度浮点数时,都会给出一定的警告。同样在编译函数调用时,编译器会根据函数原型对每个参数进行检查。
编译指令
编译器指令使用非常广泛,如在文件头添加#ifndef。。。#endif语句可以防止重复包含。
在文件头或源文件中添加编译器指令时,编译器在编译这些语句时会评估这些检查语句。如果条件满足,编译器就会执行所定义的动作。
如:
#ifndef WIN32
#error this function only can be used in _win32 platform
#endif
上面的检查语句会检查WIN32是否定义,如果未定义便会显示错误信息。
下面的语句用以检查编译器的版本:
#if _MSC_VER<1300
#error Compiler version not supported by windows ddk;
#endif
#if或#ifdef、#ifndef指令判断的条件表达式可以通过#defin语句定义,也可以使用项目属性--PreprocessorDefinitions中定义的符号。如:
标注
增加编译期检查的另一种方法是向代码中加入标注信息,这可以向编译器提供更多的信息,以便更好的帮助编译器检查出更多的问题。
标注的典型应用就是在声明函数原型时,使用特定的符号标注函数的参数和返回值。从VS2005开始,vs引入了一种名为标准标注语言SAL(standardannotationlanguage)的机制来帮助编译器和其他分析工具发现源代码中的安全问题。SAL标注符号在sal.h文件中定义。它分为两大类:一类是用来描述函数参数和返回值的,称为缓冲区标注符,另一类称为高级标注符。
缓冲区标注符
缓冲区标注符用来描述函数参数和返回值,包括指针、缓冲区长度、以及返回值的方法。一个缓冲区标注符可以描述一个参数,而且一个参数也只能有一个缓冲区标注符。但一个缓冲区标注符可以包含多个元素,分别用来描述参数某个方面的特征。多个元素可以通过_下划线连接起来构成一个完整的缓冲区标注符。
缓冲区标注符有多个元素构成,它们的用法和简介如下图。
实例如下:
void func1(
_in HWND hwnd,//该参数用作输入。由调用者初始化。
_in_opt DHC hdc,//该参数用作输入,但可能为空。
_inout char*p//该参数既用作输入,也用作输出。
);
实例二
HRESULT func2( size_t cb,_deref_bcount(cb) T**ppv);
其中_deref用以描述参数ppv指针的间接层次。此时*ppv存储指向缓冲区的指针。_bcount(cb)为*ppv指向的缓冲区的大小。由于未包括_full或_part因此该缓冲区在使用前不会初始化。
大家在查看vc提供的一些库函数的时候经常遇到包含sal标注符的函数。相信经过上面的讲解看懂它们应该不是难事。
另一类的标注符为高级标注符。顾名思义,它们比缓冲区标注符作用更高级,用来描述缓冲区标注符无法表达的约束或限定。
如下表所示:
看实例:
_success (return ==true) bool fun3( _out_ecount(MAX_PATH) char*buff,char*path);
上面标注符的含义为:func3函数成功时的返回值为true。只有当函数成功返回时buff指向的缓冲区才是以0为结束符的合法字符串。
实例二:
_checkReturn _bcount_opt(size) void* _cdecl malloc(
_in size_t size);
_checkReturn表示调用者应该检查返回值。_bcount_opt(size)表示返回值的长度为size个字节,但可能为空。参数size用于输入。由调用者初始化。
本文介绍了编译期检查,下一篇文章将会介绍运行库和运行期检查。
以上内容参考自《软件调试》张银奎著。如有纰漏,请不吝指正。谢谢!
2013、3、25于浙江杭州