Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

DIY 获取 CLR 错误代码描述信息的小工具

Posted on 2004-08-26 01:09  Flier Lu  阅读(2358)  评论(2编辑  收藏  举报
原文:http://www.blogcn.com/User8/flier_lu/index.html?id=3505936

在跟踪调试 CLR 代码,或者编写 CLR 宿主 (Host) 代码时,经常会碰到一些 CLR 本身返回的错误代码,如 0x80131010。这些 HRESULT 代码表示 CLR 的某种内部错误状态,虽然可以通过 .NET Framework SDK 的 CorError.h 文件反查到,但每次还需要手工将之转换为 16 进制数,再截取低 16 位的值,才能找到相应的错误代码定义,得到一个并不详细的注释信息。特别是我在同时对多个版本 CLR 进行调试跟踪时,手工查阅非常繁琐,而 VC 自带的那个 Error Lookup 程序,又无法正常处理 CLR 的异常信息。
以下将简单介绍如何实现一个自动针对不同 CLR 版本,查找对应错误代码的 CLR Error Lookup 程序的实现思路。

程序整体思路很简单:

1.获取当前机器上安装的多个不同版本 CLR 的版本号,以及相应的安装路径
2.根据用户选择的 CLR 版本号,载入相应的错误描述信息资源文件
3.根据用户输入的错误代码,查找并显式相应的错误描述信息

首先,我们来看看如何获取当前机器上安装的多个不同版本 CLR 的版本号,以及相应的安装路径。

注册表项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework 中保存着绝大多数机器一级的 CLR 安装信息。其中 InstallRoot 的值就是 .NET Framework 安装的根目录,例如我机器上 InstallRoot = "E:\WINDOWS\Microsoft.NET\Framework\",在此目录下,分别安装了 v1.0.3705/v1.1.4322/v2.0.40607 三个版本的 CLR 环境。
获取这些具体版本信息的方法,既可以通过对 InstallRoot 子目录名遍历,也可以通过更为稳妥的注册表项来获取。注册表键 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\policy 下会分别为每个 Major.Minor 版本号的 CLR 建立一个子键,保存当前安装的 CLR 版本和此版本适用于的版本范围。例如在 v1.1 这个子键中内容如下:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\policy\v1.1]
"4322"="3706-4322"

其意义是当前安装的 v1.1 版本的 CLR 运行时环境,其 build 号为 4322,并且兼容所有 Build 号为 3706-4322 的 v1.1 版本。也就是说在 InstallRoot 目录下,将有一个 v1.1.4322 的目录保存着我们所需要的 CLR 支持文件。获取这些信息的代码如下:
private static readonly Regex PATTERN_VERSION = new Regex(@"vd.d");

private DirectoryInfo _installRoot;

private DirectoryInfo GetDotNetFrameworkPath()
{
  
using(RegistryKey regFx = Registry.LocalMachine.OpenSubKey(@"SOFTWAREMicrosoft.NETFramework"))
  
{
    DirectoryInfo fxPath 
= new DirectoryInfo(regFx.GetValue("InstallRoot").ToString());

    
if(!fxPath.Exists)
    
{
      
throw new FileNotFoundException(".NET Framework Directory is not exist.");
    }


    
return fxPath;
  }

}


private String[] GetDotNetFrameworkVersions()
{
  ArrayList vers 
= new ArrayList();

  
using(RegistryKey regPolicy = Registry.LocalMachine.OpenSubKey(@"SOFTWAREMicrosoft.NETFrameworkpolicy"))
  
{
    
foreach(string version in regPolicy.GetSubKeyNames())
    
{
      
if(PATTERN_VERSION.IsMatch(version))
      
{
        
using(RegistryKey regClr = regPolicy.OpenSubKey(version))
        
{
          
foreach(string build in regClr.GetValueNames())
          
{
            vers.Add(String.Format(
"{0}.{1}", version, build));
          }

        }

      }

    }

  }


  
return (string[])vers.ToArray(typeof(string));
}


