这种处理的有效性主要取决于所选择的语言和平台,因此,详细了解它们的正确用法和行为非常重要,这样我们的用户和其他开发人员在诊断代码中的问题时免受痛苦。
在本文中,我们将了解C和.NET在错误处理方面的作用。

词汇表


CLR:公共语言运行时的缩写,是.NET运行时,它负责执行用所有.NET语言编译的应用程序。除了虚拟机和实时编译器之外,它还具有额外的职责,如内存管理、安全性等。

BCL:Base Class Library的缩写,是.NET framework的核心库。除了直接使用CLR操作之外,它还公开了原始数据类型和构建和运行应用程序的基本功能。也称为mscorlib。

FCL:Framework类库的缩写,是我们大多数人在.NET中所知道的“框架”。使用BCL作为构建块,它公开了大量具有各种特性的名称空间,比如系统IO, 系统安全, 系统文本,等等。

TPL:Task Parallel Library的缩写,是一个包含由异步关键字和API提供的功能的库。它是随着.NET版本4.5和C#5一起发布的。

SEH:Structured Exception Handling的缩写,是Windows的原生异常子系统,它在操作系统级别处理软件和硬件异常

MDA:托管调试助手的缩写。这些是特殊的调试扩展,向VisualStudio调试器提供与CLR执行状态相关的信息,后者由内部助手和资产公开。

重新审查异常

在.NET中,尤其是在C#中,异常是使用try、catch和finally块来处理的。首先,try块将包含预期引发异常的代码,其次是catch块,它将指定异常类型和在try块内引发与指定类型匹配的异常时将执行的代码块:

Random rnd = newRandom();try{
Console.WriteLine(
1 / rnd.Next(-100, 101));
}
catch(DivideByZeroException ex) {
Debug.WriteLine(“A division by zero attempt has occurred”);
}

如果到目前为止您还没有机会调试优化的x64代码,请不要再等待太久,也不要落后于时代!由于类似x64 fastcall的调用约定加上大量的通用寄存器,在调用堆栈中的任意点查找变量值确实非常困难。
在本文中,我想详细介绍一些我最喜欢的调试优化x64代码的技术。但是在深入研究这些技术之前,让我们先对x64调用约定有一个快速的概述。

x64调用约定

熟悉x86平台上fastcall调用约定的人将认识到与x64调用约定的相似之处。通常,您必须了解x86平台上的多个调用约定,而在x64平台上,目前只有一个。在这种情况下,通过__declspec(naked)调用(当然不包括直接调用)可以实现编码
我不会详细介绍x64呼叫约定的所有细微差别,因此我建议您查看以下链接(http://msdn.microsoft.com/en-us/library/ms794533.aspx). 但是通常,函数的前四个参数是通过寄存器rcx、rdx、r8和r9传递的。如果函数接受四个以上的参数,则这些参数将传递到堆栈上。(熟悉x86 fastcall调用约定的人,其中前两个参数是在ecx和edx中传递的,熟悉这种约定的人会认识到它们的相似之处)。
为了帮助说明x64调用约定是如何工作的,我创建了一些简单的示例代码。虽然代码是人为设计的,与真实世界中的代码相去甚远,但它演示了在实际世界中可能遇到的一些场景。代码如下所示。

#include <stdlib.h>#include<stdio.h>#include<windows.h>__declspec(noinline)voidFunctionWith4Params(int param1, int param2, intparam3,intparam4 )

{

size_t lotsOfLocalVariables1
=rand();

size_t lotsOfLocalVariables2
=rand();

size_t lotsOfLocalVariables3
=rand();

size_t lotsOfLocalVariables4
=rand();

size_t lotsOfLocalVariables5
=rand();

size_t lotsOfLocalVariables6
=rand();

DebugBreak();

printf(
"Entering FunctionWith4Params( %X, %X, %X, %X )\n",

param1, param2, param3, param4 );

printf(
"Local variables: %X, %X, %X, %X, %X, %X \n",

lotsOfLocalVariables1, lotsOfLocalVariables2,

lotsOfLocalVariables3, lotsOfLocalVariables4,

lotsOfLocalVariables5, lotsOfLocalVariables6 );

}

__declspec(noinline)
voidFunctionWith5Params(int param1, int param2, intparam3,int param4, intparam5 )

{

FunctionWith4Params( param5, param4, param3, param2 );

FunctionWith4Params( rand(), rand(), rand(), rand() );

}

__declspec(noinline)
voidFunctionWith6Params(int param1, int param2, intparam3,int param4, int param5, intparam6 )

{

size_t someLocalVariable1
=rand();

size_t someLocalVariable2
=rand();

printf(
"Entering %s( %X, %X, %X, %X, %X, %X )\n","FunctionWith6Params",

param1, param2, param3, param4, param5, param6 );

FunctionWith5Params( rand(), rand(), rand(),

param1, rand() );

printf(
"someLocalVariable1 = %X, someLocalVariable2 = %X\n",

someLocalVariable1, someLocalVariable2 );

}
intmain(int /*argc*/, TCHAR** /*argv*/)

{
//I use the rand() function throughout this code to keep//the compiler from optimizing too much. If I had used//constant values, the compiler would have optimized all//of these away. int params[] ={ rand(), rand(), rand(),

rand(), rand(), rand() };

FunctionWith6Params(
params[0], params[1], params[2],params[3], params[4], params[5] );return 0;

}

