第一章-对程序错误的处理

在开始介绍Microsoft Windows 的特性之前,必须首先了解 Wi n d o w s的各个函数是如何进行错误处理的。

当调用一个Wi n d o w s函数时,它首先要检验传递给它的的各个参数的有效性,然后再设法执行任务。如果传递了一个无效参数,或者由于某种原因无法执行这项操作,那么操作系统就会返回一个值,指明该函数在某种程度上运行失败了。表 1 - 1列出了大多数Wi n d o w s函数使用的返回值的数据类型。

一个Wi n d o w s函数返回的错误代码对了解该函数为什么会运行失败常常很有用。 M i c r o s o f t公司编译了一个所有可能的错误代码的列表,并且为每个错误代码分配了一个 3 2位的号码。

从系统内部来讲,当一个Wi n d o w s函数检测到一个错误时,它会使用一个称为线程本地存储器(thread-local storage) 的机制,将相应的错误代码号码与调用的线程关联起来( 线程本地存储器将在第2 1章中介绍) 。这将使线程能够互相独立地运行,而不会影响各自的错误代码。当函数返回时,它的返回值就能指明一个错误已经发生。若要确定这是个什么错误,请调用G e t L a s t E r r o r函数:

DWORD GetLastError();

该函数只返回线程的3 2位错误代码。当你拥有3 2位错误代码的号码时,必须将该号码转换成更有用的某种对象。 Wi n E r r o r. h头文件包含了M i c r o s o f t公司定义的错误代码的列表。下面显示了该列表的某些内容,使你能够看到它的大概样子:

如你所见,每个错误都有3种表示法:一个消息I D(这是你可以在源代码中使用的一个宏,以便与G e t L a s t E r r o r的返回值进行比较) ,消息文本(对错误的英文描述)和一个号码(应该避免使用这个号码,可使用消息I D) 。请记住,这里只显示了Wi n E r r o r. h头文件中的很少一部分内容,整个文件的长度超过2 1 0 0 0行。

当Wi n d o w s函数运行失败时,应该立即调用G e t L a s t E r r o r函数。如果调用另一个Wi n d o w s函数,它的值很可能被改写。

注意 G e t L a s t E r r o r能返回线程产生的最后一个错误。如果该线程调用的Wi n d o w s函数运行成功,那么最后一个错误代码就不被改写,并且不指明运行成功。有少数Wi n d o w s函数并不遵循这一规则,它会更改最后的错误代码;但是 Platform SDK文档通常指明,当函数运行成功时,该函数会更改最后的错误代码。

Wi n d o w s 9 8 许多Windows 98的函数实际上是用M i c r o s o f t公司的1 6位Windows 3.1产品产生的1 6位代码来实现的。这种比较老的代码并不通过 G e t L a s t E r r o r之类的函数来报告错误,而且M i c r o s o f t公司并没有在Windows 98中修改1 6位代码,以支持这种错误处理方式。对于我们来说,这意味着Windows 98中的许多Wi n 3 2函数在运行失败时不能设置最后的错误代码。该函数将返回一个值,指明运行失败,这样你就能够发现该函数确实已经运行失败,但是你无法确定运行失败的原因。

有些Wi n d o w s函数之所以能够成功运行,其中有许多原因。例如,创建指明的事件内核对象之所以能够取得成功,是因为你实际上创建了该对象,或者因为已经存在带有相同名字的事件内核对象。你应搞清楚成功的原因。为了将该信息返回, M i c r o s o f t公司选择使用最后错误代码机制。这样,当某些函数运行成功时,就能够通过调用 G e t L a d t E r r o r函数来确定其他的一些信息。对于具有这种行为特性的函数来说, Platform SDK文档清楚地说明了G e t L a s t E r r o r函数可以这样使用。请参见该文档,找出C r e a t e E v e n t函数的例子。