private void frmMain_Load(object sender, System.EventArgs e)
{
  
try
  
{
    _installRoot 
= GetDotNetFrameworkPath();

    cboVersion.Items.AddRange(GetDotNetFrameworkVersions());

    cboVersion.SelectedIndex 
=
      cboVersion.Items.IndexOf(
"v" + Environment.Version.ToString(3));
  }

  
catch(Exception)
  
{
    
string clrPath = RuntimeEnvironment.GetRuntimeDirectory();
    
int off = clrPath.LastIndexOf(RuntimeEnvironment.GetSystemVersion());

    _installRoot 
= new DirectoryInfo(clrPath.Substring(0, off));

    cboVersion.Items.Clear();
    cboVersion.SelectedIndex 
= cboVersion.Items.Add(RuntimeEnvironment.GetSystemVersion());
    cboVersion.Enabled 
= false;
  }

}


在读取注册表失败的情况下,上述代码会通过 BCL 提供的 RuntimeEnvironment 获取当前 CLR 的相关路径信息,保障最少有一种 CLR 信息可以获取。这儿的 RuntimeEnvironment.GetRuntimeDirectory() 调用将返回完整的 CLR 安装路径,如 E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322;而 RuntimeEnvironment.GetSystemVersion() 调用将返回 CLR 的版本信息,如 v1.1.4322。

其次,在获取了版本列表和安装信息后,需要对用户的版本选择进行响应,载入相应版本和相应本地化语言的错误描述信息资源文件。

此资源文件名为 mscorrc.dll,一般在 CLR 的安装目录下会有一个语言无关的版本 (英语),而在相应语言目录中会有本地化后的版本 (中文)。要处理这个载入策略,就需要通过 CultureInfo.CurrentUICulture 获取当前的本地化信息。CurrentUICulture.Name 和 CurrentUICulture.Parent.Name 将分别返回当前语言及其父语言的简单名称,如 zh-CN 及其父语言 zh-CHS。程序需要优先尝试是否有本地化的资源文件,如果没有才载入语言无关的版本。载入错误信息描述资源文件的代码如下:
[DllImport("kernel32", CharSet=CharSet.Auto, ExactSpelling=false, SetLastError=true)]
extern static IntPtr LoadLibrary(string lpFileName);

private IntPtr _hMsCorRc;

private const string MSCORRC = "mscorrc.dll";

private void cboVersion_SelectedIndexChanged(object sender, System.EventArgs e)
{
  DirectoryInfo fxPath 
= new DirectoryInfo(_installRoot.FullName + cboVersion.Text);

  
if(!fxPath.Exists)
  
{
    
throw new FileNotFoundException(".NET Framework Directory is not exist.");
  }


  
string[] files = new string[]
  
{
    fxPath.FullName 
+ Path.DirectorySeparatorChar +
    CultureInfo.CurrentUICulture.Name 
+ Path.DirectorySeparatorChar + MSCORRC,

    fxPath.FullName 
+ Path.DirectorySeparatorChar +
    CultureInfo.CurrentUICulture.Parent.Name 
+ Path.DirectorySeparatorChar + MSCORRC,

    fxPath.FullName 
+ Path.DirectorySeparatorChar + MSCORRC
  }
;

  FileInfo dll 
= null;

  
foreach(string file in files)
  
{
    dll 
= new FileInfo(file);

    
if(dll.Exists) break;

    dll 
= null;
  }


  
if(dll == null)
  
{
    
//
    
// Hack it, force CLR to load mscorrc.dll for raise exception
    
// copy from Junfeng Zhang (http://blog.joycode.com/junfeng/)
    
//
    try{ Assembly.Load("Whatever not exist module.dll"); } catch(Exception) {};
  }


  IntPtr hMsCorRc 
= LoadLibrary(dll == null ? MSCORRC : dll.FullName);

  
if(hMsCorRc == IntPtr.Zero)
    
throw new Win32Exception();

  _hMsCorRc 
= hMsCorRc;
}


上述代码通过一个 foreach 循环,遍历可能的三个存储位置,将发现的第一个资源文件,通过 LoadLibrary 函数载入。
此外根据 Junfeng Zhang 的建议,这儿增加了一个 CLR hack code。在几种策略都没有找到资源文件时,通过执行一个肯定不会成功的 CLR 操作,强迫 CLR 自动载入 mscorrc.dll 以抛出异常。然后在 LoadLibrary 时就无需指定完整路径,由操作系统自动根据同名原则加载相应的资源文件。
这儿的文件定位策略可以参考 rotor 中 CCompRC::LoadResourceLibrary (utilcode/posterror.cpp:317) 的实现。