四、系统信息流

流目录后面紧接着就是流数据了。第一个流数据就是系统信息流。

可知,这个流的起始于文件偏移0xEC,大小是0x38,也就是56个字节。

从上图可知,系统信息流就是紧挨着流目录尾部。这个流包含了如下操作系统和处理器信息:处理器架构、级别、版本,CPU信息等。数据结构如下:

typedef struct_MINIDUMP_SYSTEM_INFO {
USHORT ProcessorArchitecture;
USHORT ProcessorLevel;
USHORT ProcessorRevision;
union {
USHORT Reserved0;
struct{
UCHAR NumberOfProcessors;
UCHAR ProductType;
};
};
ULONG32 MajorVersion;
ULONG32 MinorVersion;
ULONG32 BuildNumber;
ULONG32 PlatformId;
RVA CSDVersionRva;
union {
ULONG32 Reserved1;
struct{
USHORT SuiteMask;
USHORT Reserved2;
};
};
CPU_INFORMATION Cpu;
} MINIDUMP_SYSTEM_INFO,
*PMINIDUMP_SYSTEM_INFO;

五、杂项信息流(MiscInfoStream)

在系统信息流后紧挨着的就是杂项信息流。而系统信息流信息如下

0xEC+0n56=0x124

而杂项信息流如下:

可知偏移是0x124,即证明杂项信息流是紧挨着系统信息流,大小有1364字节。

MiscInfoStream包含各种信息。数据结构如下:

typedef struct_MINIDUMP_MISC_INFO_5 {
ULONG32 SizeOfInfo;
ULONG32 Flags1;
ULONG32 ProcessId;
ULONG32 ProcessCreateTime;
ULONG32 ProcessUserTime;
ULONG32 ProcessKernelTime;
ULONG32 ProcessorMaxMhz;
ULONG32 ProcessorCurrentMhz;
ULONG32 ProcessorMhzLimit;
ULONG32 ProcessorMaxIdleState;
ULONG32 ProcessorCurrentIdleState;
ULONG32 ProcessIntegrityLevel;
ULONG32 ProcessExecuteFlags;
ULONG32 ProtectedProcess;
ULONG32 TimeZoneId;
TIME_ZONE_INFORMATION TimeZone;
WCHAR BuildString[MAX_PATH];
WCHAR DbgBldStr[
40];
XSTATE_CONFIG_FEATURE_MSC_INFO XStateData;
ULONG32 ProcessCookie;
} MINIDUMP_MISC_INFO_5,
*PMINIDUMP_MISC_INFO_5;

typedef
struct_TIME_ZONE_INFORMATION {
LONG Bias;
WCHAR StandardName[
32];
SYSTEMTIME StandardDate;
LONG StandardBias;
WCHAR DaylightName[
32];
SYSTEMTIME DaylightDate;
LONG DaylightBias;
} TIME_ZONE_INFORMATION,
*PTIME_ZONE_INFORMATION, *LPTIME_ZONE_INFORMATION;

typedef
struct_XSTATE_CONFIG_FEATURE_MSC_INFO
{
ULONG32 SizeOfInfo;
ULONG32 ContextSize;
ULONG64 EnabledFeatures;
XSTATE_FEATURE Features[MAXIMUM_XSTATE_FEATURES];
} XSTATE_CONFIG_FEATURE_MSC_INFO,
*PXSTATE_CONFIG_FEATURE_MSC_INFO;

Microsoft调试工具for Windows提供了两种创建远程调试连接的方法,包括“连接到远程会话”和“连接到远程存根”。连接到远程会话是这两个会话中最常用的,也是您通常需要的全部内容,但是它不允许客户端访问远程上缺少的调试符号。
通常,远程调试会话将在所有用户都可以访问符号信息的公司网络中完成,但是如果我们在无法自由共享符号的安全环境之外进行调试,调试将变得更加困难。
Microsoft调试工具包有两个服务器端命令行应用程序,可通过连接到远程存根来帮助我们完成此操作:Dbgsrv.exe文件用于用户模式调试和Kdsrv.exe文件用于内核模式调试。它们为调试器提供了一个精简的服务器层,以充分利用本地符号和源代码进行附加。