进行调试的时候,监控线程的最后错误代码是非常有用的。 在Microsoft Visual studio 6.0中,M i c r o s o f t的调试程序支持一个非常有用的特性,即可以配置 Wa t c h窗口,以便始终都能显示线程的最后错误代码的号码和该错误的英文描述。通过选定 Wa t c h窗口中的一行,并键入“@ e r r, h r” ,就能够做到这一点。观察图1 - 1,你会看到已经调用了C r e a t e F i l e函数。该函数返回I N VA L I D _ H A N D L E _ VA L U E(- 1)的H A N D L E,表示它未能打开指定的文件。但是Wa t c h窗口向我们显示最后错误代码(即如果调用 G e t L a s t E r r o r函数,该函数返回的错误代码)是0 x 0 0 0 0 0 0 0 2。该Wa t c h窗口又进一步指明错误代码2是指“系统不能找到指定的文件。 ”你会发现它与Wi n E r r o r. h头文件中的错误代码2所指的字符串是相同的。

OK,然后我就照着做,但是发现了一个尴尬的事情,我在vs2012上找不大watch窗口,搜索了下,说是在调试->窗口->监视(Debug->Window->Watch),然后我就去找,但是并没有找到,一开始看到的是这个:

额...后来无意中发现了,其实是当你点击运行的时候,那个地方才会出现Watch的选项:

然后就是我在vs2012中写了下上面说的那个:

然后又在网上找了一些常用技巧:

1.eg: int *p = new int[100];

如果只查看p的话,只能看到一个结果。可以使用:p,n 查看n个结果。

2.格式化数据和表达式赋值语句.

常用变量格式化符(表达式的值后跟逗号,接格式化符,如”(int)0xFFFF,d”):

d,I:有符号的十进制数.

u  :无符号的十进制数.

o  :无符号的八

x,X:十六进制数.

l,h:d,i,u,o,x,X的长前缀或短前缀.

f  :有符号浮点数.

e  :有符号的科学计数法.

g  :有符号的浮点或有符号的科学计数法,用其中较短的一个.

c  :单字符.

s  :字符串.

su :双字节字符串.

st :双字节字符串或ANSI字符串,取决于AUTOEXP.DAT中的Unicode String设置.

hr :Windows类标记.

wm :Windows消息码.

常用内存转储对象的格式化符(用法同变量格式化符):

ma :64个ASCII码字符.

m  :以16进制书写的16字节,后跟16个ASCII字符.

mb :以16进制书写的16字节,后跟16个ASCII字符.

mw :8个字长.

md :4个双精度字.

mq :4个四倍字长的字.

mu :2字节字符(Unicode标准).

#  :将指针扩展到指定的数值数目的内存存储单元上.(#代表一个数字)

WATCH窗口允许重新设置数据变量的格式,

如:可用BY,DW表达式来定位指针的偏移量;

可用&和*运算符,且两运算符都可直接操作内存地址;

甚至可用上下说明符明确指定变量的上下文.

总之,所有格式化方法和指定方法在WATCH窗口都有效

WATCH窗口是一个完整的表达式求值程序,可以在其中查看任何条件语句.

表达式中可用的伪寄存器(可当普通变量进行查看):

@ERR:最后一个错误值,GetLastError API返回相同的值.

@TIB:当前线程的线程信息块.(调试器不能处理”FS:0″格式).

@CLK:时钟寄存器.

@EAX,@EBX,@ECX,@EDX,@ESI,@EDI,@DIP,@ESP,@EBP,@EFL

:Intel CPU寄存器.

@CS,@DS,@ES,@SS,@FS,@GS

:Intel CPU段寄存器.

@ST0,@ST1,@ST2,@ST3,@ST4,@ST5,@ST6,@ST7

:Intel CPU浮点寄存器.

3.适时编码

许多时候只想对两断点间的执行时间有个大致印象,可用@CLK得出两断点间所需执行时间(包括调试器占用的时间).

需要输入两个@CLK观察符,第一个是@CLK,第二个是@CLK=0.第二个的目的是重新运行时将定时器清0.

时间以微秒为单位,大多数情况下需要格式化为毫秒:”@CLK/1000,d”.

4.在WATCH窗口中调用函数

大多数情况下用于执行专门编写的校验数据结构,保证数据的相关性的函数.在释放构件中,从未调用过的函数不会被链接,因此不必担

心这类函数会对影响发布构件.

如函数没有参数,也要求使用括号”()”,调用时像用普通函数一样传送参数.WATCH右边将显示函数返回值.

这里有些限制:

1.只能在一个单线程上下文中执行函数.如是多线程程序,将函数输入到WATCH窗口中检查结果后应立即从WATCH窗口清除,否则,如调