最后,解析用户输入的错误代码,根据用户输入查找并显式相应的错误描述信息。

解析输入时,如果用户输入以 0x 开头,则直接以 16 进制解析解析;否则将首先尝试以 10 进制解析,失败后再尝试 16 机制解析。代码如下:
private void btnLookup_Click(object sender, System.EventArgs arg)
{
  
string str = edtValue.Text;

  
if(str == ""return;

  
uint code = 0;
  NumberStyles style 
= NumberStyles.Integer;

  
if(str.StartsWith("0x"))
  
{
    str 
= str.Substring(2);

    style 
= NumberStyles.HexNumber;
  }


  
try
  
{
    code 
= UInt32.Parse(str, style);
  }

  
catch(Exception e)
  
{
    
string errMsg = e.Message;

    
if(style != NumberStyles.HexNumber)
    
{
      
try
      
{
        code 
= UInt32.Parse(str, NumberStyles.HexNumber);

        errMsg 
= null;
      }

      
catch(Exception e2)
      
{
        errMsg 
= e2.Message;
      }

    }


    
if(errMsg != null)
    
{
      MessageBox.Show(
this, errMsg, "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);

      
return;
    }

  }


  
string msg = GetErrorMessage(code);

  
if(msg.Length == 0)
    MessageBox.Show(
this"Message not found""Information", MessageBoxButtons.OK, MessageBoxIcon.Information);
  
else
    memErrMsg.Text 
= msg;
}

多解析成功的错误代码,调用 GetErrorMessage 函数,根据其错误代码分类来判断去哪儿获取错误描述信息。对 CLR 来说,所有错误信息 HRESULT 值的严重程度(SEVERITY)都是 SEVERITY_ERROR,而其设备分类号(FACILITY)都是 0x13。获取错误描述信息代码如下:
[DllImport("user32", CharSet=CharSet.Auto, ExactSpelling=false, SetLastError=true)]
extern static int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax);

public const uint SEVERITY_SUCCESS = 0;
public const uint SEVERITY_ERROR = 1;
public const uint SEVERITY_MASK = 1;

public const uint FACILITY_URT = 0x13;

private uint HRESULT_SEVERITY(uint hr)
{
  
return (hr >> 31& SEVERITY_MASK;
}


private uint HRESULT_FACILITY(uint hr)
{
  
return (hr >> 16& 0x1FFF;
}


private uint HRESULT_CODE(uint hr)
{
  
return hr & 0xFFFF;
}


private string GetErrorMessage(uint ErrorCode)
{
  
if(HRESULT_SEVERITY(ErrorCode) == SEVERITY_ERROR &&
     HRESULT_FACILITY(ErrorCode) 
== FACILITY_URT)
  
{
    StringBuilder sb 
= new StringBuilder(1024);

    sb.Length 
= LoadString(_hMsCorRc, HRESULT_CODE(ErrorCode), sb, sb.Capacity);

    
return sb.ToString();
  }

  
else
  
{
    
return new Win32Exception((int)ErrorCode).Message;
  }

}

对 CLR 的信息,从刚刚载入的 mscorrc.dll 中通过 LoadString 获取错误信息;而对于其他的错误代码,则通过构造一个新的 Win32Exception 异常对象,然后借助 Win32Exception.Message 的实现代码,最终通过 FormatMessage 系统调用完成消息的获取和格式化工作。
完整的代码和编译后程序可以从这里下载:

CLR Error Lookup

不过据 Junfeng Zhang 透露,CLR 2.0 中可能会对类似功能提供直接的支持。我注意到 CLR 2.0 beta 中,在 mscoree.dll 中提供了 LoadStringRC/LoadStringRCEx 函数,将前面提到的 CCompRC 类的功能提供给最终使用者。通过这两个函数,可以很容易地实现上述功能,因为大部分工作,如 mscorrc.dll 的载入,都由 CLR 自动完成了。

以下内容为程序代码:

STDAPI LoadStringRC(UINT iResouceID, LPWSTR szBuffer, int iMax, int bQuiet);
STDAPI LoadStringRCEx(LCID lcid, UINT iResouceID, LPWSTR szBuffer, int iMax, int bQuiet, int *pcwchUsed);


期待 CLR 2.0 ... :P