让我们通过展示连接到远程会话时遇到的问题来说明使用远程存根的好处。

此图显示了传统的远程调试会话。调试主机和目标都在运行windbg,不过我们可以使用KD或CDB。目标是调试一个有缺陷的应用程序Getif.exe它在调试目标上运行。我们在调试主机上有应用程序的符号,但是出于安全原因,调试目标没有符号访问权限Getif.exe. 在这两台计算机上,我们都从http://msdl.microsoft.com/download/symbols取符号。
以下是调试目标计算机上的符号路径。因为此系统不在我们的安全环境之外,所以它没有Getif.exe应用程序。这对于调试是有问题的。
这是调试主机上的符号路径,其中包括指向应用程序符号的路径,因为它位于安全位置内

 

 

要在目标计算机上创建远程调试服务器,请使用.server命令指定用于连接的本地端口:.server tcp:port=<localport>

 

 

接下来,我在调试主机上选择“connect to remotesession”。

 

 

然后输入端口和服务器名称:

这就是我们头疼的地方!因为我们没有Getif.exe符号访问位于安全环境外部的计算机上,堆栈getif.exe不显示符号信息。这使得调试应用程序变得很困难,因为我们无法转储数据类型或查看其他符号信息。

如果我们想调试Getif.exe对于符号,我们需要将符号文件复制到调试目标计算机,并将位置添加到目标的符号路径中。在我们的场景中,这可能是不可能的,因为符号通常仅限于内部使用,不允许自由复制。

通过在调试目标计算机上激活Dbgsrv,调试主机上的调试器将通过对目标计算机上用户模式进程的完全符号访问进行连接。上图说明了这个过程。
让我们在调试目标上运行一个使用dbgsrv的示例。在调试目标上输入:<path to debugging tools>\dbgsrv -t tcp:port=<localport>

Dbgsrv返回命令提示符,命令窗口中没有输出,调试端口可以是系统上任何打开的端口。最好用netstat确认我们正在监听我们选择的端口。下面是一个例子

现在让我们把焦点转移到调试主机上。我们没有选择典型的“连接远程会话”,而是使用“文件”菜单中的“连接到远程存根”选项。

接下来输入端口和服务器名称。

选择OK之后,我们将返回到空的命令窗口。在连接到目标进程之前,我们不会看到任何文本:

下一步选择“文件附加到进程”选项

请注意,“附加到进程”对话框显示调试目标计算机上运行的进程列表。

现在让我们来看看这个过程。标题栏确认我们已连接到kdsrv目标上的进程:

在这里您可以看到与远程存根连接的好处。符号getif.exe正在为运行在安全环境之外的计算机上的应用程序工作。

但是如果我需要内核调试呢?您可以使用Kdsrv对内核调试执行相同的操作。

如果需要的话,我们可以使用内核调试器srkdv。与前面的示例非常相似,我们输入命令:kdsrv -t tcp:port=<localport>

我们可以看到我们正在监听我们选择的本地端口:

现在,我们必须在调试主机上启动调试器,命令行包含连接到KdSrv主机所需的信息。从Windbg 6.10.3.233开始,我们无法从Windbg图形界面连接到kdsrv远程存根,然后再将内核连接到调试目标。

我在上面的图中用的是字符串-windbg -k kdsrv:server=@{tcp:server=rodolpho,port=5004},trans=@{com:pipe,port=\\mimi\pipe\2008target}

请注意,我们正在调试运行在Microsoft Hyper-V下的内核调试目标上的命名管道!这是一个很好的调试方法,无需设置第二台计算机和连接空调制解调器电缆。如果我们想用空调制解调器电缆连接到COM1,我们将使用:windbg -k kdsrv:server=@{tcp:server=rodolpho,port=5004},trans=@{com:port=com1,baud=11500}

Windbg启动后,命令窗口看起来就像我们附加了一个本地调试器一样。标题栏将确认我们正在通过调试服务器附加:

我们还可以将上下文更改为Getif.exe连接到远程存根后处理。当我们查看应用程序堆栈时,我们有应用程序的符号:

在我们有了远程存根连接之后,我们就可以作为远程会话共享我们的客户机,但是我们将始终处理远程存根客户机上的符号。一旦你设置了一个远程存根几次,它就像设置一个远程会话一样简单,并且可以使远程调试更加顺利。
安全说明:这些示例使用TCP/IP或命名管道,不受密码保护。如果您需要更高的安全性,调试器远程将允许SSL和安全管道连接以及密码和证书身份验证。