试函数在第二个线程上下文中执行,会立即终止第二个线程的运行.

2.调试函数必须在20秒内执行.如执行过程中出现异常,程序会在调试器中中止.

3.(常识)只对数据验证进行内存读取,如有问题,调用OutputDebugString类的函数.如更改内存或调用API函数—-尽管这是可能的,但

无法预知可能会发生什么.

只要在WATCH窗口中重新计算表达式,已输入WATCH窗口的调试函数就会执行:

.程序处于运行状态并触发某一断点时.

.单步调试某一代码行或某一指令时.

.在WATCH窗口左边编辑完成调试函数的文本并按下回车时.

.在运行程序时出现异常情况,并让你返回调试器中时.

使用调试函数的建议:输入调试函数并查看值后,立即从WATCH窗口清除;只为最关键的数据结构编写调试函数;不要更改个别结构的转

储内像.

5.自动扩展自己的类型

常见的自动扩展是RECT,输入RECT型的变量后直接显示其中的某些数据成员的值.

自定义类型扩展时,只需将自己的类型入口加入<VS Common>\MSDev98\Bin目录的AUTOEXP.DAT文件中.

例:

扩展CreateProcess()所用到的PROCESS_INFORMATION结构

(1).检查调试器将该类型识别为什么.将PROCESS_INFORMATION变量输入WATCH窗口,右击变量,选择Properties,在这里它被标注为

_PROCESS_INFORMATION类型.

(2).打开AUTOEXP.DAT文本文件,加入扩展入口.语法如下:

Type=[text]<member[format]>

本例中要查看hProcess和hThread值,故输入:

_PROCESS_INFORMATION=hProcess=<hProcess,X> hThread=<hThread,X>

其中X表示以16进制查看.有个特殊的格式化符<,t>,用于通知调试器输入最易派生类型的类型名.如B派生至A,只有B有自动扩展规则,

则B的自动扩展将会是后面跟随着类A的自动扩展规则的类型名B.

6.Set Next Statement命令

可以在调试时从菜单运行,但也可在WATCH窗口中直接设置EIP寄存器—-小心,可能很容易摧毁程序.在最优化的释放构件中,最安全的

方法是在Disassembly窗口中使用该命令.如代码在堆栈上创建了临时变量,更要多加小心.

最常用的情况是:在出问题的函数前设置一个断点,检查进入的参数,单步调试整个函数;如问题不是重复的,使用Set Next Statement

设置返回到断点的执行点,并更改参数.这样可在一个调试会话中测试多个假设,节省测试时间,但它不能用于所有场合,因为函数执行

会破坏其状态.另一个常用地点是测试时填充数据结构,如表和数组,可用它输入额外的数据并查看代码如何处理–当某些数据条件难于复制时更为方便.

Visual studio还配有一个小的实用程序,称为Error Lookup。可以使用Error Lookup

将错误代码的号码转换成相应文本描述(见图1 - 2) 。如果在编写的应用程序中发现一个错误,可能想要向用户显示该错误的文本描述。Wi n d o w s提供了一个函数,可以将错误代码转换成它的文本描述。该函数称为 F o r m a t -M e s s a g e,显示如下:

F o r m a t M e s s a g e函数的功能实际上是非常丰富的,在创建向用户显示的字符串信息时,它是首选函数。该函数之所以有这样大的作用,原因之一是它很容易用多种语言进行操作。该函数能够检测出用户首选的语言(在Regional Settings Control Panel小应用程序中设定) ,并返回相应的文本。当然,首先必须自己转换字符串,然后将已转换的消息表资源嵌入你的 . e x e文件或D L L模块中,然后该函数会选定正确的嵌入对象。 E r r o r S h o w示例应用程序(本章后面将加以介绍)展示了如何调用该函数,以便将M i c r o s o f t公司定义的错误代码转换成它的文本描述。

