.net FrameWork怎样映射HRESULT和异常
COM方法通过返回HRESULT报告错误;.NET方法通过引发异常来报告错误。运行时处理两者之间的转换。.NET Framework中的每个异常类都映射到一个HRESULT。
用户定义的异常类可以指定任何适合的HRESULT。这些异常类可以通过在异常对象上设置HRESULT字段来动态更改生成异常时返回的HRESULT。有关异常的其他信息通过IErrorInfo接口提供给客户端,该接口在非托管进程的.NET对象上实现。如果创建一个扩展System.Exception,必须在构造期间设置HRESULT字段。否则,基类将分配HRESULT值。通过在异常的构造函数中提供值,可以将新的异常类映射到现有的HRESULT。
使用Map文件查找崩溃信息
简介
编写整洁的应用程序是一回事。但是当用户告诉你你的软件已经崩溃时,你知道在添加其他功能之前最好先解决这个问题。如果你够幸运的话,用户会有一个崩溃地址。这将大大有助于解决这个问题。但是你怎么能用这个崩溃地址来判断出了什么问题呢?
创建Map文件
首先,你需要一个Map文件。如果没有,使用崩溃地址几乎不可能找到应用程序崩溃的位置。首先,我将向您展示如何创建一个好的Map文件。为此,我将创建一个新项目(MAPFILE)。你也可以这样做,或者调整你自己的项目。我在VC++ 6.0中使用Win32应用程序选项创建一个新项目,选择“典型的”Hello World!应用程序'。
而vs2013里如下,从vs2005开始不在支持/MAPINFO:LINES
现在,您可以编译并链接您的项目了。链接后,您将在中间目录中找到一个.map文件(连同您的EXE文件)。
读取Map文件
我们将使用一个崩溃示例来实现这一点。所以首先:如何崩溃你的应用程序。我在InitInstance()函数的末尾添加了这两行:
char* pEmpty = NULL;
*pEmpty = 'x'; //This is line 119
我相信你能找到其他会使你的申请失败的指令。现在重新编译并链接。如果启动应用程序,它将崩溃,您将收到如下消息:“0x004011a1处的指令”引用了“0x00000000”处的内存。无法“写入”内存。
现在,是时候用记事本或类似的东西打开Map文件了。您的Map文件将如下所示:Map文件的顶部包含模块名、指示项目链接的时间戳和首选加载地址(除非使用DLL,否则可能是0x00400000)。头之后是显示链接器从各种OBJ和LIB文件中引入的节的节信息。
MAPFILE Timestamp is 3df6394d (Tue Dec 10 19:58:21 2002) Preferred load address is 00400000 Start Length Name Class 0001:00000000 000038feH .text CODE 0002:00000000 000000f4H .idata$5 DATA 0002:000000f8 00000394H .rdata DATA 0002:0000048c 00000028H .idata$2 DATA 0002:000004b4 00000014H .idata$3 DATA 0002:000004c8 000000f4H .idata$4 DATA 0002:000005bc 0000040aH .idata$6 DATA 0002:000009c6 00000000H .edata DATA 0003:00000000 00000004H .CRT$XCA DATA 0003:00000004 00000004H .CRT$XCZ DATA 0003:00000008 00000004H .CRT$XIA DATA 0003:0000000c 00000004H .CRT$XIC DATA 0003:00000010 00000004H .CRT$XIZ DATA 0003:00000014 00000004H .CRT$XPA DATA 0003:00000018 00000004H .CRT$XPZ DATA 0003:0000001c 00000004H .CRT$XTA DATA 0003:00000020 00000004H .CRT$XTZ DATA 0003:00000030 00002490H .data DATA 0003:000024c0 000005fcH .bss DATA 0004:00000000 00000250H .rsrc$01 DATA 0004:00000250 00000720H .rsrc$02 DATA
在节信息之后,您将获得public function信息。注意“public”部分。如果您有静态声明的C函数,它们不会显示在映射文件中。幸运的是,行号仍将反映静态函数。public function信息的重要组成部分是函数名和Rva+Base列中的信息,Rva+Base列是函数的起始地址。
Address Publics by Value Rva+Base Lib:Object 0001:00000000 _WinMain@16 00401000 f MAPFILE.obj 0001:000000c0 ?MyRegisterClass@@YAGPAUHINSTANCE__@@@Z 004010c0 f MAPFILE.obj 0001:00000150 ?InitInstance@@YAHPAUHINSTANCE__@@H@Z 00401150 f MAPFILE.obj 0001:000001b0 ?WndProc@@YGJPAUHWND__@@IIJ@Z 004011b0 f MAPFILE.obj 0001:00000310 ?About@@YGJPAUHWND__@@IIJ@Z 00401310 f MAPFILE.obj 0001:00000350 _WinMainCRTStartup 00401350 f LIBC:wincrt0.obj 0001:00000446 __amsg_exit 00401446 f LIBC:wincrt0.obj 0001:0000048f __cinit 0040148f f LIBC:crt0dat.obj 0001:000004bc _exit 004014bc f LIBC:crt0dat.obj 0001:000004cd __exit 004014cd f LIBC:crt0dat.obj 0001:00000591 __XcptFilter 00401591 f LIBC:winxfltr.obj 0001:00000715 __wincmdln 00401715 f LIBC:wincmdln.obj //SNIPPED FOR BETTER READING 0003:00002ab4 __FPinit 00408ab4 <common> 0003:00002ab8 __acmdln 00408ab8 <common> entry point at 0001:00000350 Static symbols 0001:000035d0 LeadUp1 004045d0 f LIBC:memmove.obj 0001:000035fc LeadUp2 004045fc f LIBC:memmove.obj //SNIPPED FOR BETTER READING 0001:00000577 __initterm 00401577 f LIBC:crt0dat.obj 0001:0000046b _fast_error_exit 0040146b f LIBC:wincrt0.obj
public function部分后面是行信息(如果您在链接选项卡中使用了/MAPINFO:LINES,在C/C++选项卡中选择了“行号”,Vs2005开始没有了)。之后,如果项目包含导出的函数,并且在“链接”选项卡中包含/MAPINFO:EXPORTS,则将获得导出信息。
Line numbers for .\Release\MAPFILE.obj(F:\MAPFILE\MAPFILE.cpp) segment .text 24 0001:00000000 30 0001:00000004 31 0001:0000001b 32 0001:00000027 35 0001:0000002d 53 0001:00000041 40 0001:00000047 43 0001:00000050 45 0001:00000077 47 0001:00000088 48 0001:0000008f 52 0001:000000ad 53 0001:000000b3 71 0001:000000c0 80 0001:000000c3 81 0001:000000c8 82 0001:000000ff 86 0001:00000114 88 0001:00000135 89 0001:00000145 102 0001:00000150 108 0001:00000155 110 0001:00000188 122 0001:0000018d 115 0001:0000018e 116 0001:0000019a 119 0001:000001a1 121 0001:000001a8 122 0001:000001ae 135 0001:000001b0 143 0001:000001cc 172 0001:000001ee 175 0001:0000020d 149 0001:00000216 157 0001:0000022c 175 0001:00000248 154 0001:00000251 174 0001:0000025f 175 0001:00000261 151 0001:0000026a 174 0001:00000287 175 0001:00000289 161 0001:00000294 164 0001:000002a8 165 0001:000002b6 166 0001:000002d8 174 0001:000002e7 175 0001:000002e9 169 0001:000002f2 174 0001:000002fa 175 0001:000002fc 179 0001:00000310 186 0001:0000031e 193 0001:0000032e 194 0001:00000330 188 0001:00000333 183 0001:00000344 194 0001:00000349
现在我们来看看坠机地点。首先,我们将确定哪个函数包含崩溃地址。在“Rva+Base”列中搜索第一个地址大于崩溃地址的函数。映射文件中的前一项是导致崩溃的函数。在我们的示例中,崩溃地址是0x004011a1。这在0x00401150和0x004011b0之间,所以我们知道崩溃函数是?InitInstance@@YAHPAUHINSTANCE__@@H@Z 。以问号开头的任何函数名都是C++修饰的名称。要转换名称,请将其作为命令行参数传递给平台SDK程序UNDNAME.EXE(在bin dir中)。大多数时候,您不需要这样做,因为您可以通过查看它来了解它(这里是MAPFILE.obj中的InitInstance())。
这是追踪错误的一大步。但情况变得更好了:我们可以查出车祸发生在哪条线上!我们需要做一些基本的十六进制数学,所以没有计算器就做不到的人:现在是使用它的时候了。第一步是以下计算:crash_address-preferred_load_address-0x1000。
地址是从第一个代码段开始的偏移量,因此我们需要进行此计算。减去首选加载地址是合乎逻辑的,但为什么我们需要再减去一个0x1000?崩溃地址是代码段开头的偏移量,但二进制文件的第一部分不是代码段!二进制文件的第一部分是可移植可执行文件(PE),长度为0x1000字节。谜团解开了。在我们的示例中,这是:0x004011a1-0x00400000-0x1000=0x1a1
现在是时候查看Map文件的行信息部分了。这些行如下所示:30 0001:00000004。第一个数字是行号,第二个数字是该行所在代码段开头的偏移量。如果我们想查找行号,我们只需做与函数相同的事情:确定第一次出现的偏移量大于我们刚刚计算的偏移量。崩溃发生在前面的条目中。在我们的示例中:0x1a1在0x1a8之前。所以我们的崩溃发生在MAPFILE.CPP的119行。
如果我们是VS2005及以后的版本,只能借助其他手段来获取代码行了。
保留Map文件
关于异常STATUS_IN_PAGE_ERROR(0xC0000006)
简介
STATUS_IN_PAGE_ERROR---页内故障,这是当Windows尝试从内存映射文件将数据读入RAM并发生阻止数据读取的I / O错误时发生的非常低级别的错误。当它发生在.exe时,如setup.exe程序,这是一个致命的错误,因为它无法执行无法读取的代码。表示0x%p处的指令引用了位于0x%p的内存。由于0x%x的I/O错误状态,未将所需数据放入内存。线程试图访问一个不存在的页面,并且系统无法加载页面。-即程序或内存映射文件无法调出,因为它不再可访问。设备如果读取出错,驱动程序可以返回此异常。其定义如下:
//
// MessageId: STATUS_IN_PAGE_ERROR
//
// MessageText:
//
// The instruction at 0x%p referenced memory at 0x%p. The required data was not placed into memory because of an I/O error status of 0x%x.
//
#define STATUS_IN_PAGE_ERROR ((NTSTATUS)0xC0000006L) // winnt
异常结构说明
ExceptionAddress: 03638c90 (libcef!v8::internal::CopyCharsUnsigned<unsigned char,unsigned short>+0x00000020)
ExceptionCode: c0000006 (In-page I/O error)
ExceptionFlags: 00000000
NumberParameters: 3
Parameter[0]: 00000000
Parameter[1]: 07b41828//IO操作失败的内存地址
Parameter[2]: c000000e//具体IO操作错误
Inpage operation failed at 07b41828, due to I/O error c000000e
说明
应用程序有时从网络共享启动,一些客户在运行应用程序时报告了外部异常C0000006。根据我在谷歌的研究,这个“可能”与Image被调出和无法从网络重新加载有关。解决方法是通过设置IMAGE_FILE_NET_RUN_FROM_SWAP标志,告诉Windows将完整的映像文件加载到交换中并从那里运行它。我的应用程序还依赖于运行时加载的各种.bpl和.dll库。其中只有一部分可以由我更改,有些由其他供应商提供。如果exe设置了这个标志,这个库会发生什么?是否也加载到交换文件中,还是仍被调出并在需要时重新加载?我是否也需要在库中包含此标志?该标志仅适用于设置它的PE模块。因此,在EXE中设置标志并不意味着该EXE加载的模块会受到该标志的影响。由EXE加载的每个模块(DLL、包等)将由加载程序根据该模块中指定的PE选项进行处理。因此,您需要在网络共享上的每个模块上设置PE标志。
我还建议添加IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP。
使用Windows事件查看器调试崩溃
本文讨论如何使用Windows事件查看器获取实际崩溃的模块以及代码中崩溃的位置。示例代码是用C++编写的,以生成不同类型的崩溃,例如访问冲突和堆栈溢出。
简介
我经常听同事和QA那里听说,一个特定的崩溃很容易在客户机上重现,而不是在他们的机器上重现。这是一个棘手的问题,因为开发人员无法在客户机上调试崩溃。最终的结果是支持团队和客户之间无休止的沟通,甚至是现场会议。很少有聪明的程序员自己开发一个崩溃日志系统来确定导致崩溃的代码。很少有人会在代码中全面地实现try-catch块,以缩小问题的范围。
背景
近年来,我开始使用事件查看器检查在特定计算机上注册的各种警告和错误的日志。我注意到应用程序或程序崩溃记录在应用程序事件日志中,并且在大多数情况下都有足够的信息来获取崩溃或问题位置。事件查看器通常位于C:\ Windows\system32\eventvwr.exe中,一旦启动,就可以轻松查看应用程序事件日志。
当应用程序或程序在特定机器上崩溃时,类似的信息也会显示给用户。
如何调试崩溃?
为了更好地理解事件记录器/查看器,我决定创建一个简单的程序,当某个特定的命令行参数传递给它时,该程序将崩溃。
HowToFindCrashInExeCode.exe以1到4之间的数字作为参数,然后通过生成适当的异常来相应地崩溃。1号和3号生成访问冲突异常,而2号和4号分别在从属DLL和主EXE中生成StackOverflow异常。下面的两个图像显示了当程序在命令行上崩溃时,使用1作为输入参数的崩溃报告和应用程序事件日志。
应用程序事件日志提供给我们的重要细节是错误应用程序路径、错误模块名称和路径、异常代码以及最重要的错误偏移量。错误应用程序路径、错误模块名称和路径的目的非常明显。异常代码揭示了崩溃发生的细节和/或情况。故障偏移量是加载的故障模块内的内存位置,即它为我们提供了日志中提到的故障模块中的准确故障位置。从客户处获取应用程序事件日志后,请检查故障模块名称、路径和故障偏移量,然后在计算机上启动应用程序并将其附加到调试器。找到加载的故障模块的起始内存地址,并将故障偏移量添加到此地址。然后使用反汇编跳转到内存地址。反汇编将准确地告诉您崩溃的位置。这不是一个很酷很快就能解决问题的方法吗。上面的事件管理器日志告诉我们,错误模块是HowToFindCrashInDLLCode.dll,异常代码是0xc000005,这是访问冲突异常,错误偏移量是0x00001032。下图描述了howtoFindCrashHindllcode.dll的反汇编以及模块加载地址。
模块加载地址为0x73D60000,现在添加错误偏移量0x00001032。产生的内存地址是0x73D61032。跳转到此内存位置后,可以看到崩溃来自函数crashForAccessViolation,生成此崩溃的代码是pVal[0]=10;因为pVal是未实例化的整数指针。
比较有趣的点
在开发人员的机器上调试相同版本/配置/平台的程序以获得准确的错误位置是很重要的。另外,如果为程序生成了pdb,那么一旦跳转到错误偏移量,就可以看到反汇编和源代码。不需要在禁用优化的情况下构建程序,因为该程序的错误偏移量是通用的,开发人员需要自己做一些基本的数学计算。有时,崩溃模块是系统DLL之一,例如kernel.DLL、nt.DLL或msvcr100.DLL,然后按上述方法检查故障偏移量,并检查异常代码。这两件事将帮助您猜测代码中的问题,例如STL或CRT库抛出一些异常,如逻辑错误,有时会生成未处理的异常,这些异常会被系统DLL捕获。