有些人常常问我,M i c r o s o f t公司是否建立了一个主控列表,以显示每个 Wi n d o w s函数可能返回的所有错误代码。可惜,回答是没有这样的列表,而且 M i c r o s o f t公司将永远不会建立这样的一个列表。因为在创建系统的新版本时,建立和维护该列表实在太困难了。建立这样一个列表存在的问题是,你可以调用一个 Wi n d o w s函数,但是该函数能够在内部调用另一个函数,而这另一个函数又可以调用另一个函数,如此类推。由于各种不同的原因,这些函数中的任何一个函数都可能运行失败。有时,当一个函数运行失败时,较高级的函数对它进行恢复,并且仍然可以执行你想执行的操作。为了创建该主控列表, M i c r o s o f t公司必须跟踪每个函数的运行路径,并建立所有可能的错误代码的列表。这项工作很困难。而且,当创建系统的新版本时,这些函数的运行路径还会改变。

1.1 定义自己的错误代码

前面已经说明 Wi n d o w s函数是如何向函数的调用者指明发生的错误,你也能够将该机制用于自己的函数。比如说,你编写了一个希望其他人调用的函数,你的函数可能因为这样或那样的原因而运行失败,你必须向函数的调用者说明它已经运行失败。

若要指明函数运行失败,只需要设定线程的最后的错误代码,然后让你的函数返回FA L S E、I N VA L I D _ H A N D L E _ VA L U E、N U L L或者返回任何合适的信息。若要设定线程的最后错误代码,只需调用下面的代码:

请将你认为合适的任何 3 2位号码传递给该函数。尝试使用 Wi n E r r o r. h中已经存在的代码,图1-2 Error Lookup窗口只要该代码能够正确地指明想要报告的错误即可。如果你认为 Wi n E r r o r. h中的任何代码都不能正确地反映该错误的性质,那么可以创建你自己的代码。错误代码是个 3 2位的数字,划分成表

1-2 所示的各个域。

表1-2 错误代码的域

这些域将在第2 4章中详细讲述。现在,需要知道的重要域是第 2 9位。M i c r o s o f t公司规定,他们建立的所有错误代码的这个信息位均使用 0。如果创建自己的错误代码,必须使 2 9位为1。这样,就可以确保你的错误代码与M i c r o s o f t公司目前或者将来定义的错误代码不会发生冲突。

来一个自定义的例子:SetLastError((1<<29) + (1<<30) * 0 + 1);

1.2 ErrorShow示例应用程序

E r r o r S h o w应用程序“01 ErrorShow. e x e”(在清单1 - 1中列出)展示了如何获取错误代码的文本描述的方法。该应用程序的源代码和资源文件位于本书所附光盘上的 0 1 - E r r o r S h o w目录下。一般来说,该应用程序用于显示调试程序的 Wa t c h窗口和Error Lookup程序是如何运行的。当启动该程序时,就会出现

如图1 - 3所示的窗口。

可以将任何错误代码键入该编辑控件。当单击 Look up按钮时,在底部的滚动窗口中就会显示该错误的文本描述。该应用程序唯一令人感兴趣的特性是如何调用F o r m a t M e s s a g e函数。下面是使用该函数的方法:

第一个代码行用于从编辑控件中检索错误代码的号码。然后,建立一个内存块的句柄并将它初始化为N U L L。F o r m a t M e s s a g e函数在内部对内存块进行分配,并将它的句柄返回给我们。当调用F o r m a t M e s s a g e函数时,传递了F O R M AT _ M E S S A G E _ F R O M _ S Y S T E M标志。该标志告诉F o r m a t M e s s a g e函数,我们想要系统定义的错误代码的字符串。还传递了 F O R M AT _M E S S A G E _ A L L O C AT E _ B U F F E R标志,告诉该函数为错误代码的文本描述分配足够大的内存块。该内存块的句柄将在 h l o c a l变量中返回。第三个参数指明想要查找的错误代码的号码,第四个参数指明想要文本描述使用什么语言。

如果F o r m a t M e s s a g e函数运行成功,那么错误代码的文本描述就位于内存块中,将它拷贝到对话框底部的滚动窗口中。如果F o r m a t M e s s a g e函数运行失败,设法查看N e t M s g . d l l模块中的消息代码,以了解该错误是否与网络有关。使用 N e t M s g . d l l模块的句柄,再次调用F o r m a t M e s s a g e函数。你会看到,每个 D L L(或. e x e)都有它自己的一组错误代码,可以使用Message Compiler(M C . e x e)将这组错误代码添加给该模块,并将一个资源添加给该模块。这就是Visual Studio的Error Lookup工具允许你用M o d u l e s对话框进行的操作。

windows核心编程-第一章 对程序错误的处理相关推荐

  1. Widows核心编程第一章:错误处理

    Windows API同linux一样, 在返回值的基础上可以通过错误码确认具体的程序错误原因.当一个 Windows 函数检测到错误时,它会使用一个名为"线程本地存储区" (th ...

  2. 学习完windows网络编程第一章后做的UDPTest程序

    该程序分成Client和Server两部分,在一个solution中,两个Project client会给server分别发送一个ASCII的字符串,一个含有中文字符的Unicode字符串,最后发一个 ...

  3. windows核心编程-第二章 Unicode

    第2章U n i c o d e 随着M i c r o s o f t公司的Wi n d o w s操作系统在全世界日益广泛的流行,对于软件开发人员来说,将目标瞄准国际上的各个不同市场,已经成为一个 ...

  4. 【Windows核心编程+第一个内核程序】爆肝120小时整理-80%程序员最欠缺的能力,一半以上研究生毕业了还不懂?理解各种深度技术的基本功

  5. Windows核心编程 第九章 线程与内核对象的同步(下)

    9.4 等待定时器内核对象 等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象.它们通常用来在某个时间执行某个操作. 若要创建等待定时器,只需要调用C r e a t e Wa i ...

  6. Windows核心编程 第九章 线程与内核对象的同步(上)

    第9章 线程与内核对象的同步 上一章介绍了如何使用允许线程保留在用户方式中的机制来实现线程同步的方法.用户方式同步的优点是它的同步速度非常快.如果强调线程的运行速度,那么首先应该确定用户方式的线程同步 ...

  7. Scala核心编程 第一章—Scala语言概述

    一.Scala语言解释 1.什么是Scala语言 Spark-新一代内存级大数据计算框架,是大数据的重要内容. Spark就是使用Scala编写的.因此为了更好的学习Spark, 需要掌握Scala这 ...

  8. Windows核心编程之核心总结(第一章 错误处理)(2018.5.26)

    前沿 学习Windows核心编程是步入Windows编程殿堂的必经之路,2018年寒假重温了计算机操作系统知识,前阵子又过学习Windows程序设计方面的基础,正所谓打铁要乘热,所以我又入了Windo ...

  9. 对程序错误的处理——Windows核心编程学习手札之一

    对程序错误的处理 --Windows核心编程学习手札之一 函数被调用执行时,先检验传递给它的各个参数的有效性,后执行任务.函数执行中若因参数无效或因某种原因导致无法正常完成函数任务,那么操作系统会返回 ...

最新文章

  1. 一种新的验证码(改进版)
  2. Python Django 惰性查询(懒加载)
  3. react native中一次错误排查 Error:Error: Duplicate resources
  4. 简单好用的计算器:bc
  5. MATLAB高光谱图像构建KNN图
  6. geoserver 3_使用GeoServer 和 mapbox-gl 搭建离线地图服务
  7. 如何做一个国产数据库(二)
  8. 6467t 1080php,DM6467T开发板领航——开发环境的文件配置
  9. tp5部署到nginx后所有分页404的解决办法
  10. git合并分支,发布代码
  11. 安徽计算机技术学院蚌埠,安徽蚌埠技师学院2021年招生简章
  12. Craw the data of the web page and parse to pdf
  13. 难受难受,真它吗的难受... ...
  14. guanyongyu2
  15. 纸本书变电子书是很小的事——詹宏志谈数字出版时代
  16. 如何判断点是否在圆弧上
  17. Android实现第三方登录并获取到头像、名字
  18. java-se项目--嗖嗖移动
  19. 未来十年,将是物联网的天下?!
  20. VCSA6.7-VCSA7.0部署经常踩中的坑【 两个 】

热门文章

  1. “/”应用程序中的服务器错误
  2. Using mongoDB's Profiler analyze the performance of database operations
  3. 天黑的时候,我又想起那首歌
  4. 解决非浏览器客户端请求nginx无法命中缓存的问题
  5. 查看数据库表使用空间大小
  6. http、https比较
  7. 解题报告 『生活大爆炸版石头剪刀布(模拟)』
  8. 浏览器缓存:强缓存和协商缓存
  9. 一天搞定CSS:表格(table)--19
  10. 两种三维点云密度聚类方法的研究与对比