Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西
posts - 115, comments - 322, trackbacks - 42, articles - 0

2004年7月8日

初来乍到,多谢各位捧场了,呵呵

这儿贴的都是些平日自娱自乐的原创文章,有意见建议讨论板砖什么的尽管上,溢美之辞就不必了

以后关于 .NET 方面的文章我会往这边转贴一份,其他过于 Windows 等方向的问题,有兴趣的朋友可以到我在 blogcn 上的主站上与我讨论。

http://flier_lu.blogone.net/

此外多谢hBifTs的热心推荐,以及帮忙转贴旧时文章。

posted @ 2004-07-08 14:23 Flier Lu 阅读(1392) 评论(12) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2602647&run=.0A0B923

实际上,在 C# 中也提供了隐藏的对 vararg 类型方法定义和调用的支持,那就是 __arglist 关键字。
以下内容为程序代码:

public class UndocumentedCSharp
{
  [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
  extern static int printf(string format, __arglist);

  public static void Main(String[] args)
  {
    printf("%s %d", __arglist("Flier Lu", 1024));
  }
}

    可以看到 __arglist 关键字实际上起到了和 C++ 中 va_list 类似的作用,直接将任意多个参数按顺序压入堆栈,并在调用时处理。而在 IL 代码一级,则完全类似于上述 IL 汇编和 Managed C++ 的例子:
以下内容为程序代码:

.method private hidebysig static pinvokeimpl("msvcrt.dll" ansi cdecl)
        vararg int32  printf(string format) cil managed preservesig
{
}

.method public hidebysig static void  Main(string[] args) cil managed
{
  IL_0033:  ldstr      "%s %d"
  IL_0038:  ldstr      "Flier Lu"
  IL_003d:  ldc.i4     0x400
  IL_0042:  call       vararg int32 UndocumentedCSharp::printf(string,
                                                               ...,
                                                               string,
                                                               int32)
}

    __arglist 除了可以用于与现有代码进行互操作,还可以在 C# 内作为与 params 功能上等同的特性来使用。只不过因为没有 C# 编译器在语义一级的支持,必须用相对复杂的方式进行操作。
以下内容为程序代码:

using System;
using System.Runtime.InteropServices;

public class UndocumentedCSharp
{
  private static void Show(__arglist)
  {
    ArgIterator it = new ArgIterator(__arglist);

    while(it.GetRemainingCount() >0)
   {
   TypedReference tr = it.GetNextArg();

   Console.Out.WriteLine("{0}: {1}", TypedReference.ToObject(tr), __reftype(tr));
   }
  }

  public static void Main(String[] args)
  {
    Show(__arglist("Flier Lu", 1024));
  }
}

    与 C++ 中不同,__arglist 参数不需要一个前导参数来确定其在栈中的起始位置。
    ArgIterator则是一个专用迭代器,支持对参数列表进行单向遍历。对每个参数项,GetNextArg 将会返回一个 TypedReference 类型,表示指向参数。
    要理解这里的实现原理,就必须单独先介绍一下 TypedReference 类型。
    我们知道 C# 提供了很多 CLR 内建值类型的名称映射,如 Int32 在 C# 中被映射为 int 等等。但实际上有三种 CLR 类型并没有在 C# 中被映射为语言一级的别名:IntPtr, UIntPtr 和 TypedReference。这三种类型在 IL 一级分别被称为 native int、native unsigned int 和 typedref。但在 C# 一级,则只能通过 System.TypedReference 类似的方式访问。而其中就属这个 TypedReference 最为奇特。
    TypedReference 在 MSDN 中的描述如下:
以下为引用:

    Describes objects that contain both a managed pointer to a location and a runtime representation of the type that may be stored at that location.

[CLSCompliant(false)]
public struct TypedReference

Remarks

    A typed reference is a type/value combination used for varargs and other support. TypedReference is a built-in value type that can be used for parameters and local variables.
    Arrays of TypedReference objects cannot be created. For example, the following call is invalid:

Assembly.Load("mscorlib.dll").GetType("System.TypedReference[]");


    也就是说,值类型 TypedReference 是专门用于保存托管指针及其指向内容类型的,查看其实现代码(bclsystemTypedReference.cs:28)可以验证这一点:
以下内容为程序代码:

public struct TypedReference
{
private int Value;
private int Type;

// 其他方法
}

    这儿 Value 保存了对象的指针,Type 保存了对象的类型句柄。
    使用的时候可以通过 __arglist.GetNextArg() 返回,也可以使用 __makeref 关键字构造,如:
以下内容为程序代码:

int i = 21;

TypedReference tr = __makeref(i);

    而其中保存的对象和类型,则可以使用 __refvalue 和 __reftype 关键字来获取。
以下内容为程序代码:

int i = 32;

TypedReference tr1=__makeref(i);

Console.Out.WriteLine("{0}: {1}", __refvalue(tr, int), __reftype(tr1));

    注意这儿的 __refvalue 关键字需要指定目标 TypedReference 和转换的目标类型,如果结构中保存的类型不能隐式转换为目标类型,则会抛出转换异常。相对来说,TypedReference.ToObject 虽然要求强制性 box 目标值,但易用性更强。

    从实现角度来看,__refvalue 和 __reftype 是直接将 TypedReference 的内容取出,因而效率最高。
以下内容为程序代码:

int i=5;
TypedReference tr = __makeref(i);
Console.Out.WriteLine("{0}: {1}", __refvalue(tr, int), __reftype(tr));

    上面这样一个代码片断,将被编译成:
以下内容为程序代码:

  IL_0048:  ldc.i4.5
  IL_0049:  stloc.0
  IL_004a:  ldloca.s   V_0
  IL_004c:  mkrefany   [mscorlib]System.Int32
  IL_0051:  stloc.1
  IL_0052:  call       class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
  IL_0057:  ldstr      "{0}: {1}"
  IL_005c:  ldloc.1
  IL_005d:  refanyval  [mscorlib]System.Int32
  IL_0062:  ldind.i4
  IL_0063:  box        [mscorlib]System.Int32
  IL_0068:  ldloc.1
  IL_0069:  refanytype
  IL_006b:  call       class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
  IL_0070:  callvirt   instance void [mscorlib]System.IO.TextWriter::WriteLine(string,
                                                                               object,
                                                                               object)

    可以看到 __makeref、__refvalue 和 __reftype 是通过 IL 语言的关键字 mkrefany、refanyval 和 refanytype 直接实现的。而这样的实现是通过直接对堆栈进行操作完成的,无需 TypedReference.ToObject 那样隐式的 box/unbox 操作,故而效率最高。
    JIT 中对 refanyval 的实现(fjit jit.cpp:8361)如下:
以下内容为程序代码:

FJitResult FJit::compileCEE_REFANYTYPE()
{

    // There should be a refany on the stack
    CHECK_STACK(1);
    // There has to be a typedref on the stack
    // This should be a validity check according to the spec, because the spec says
    // that REFANYTYPE is always verifiable. However, V1 .NET Framework throws verification exception
    // so to match this behavior this is a verification check as well.
    VERIFICATION_CHECK( topOpE() == typeRefAny );
    // Pop off the Refany
    POP_STACK(1);
    _ASSERTE(offsetof(CORINFO_RefAny, type) == sizeof(void*));      // Type is the second thing

    emit_WIN32(emit_POP_I4()) emit_WIN64(emit_POP_I8());            // Just pop off the data, leaving the type.

    CORINFO_CLASS_HANDLE s_TypeHandleClass = jitInfo->getBuiltinClass(CLASSID_TYPE_HANDLE);
    VALIDITY_CHECK( s_TypeHandleClass != NULL );
    pushOp(OpType(typeValClass, s_TypeHandleClass));
    return FJIT_OK;
}

    从以上代码可以看到,JIT 在处理 refanyval 指令时,并没有对堆栈内容进行任何操作,而是直接操作堆栈。

    如果希望进一步了解相关信息,可以参考以下介绍:

    Undocumented C# Types and Keywords

    Undocumented TypedReference

    A Sample Chapter from C# Programmers Reference - Value types

ps: 实测了一下发现,MS不公开 vararg 这种调用方式,大概是因为考虑效率方面的原因。与 params 相比,使用 vararg 的调用方式,纯粹函数调用的速度要降低一个数量级 :(
    下面这篇文章也讨论了这个问题,结论是不到万不得已情况下尽量少用,呵呵

    Why __arglist is undocumented

posted @ 2004-07-08 12:03 Flier Lu 阅读(1785) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2602611&run=.09D4C2F

C++ 语言因为缺省使用 cdecl 调用方式,故而可以很方便实现参数可变参数。详细的原理可以参考我另外一篇文章《The history of calling conventions
。具体到使用上,就是我们最常用的 printf 系列函数:
以下内容为程序代码:

int printf(const char *format, ...);

    对应到 C# 中,则是通过 params 关键字模拟类似的语法:
以下内容为程序代码:

using System;
public class MyClass
{
   public static void UseParams(params int[] list)
   {
      for ( int i = 0 ; i < list.Length ; i++ [img]/images/wink.gif[/img]
         Console.WriteLine(list[i]);
      Console.WriteLine();
   }

   public static void UseParams2(params object[] list)
   {
      for ( int i = 0 ; i < list.Length ; i++ [img]/images/wink.gif[/img]
         Console.WriteLine(list[i]);
      Console.WriteLine();
   }

   public static void Main()
   {
      UseParams(1, 2, 3);
      UseParams2(1, 'a', "test"[img]/images/wink.gif[/img];

      int[] myarray = new int[3] {10,11,12};
      UseParams(myarray);
   }
}

    可以看到,这个 params 关键字实际上是将传递数组的语义,在 C# 编译器一级做了语法上的增强,以模拟 C++ 中 ... 的语法和语义。在 IL 代码一级仔细一看就一目了然了。
以下内容为程序代码:

.class public auto ansi beforefieldinit MyClass extends [mscorlib]System.Object
{
  .method public hidebysig static void  UseParams(int32[] list) cil managed
  {
    //...
  }

  .method public hidebysig static void  UseParams2(object[] list) cil managed
  {
    //...
  }

  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       93 (0x5d)
    .maxstack  3
    .locals init (int32[] V_0,
             int32[] V_1,
             object[] V_2)
    IL_0000:  ldc.i4.3
    IL_0001:  newarr     [mscorlib]System.Int32 // 构造一个 size 为 3 的 int 数组
    //...
    IL_0014:  call       void MyClass::UseParams(int32[])
    //...
  }
}

    这种 syntax sugar 在 C# 这个层面来说应该是足够满足需求了的,但如果涉及到与现有 C++ 代码的交互等问题,其模拟的劣势就暴露出来了。例如前面所提到的 printf 函数的 signature 就不是使用模拟语法的 params 能够处理的。MSDN 中给出的解决方法是:
以下内容为程序代码:

using System;
using System.Runtime.InteropServices;

public class LibWrap
{
  // C# doesn't support varargs so all arguments must be explicitly defined.
  // CallingConvention.Cdecl must be used since the stack is
  // cleaned up by the caller.

  // int printf( const char *format [, argument]... [img]/images/wink.gif[/img]

  [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
  public static extern int printf(String format, int i, double d);

  [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
  public static extern int printf(String format, int i, String s);
}

public class App
{
    public static void Main()
    {
        LibWrap.printf(" Print params: %i %f", 99, 99.99);
        LibWrap.printf(" Print params: %i %s", 99, "abcd"[img]/images/wink.gif[/img];
    }
}

    通过定义多个可能的函数原型,来枚举可能用到的形式。这种实现方式感觉真是 dirty 啊,用中文形容偶觉得“龌龊”这个词比较合适,呵呵。

    但是实际上 C# 或者说 CLR 的功能绝非仅此而已,在 CLR 一级实际上早已经内置了处理可变数量参数的支持。
    仔细查看 CLR 的库结构,会发现对函数的调用方式实际上有两种描述:
以下内容为程序代码:

namespace System.Runtime.InteropServices
{
  using System;

[Serializable]
public enum CallingConvention
  {
    Winapi          = 1,
    Cdecl           = 2,
    StdCall         = 3,
    ThisCall        = 4,
    FastCall        = 5,
  }
}

namespace System.Reflection
{
using System.Runtime.InteropServices;
using System;

  [Flags, Serializable]
  public enum CallingConventions
  {
   Standard   = 0x0001,
   VarArgs   = 0x0002,
   Any     = Standard | VarArgs,
    HasThis       = 0x0020,
    ExplicitThis  = 0x0040,
  }
}

    System.Runtime.InteropServices.CallingConvention 是在使用 DllImport 属性定义外部引用函数时用到的,故而使用的名字都是与现有编程语言命名方式类似的。而 System.Reflection.CallingConventions 则是内部用于 Reflection 操作的,故而使用的名字是直接与 CLR 中方法定义对应的。
    这儿的 CallingConventions.VarArgs 正是解决我们问题的关键所在。在随 .NET Framework SDK 提供的 Tool Developers Guide 中,Partition II Metadata.doc 文档中是这样介绍 VarArgs 调用方式的:

以下为引用:

vararg Methods

    vararg methods accept a variable number of arguments.  They shall use the vararg calling convention (see Section 14.3).
    At each call site, a method reference shall be used to describe the types of the actual arguments that are passed.  The fixed part of the argument list shall be separated from the additional arguments with an ellipsis (see Partition I).
    The vararg arguments shall be accessed by obtaining a handle to the argument list using the CIL instruction arglist (see Partition III). The handle may be used to create an instance of the value type System.ArgIterator which provides a typesafe mechanism for accessing the arguments (see Partition IV).



以下内容为程序代码:

[b]Example (informative): [/b]

    The following example shows how a vararg method is declared and how the first vararg argument is accessed, assuming that at least one additional argument was passed to the method:

.method public static vararg void MyMethod(int32 required) {
.maxstack 3
.locals init (valuetype System.ArgIterator it, int32 x)
ldloca it // initialize the iterator
initobj  valuetype System.ArgIterator
ldloca it
arglist // obtain the argument handle
call instance void System.ArgIterator::.ctor(valuetype System.RuntimeArgumentHandle) // call constructor of iterator
/* argument value will be stored in x when retrieved, so load
   address of x */
ldloca x
ldloca it
// retrieve the argument, the argument for required does not matter
call instance typedref System.ArgIterator::GetNextArg()
call object System.TypedReference::ToObject(typedref) // retrieve the object
castclass System.Int32 // cast and unbox
unbox int32
cpobj int32 // copy the value into x
// first vararg argument is stored in x
ret
}


    可以看到在 CLR 一级实际上是提供了对参数数目可变参数的支持的,只不过 C# 的 params 关键字因为某些原因并没有使用。而如果你考察 Managed C++ 的实现,就会发现其正是使用这个机制。
以下内容为程序代码:

// cl /clr param.cpp

#include <stdio.h>
#include <stdarg.h>

void show(const char *fmt, ...)
{
  va_list args;

  va_start(args, fmt);

  vprintf(fmt, args);

  va_end(args);
}

int main(int argc, const char *argv[])
{
  show("%s %d", "Flier Lu", 1024);
}

    编译成 Managed 代码后,其函数 signature 如下:
以下内容为程序代码:

.method public static pinvokeimpl(/* No map */)
        vararg void modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
        show(int8 modopt([Microsoft.VisualC]Microsoft.VisualC.NoSignSpecifiedModifier) modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)* A_0) native unmanaged preservesig
{
  //...
}

posted @ 2004-07-08 12:02 Flier Lu 阅读(1028) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2573246&run=.0A2F3E7

大概半年前曾写过一个在 WinForm 程序中嵌入 ASP.NET 的简单例子,《在WinForm程序中嵌入ASP.NET》。因为是试验性质的工作,所以当时偷懒直接使用系统自带的 SimpleWorkerRequest 完成 ASP.NET 页面请求的处理工作。使用自带工具类在实现上虽然简单,但受到系统的诸多功能限制,如后面有朋友提到无法直接处理多级子目录的问题等等。(如虚拟目录为 "/" 时无法处理 "/help/about.aspx" 类型的页面请求)
    对于此类需求,一个最好的实现实例就是 www.asp.net 提供的 Cassini。这个例子完整地演示了如何实现一个支持 ASP.NET 的简单 Web 服务器功能,并被 Borland 的 Delphi.NET 等许多开源项目,当作调试用 Web 服务器。虽然只有几十 K 的源代码,但麻雀虽小五脏俱全,还是非常值得一看的。但因为 Cassini 是为处理 Web 服务而设计,因此需要在了解其结构的基础上,做一些定制来满足我们的需求。

    首先来看看 Cassini 的程序结构。

    与我前文例子中采用的结构类似,Cassini 包括界面(CassiniForm)、服务器(Server)、宿主(Host)和请求处理器(Request)等几个主要部分,并通过 Connection 等几个工具类,完成 Web 请求的解析与应答功能。

    总体工作流程图如下:
以下内容为程序代码:

    +-------+ [1] +-------------+ [2] +--------+
    | Admin |---->| CassiniForm |---->| Server |
    +-------+     +-------------+     +--------+
                                          | [3]
                                          V
                       +--------+ [4] +------+
                       | Client |---->| Host |
                       +--------+     +------+
                           ^              | [5]
                           |              V
                           |        +------------+ [6] +---------+
                        [7]|        | Connection |---->| Request |--+
                           |        +------------+     +---------+  | [7]
                           +----------------------------------------+

    [1] Cassini 的管理者(Admin)首先通过 CassiniForm 的界面,设定 Web 服务器端口、页面物理目录和虚拟目录等配置信息;
    [2] 然后以配置信息构造 Server 对象,并调用 Server.Start 方法启动 Web 服务器;
以下内容为程序代码:

public class CassiniForm : Form
{
  private void Start()
  {
    // ...
    try {
        _server = new Cassini.Server(portNumber, _virtRoot, _appPath);
        _server.Start();
    }
    catch {
      // 显示错误信息
    }
    // ...
  }
}

    [3] Server 对象在建立时,将获取或自动初始化 ASP.NET 的注册表配置。这个工作是通过 Server.GetInstallPathAndConfigureAspNetIfNeeded 方法完成的。工作原理是通过 HttpRuntime 所在 Assembly (System.Web.dll) 的版本获得合适的 ASP.NET 版本;然后从注册表中查询 HKEY_LOCAL_MACHINESOFTWAREMicrosoftASP.NET 下是否有正确的 ASP.NET 的安装路径;如果有则返回之;否则会根据 System.Web.dll 的版本,以及 HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFramework 下 .NET Framework 按照目录等信息,动态构造一个合适的 ASP.NET 注册表配置。进行这个工作的原因是 ASP.NET 可以在按照 .NET Framework 后,使用 aspnet_regiis.exe 手工注销掉,而运行支持 ASP.NET 的 Web 服务器,又必须有合适的设置。
    在完成配置和 ASP.NET 安装路径后,Server 将建立并配置 Host 对象作为 ASP.NET 的宿主。
以下内容为程序代码:

public class Server : MarshalByRefObject
{
  private void CreateHost() {
    _host = (Host)ApplicationHost.CreateApplicationHost(typeof(Host), _virtualPath, _physicalPath);
    _host.Configure(this, _port, _virtualPath, _physicalPath, _installPath);
  }

  public void Start() {
    if (_host != null)
        _host.Start();
  }
}

    [4] Host 类作为 ASP.NET 的宿主类,主要完成三部分工作:配置 ASP.NET 的运行时环境、响应客户端(Client)发起的 Web 页面请求、以及判断客户端请求的有效性。
    配置 ASP.NET 的运行时环境主要工作是,为 ASP.NET 的执行和后面请求有效性的判断获取足够的配置信息。例如 Server 能够提供的 Web 服务端口、页面虚拟路径、页面物理路径以及 ASP.NET 程序安装路径等等,以及 Host 根据这些信息计算出的 ASP.NET 客户端脚本的虚拟和物理路径等等。此外还会接管线程所在 AppDomain 的卸载事件 AppDomain.DomainUnload,在 Web 服务器停止的时候自动终止 Web 服务。
    响应客户端(Client)发起的 Web 页面请求功能,是通过建立 Socket 监听 Server 对象指定的 Web 服务 TCP 端口来完成的。Host.Start 方法建立 Socket,并通过线程池异步调用 Host.OnStart 方法在后台监听请求;Host.OnStart 方法则在 接收到 Web 请求后,通过线程池异步调用 Host.OnSocketAccept 方法完成请求的响应工作;Host.OnSocketAccept 则负责在处理 Web 请求的时候,建立 Connection 对象,并进一步调用 Connection.ProcessOneRequest 方法处理 Web 请求。虽然 Host 没有使用复杂的请求分配算法,但因为线程池的灵活使用,使得其性能完全不受处理瓶颈的限制,也是线程池使用的良好范例。
以下内容为程序代码:

internal class Host : MarshalByRefObject
{
  public void Start() {
    if (_started)
      throw new InvalidOperationException();

    // 建立 Socket 监听 Web 服务端口
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _socket.Bind(new IPEndPoint(IPAddress.Any, _port));
    _socket.Listen((int)SocketOptionName.MaxConnections);

    _started = true;
    ThreadPool.QueueUserWorkItem(_onStart); // 通过线程池异步调用
  }

  private void OnStart(Object unused) {
    while (_started) {
      try {
        Socket socket = _socket.Accept(); // 响应客户端请求
        ThreadPool.QueueUserWorkItem(_onSocketAccept, socket); // 通过线程池异步调用
      }
      catch {
        Thread.Sleep(100);
      }
    }
    _stopped = true;
  }

  private void OnSocketAccept(Object acceptedSocket) {
    Connection conn =  new Connection(this, (Socket)acceptedSocket);
    conn.ProcessOneRequest(); // 处理客户端请求
  }
}

    最后,判断客户端请求的有效性的功能,是通过三个重载的 Host.IsVirtualPathInApp 方法,提供给 Connection 在具体处理客户端请求时调用,来判断请求的有效性,下面讨论 Connection 时再详细解释。

    [5] Host 在建立 Connection 对象并调用其 ProcessOneRequest 方法处理用户请求时,Connection 对象会首先等待客户端请求数据(WaitForRequestBytes),然后创建 Request 对象,并调用 Request.Process 方法处理请求。而其自身,则通过一堆 WaitXXX 函数,为 Request 类提供支持。
以下内容为程序代码:

internal class Connection {
  public void ProcessOneRequest() {
    // wait for at least some input
    if (WaitForRequestBytes() == 0) { // 等待客户端请求数据
      WriteErrorAndClose(400); // 发送 HTTP 400 错误给客户端
      return;
    }

    Request request = new Request(_host, this);
    request.Process();
  }

  private int WaitForRequestBytes() {
    int availBytes = 0;

    try {
      if (_socket.Available == 0) {
        // poll until there is data
        _socket.Poll(100000 /* 100ms */, SelectMode.SelectRead); // 等待客户端数据 100ms 时间
        if (_socket.Available == 0 && _socket.Connected)
          _socket.Poll(10000000 /* 10sec */, SelectMode.SelectRead);
      }

      availBytes = _socket.Available;
    }
    catch {
    }

    return availBytes;
  }


    [6] Request 在接收到 Connection 的请求后,将从客户端读取请求内容,并按照 HTTP 协议进行分析。因为本文不是做 HTTP 协议的分析工作,所以这部分代码就不详细讨论了。
    在 Request.ParseRequestLine 函数分析 HTTP 请求获得请求页面路径后,会调用前面提到过的 Host.IsVirtualPathInApp 函数判断此路径是否在 Web 服务器提供的虚拟路径下级,并且返回此虚拟路径是否指向 ASP.NET 的客户端脚本。如果 Web 请求的虚拟路径以 "/" 结尾,则调用 Request.ProcessDirectoryListingRequest 方法返回列目录的响应;否则调用 HttpRuntime.ProcessRequest 方法完成实际的 ASP.NET 请求处理工作。
    HttpRuntime 通过 Request 的基类 HttpWorkerRequest 提供的统一接口,采用 IoC 的策略获取最终页面的所在。与我前面文章中使用的 SimpleWorkerRequest 实现最大不同在于 Request.MapPath 完成了一个较为完整的虚拟目录到物理目录映射机制。
    SimpleWorkerRequest.MapPath 实现相对简陋:
以下内容为程序代码:

public override string SimpleWorkerRequest.MapPath(string path)
{
  if (!this._hasRuntimeInfo)
  {
    return null;
  }

  string physPath = null;
  string appPhysPath = this._appPhysPath.Substring(0, (this._appPhysPath.Length - 1)); // 去掉末尾斜杠

  if (((path == null) || (path.Length == 0)) || path.Equals("/"))
  {
    physPath = appPhysPath;
  }

  if (path.StartsWith(this._appVirtPath))
  {
    physPath = appPhysPath + path.Substring(this._appVirtPath.Length).Replace('/', '\');
  }

  InternalSecurityPermissions.PathDiscovery(physPath).Demand();

  return physPath;
}

    Request.MapPath 的实现则相对要完善许多,考虑了很多 SimpleWorkerRequest 无法处理的情况,使得 Request 的适应性更强。
以下内容为程序代码:

public override String Request.MapPath(String path) {
  String mappedPath = String.Empty;

  if (path == null || path.Length == 0 || path.Equals("/")) {
    // asking for the site root
    if (_host.VirtualPath == "/") {
      // app at the site root
      mappedPath = _host.PhysicalPath;
    }
    else {
      // unknown site root - don't point to app root to avoid double config inclusion
      mappedPath = Environment.SystemDirectory;
    }
  }
  else if (_host.IsVirtualPathAppPath(path)) {
    // application path
    mappedPath = _host.PhysicalPath;
  }
  else if (_host.IsVirtualPathInApp(path)) {
    // inside app but not the app path itself
    mappedPath = _host.PhysicalPath + path.Substring(_host.NormalizedVirtualPath.Length);
  }
  else {
    // outside of app -- make relative to app path
    if (path.StartsWith("/"))
      mappedPath = _host.PhysicalPath + path.Substring(1);
    else
      mappedPath = _host.PhysicalPath + path;
  }

  mappedPath = mappedPath.Replace('/', '\');

  if (mappedPath.EndsWith("\") && !mappedPath.EndsWith(":\"))
    mappedPath = mappedPath.Substring(0, mappedPath.Length-1);

  return mappedPath;
}


    关于 Cassini 的进一步讨论,可以参考 www.asp.net 上的讨论专版

    [7] 在 HttRuntime 完成具体的 ASP.NET 页面处理工作后,会通过 Request.SendResponseFromXXX 系列函数,将页面结果返回给客户端。

    虽然 SimpleWorkerRequest.MapPath 方法实现简单,但理论上完全可以处理多级目录的情况。之所以在使用 SimpleWorkerRequest 时,无法处理嵌套目录,是因为 SimpleWorkerRequest 在构造函数中错误地分解了请求的页面所在虚拟目录等信息。
    SimpleWorkerRequest 的两个构造函数,在将请求页面虚拟路径(如"/help/about.aspx")保存后,都调用了 ExtractPagePathInfo 方法对页面路径做进一步的分解工作。
以下内容为程序代码:

private void SimpleWorkerRequest.ExtractPagePathInfo()
{
  int idx = this._page.IndexOf('/');
  if (idx >= 0)
  {
    this._pathInfo = this._page.Substring(idx);
    this._page = this._page.Substring(0, idx);
  }
}

    this._pathInfo 是为实现 HttpWorkerRequest.GetPathInfo 提供的存储字段。而 GetPathInfo 将返回 URL 中在页面后的路径信息,例如对 "path/virdir/page.html/tail" 将返回 "/tail"。早期的许多 HTTP 客户端程序,如 Delphi 中 WebAction 的分发,都利用了这个路径信息的特性,在 Web 页面或 ISAPI 一级之后,再次进行请求分发。但因为 SimpleWorkerRequest 实现上或者设计上的限制,导致在处理 PathInfo 时会将 "/help/about.aspx" 类似的多级 url 错误切断。最终返回给 HttpRuntime 的 this._path 将变成空字符串,而 this._pathInfo 被设置为 "/help/about.aspx",而单级路径如 "about.aspx" 不受影响。
    知道了这个原理后,就可以对 SimpleWorkerRequest 稍作修改,重载受到 ExtractPagePathInfo 影响的几个方法,即可完成对多级目录结构下页面的支持。如果需要进一步的映射支持,如同时支持多个虚拟子目录,可以参照 Cassini 的 Request 实现 MapPath 等方法。
以下内容为程序代码:

public class Request : SimpleWorkerRequest
{
  private string _appPhysPath;
  private string _appVirtPath;

  private string _page;
  private string _pathInfo;

  public Request(string page, string query, TextWriter output) : base(page, query, output)
  {
    this._appPhysPath = Thread.GetDomain().GetData(".appPath").ToString();
    this._appVirtPath = Thread.GetDomain().GetData(".hostingVirtualPath").ToString();

    this._page = page;

    // TODO: 从 page 中进一步解析 Path Info
  }

  public override string GetPathInfo()
  {
    if (this._pathInfo == null)
    {
      return string.Empty;
    }
    return this._pathInfo;
  }

  private string GetPathInternal(bool includePathInfo)
  {
    string path = (_appVirtPath.Equals("/") ? _page : _appVirtPath + _page);

    if (includePathInfo && (_pathInfo != null))
    {
      return path + this._pathInfo;
    }
    else
    {
      return path;
    }
  }

  public override string GetUriPath()
  {
    return GetPathInternal(true);
  }

  public override string GetFilePath()
  {
    return GetPathInternal(false);
  }

  public override string GetRawUrl()
  {
    string query = this.GetQueryString();

    if ((query != null) && (query.Length > 0))
    {
      return GetPathInternal(true) + "?" + query;
    }
    else
    {
      return GetPathInternal(true);
    }
  }

  public override string GetFilePathTranslated()
  {
    return _appPhysPath + _page.Replace('/', '\');
  }

  public override string MapPath(string path)
  {
    string physPath = null;

    if (((path == null) || (path.Length == 0)) || path.Equals("/"))
    {
      physPath = this._appPhysPath;
    }

    if (path.StartsWith(this._appVirtPath))
    {
      physPath = this._appPhysPath + path.Substring(this._appVirtPath.Length).Replace('/', '\');
    }

    return physPath;
  }
}

posted @ 2004-07-08 12:01 Flier Lu 阅读(720) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2513220&run=.05A880F

 因为手头工作的原因,最近集中精力把《XML Schema数据库编程指南》一书看完,感觉还是相当不错的。
    目前 XML 的使用越来越广泛,但是缺乏良好格式定义的 XML 不但起不到数据交互和数据存储的作用,反而会导致数据规范性和格式版本兼容性方面的问题。对大量使用,特别是多人共用的 XML 结构,一个严格的 XML Schema 定义是必不可少的。
    虽然从 XML 标准出现之前就开始跟踪这块的技术发展,但一直一来都没有系统学习研究过 XML Schema 的思想和技术。各种 XML 相关书籍,大多都会提到一些 XML Schema 的内容,但往往浅尝辄止无法深入介绍。这就造成包括笔者在内的很多人,知其然不知其所以然。在上个项目中,凭着对 XML Schema 的粗浅了解,笔者也写了好几个较为复杂的 XML 格式定义,其中不乏几千行的 XML Schema 结构定义。通过实践中的不断重构,能够体会出一些思路和模式,但感觉总是无法整体上把握其思想。
    《XML Schema数据库编程指南》一书可以说是国内目前难得的一本专著,500页的容量保证至少在面上能够完整覆盖,而其他 XML 书籍区区十几、几十页,很难想像能真正把 XML Schema 说清楚。更加可贵的是,此书不光从技术上全面介绍了 XML Schema 的功能点,而且通过一些章节(第3章:文档设计过程、第8章:XML Schema 文档的设计等),从思路上给予总结和指导,这是纯技术层面书籍最薄弱的环节。
    对 XML Schema 的前辈 DTD,此书也不像某些书籍那样一味损贬,而是用了4章的较大篇幅,从DTD的思想(第2章:DTD与模式)到使用(第4章:XML DTD 的作用)、移植(第9章:DTD到模式的转换)到比较(第11章:使用适当的元数据),客观的评价了 DTD 的优缺点。此外本书中还对 XML Schema 和 DTD 的使用方式做了一些模式层面的总结,虽然不太全面,但提供了一个很好的思路。
    最后书中还简要介绍了其他模式语言(第12章)、基于模式的产品(第13章)和工具(第14章),不过因为这些内容的实效性较强,只能作为背景知识,实际参考价值不算太大。
    最大的败笔是,译者居然把原书名 XML Schemas 翻译成 《XML Schema数据库编程指南》,真是不知道他怎么想的,难道想通过靠上数据库这条大船多拖几个人下水?要知道 XML Schema 跟数据库可是八竿子打不着的,呵呵。而且此书最大的亮点,也不是什么编程指南,而是作者从整体上的把握,这比那些手册性质的书要有用百倍。译者显然没有体会到作者的良苦用心,而是自作聪明加上“数据库”和“编程指南”这两个与原书内容牛马不相及的名字。甚至可以说此书基本上没有谈论编程层面的知识,那些应该是讨论 SAX/DOM 和具体 MS XML Core Service 和 Apache Xerces 实现的书籍干的事情。至于书中的翻译质量倒还是凑合,基本上能够看懂吧。
    总而言之,此书还是非常值得一读的,特别是作为 XML 结构设计人员,此书是目前不多的较好选择之一。
    

以下为引用:

基本信息

    无论用于万维网开发、文档创建、还是商务伙伴之间的数据交换,作为一个十分灵活的文档设计与数据建模工具,XML正变得越来越重要。XML Schema规范实现了W3C推荐标准,因而提供了一种可替代DTD的方法,使开发人员能够更精确地结构化XML数据。就设计和使用基于XML的内容与数据来说,XML Schema提供了实现这类设计和最大化这类使用的关键要素。 本书介绍了相关的原理、术语及概念,并配有大量的实例进行说明,使读者对所学的知识有深刻的体验,并能学以致用。 
    
    
目录    
 
第1章  元数据概述
第2章  DTD与模式
第3章  文档设计过程
第4章  XMLcDTD的作用
第5章  XMLcSchemac 
第6章  了解模式:结构与成分
第7章  模式数据类型
第8章  XMLcSchema文档的设计
第9章  DTD到模式的转换
第10章  重要的XML模式
第11章  使用适当的元数据
第12章  其他模式语言
第13章  基于模式的新产品
第14章  模式与相关工具
附录A  重要的规范与标准
附录B  用于XMLcSchema的DTD:结构
附录C  XMLcSchema成分与数据类型

posted @ 2004-07-08 12:00 Flier Lu 阅读(3562) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2507847&run=.0FB98AB

WDM (Windows Driver Model) 都还没有完全弄明白,M$ 居然在 WinHEC 上又推出了将全面取代 WDM 的 WDF (Windows Driver Foundation)。OSR Online 上几篇文章简要地介绍了 WDF 的一些新特性,并在 A New Framework 一文中给出了一个实际的 WDF 例子。
    因为没有开发环境,只能粗略看了一下文章,感觉对我的需求来说结构上调整不算很大。亮点主要是针对新的 PnP/Power 模型的进一步优化和调整、可放弃的驱动创建(例如在 Longhorn 中可以中断并放弃 IRP_MJ_CREATE 操作)、新的存储架构(不明白 :()、灵活的任务请求队列(支持串行、并行和定制任务分发)、以及一些其他细节方面的改进。

    在驱动模型方面,WDF 使用一些新的类型驱动 WDM 的相应类型,并做了一定的扩展:

    WDF 类型      WDM 类型

    WDFDRIVER     DRIVER_OBJECT
    WDFDEVICE     DEVICE_OBJECT
    WDFREQUEST    IRP
    WDFQUEUE      DPC 队列
    WDFINTERRUPT  ISR & DPCforISR

    DriverEntry 在 WDF 中只负责 WDFDRIVER 类型对象的初始化和构造工作,将设备的管理完全丢到 DioEvtDeviceAdd 函数中,由 WDF 框架在合适的时候调用。
以下内容为程序代码:

NTSTATUS
DriverEntry(PDRIVER_OBJECT DriverObj, PUNICODE_STRING RegistryPath)
{
    NTSTATUS code;
    WDF_DRIVER_CONFIG config;
    WDFDRIVER hDriver;

    DbgPrint(" WDFDIO Driver -- Compiled %s %s ",__DATE__, __TIME__);

    //
    // Initialize the Driver Config structure:
    //      Specify our Device Add event callback.
    //
    WDF_DRIVER_CONFIG_INIT_NO_CONSTRAINTS(&config, DioEvtDeviceAdd);

    //
    //
    // Create a WDFDRIVER object
    //
    // We specify no object attributes, because we do not need a cleanup
    // or destroy event callback, or any per-driver context.
    //
    code = WdfDriverCreate(DriverObj,
                             RegistryPath,
                             WDF_NO_OBJECT_ATTRIBUTES,
                             &config,   // Ptr to config structure
                             NULL);     // Optional ptr to get WDFDRIVER handle

    if (!NT_SUCCESS(code)) {

        DbgPrint("WdfDriverCreate failed with status 0x%0x ", code);
    }

#if DBG
    DbgPrint("DriverEntry: Leaving ");
#endif

    return(code);
}

    而 DioEvtDeviceAdd 函数则首先初始化 PnP 和电源管理相关结构。
以下内容为程序代码:

WDFSTATUS
DioEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
    WDFSTATUS status = STATUS_SUCCESS;
    WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
    WDF_OBJECT_ATTRIBUTES objAttributes;
    WDFDEVICE device;
    PDIO_DEVICE_CONTEXT devContext;
    WDF_IO_QUEUE_CONFIG ioCallbacks;
    WDF_INTERRUPT_CONFIG interruptConfig;
    WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS idleSettings;

    //
    // Initialize the PnpPowerCallbacks structure.
    //
    WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);

    //
    // Setup the callbacks to manage our hardware resources.
    //
    // Prepare is called at START_DEVICE time
    // Release is called at STOP_DEVICE or REMOVE_DEVICE time
    //
    pnpPowerCallbacks.EvtDevicePrepareHardware = DioEvtPrepareHardware;
    pnpPowerCallbacks.EvtDeviceReleaseHardware = DioEvtReleaseHardware;

    //
    // These two callbacks set up and tear down hardware state that must

    // be done every time the device moves in and out of the D0-working
    // state.

    //
    pnpPowerCallbacks.EvtDeviceD0Entry= DioEvtDeviceD0Entry;
    pnpPowerCallbacks.EvtDeviceD0Exit = DioEvtDeviceD0Exit;

    //
    // Register the PnP and power callbacks.

    //
    WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit,

                                           pnpPowerCallbacks);
    // ...
}

    然后初始化并构造设备对象,类似以前 WDM 中的 CreateDevice 和 IoCreateSymbolicLink 调用。
以下内容为程序代码:

WDFSTATUS
DioEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
    // ...

    // Create our Device Object and its associated context
    //
    WDF_OBJECT_ATTRIBUTES_INIT(&objAttributes);

    WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE(&objAttributes,

                                           DIO_DEVICE_CONTEXT);

    //
    // We want our device object NAMED, thank you very much
    //
    status = WdfDeviceInitUpdateName(DeviceInit, L"\device\WDFDIO");

    if (!NT_SUCCESS(status)) {
        DbgPrint("WdfDeviceInitUpdateName failed 0x%0x ", status);
        return(status);
    }

    //
    // Because we DO NOT provide callbacks for Create or Close, WDF will
    // just succeed them automagically.
    //

    //
    // Create the device now
    //
    status = WdfDeviceCreate(&DeviceInit,   // Device Init structure
                            &objAttributes, // Attributes for WDF Device
                            &device);      // returns pointer to new

                                                                WDF Device

    if ( !NT_SUCCESS(status)) {
        DbgPrint("WdfDeviceInitialize failed 0x%0x ", status);
        return(status);
    }

    //
    // Device creation is complete
    //
    // Get our device extension
    //
    devContext = DioGetContextFromDevice(device);

    devContext->WdfDevice = device;

    //
    // Create a symbolic link for the control object so that usermode can
    // open the device.
    //
    status = WdfDeviceCreateSymbolicLink(device, L"\DosDevices\WDFDIO");

    if (!NT_SUCCESS(status)) {
        DbgPrint("WdfDeviceCreateSymbolicLink failed 0x%0x ", status);
        return(status);
    }

    // ...
}

    比较有趣的是,WDF 直接提供了请求队列的概念。一个设备可以有多个请求队列,每个请求队列可以有一种模式,如最简单的 WdfIoQueueDispatchSerial 模式下,请求队列将请求串行化后进行处理;而 WdfIoQueueDispatchParallel 模式则自动在每个请求到来时调用 IO 回调函数;最后也可以通过 WdfIoQueueDispatchManual 模式,在请求到来时调用 EvtIoStart 事件处理函数来手工分发请求,类似现在 WDM 的工作方式。而请求队列更是提供了在 Power down 时对请求队列当前请求的自动保存和恢复机制。这样一来驱动的开发又可以剩一些事情了,呵呵。
    A New Framework 一文中过于队列有较为广泛的讨论,这儿就不罗嗦了。
以下内容为程序代码:

WDFSTATUS DioEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
    // ...

    //
    // Configure our queue of incoming requests
    //
    // We only use the default queue, and we only support

    // IRP_MJ_DEVICE_CONTROL.

    //
    // Not supplying a callback results in the request being completed
    // with STATUS_NOT_SUPPORTED.
    //
    WDF_IO_QUEUE_CONFIG_INIT(&ioCallbacks,
                             WdfIoQueueDispatchSerial,
                             WDF_NO_EVENT_CALLBACK,     // StartIo
                             WDF_NO_EVENT_CALLBACK);    // CancelRoutine

    ioCallbacks.EvtIoDeviceControl = DioEvtDeviceControlIoctl;

    status = WdfDeviceCreateDefaultQueue(device,
                                        &ioCallbacks,
                                        WDF_NO_OBJECT_ATTRIBUTES,
                                        NULL); // pointer to default queue

    if (!NT_SUCCESS(status)) {
        DbgPrint("WdfDeviceCreateDefaultQueue failed 0x%0x ", status);
        return(status);
    }

    // ...
}

    对中断的处理,WDF 也使用 OO 思想将 WDM 中的几个回调函数组织了起来,但功能上还是类似的。
以下内容为程序代码:

WDFSTATUS DioEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
    // ...

    //
    // Create an interrupt object that will later be associated with the
    // device's interrupt resource and connected by the Framework.
    //



    //
    // Configure the Interrupt object
    //
    WDF_INTERRUPT_CONFIG_INIT(&interruptConfig,
                              FALSE,                // auto-queue DPC?
                              DioIsr,
                              DioDpc);

    interruptConfig.EvtInterruptEnable  = DioEvtInterruptEnable;
    interruptConfig.EvtInterruptDisable = DioEvtInterruptDisable;

    status = WdfInterruptCreate(device,
                                &interruptConfig,
                                &objAttributes,
                                &devContext->WdfInterrupt);
    if (!NT_SUCCESS (status))
    {
        DbgPrint("WdfInterruptCreate failed 0x%0x ", status);
        return status;
    }

    // ...
}

    最后,WDF 中为了支持电源管理的多种状态切换,提供了一些辅助的状态变迁时的回调函数,简化了驱动中的管理代码实现。
以下内容为程序代码:

WDFSTATUS DioEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
    // ...

    //
    // Initialize our idle policy
    //
    WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT(&idleSettings,
                                               IdleCannotWakeFromS0);


    status = WdfDeviceUpdateS0IdleSettings(device, &idleSettings);

    if (!NT_SUCCESS(status)) {
        DbgPrint("WdfDeviceUpdateS0IdleSettings failed 0x%0x ", status);
        return status;
    }


    return(status);
}

    因为还没有拿到开发调试环境,连 OSR 的例子都下载不下来,只能泛泛而谈。等弄到实际东西再详细讨论吧,呵呵,有兴趣的朋友可以先看看 OSR Online 上几篇相关文章,讲的还是比较详细的。

posted @ 2004-07-08 11:59 Flier Lu 阅读(3613) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2401390&run=.0D9CAA6

完成端口是 NT 架构下一种高效的异步 IO 辅助机制,其使用方法已经被广为讨论,MSDN里面也有很详细的说明和示例。《Windows网络编程》一书中有关于通过完成端口实现高效网络服务器设计的详细说明;《Windows核心编程》一书中有关于异步IO以及线程池等相关知识的使用介绍;而《Windows 2000 内部揭密 》一书中则有对完成端口实现原理的简要介绍;此外一些关于 Win32 下多线程编程方面的书籍也对完成端口有所提及。
    因此,对于完成端口的使用我这里就不再罗嗦,下面将直接从实现角度对其进行分析。

    首先,从设计角度来看:完成端口是一个 NT 执行子系统的核心对象。通过将完成端口与任意个 I/O 句柄(文件或Socket等)关联,使得用户可以通过完成端口这一统一途径,异步的获取并处理 I/O 操作的结果,同时能够最大限度利用多 CPU 的优势。而且因为完成端口实际上是 NT 系统很多底层机制如 APC 的实现手段,故而在效率上是最高的,因为绝大多数其他类似解决方法最终还是使用完成端口实现。与 WaitForMultipleObjects 函数不同,完成端口是由系统直接提供并行优化支持的,通过建立完成端口时指定的并行线程值,系统可以保证工作在同一完成端口上的线程数量受控(一般等于系统CPU数量),这样就可以避免无意义的线程上下文切换,获取更高的性能。

    其次,从使用角度来看:完成端口使用 CreateIoCompletionPort 函数和 CloseHandle 函数创建和释放,使用 GetQueuedCompletionStatus 函数和 PostQueuedCompletionStatus 函数实现完成端口的读写操作。因此我们的分析从这三个函数开始。

    CreateIoCompletionPort 函数在 ExistingCompletionPort 参数为空的时候调用 NtCreateIoCompletion 函数(iocomplete.c:51)创建一个新的完成端口对象;然后处理 FileHandle 参数为 INVALID_HANDLE_VALUE 时的情况;最后调用 NtSetInformationFile 函数(dll estrfil.c:740)试图将 I/O 句柄绑定到完成端口上。伪代码如下:
以下为引用:

#define IO_COMPLETION_ALL_ACCESS 0x001F0003

typedef enum _FILE_INFORMATION_CLASS
{
  //...
  FileCompletionInformation = 30,
  //...
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;

typedef struct _IO_STATUS_BLOCK
{
  union {
    NTSTATUS Status;
    PVOID Pointer;
  };

  ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

HANDLE CreateIoCompletionPort(
  HANDLE FileHandle,
  HANDLE ExistingCompletionPort,
  ULONG_PTR CompletionKey,
  DWORD NumberOfConcurrentThreads)
{
  HANDLE CompletionPort = ExistingCompletionPort;

  // 创建一个新的完成端口对象
  if(ExistingCompletionPort == NULL)
  {
    NTSTATUS st = NtCreateIoCompletion(&CompletionPort, IO_COMPLETION_ALL_ACCESS, NULL, NumberOfConcurrentThreads);

    if(!NT_SUCCESS(st))
    {
      SetLastNTError(RtlNtStatusToDosError(st));
      return NULL;
    }
  }

  if(FileHandle == INVALID_HANDLE_VALUE)
  {
    // FileHandle 参数为 INVALID_HANDLE_VALUE 时 ExistingCompletionPort 参数必须为空
    if(ExistingCompletionPort != NULL)
    {
      CompletionPort = NULL;
      BaseSetLastNTError(STATUS_INVALID_PARAMETER);
    }

    // 只创建完成端口对象,但并不绑定 I/O 句柄到完成端口上
    return CompletionPort;
  }

  IO_STATUS_BLOCK blk;

  // 将 I/O 句柄绑定到完成端口上
  NTSTATUS st = NtSetInformationFile(FileHandle, &blk, &CompletionKey, 8, FileCompletionInformation);

  if(NT_SUCCESS(st))
  {
    return CompletionPort;
  }
  else
  {
    SetLastNTError(RtlNtStatusToDosError(st));

    if(CompletionPort)
    {
      CloseHandle(CompletionPort);
    }

    return NULL;
  }
}


    CloseHandle 在处理完成端口时则直接调用 IopDeleteIoCompletion 函数(iocomplete.c:675)。而 GetQueuedCompletionStatus 函数和 PostQueuedCompletionStatus 函数,则分别是 NtRemoveIoCompletion 函数(iocomplete.c:485)和 NtSetIoCompletion 函数(iocomplete.c:417)的简单包装。    
以下为引用:

BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes, 
  PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds)
{
  LARGE_INTEGER Timeout;
  
  BaseFormatTimeOut(&Timeout, dwMilliseconds);
  
  IO_STATUS_BLOCK blk;
  
  NTSTATUS st = NtRemoveIoCompletion(CompletionPort, lpCompletionKey, &dwMilliseconds, &blk, &Timeout);
  
  if(FAILED(st) || st == STATUS_TIMEOUT)
  {
    // 设置错误代码
  }
  else
  {
    if(SUCCEEDED(blk.Status))
    {
      // 设置 lpOverlapped 和 lpNumberOfBytes
    }
  }
}  


    从实现角度来看,完成端口实际上是一个 IoCompletionObjectType 类型的内核对象,其对象体就是一个 KQUEUE 结构维护的内核队列。创建此内核对象类型信息的代码如下(io/ioinit.c:1527):
以下为引用:

//
// Create the object type for I/O completion objects.
//

RtlInitUnicodeString( &nameString, L"IoCompletion" );                   // 对象名称
objectTypeInitializer.DefaultNonPagedPoolCharge = sizeof( KQUEUE );     // 对象体
objectTypeInitializer.InvalidAttributes = OBJ_PERMANENT | OBJ_OPENLINK;
objectTypeInitializer.GenericMapping = IopCompletionMapping;
objectTypeInitializer.ValidAccessMask = IO_COMPLETION_ALL_ACCESS;
objectTypeInitializer.DeleteProcedure = IopDeleteIoCompletion;          // CloseHandle 调用函数
if (!NT_SUCCESS( ObCreateObjectType( &nameString, &objectTypeInitializer,
  (PSECURITY_DESCRIPTOR) NULL, &IoCompletionObjectType )))
{
  return FALSE;
}



    NtCreateIoCompletion 函数(iocomplete.c:51)只是简单地构造并初始化这个内核对象。
    首先对来自用户态的调用验证 IoCompletionHandle 参数指向内存可写;然后调用 ObCreateObject 函数构造内核对象;如果成功则调用 KeInitializeQueue 函数初始化内核队列;并且调用 ObInsertObject 函数将内核对象加入到当前进程的句柄表中;最后将构造的内核对象的句柄返回给调用者。伪代码如下:
以下为引用:

NTSTATUS
NtCreateIoCompletion (
  IN PHANDLE IoCompletionHandle,
  IN ACCESS_MASK DesiredAccess,
  IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
  IN ULONG Count OPTIONAL)
{
  NTSTATUS st;
  try
  {
    // 对来自用户态的调用验证 IoCompletionHandle 参数指向内存可写
    if(KeGetPreviousMode() != KernelMode)
    {
      ProbeForWriteHandle(IoCompletionHandle);
    }

    PVOID IoCompletion; // 完成端口内核对象

    // 构造内核对象
    st = ObCreateObject(..., IoCompletionObjectType, ..., sizeof(KQUEUE), ..., &IoCompletion);

    if(NT_SUCCESS(st))
    {
      // 初始化内核队列
      KeInitializeQueue((PKQUEUE)IoCompletion, Count);

      HANDLE Handle;

      // 将内核对象加入到当前进程的句柄表中
      st = ObInsertObject(IoCompletion, ..., &Handle);

      if(NT_SUCCESS(st))
      {
        try
        {
          // 将构造的内核对象的句柄返回给调用者
          *IoCompletionHandle = Handle;
        }
        catch(...)
        {
          // 完成端口内核对象已创建并插入当前进程句柄表,因此需要忽略错误
        }
      }
    }
  }
  catch(...)
  {
    st = GetExceptionCode();
  }
  return st;
}


    系统还提供了一个相应的 NtOpenIoCompletion 函数(iocomplete.c:178)支持打开已经建立的有名字的完成端口。其调用 ObOpenObjectByName 函数完成名字到内核对象的映射,并最终将完成端口对象句柄返回给调用者。伪代码如下:
以下为引用:

NTSTATUS
NtOpenIoCompletion (
  OUT PHANDLE IoCompletionHandle,
  IN ACCESS_MASK DesiredAccess,
  IN POBJECT_ATTRIBUTES ObjectAttributes)
{
  NTSTATUS st;
  try
  {
    // 对来自用户态的调用验证 IoCompletionHandle 参数指向内存可写
    if(KeGetPreviousMode() != KernelMode)
    {
      ProbeForWriteHandle(IoCompletionHandle);
    }

    HANDLE Handle;

    st = ObOpenObjectByName(ObjectAttributes, IoCompletionObjectType, ..., DesiredAccess, ..., &Handle);

    if(NT_SUCCESS(st))
    {
      try
      {
        // 将构造的内核对象的句柄返回给调用者
        *IoCompletionHandle = Handle;
      }
      catch(...)
      {
      }
    }
  }
  catch(...)
  {
    st = GetExceptionCode();
  }
  return st;
}


    在建立了完成端口对象后,可以通过 NtQueryIoCompletion 函数(iocomplete.c:282)查询此完成端口队列中阻塞的 I/O 完成包的数量。此函数实际上是对读取内核队列状态 KeReadStateQueue 函数(kequeueobj.c:92)的封装,从 KQUEUE::Header::SignalState 字段读取完成端口内部使用的内核队列对象的阻塞深度。
    NtRemoveIoCompletion 函数(iocomplete.c:485)和 NtSetIoCompletion 函数(iocomplete.c:417) 实际上也只是直接调用内核队列维护函数 KeRemoveQueue 函数(kequeueobj.c:238) 和 KeRundownQueue 函数(kequeueobj.c:566) 完成对内核队列的操作。

    在构造和初始化完成端口内核对象后,需要开始对完成端口进行操作,以实现对完成端口获取或发送完成包的操作。而这些操作归根结底实际上都是对内核队列进行操作,因此得先对内核队列有所了解。我会单独写一篇 BLog 讨论内核对象的实现思路,这里暂且不深入讨论。


... to be continue ...

posted @ 2004-07-08 11:58 Flier Lu 阅读(4485) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2399723&run=.06FE977

最近工作比较忙,闲暇时间要先保障学习、读书和游戏,呵呵,没太多时间做 BLog 的“功课”,就推荐几本正在读或准备读的杂书滥竽充数吧 :P
    
  
    
    吴思的《潜规则》和《血酬定律》是介于历史和杂文之间的一种特殊存在。因其观点过于直白,时时说史却处处讽近,前段时间大大风光了一把。加上前段风行的《中国农民调查》一书,使得人们的关注焦点暂时从那些所谓歌星影星身上,回到真正需要社会关注的那些弱势群体身上。不过《潜规则》也因此一读被禁,动手晚了几天的就只能通过借阅、网上留传的电子版和街头的盗版来一睹风采了。
    最近吴思将其两本著作旧瓶装新水,重新整理出版了《隐蔽的秩序——拆解历史弈局》一书。虽不知是否被部分阉割了,但还是决定定一本回来看看,应该还是很值得一读的。
 
以下为引用:

隐蔽的秩序——拆解历史弈局
 
    潜规则 隐身份 明枪暗箭 人间对局 跃然纸上 

一束强光探照千年国史

    本书将《潜规则——中国历史中的真实游戏》与《血酬定律-中国历史中的生存游戏》两部经典力作的正编共二十五篇文章打散,重新辑为七编,并加入了正编之外的五篇文章,前后融会贯通,合集而成。较前两部著作逻辑关系更加合理清晰,见解更为深刻精到,更便于阅读、收藏、研究。  
    
  吴思的文字耐读而且很好读……只是那些力透纸背的思想常常令寒噤,难以下咽,堵在心上。-《新周刊》
  吴思属于大陆上为数不多,但是汇合起来却能构成高原厚土般文化底蕴的人。-台湾《中国时报》
  吴思的著作的一个鲜明特色,就是创造了大量有“本土特色”的新概念,如潜规则,合法伤害权,血酬……这些早已存在于我们心里的东西,一经命名,立刻就凸现出来,变得可以研究,也容易研究了。-《博览群书》
   对事实的重新发现……正视了我们视而不见的事实,这事实的重现让我们震惊。-《厦门晚报》 

    


  
    
    自从读了《美丽与哀愁:一个真实的林徽因》一书后,我无可救要的成了这位传奇女性的fans。适逢林徽因女士一百周内诞辰,几本有关她的书籍陆续得以出版。

    《记忆中的林徽因》是一本文集,收录了与她相关的一些人物追忆她的文章,读后感觉还行。

以下为引用:

记忆中的林徽因

    因为有人误读她,有人读不懂她,所以才有了这本《记忆中的林徽因》。这本书不再像以往那样在林徽因前面贯以“绝代佳人”、“一代才女”等词汇,而是更注重林徽因作为建筑大师的特质,还原她作为学者与人妻的真实一面。 
 

内容简介
 
    本书从历年来林徽因亲友、学生发表的有关文章中摘取那些刻在他们心上的记忆,汇编成册。

    今年是林徽因(1904.6-1955.4)的百岁诞辰,4月1日,也是她离世49周年。生存于上个世纪的中国女性作家在今天依然拥有众多追随者的,除了张爱玲,恐怕就是她。张爱玲凭文字立身,以身世个性传奇。但对林徽因来说,文字只是生命中的一部分,身世氛围更多折射着那个时代的文化风尚。此种风尚的温婉不堪历史激烈演进的冲击渐行渐远,所以,给怀旧的人以无穷的感伤与联想。

林徽因,这位49年前就已去世的女子,凭什么依然这样深刻留在人们的记忆中?
网上的表白归结起来不外乎有三点:1.她的美貌与气质;2.她丰富而含蓄的情感世界;3.她多方面的才华。若仅以此论,她同时代的好几位美女作家都符合这些要求。但人们独独钟情于她,恐怕是一种综合了各种因素的原因,其中既有世俗的情感投射——温饱之后精神上追求社会认同而对出身与受教育程度的势利苛求;还有对竞争社会中理想女性失落的叹息——女性既具有现代独立人格与个性,同时又不失传统美德及本质的温婉美好,在今天已经越来越难。
林徽因恰恰契合了人们的这种理想需求。... 


    
    《林徽因讲建筑》一书则是她过于建筑论文的合集,呵呵,没敢买。因为两年前买过粱思成与林徽因合著的中英文对照版《中国建筑史》,但几次试图看都没有坚持下来。:P 

以下为引用:

林徽因讲建筑

    封面是湛蓝色的,书中的内容都是关于林徽因的建筑理念与实践的总结文字,没有半点林徽因传奇一生的介绍,一点也不香艳,但倍觉清雅。
 
内容简介
 
    封面的色很是衬林徽因的高贵,书中如斯编排是对林徽因的尊重、也是对建筑艺术的尊重。这种出书理念,在现在的书籍出品人中已非常少见。也许是因为那份爱护文物建筑之心,书中论及中国建筑之几个特征,并对北京的文物建筑着以浓重的笔墨。林徽因对中国建筑传统韵味的理解之深透,怕是无几人能及。喜欢林徽因的人,相信也会喜欢这本书。因为懂得,所以慈悲,想必是那个时代才情奇女子们的共有心性与品性。 


    另一本不相干的《城记》则是强烈推荐。此书详细介绍了北京城历史上的沧桑变化,以及在其背后粱思成、林徽因等建筑师的多劫人生,堪称一本城的传记。

以下为引用:

城记(北京城半个多世纪的沧桑传奇)

    新华社记者王军十年著就此书。《城记》试图廓清北京城半个多世纪的空间演进,还有为人熟知的建筑背后,鲜为人知的悲欢承启;历史见证者的陈述使逝去的记忆复活,尘封已久的文献、三百余帧图片让岁月不再是传说;梁思成、林徽因、陈占祥、华揽洪……建筑师多劫的人生,演绎着一出永不落幕的戏剧;这一切的缘起,只是因为北京,这个“在地球表面上人类最伟大的个体工程”,拥有一段抹不去的传奇。 
 
内容简介
 
    在完成本书写作的10年间,作者共采访当事人50余位,收集、查阅、整理大量第一手史料,实地考察京、津、冀、晋等地重要古建筑遗迹,跟踪北京城市发展模式、文物保护等专题作出深入调研。全书分为十章,从北京的现实入手,以五十多年来北京城营建史中的历次论争为主线展开叙述,其中又以20世纪五六十年代为重点,将梁思成、林徽因、陈占祥、华揽洪等一批建筑师、规划师的人生故事穿插其间,试图廓清“梁陈方案”提出的前因后果,以及后来城市规划的形成,北京出现所谓“大屋顶”建筑、拆除城墙等古建筑的情况,涉及“变消费城市为生产城市”、“批判复古主义”、“大跃进”、“整风鸣放”、“文化大革命”等历史时期。

    与文字同样重要的是书中选配的三百余幅插图,不乏私人珍藏的照片及画作,如梁思成先生工作笔记中的画作和首次发表的梁思成水彩写生画。 

    

  

    《光荣与梦想》是个大部头,考虑再三还是决定买一本回来安排时间看看,因为实在无法忍受各种媒体上的溢美之辞,呵呵。能引起如此广泛关注的书,一定是有一些原因的 :P
    
以下为引用:

光荣与梦想(1932-1972年美国社会实录,上下册,威廉.曼切斯特作品)

    本书是美国的一部断代史,勾画了从1932年罗斯福总统上台前后,到1972年尼克松总统任期内水门事件的四十年间美国政治、经济、文化,以及社会生活的全景式画卷。这是一部场景宏大,又描写细腻的历史巨著。作者对这四十年间美国历届政府从诞生到终结全过程中的两党纷争和内部分歧,对这一阶段全世界所经历的每一件大事都给予了特别的关注和叙述;同时,对这四十年间发生在全世界,尤其是与美国有关联的方方面面都给予了细致入微的刻画。从“补偿金大军”风潮、经济大萧条、总统竞选、二次世界大战、原子弹爆炸、朝鲜战争、越南战争、肯尼迪遇刺身亡、尼克松卷入水门事件,到美国人的精神风貌、社会时尚、各阶层的生活状况,再到演艺界的奇闻趣事、妇女的流行服装、青少年的时髦追求,甚至英语词汇的最新演变和人们的性观念都做了面面俱到的讲述。对于每一位读者来说,阅读此书是从本质上深刻了解美国的最简单、最有效的方法。
    本书在1979年曾由商务印书馆出版发行过,它的面世在当时及以后的漫长岁月里曾在中国读者中引起过较好的反响,甚至对一大批中国记者的写作都产生了极大的影响。在网上,这套《光荣与梦想》的旧书曾经被炒数百元一套,成为传媒人的必读书。 25年后,一代人终于等到了威廉·曼彻斯特《光荣与梦想》中文版的再版。在《光荣与梦想》英文版出版31周年之际,我们将再次重温威廉·曼彻斯特在给我们青春期所制造的历史幻觉。


内容简介
 
    《光荣与梦想》是美国的一部断代史,作者对1932~1972年四十年间美国社会的各个层面给予了场景宏大又细致入微的描述。
20世纪70年代初,《光荣与梦想》初版,在美国乃至全世界都引起了较为强烈的反响,美国各大报章赞誉纷纷。1979年,商务印书馆曾出版本书的中文简体字版本,在中国广大读者群,尤其是在知识分子中影响深远,也极大地影响了一大批国内记者的写作手法。这些影响至今仍发挥着积极的作用。
    世界上的任何国家都有它的发展历程,《光荣与梦想》对美国如何摆脱困境、走出萧条,进而演变成一个超级大国的历程给予了介绍,书中对美国政治、军事、经济、文化、社会生活等方面的描写对当今的读者仍然具有较高的参考价值。现在我们重新出版这部书,乃是希望更多的人通过它了解美国这一段历史,进而更深入地认识当代美国社会。 



    《1688年的全球史》则是一本非常有特色的横行比较的历史书籍,介绍了1688年左右世界各个角落里人们在做什么想什么。而且文笔优美,可读性极强,强烈推荐。

以下为引用:

1688年的全球史

    1688年是一个非凡的年代这一天,彼得大帝建立了他的行将改变俄国乃至称霸欧洲的政权,路易十四在他辉煌的凡尔赛宫展示着法国的国力,康熙大帝在紫禁城统治着大清帝国。这一年,发生了光荣革命,斯图亚特王朝倾覆了。这一年,已经完成了《自然哲学的数学原理》的牛顿继续为现代科学的发展而努力,洛克完成了《人类理解论》,决定了此后几百年西方的思想走向,莱布尼茨周游列国,寻访明君,想为天下人谋福利。1688年,令人难忘的还有松尾芭蕉的俳句,石涛的画作,胡安娜的爱情咏叹调……
 
内容简介
 
    备受史景迁等多位史学家称颂的作品,北京大学历史系教授朱孝远作序推荐。威尔斯教授给我们讲述的故事无疑是新鲜和纯净的,它暗示着一种自然的天真,让关爱历史艺术的学子们着迷。在这部激荡人心的著作里,作者对1688年的世界做了一个“横向”的比较,向我们了来自四海的回声,他给我们展示了那一年发生在中国,日本,俄国,非洲,欧洲和美洲的故事。一本令人屏息阅读的书,威尔斯的《1688年的全球史》迸发着想像力,展现了丰富学养,充满令人惊叹的偶然和巧合,以及睿智洞见的闪烁光芒。历史学家威尔斯融合了文化人类学和史学,透过单一年度的棱镜,映现出站在“现代”门槛的世界图像。 



    王则柯的《新编搏弈论平话》也是非常值得一读的书,因为几年前我曾买过岭南系列的一本小册子《博弈论平话》,感觉非常有意思。加上近几年经济学诺贝尔奖获得者多多少少有一些这方面背景,了解一下这些热门的知识还是很有必要的。

以下为引用:

新编搏弈论平话(王则柯著)

    经济学正经历着一场博弈论革命,人们表现出了解博弈论的热情。这是因为现代经济活动早已超出传统经济学讨论的模式。 经济学正经历着一场博弈论革命,人们表现出了解博弈论的热情。这是因为现代经济活动早已超出传统经济学讨论的模式。 

内容简介
 
前言:
    作为一门学科系统地学习博弈论,不是一件容易的事情。事实上目前在我国,许多最好的大学,也只在经济学研究生中开设博弈论课程。我们正着手编写一本供经济类大学本科学生使用的博弈论教程,大概还需要一段时间才能够完工。这么说来,博弈论对于广大读者似乎只好敬而远之了,其实不然。系统地讲授博弈论固然对学生有很高的要求,但是通过比较浅显的例子和故事普及博弈论的一些知识和方法,阐发博弈论的一些思想和观念,应该是大有作为的。博弈论的认真必须有人去做,博弈论的启蒙和普及也很有意义。常常听说人们抱怨国人素质不高。与其抱怨,不若做一些实实在在的事情。田忌赛马等历史故事说明,我们的人民产不欠缺博弈论方面的天分。
    本着这个宗旨,本书从价格大战、银行挤兑,搭便车行为、诺曼底登陆、破釜沉舟、所罗门王断案和慕尼黑谈判等入手,介绍静态博弈、动态博弈、纳什均衡、帕累托优势、风险优势、路径依赖、先动优势和后动优势、威胁的可信性等博弈论的基本概念,以及劣势策略消去法、相对优势策略画线法、确定混合策略等博弈论基本方法,帮助具有中学文化程度的读者了解博弈论的若干初步知识。 



    最后强烈推荐一本《达·芬奇密码》,将优秀小说的诸多因素,神秘、解密、宗教、爱情、凶杀等等不同因素有机融合到一体,环环相扣又不落俗套,堪称今年看过的最好的悬疑小说。

以下为引用:

达·芬奇密码(雄居2003年亚马逊排行榜第一名)

    2003年世界头号畅销书!侦探小说家丹·布朗的新作,今年3月上市一周后就登上《纽约时报》畅销书排行榜,并很快雄居亚马逊排行榜第一名!甚至把更有名气的约翰·格里逊挤下了冠军宝座。俨然是美国书市今年立春以来最受瞩目的畅销小说。《达·芬奇密码》的成功,也反映了英语文学中近几年兴起的“类型化”推理侦探小说的写作风潮。 
 
内容简介
 
    午夜,卢浮宫博物馆年迈的馆长被人杀害在艺术大画廊的拼花地板上。在人生的最后时刻,馆长脱光了衣服,明白无误地用自己的身体摆成了达·芬奇名画《维特鲁威人》的样子,还在尸体旁边留下了一个令人难以捉摸的密码。符号学专家罗伯特·兰登与密码破译天才索菲·奈芙,在对一大堆怪异的密码进行整理的过程当中,发现一连串的线索竟然隐藏在达·芬奇的艺术作品当中! 兰登猛然领悟到,馆长其实是峋山隐修会的成员——这是一个成立于1099年的秘密组织,其成员包括西方历史上诸多伟人,如:牛顿、波提切利、维克多·雨果以及达·芬奇!兰登怀疑他们是在找寻一个石破天惊的历史秘密,一个既能给人启迪又异常危险的秘密。 兰登与奈芙跟一位神秘的幕后操纵者展开了斗智斗勇的角逐,足迹遍及巴黎、伦敦,不断遭人追杀。除非他们能够解开这个错综复杂的谜,否则,峋山隐修会掩盖的秘密,里面隐藏的那个令人震惊的古老真相,将永远消逝在历史的尘埃之中。  

    

全文连载 - 达芬奇密码/作者:丹·布朗

posted @ 2004-07-08 11:58 Flier Lu 阅读(2903) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2349047&run=.01EAE25

这期 MSDN 杂志 中,James Avery 在一篇文章,Ten Must-Have Tools Every Developer Should Download Now,中推荐了十个非常不错的 .NET 方面的辅助开发工具。
    
    Snippet Compiler 是一个非常有趣的代码片断开发环境。在开发中经常会编写一些代码片断或者摘抄一些代码片断来验证某个功能的使用,通常情况下需要新建一个测试项目,比较麻烦。有了 Snippet Compiler 就可以省去繁琐的步骤,在其麻雀虽小五脏俱全的开发环境中能够方便的进行试验。其编辑器居然还能支持代码自动完成功能,呵呵,感觉真是很精致。
    
      
以下为引用:

Features List:

Compiles and runs single or multiple C#, VB.NET and ASP.NET snippets. 
Optionally builds WinForm EXEs, console EXEs or DLLs. 
The user can store a library of templates.   
Displays compile errors and warning, including wave lines in editor with error/warning tooltips. 
IntelliSense for static members and method signatures, as well as constructor signatures.  
Imports VS.NET projects. 
Conveniently sits in the notification area waiting to be useful. 
Exports snippets to HTML/RTF.   



    Regulator 则是一个非常专注的正则表达式开发调试环境。以前在编写正则表达式时,都得找两本相关书籍放在手边随用随查。虽然常用语法基本上记得,但稍微复杂一点的还是得现学现卖,呵呵,写完过后可能自己都不认识了 :P 有了这个强大工具的支持,就可以把那些书抛到一边去了,呵呵,真正可视化开发正则表达式 :D
    
      
    
以下为引用:

Supported Regex actions

The Regulator supports the three most common Regex actions:

Find multiple Matches  
Replace text according to expression 
Split text according to expression


Regexlib.com integration

Search and import expressions and examples from RegexLib.com’s database 
Post your own regular expressions into regexlib's database, direct from The regulator using a graphical wizard 
Full Proxy support for secure connections


Text Editing

Syntax highlighting 
Multiple document tabbed interface 
Quick menus allow you to easily select syntax operations to insert 
Quick Menus are fully customizable simply by changing a simple XML file in the program directory 
Brace matching support 
Make selected text "escaped" to match a specific string exactly without needing to manually write those s,  and other escape sequences 
Collapse/expand selection: you can create collapsible regions in your regex code for easy reading 
Select only a portion of the expression in the editor and only that selection will be executed as a match eplace


Performance analysis

A graphical chart of the latest matches/replaces timings shows you if your optimization efforts are working 
an exact number (in milliseconds) of last match/replace operation
 

Expert Snippets

An easy and simple toolbar of frequently used text snippets is always handy for hard to type expressions 
The snippets toolbar is fully customizable and any snippet can be edited/deleted or you can just add your own 
Inserted snippets that contain the "<>" signs will automatically make the text editor set the selection between those signs, allowing you to make generic snippets that save you typing time 


General usability features

Multi threaded operations : ability to cancel long running expression matches and replaces 
Minimize to tray option 
Recent files menu 
Other small features

    

    CodeSmith 是一个基于模板的代码自动化生成工具,能够根据预定义和自定义的代码模板,基于规则地自动生成代码片断,可以说是“产生式编程”目前最好的体现之一。特别是在目前 .NET 语言不支持泛型的情况下,可以通过开发模板一定程度上解决代码重用问题,而不用象以前那样通过编写 XSLT + XML 来自动生成代码了。:D
    
     

  

    NUnitNDoc 和 NAnt 我就不多说了,呵呵。目前最好的开源测试用例、文档生成和自动构建工具。
    
    FxCop 是一个久负盛名的静态代码分析工具,能够基于规则对二进制形式的 IL 代码进行潜在问题的分析,相关介绍也很多。
    
    Lutz Roeder's .NET Reflector 对我来说更是基本上天天要用的强大的逆向工程工具,属于绝对必备的工具之一,呵呵。前段时间更是推出了支持 .NET Framework 2.0 的最新版本 .NET Reflector 4.0。详情可参看我以前的一篇 blog 文章
    
    最后还有一个 ASP.NET Version Switcher 工具,提供对 ASP.NET 当前使用 .NET 版本的切换工作。对同时需要提供 .NET Framework v1.0 和 v1.1 甚至 v2.0 支持的 ASP.NET 开发者来说,应该是非常有用的一个工具吧,免得每次要用 aspnet_regiis.exe 注册来注销去的,呵呵
    不过好像在 IIS 6 中,如果不使用 VS.NET 调试,可以直接在 IIS 的管理器中启用或禁用某个版本的 ASP.NET 支持。
    
      

flier_lu 发表于 >2004-6-16 23:17:10 保存该日志到本地
查看留言 [评论] [推荐] [引用] [档案] [主页]
※相关文章※


评论/留言
我要留言


作者:Flier Lu 个人主页 

这个 ASP.NET Version Switcher 的作者还有另外一个非常有用的小工具 Reflector FileDisassembler,是Lutz Roeder's .NET Reflector 的一个插件,支持将反编译结果直接写入文件,非常方便!




作者:lonelystranger 个人主页 

呵呵,我在以前的blog里面也推荐了codesniper,一直想做一个类似的东西,可惜没时间

posted @ 2004-07-08 11:57 Flier Lu 阅读(2372) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2309231&run=.0C9B086

随着安全性编程逐渐受到重视,我们需要面对一些以前容易忽视的安全隐患。例如在一个系统字符串中保存当前用户密码或其他敏感信息,则具备权限的其他进程可以很轻松的通过系统提供的 ReadProcessMemory 函数或调试接口,搜索并读取这个字符串的内容,进而了解对此字符串的维护逻辑。在破解软件时一个很常见的方法,就是提供一个特殊的注册码,然后用调试器找到注册算法保存注册码的位置,再通过设置数据断点跟踪注册码的验证算法。要避免这种安全隐患,一个简单的办法是将字符串加密再保存,只在使用的时候解密使用,这样可以一定程度避免敏感信息泄露。CLR 中系统提供的 System.String 虽然功能强大,但因为系统封装的不透明性以及设计上的一些硬伤,使之不适用于保存敏感数据。Shawn Farkas 在其 BLog 的一篇文章(Making Strings More Secure)中讨论了 Whidbey 中为什么要用一个新的 System.Security.SecureString 替换现有 System.String 来实现类似功能。

    首先看看 System.String 为什么不适合保存敏感数据的需求

    1.字符串的内存是在堆中分配的,也就是说其内存完全由 GC 来管理。GC 在进行垃圾收集时,根据具体使用算法不同,完全可能将保存明文密码的字符串在内存中多次拷贝,并留下多个副本,造成安全隐患。
    2.字符串的内容是不加密的,其他进程可以很容易被其他进程通过读进程内存的方式访问。如果进程数据页被交互到硬盘,则还会在硬盘的交换文件中保存敏感数据的内容。
    3.字符串是不可变的,因此一旦要修改一个字符串,则会在内存中留下新旧两份字符串。具体原因和 CLR 内部优化策略请参见我另外一篇文章《CLR中字符串不变性的优化》
    4.因为字符串不可变,所以没有什么好的办法能够显式清除一个字符串的内容。

    在 Whidbey 之前,一般推荐使用字节数组来保存敏感数据,因为字节数组可以被 pin 到一个固定内存位置,并能够显式加密和清除内容。而从 Whidbey 开始,将引入一个新的 System.Security.SecureString 类型专门处理自动加密的需求。
    SecureString 将使用 DPAPI (Data Protection Application-Programming Interface) 完成字符串内容的加密工作,并确保 GC 不会自动处理字符串内容,而是通过 IDisposable 接口和 finalizer 完成加密字符串资源的生命期维护工作。同时 SecureString 还支持将自己设置为只读,避免其他代码修改其内容。
    SecureString 可以从一个数组构造而来,也可以建立空字符串后一个字符一个字符地添加。可以通过 AppendChar()、InsertAt()、RemoveAt() 和 SetAt() 函数对字符串内容进行字符粒度的维护;MakeReadOnly() 和 IsReadOnly() 函数可以确保字符串的只读性;Clear()、Dispose()和 finalizer 可以对字符串的生命期进行维护。
    下面是一个使用 SecureString 的简单例子:
以下为引用:

public static SecureString GetPassword()
{
    SecureString password = new SecureString();

    // get the first character of the password
    ConsoleKeyInfo nextKey = Console.ReadKey(true);

    while(nextKey.Key != ConsoleKey.Enter)
    {
        if(nextKey.Key == ConsoleKey.BackSpace)
        {
            password.RemoveAt(password.Length - 1);

            // erase the last * as well
            Console.Write(nextKey.KeyChar);
            Console.Write(" ");
            Console.Write(nextKey.KeyChar);
        }
        else
        {
            password.AppendChar(nextKey.KeyChar);
            Console.Write("*");
        }

        nextKey = Console.ReadKey(true)
    }

    Console.WriteLine();

    // lock the password down
    password.MakeReadOnly();
    return password;
}


    而在使用 SecureString 的时候也需要注意不要通过 System.String 操作,否则就白忙了,呵呵。可以用类似下面代码的方法,直接操作其内容,如
以下为引用:

IntPtr bstr = Marshal.SecureStringToBSTR(password);

try
{
    // ...
    // use the bstr
    // ...
}
finally
{
    Marshal.ZeroFreeBSTR(bstr);
}



    然后,来看看 SecureString 使用的 DPAPI 是如何对数据进行保护的。Shawn Farkas 在其 Managed DPAPI Part I: ProtectedData 和 Managed DPAPI Part II: ProtectedMemory 两篇文章里面简要的介绍了 DPAPI 的功能和使用方法。

    简单说来常用的就是两对函数,CryptProtectData 和 CryptUnprotectData 使用给定的密钥对指定数据块进行加/解密;CryptProtectMemory 和 CryptUnprotectMemory 则通过 LSA 提供的在一定范围内有效的密钥对数据进行加/解密。前者适用于给定密钥情况下;后者则根据指定范围不同,支持进程内、跨进程和同一登陆帐号等不同范围内的透明加/解密。对面向运行时需求的 SecureString 来说,使用后者足以;而如果需要将加密后内容序列化到磁盘文件或数据库,则需要使用前者。此外还可以通过 CryptAPI 相关函数,提供跨网络的加/解密支持,这是上述两者无法提供的。
以下为引用:

BOOL WINAPI CryptProtectData(
  DATA_BLOB* pDataIn,
  LPCWSTR szDataDescr,
  DATA_BLOB* pOptionalEntropy,
  PVOID pvReserved,
  CRYPTPROTECT_PROMPTSTRUCT* pPromptStruct,
  DWORD dwFlags,
  DATA_BLOB* pDataOut
);

BOOL WINAPI CryptUnprotectData(
  DATA_BLOB* pDataIn,
  LPWSTR* ppszDataDescr,
  DATA_BLOB* pOptionalEntropy,
  PVOID pvReserved,
  CRYPTPROTECT_PROMPTSTRUCT* pPromptStruct,
  DWORD dwFlags,
  DATA_BLOB* pDataOut
);

BOOL CryptProtectMemory(
  LPVOID pData,
  DWORD cbData,
  DWORD dwFlags
);

BOOL CryptUnprotectMemory(
  LPVOID pData,
  DWORD cbData,
  DWORD dwFlags
);



    最后,我根据 Whidbey 的 SecureString 实现,移植了一个版本到 v1.1 下,但因为缺少 CER (Constrained Execution Regions) 和 CF (Critical Finalization) 的支持,其安全性还是无法完全保障,权且作为 v2.0 之前的过渡品吧,呵呵
    
to be continue...    

posted @ 2004-07-08 11:56 Flier Lu 阅读(1085) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2232621&run=.0999083

 cbrumme 在 Finalization 一文中多次提到了资源包装类生命期管理和句柄重用攻击的问题,其另外一篇文章 Lifetime, GC.KeepAlive, handle recycling 详细讨论了这个问题。

    首先考察一个外部资源包装类及其使用代码
以下为引用:

class C {
   IntPtr _handle;

   public ~C() { ... }

   Static void OperateOnHandle(IntPtr h) { ... }

   void m() {
      OperateOnHandle(_handle);
      ...
   }
   ...
}

class Other {
   void work() {
      if (something) {
         C aC = new C();

         aC.m();
         ...

         // most guess here
      } else {
         ...
      }
   }
}


    以 C++ 背景程序员的思路来看,aC 指向对象的生命期应该 if 语句块的末尾结束,无论是否增加一句 aC = null,aC 都会显式被标记为不再使用。如果是 C++ 代码的话,还会在 else 之前由编译器自动加入的 aC.~C() 析构函数调用等等。
    但是在 IL 代码一级,这个 "}" 实际上并不存在,它只是 C# 编译器增加的一个逻辑上的范围而已。对 JITer 所看到的 IL 代码这个层面,Other.work 函数中对 aC 的使用,在 aC.m() 调用之后就结束了。也就是说,GC 可以在 aC.m() 调用之后马上开始对 aC 的垃圾收集工作。
    有人会进一步猜测,aC 将在 C.m() 函数或 C.OperateOnHandle() 函数调用完成后可被回收。但实际上,在 m 函数中,this 指针在用来获取 _handle 内容后,就失去作用了。也就是说,在实际调用静态函数 OperateOnHandle 之前,aC 保存的对象就可以被回收。

    这样一来就会造成一种竞争条件的出现,在用户线程调用 C.OperateOnHandle() 函数处理 _handle 的句柄的同时,后台 Finalizer 线程可能已经在调用 C 类型的 Finalize 函数关闭 _handle 句柄了。出现这种问题的根本原因在于代码打破了对外部资源句柄封装的透明性,封装类 C 和被封装的句柄 _handle 的生命周期被分离开来。导致此问题的原因,还可以由于将 _handle 通过函数返回给最终使用者或者放入某个静态变量中。

    现有情况下一个解决办法是在 OperateOnHandle 函数调用后添加一个 GC.KeepAlive(this) 调用,向 JIT 和 GC 标记当前对象的生命期将显式被延续到其所封装外部资源句柄的生命期之后。此函数不进行任何操作,只是 touch 目标对象一下,表示我还需要使用它,呵呵。
    而这种将生命期维护工作交给最终用户来完成的策略,某种程度上将大大增加潜在问题出现的可能性,必须以其他替代机制保护这种被分离生命期的外部资源对象。

    v1.1 内部处理这种问题的解决方法是通过提供诸如 System.Threading.__HandleProtector 这样的内部类。当外部句柄从包装类的生命期中分离出去时,通过新增保护机制来单独维护外部对象的生命期,从而彻底分离两者。例如 System.Threading.WaitHandle 类型的 Handle 属性就通过
以下为引用:

namespace System.Threading
{
  class WaitHandle
  {
    public virtual IntPtr Handle
    {
      get
      {
        if (this.waitHandleProtector != null)
        {
          return this.waitHandleProtector.Handle;
        }
        return WaitHandle.InvalidHandle;
      }
      set
      {
        this.waitHandleProtector = ((value == WaitHandle.InvalidHandle) ? null : new __WaitHandleHandleProtector(value));
        this.waitHandle = value;
      }
    }
  }
}


    类似的问题还存在于提供了 IDisposable 接口的包装类。当一个线程在调用 Dispose 方法时,另一个线程可能正在使用这个资源。GC.KeepAlive 是无法完全解决这类问题的,而通过大范围的锁来彻底解决,则在性能和功能上是不现实的。
    而这类问题还可能导致前面所说的句柄重用攻击的安全漏洞。如恶意代码可以打开一个具有足够权限的文件,然后同时调用 Read 和 Dispose 方法。而如果恰好发生竞争条件时,另外一个重要文件被打开并重用了此句柄,则本不应被授权的读操作将利用竞争条件被执行。者就是句柄重用攻击。
    要完全解决此问题,在现有架构下需要增加自动对资源引用次数的追踪,通过计数器来跟踪句柄的使用情况,防止竞争条件的发生。但这样做会带来性能上的代价,以及复杂的 unmanaged 资源跟踪机制,并且需要由 CLR 自动或用户手工来确定哪些资源是需要跟踪的。这显然并不现实可行,并且会因为另外的如 unsafe 代码等问题导致连锁问题。
    
    为此 Whidbey 提供了上述的 SafeHandle 类型,从其继承出来的子类,如 SafeWaitHandle 类型,防止上述问题的发生。如 CLR 2.0 中 WaitHandle.Handle 属性的处理代码改成下面的形式:
以下为引用:

namespace System.Threading
{
  class WaitHandle
  {
    protected virtual void Dispose(bool explicitDisposing)
    {
      if (this.safeWaitHandle != null)
      {
        this.safeWaitHandle.Close(); 
      } 
    }
    public virtual IntPtr Handle
    {
      get
      {
        if (this.safeWaitHandle != null)
        {
          return this.safeWaitHandle.DangerousGetHandle();
        }
        return WaitHandle.InvalidHandle;
      }
      set
      {
        if (value == WaitHandle.InvalidHandle)
        {
          this.safeWaitHandle.SetHandleAsInvalid();
          this.safeWaitHandle = null;
        }
        else
        {
          this.safeWaitHandle = new SafeWaitHandle(value, true);
        }
        this.waitHandle = value;
      }
    }
  }
}

posted @ 2004-07-08 11:55 Flier Lu 阅读(748) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2231935&run=.0210194

 在了解了 Finalization 存在的问题后,接下来看看 CLR 1.0 和 1.1 中的现状,以及 Whidbey (v2.0) 中是如何尝试解决这些问题的。

    在 v1.0 和 v1.1 中,一旦创建一个 finalizable 对象,则此对象被加入到 RegisteredForFinalization 系统队列中,此对象结束前可能出现一下的情况

    1.完成正常的 Finalization 操作流程。对象顺利地被 Finalizer 线程调用 Finalize 方法,并最后被 GC 回收。这是最理想的状况 :P

    2.不经过 CLR 关闭流程。这种情况发生在进程被通过 TerminateProcess 或 ExitProcess 函数直接停止了,CLR 只是通过 DllMain 函数的 DLL_PROCESS_DETACH 消息获知进程终止,而此时又不能调用 Managed 代码,所以 Finalize 方法不会被调用,只能依赖于操作系统在进程级对资源的回收。所以内存和句柄引用的内核对象将被正确回收,但保存在内存中的缓冲区等“软”资源会丢失。

    3.经过 CLR 关闭流程。这种情况发生在进程通过 System.Environment.Exit 方法等 Managed 方法停止,CLR 会被引发相应的处理函数,完成所以线程的终止操作,以及 RegisteredForFinalization 和 ReadyToFinalize 队列的处理工作。这种情况也可以接受。

    System.Environment.Exit 方法 (bclsystemEnvironment.cs:83) 调用 System.Environment.ExitNative 外部方法,通过 CLR 在 SystemNative::Exit 函数实现 (vmcomsystem.cpp:833) 中调用 ForceEEShutdown 函数 (vmceemain.cpp:658),强制调用 EEShutDown 函数 (vmceemain.cpp:739) 完成 CLR 执行环境的关闭流程。EEShutDown 函数中分两步调用 GCHeap::FinalizerThreadWatchDog (vmgcee.cpp:975) 完成 Finalization 流程。

    4.经过 GC 流程强制被回收。这种情况通常发生在对象类型所在 AppDomain 被 Unload 时,AppDomain 只有在其所有类型的对象所在 RegisteredForFinalization 和 ReadyToFinalize 队列的处理工作完成之后,才会被实际卸载。

    System.AppDomain.Unload 方法 (bclsystemAppDomain.cs:696) 调用 System.AppDomain.UnloadWorker.Unload 方法 (bclsystemAppDomain.cs:1702) 间接通过 System.AppDomain.UnloadThreadWorker.Unload 方法 (bclsystemAppDomain.cs:1746) 创建一个后台线程完成实际的 AppDomain 卸载工作。而在 AppDomainNative::Unload 函数 (vmappdomainnative.cpp:471) 中实现的 AppDomain.nUnload 完成真正的卸载工作。而 AppDomainNative::Unload 函数内部调用 AppDomain::Exit 函数 (vmappdomain.cpp:5623) 完成类型卸载、资源回收,以及在调用 GCHeap::GarbageCollect 函数 (vmgcee.cpp:5351) 回收被卸载类型对象后,调用 GCHeap::FinalizerThreadWait 函数 (vmgcee.cpp:934) 释放 finalizable 对象。

    此外 cbrumme 还提到了一些细节上需要注意的地方,例如除了 System.Threading.Thread 类的对象外,所有对象都是在其创建的 AppDomain 中被析构,而 Thread 对象在进程终止的过程中不被析构等等。因为这涉及到太多相关内容,这儿就不详细展开解释了,呵呵,每一条都足够单独写篇文章分析了 :P
    如果有进一步了解的兴趣,可以参考 cbrumme 的另一篇文章 Startup, Shutdown & related matters

    在提出并分析问题后,cbrumme 给出了 Whidbey 中解决问题的一些思路和方法。

    cbrumme 首先列举了一个 v2.0 中新增的 System.Runtime.InteropServices.SafeHandle 类型以及其种种优点。简单来说,这是一个操作安全的对 Win32 句柄进行包装的类,v2.0 的 BCL 从此类中派生出很多对句柄进行维护的类型,如 Win32SafeHandle 以及 Microsoft.Win32.SafeHandles 名字空间下一堆句柄包装类。这些类能够一定程度上解决构造和析构过程原子化等问题,杜绝诸如句柄重用攻击和资源耗尽攻击。而这一神奇类型真正的幕后功臣是 Critical Finalization 概念的出现和应用。

    任一从 System.Runtime.Reliability.CriticalFinalizerObject 类型继承的子类,都自动获得 Critical Finalization (CF)的能力,CLR v2.0 将保障这些类型满足一下需求:

    1.此类对象构造之前,CLR 将预先准备好调用此对象 Finalize 方法所需的各种资源。这种准备包括预先 JIT 代码,允许类构造函数 (class constructor),以及静态可达的所以其他类型。虽然如前面所述,这种静态分析无法处理通过虚函数或接口间接引用的对象,那些问题需要程序员来解决。

    2.CLR 在执行这些类型的 Finalize 函数时不会超时。这就要求此类函数的 Finalze 代码编写非常小心,值得信任,呵呵

    3.调用 Finalize 函数时,CLR 会进入一种保护状态,防止因为异步异常中断执行操作。例如 JIT 的 OutOfMemoryExceptions 或调用 .cctors 的 TypeInitializationExceptions 异常将被暂时屏蔽,保障 Finalize 函数运行的原子性。不过应该程序原因导致的异常,如分配对象导致 OutOfMemoryException 异常,这属于程序自身的问题,可以通过 try...catch 保护,不在系统保护范畴之内。

    4.所有其它的普通对象在此类对象之前被析构,无论析构成功与否,都能保障在此类对象执行析构操作时,它们已经完成工作。

    实际上 CF 的前三点跟 Constrained Execution Regions (CER) 要解决的问题是一致的,也就是对异步异常的处理模型。cbrumme 在另一篇文章 Reliability 中详细讨论了这方面问题,这里就不罗嗦了。而 v2.0 BCL 中 System.Runtime.Reliability 名字空间下增加的几个类型,就是为了解决这方面问题,可惜介绍的资料太少了。

    而对于 CF 来说,在普通的程序中其重要意义并不能很好体现出来,毕竟碰到内存耗尽和调用 Thread.Abort 的机会并不多。不过在某些受限情况下,如 SQL Server (Yukon) 中,这种问题的解决就具有重要意义。例如作为 CLR 宿主的 Yukon 经常运行在临界资源状态下,异步 异常和强制用 Thread.Abort 中断时间过长的查询都是经常碰到的。好在 Whidbey 将提供基于策略的执行机制,AppDomain 卸载、普通的中断操作等等都会加入超时策略机制,通过配置来缓解部分问题。
    
    看到这里,偶对 cbrumme 的仰慕之情如滔滔江水连绵不决,又有如黄河泛滥一
发而不可收拾...呵呵

posted @ 2004-07-08 11:54 Flier Lu 阅读(747) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2203965&run=.08E8850

Finalization 机制是 CLR 中完成显式资源释放的地方,将之与 IDisposable 接口机制配合,能够完成在 CLR 中对资源显式管理。但因为设计上的一些问题,导致正确编写 finalizer 是一件非常困难的事情,cbrumme 在其 BLog 上的一篇文章,Finalization,中详细介绍了为什么 finalizer 可能编写出错,以及在 Whidbey 中是如何尝试去解决这些问题。
    下面我将根据我的理解简述这篇重要文章的内容,备忘。

    在 CLR 中使用 Finalization 机制可能需要付出较为昂贵的代价:

    首先,创建一个 finalizable 对象时,需要将此对象放入系统 RegisteredForFinalization 队列中,以便在执行 GC 操作时能够区别对待这些对象。而这个工作相当于给每个 finalizable 对象增加并初始化一个指针域,这种代价在高速创建小对象时尤为明显。

    堆的实现类 GCHeap (vmgc.h:150) 在其 Alloc 方法 (vmgcsmp.cpp:5140, 5270) 创建一个对象时,会检查此对象是否实现了 finalizer (检查调用的标志参数 glags 是否包含 GC_ALLOC_FINALIZE),如果有则调用堆的 CFinalize 类实例的 RegisterForFinalization 方法 (vmgcsmp.cpp:5808),将此对象加入到 RegisteredForFinalization 队列中。 RegisteredForFinalization 队列实现上是一个初试大小为 100 的对象指针数组 m_Array (vmgcsmppriv.h:777),由堆内多个分代共享,m_FillPointers 数组 (vmgcsmppriv.h:778) 保存了每个分代所用的 RegisteredForFinalization 队列位置。初始化的代码片断如下:
以下为引用:

#define NUMBERGENERATIONS   5               //Max number of generations

class CFinalize
{
private:
  Object** m_Array;
  Object** m_FillPointers[NUMBERGENERATIONS+2];
  Object** m_EndArray;
public:
  CFinalize::CFinalize()
  {
    m_Array = new(Object*[100]);
    m_EndArray = &m_Array[100];

    for (unsigned int i =0; i < NUMBERGENERATIONS+2; i++)
    {
        m_FillPointers[i] = m_Array;
    }
  }
};


    因此在 CFinalize::RegisterForFinalization 执行时,需要逆向检索到对象所在分代的位置,并将对象指针插入到此位置。频繁分配的 finalizable 小对象可能会造成此队列迅速增长。(此队列的增长速度为原有长度的 1.2 倍,详情见 CFinalize::GrowArray vm:gcsmp.cpp:6094)

    其次,每次 GC 操作都需要扫描 RegisteredForFinalization 队列,将可回收的 finalizable 对象放入 ReadyToFinalize 队列中。

    CFinalize 使用 m_FillPointers[NUMBERGENERATIONS] 指向的区域保存 ReadyToFinalize 队列内容,在 CFinalize::ScanForFinalization 函数 (vmgcsmp.cpp:6011) 中完成对 RegisteredForFinalization 队列的扫描。

    而所有 ReadyToFinalize 队列的对象,以及通过他们能够触及 (reachable) 的对象,都将在此次 GC 过程中被标记 (Masked),并被提升 (Promoted) 到下一个分代中,以便完成 finalizer 的调用工作。而 ReadyToFinalize 队列的对象以及通过他们能够触及的对象,可能是一簇非常大的对象集。也就是说如果一个复杂的引用很多对象的类实现了 finalizer,将比简单的 Unmanaged 资源包装类更大地造成性能和垃圾回收率的损失。

    CFinalize::ScanForFinalization 函数在发现 ReadyToFinalize 队列有内容后,调用 CFinalize::GcScanRoots 函数 (gcsmp.cpp:5992) 遍历 ReadyToFinalize 队列,对每个可回收的 finalizable 对象调用 GCHeap::Promote 函数 (vmgcsmp.cc:4917),提升其分代。

    因为更老分代代将以比较新分代更少的比例被垃圾收集处理,所以因为 Finalizaion 造成的 finalizable 对象提升会显著增加本应被回收的对象的生命时间。

    最后,CLR 目前版本只使用了一个高优先级 Finalizer 线程负责遍历 ReadyToFinalize 队列,此线程顺序处理 ReadyToFinalize 队列中的每个对象的 Finalize 函数。在某些情况下,如终端服务中,多个进程的 finalizer 线程可能造成性能上的瓶颈;在某些情况,如多个 CPU 同时告诉创建 finalizable 对象时,可能造成性能瓶颈。最终这个单独的 Finalizer 线程可能变成稀缺资源,因为某些情况下它会被不确定地阻塞,这会造成进程资源不足并最终崩溃。cbrumme 在其 Apartments and Pumping in the CLR 一文中讨论了可能造成这种问题的原因。

    此外 Finalization 机制对 managed 代码开发者来说存在概念成本,很难编写完全正确的 Finalize 方法。

    要解决这些问题,CLR 的后续版本可能会通过使用线程池完成 Finalizer 线程工作减少潜在性能瓶颈;还可以通过修剪 finalizable 对象的引用对象树,降低提升此类对象带来的损耗。但因为修剪引用对象树时,只能通过 finalizable 对象的 Finalize 方法进行静态分析,而不能处理通过虚函数或接口就行调用的间接引用,这造成这种方法并不实用。

    要完全弄清楚这里的问题,需要考察对象可触性(Reachability)、析构顺序(Ordering)和部分信任(Partial Trust)等多方面因素。

    可触性(Reachability)

    编写正确 Finalize 方法的一个很重要的原则是不能在 Finalize 方法中使用其他对象,因为 Finalize 方法的调用顺序是无序的,不能假设一个外部对象在 Finalize 方法被调用时还存在并可访问。唯一例外的情况是只被此对象私有引用的外部对象,可以在 Finalize 方法中安全调用。因为正如上面分析的那样,这些私有引用的对象只有在此 finalizable 对象正确释放后,才会被垃圾收集,故而不存在访问问题。
    也可以通过设计上的技巧解决这个问题,如一个文件和一个缓冲区对象可以互相引用。则一旦他们被回收,肯定是在一代中进行,并按不定顺序调用其 Finalize 方法。在绑定的对象对的某个对象的 Finalize 方法被调用时,通知另外一个对象采取相应行动,可以让析构操作的顺序与 Finalize 方法调用的顺序无关,并最终解决这个问题。示例代码如下:
以下为引用:

class File
{
  private FileBuffer _buf;

  internal bool _finalized;

  public void CloseHandle()
  {
    // ...
  }

  internal void DoClose()
  {
    _buf.Flush();

    CloseHandle();

    _finalized = true;
    _buf._finalized = true;
  }

  public File() : _finalized(false)
  {
    // ...
  }

  public ~File()
  {
    if(!_finalized)
    {
      DoClose();
    }
  }
}

class FileBuffer
{
  private File _file;

  internal bool _finalized;

  public FileBuffer() : _finalized(false)
  {
    // ...
  }

  public ~FileBuffer()
  {
    if(!_finalized)
    {
      _file.DoClose();
    }
  }

  public void Flush()
  {
    // ...
  }
}



    析构顺序(Ordering)

    一个常见的问题是为什么 Finalization 的过程中对象必须是无序被调用的。如果能通过增加一些装饰性的对象引用来指导析构的顺序,则使用起来可以较为简单。但在实现上,必须使用很复杂的跨分代对象析构顺序排序,并很可能造成无法解决的环状互相引用的问题,这就跟通过引用计数实现垃圾收集一样,存在致命的理论上的硬伤。同时,无序的析构过程可以让处理 RegisteredForFinalization 和 ReadyToFinalize 队列的过程更有效,并容易向多线程处理移植。

    部分信任(Partial Trust)

    因为编写 finalizable 对象并不受安全权限的限制,所以通过 Finalize 方法对系统进行 DoS 攻击是非常现实的问题,即使是在部分信任情况下,也无法杜绝。虽然部分信任的代码可能无法直接访问 unmanaged 资源,但他们可以通过其他被信任的代码或方法获取此类资源。同时对 managed 资源也存在需要使用 Finalize 方法的情况,如对象池和缓冲区等等。例如 SQL Server (Yukon) 通过部分信任的 Assembly 实现数据库的 constraints,这些对象不使用 unmanaged 资源但必须能够回收。

    在考察了这些因素后,cbrumme 解释了为什么完美的 Finalize 方法很难编写:

    Finalize 方法必须能够容忍部分构造的对象

    一个从完全信任类型中继承出来的部分信任类型,可能在调用基类的构造函数之前就抛出异常,造成 Finalize 方法必须处理一个对象内容被全部置零,但没有初始化的实例;此外异步异常,如StackOverflowException、OutOfMemoryException 或 AppDomainUnloadException ,也可能造成构造函数的中断。

    对象可能在 Finalization 后变得可调用

    因为对象在 Finalize 方法中,可以将自己重新放入 GC 的根节点中重新可用(也就是 CLR 中的 resurrected 概念),所以对象必须保存是否已经被析构的信息。以便在 Finalize 方法被调用后,能够抛出 ObjectDisposedException 异常,防止错误的调用(或者重新构造自己)。

    对象可能在 Finalization 过程中变得可调用

    因为对象的 Finalize 方法是在单独的 Finalizer 线程中被调用,而引用此对象的其他对象可能在 Finalize 方法被调用时重生,这就造成对象在 Finalization 过程中,可能同时被应用程序和 Finalizer 线程使用。如果对象包装了一个操作系统的句柄,则还会在某些竞争条件下出现 handle recycling 攻击的可能性,具体讨论见 cbrumme 的 Lifetime, GC.KeepAlive, handle recycling 一文。

    Finalize 方法可能被多次调用

    与取消 Finalize 方法调用的 GC.SuppressFinalize 方法对应,GC.ReRegisterForFinalize 强制进行对象的 Finalize 方法调用,这就造成一个 Finalize 方法可能被调用多次。

    Finalizer 线程工作在不同的安全上下文(security context)中

    与 ThreadPool.QueueUserWorkItem 或 Control.BeginInvoke 类似,Finalize 方法被调用时,Finalizer 线程处于此对象构造时不同的安全上下文中。也就是说,如果 Finalize 函数编写不当,可能造成潜在的安全漏洞。如一个不恰当的例子中,一个完全信任的对象在构造函数中接受一个文件名参数,并在 Finalize 函数中打开处理这个文件,这就可能造成安全隐患。

    由此可见,编写一个真正完美的 Finalize 函数实在是太麻烦了,呵呵,反正我看完是头大了一圈 :P

待续...Whidbey 中对的改进

posted @ 2004-07-08 11:54 Flier Lu 阅读(807) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=2178387&run=.0F8ED5D

在仔细阅读 scz 的《MSDN系列(11)--给SoftICE写插件》一文后,忍不住自己动手试试 WinDbg 插件的编写,呵呵。不过我选择的是与小四不同的另一种 WinDbg 插件编写方法。
    WinDbg 最新版本的 sdkhelp 目录下有一个 debugext.chm 文件,里面有很详细的 WinDbg 插件编写文档。其中提到 WinDbg 支持两种类型的插件:DbgEng 扩展和 WdbgExts 扩展。前者是使用在 dbgeng.h 中定义的针对 Debugger Engine API 的调试扩展;后者则是使用在 wdbgexts.h 中定义的专门针对 WinDbg 的调试扩展。小四文章中使用的就是后者的接口,较为简明,也可以被 SoftIce 很好支持;我则选择前一种插件类型,功能更强大,而且可以被除 WinDbg 外的其他支持 Debugger Engine API 的工具,如 Visual Studio.NET 支持。
    与 WdbgExts 类型扩展插件类似,DbgEng 类型扩展插件必须实现一个初始化回调函数:
以下为引用:

HRESULT CALLBACK DebugExtensionInitialize(OUT PULONG  Version, OUT PULONG  Flags);


    此函数在使用 .load 命令载入插件时被调用,返回插件的版本信息。如
以下为引用:

const int EXTS_VERSION_MAJOR = 1;
const int EXTS_VERSION_MINOR = 0;

extern "C" HRESULT CALLBACK DebugExtensionInitialize(OUT PULONG Version, OUT PULONG Flags)
{
  *Version = DEBUG_EXTENSION_VERSION(EXTS_VERSION_MAJOR, EXTS_VERSION_MINOR);
  *Flags = 0;

  return S_OK;
}


    定义插件回调函数时,必须使用 extern "C" 指定此函数的函数名使用与 C 兼容的命名格式,并建立一个 .def 文件定义入口名字,如
以下为引用:

LIBRARY ClrExts

EXPORTS
  DebugExtensionInitialize
  DebugExtensionUninitialize
  DebugExtensionNotify
  KnownStructOutput

  help
  showcontext


    这里建立一个新的 DbgEng 类型插件 ClrExts 完成对 CLR 调试支持扩展功能,并导出四个标准回调函数。除 DebugExtensionInitialize 必须有以为,另外三个回调函数是可选的。
以下为引用:

void CALLBACK DebugExtensionNotify(IN ULONG Notify, IN ULONG64 Argument);


    DebugExtensionNotify 函数在调试会话的状态转换的时候被调用,以通知插件调整自己的状态。Notify 参数可以有四个值:

    DEBUG_NOTIFY_SESSION_ACTIVE:        调试会话被激活
    DEBUG_NOTIFY_SESSION_INACTIVE:      没有被激活的调试会话
    DEBUG_NOTIFY_SESSION_ACCESSIBLE:    调试会话被中断并可访问
    DEBUG_NOTIFY_SESSION_INACCESSIBLE:  调试会话恢复执行并不能访问

    调试会话的概念,表示是否正在调试一个进程中;而根据调试状态,中断目标程序运行并由调试器获得控制权时,调试会话被中断并可访问(DEBUG_NOTIFY_SESSION_ACCESSIBLE)。
    调试插件可以通过跟踪这几个状态的改变,调整自己对目标调试进程的控制方法。
以下为引用:

void CALLBACK DebugExtensionUninitialize(void);


    DebugExtensionUninitialize函数则是在插件被 .unload 命令卸载的时候被调用。
以下为引用:

HRESULT CALLBACK KnownStructOutput(IN ULONG Flag, IN ULONG64 Address, IN PSTR StructName, OUT PSTR Buffer, IN OUT PULONG BufferSize);


    最后一个 KnownStructOutput 回调函数较少被用到,用于提供此调试插件支持打印的结构列表,并可打印指定地址的指定结构内容。

    与 WdbgExts 类型插件不太,DbgEng 类型插件的调试接口可以通过 DebugCreate 函数,调用 COM 接口自行获取
以下为引用:

HRESULT DebugCreate(IN REFIID InterfaceId, OUT PVOID *Interface);


    也可以通过插件命令的参数获得。插件的通用命令接口如下:
以下为引用:

HRESULT CALLBACK (* PDEBUG_EXTENSION_CALL)(IN IDebugClient *Client, IN OPTIONAL PCSTR Args);


    第一个参数 Client 就是调试接口,另外一个则是命令的参数字符串。

    可以使用一个简单的包装类 CDebugClient 对 IDebugClient 接口就行包装,其构造函数自动获取调试接口
以下为引用:

class CDebugClient
{
private:
  HRESULT m_hr;

  CComPtr<IDebugClient> m_spDebugClient;
  CComQIPtr<IDebugControl> m_spDebugControl;

  WINDBG_EXTENSION_APIS32 m_extensionApis;
public:
  CDebugClient(void);
};

CDebugClient::CDebugClient(void)
{
  m_hr = DebugCreate(__uuidof(IDebugClient), (PVOID *)&m_spDebugClient);

  if(SUCCEEDED(m_hr))
  {
    m_spDebugControl = m_spDebugClient;

    m_extensionApis.nSize = sizeof(m_extensionApis);

    m_hr = m_spDebugControl->GetWindbgExtensionApis32(&m_extensionApis);
  }
}


    DebugCreate 函数构造一个新的 IDebugClient 接口实例,并放入 ATL 接口包装类 CComPtr<IDebugClient> 的对象 m_spDebugClient 中,并可从此接口查询获取 IDebugControl 接口实例。IDebugControl::GetWindbgExtensionApis32 则可以获取与 WdbgExts 类型插件兼容的调试接口函数集。不过我们后面将看到,DbgEng 的相应接口,比 WinDbg 的传统函数集功能要强大得多。
    对于插件命令的入口直接给出的 IDebugClient 实例,则可以省去构造过程,如
以下为引用:

CDebugClient::CDebugClient(IDebugClient *dbg)
  : m_outLevel(olDefault), m_hr(S_OK), m_spDebugClient(dbg)
{
  if(dbg)
  {
    m_spDebugControl = m_spDebugClient;

    m_extensionApis.nSize = sizeof(m_extensionApis);

    m_hr = m_spDebugControl->GetWindbgExtensionApis32(&m_extensionApis);
  }
}


    在了解了调试接口的创建和包装方法后,可以建立第一个插件命令,help,显示一个帮助字符串给调试器
以下为引用:

extern "C" HRESULT CALLBACK help(IN IDebugClient *Client, IN OPTIONAL PCSTR Args)
{
  UNREFERENCED_PARAMETER(Args);

  CDebugClient DebugClient(Client);

  if(FAILED(DebugClient.getLastHResult())) return DebugClient.getLastHResult();

  DebugClient.Info("Help for %s "
                   " help - Show this help ", EXTS_NAME);

  return DebugClient.getLastHResult();
}


    UNREFERENCED_PARAMETER 是一个宏,用于显式引用一次不会用到的函数参数,避免编译器警告;
    然后使用命令参数构造 CDebugClient 实例,并判断其构造过程是否有效;
    接着调用 CDebugClient 封装的 Info 函数打印一堆帮助字符串;
    最后返回 DebugClient 的最后调用状态。

    函数逻辑非常简单,就不罗嗦了,下面看看对字符串的输出
以下为引用:

enum OutputLevel
{
  olAll,
  olDebug,
  olInfo,
  olWarning,
  olError,
#ifdef _DEBUG
  olDefault = olAll
#else
  olDefault = olInfo
#endif
};

class CDebugClient
{
private:
  OutputLevel m_outLevel;
};


    首先定义了5个缺省的输出级别,所有、调试、信息、警告和错误;然后定义调试接口的信息显示级别。
以下为引用:

class CDebugClient
{
public:
  void OutputString(OutputLevel lvl, const char *fmt, va_list args) const;
  void OutputString(OutputLevel lvl, const char *fmt, ...) const;

  void DoLog(OutputLevel level, const char *fmt, va_list args) const
  {
    if(m_outLevel <= level) OutputString(level, fmt, args);
  }

#define DEF_LOG_LEVEL(name) void name(const char *fmt, ...) const 
                            { 
                              va_list args; 
                              va_start(args, fmt); 
                              
                              DoLog(ol ## name, fmt, args); 
                              
                              va_end(args); 
                            }

  DEF_LOG_LEVEL(Debug);
  DEF_LOG_LEVEL(Info);
  DEF_LOG_LEVEL(Warning);
  DEF_LOG_LEVEL(Error);
};


    实际的信息输出放在 OutputString 函数中完成,而 DoLog 则根据当前调试接口的信息级别判断是否需要输出信息。并使用 DEF_LOG_LEVEL 宏定义四种常用的信息输出函数。
以下为引用:

void CDebugClient::OutputString(OutputLevel lvl, const char *fmt, va_list args) const
{
#if 1
  static ULONG OutputMask[] = {
    0,
    DEBUG_OUTPUT_VERBOSE,
    DEBUG_OUTPUT_NORMAL,
    DEBUG_OUTPUT_WARNING,
    DEBUG_OUTPUT_ERROR
  };

  m_spDebugControl->OutputVaList(OutputMask[lvl], fmt, args);
#else
  std::string str;

  str.resize(_vscprintf(fmt, args)+1, 0);
  _vsnprintf(const_cast<char *>(str.c_str()), str.size(), fmt, args);

  m_extensionApis.lpOutputRoutine(str.c_str());
#endif
}

void CDebugClient::OutputString(OutputLevel lvl, const char *fmt, ...) const
{
  va_list args;
  va_start(args, fmt);

  OutputString(lvl, fmt, args);

  va_end(args);
}


    OutputString 可以通过 IDebugControl 的 OutputVaList 方法输出,也可以通过传统的 WdbgExts 调试接口的 lpOutputRoutine 函数输出。前者的优点是可以根据信息输出级别,设定相应的输出掩码。如 olDebug 对应于 DEBUG_OUTPUT_VERBOSE,此类型信息只有在 WinDbg 打开了 Verbose 模式(菜单 View/Verbose Output)时才会显示,非常适合对插件就行调试跟踪。

    在了解了调试接口函数的大致使用流程后,接着编写一个有实际意义的功能,也就是小四文章中的 showcontext 函数,代码如下:
以下为引用:

#define OFFSETOF(TYPE, MEMBER)          ((size_t)&((TYPE)0)->MEMBER)

extern "C" HRESULT CALLBACK showcontext(IN IDebugClient *Client, IN OPTIONAL PCSTR Args)
{
  CDebugClient DebugClient(Client);

  if(FAILED(DebugClient.getLastHResult())) return DebugClient.getLastHResult();

  DebugClient.Debug("%s: call externsion function showcontext with arguments - %s ", EXTS_NAME, Args);

  std::string buf;

  DWORD dwSize = OFFSETOF(PCONTEXT, ExtendedRegisters);

  buf.resize(dwSize);

  DWORD dwAddress = DebugClient.Evaluate(Args);

  DebugClient.Debug("%s: get expression "%s" 's value %x ", EXTS_NAME, Args, dwAddress);

  if(DebugClient.ReadMemory(dwAddress, buf) == dwSize)
  {
    PCONTEXT pCtxt = (PCONTEXT)buf.c_str();

    DebugClient.Info("EAX=%08X   EBX=%08X   ECX=%08X   EDX=%08X   ESI=%08X "
                     "EDI=%08X   EBP=%08X   ESP=%08X   EIP=%08X   EFLAGS=%08X "
                     "CS=%04X  DS=%04X  SS=%04X  ES=%04X  FS=%04X  GS=%04X ",
                     pCtxt->Eax, pCtxt->Ebx, pCtxt->Ecx, pCtxt->Edx, pCtxt->Esi,
                     pCtxt->Edi, pCtxt->Ebp, pCtxt->Esp, pCtxt->Eip, pCtxt->EFlags,
                     (WORD)pCtxt->SegCs, (WORD)pCtxt->SegDs, (WORD)pCtxt->SegSs,
                     (WORD)pCtxt->SegEs, (WORD)pCtxt->SegFs, (WORD)pCtxt->SegGs);
  }
  else
  {
    DebugClient.Warning("%s: Cannot read process memory @ %x ", EXTS_NAME, dwAddress);
  }

  return DebugClient.getLastHResult();
}


    代码逻辑很简单:首先获取调试接口;然后调用 DebugClient.Evaluate 函数分析命令参数的表达式,获取目标地址;然后调用 DebugClient.ReadMemory 函数从指定地址读取 CONTEXT 结构的部分内容;最后调用 DebugClient.Info 函数输出信息。
    OFFSETOF 宏是一个获取结构部分内容长度的小技巧,通过将 0 地址强制转换为结构指针,来获得指定字段在结构中的相对偏移。
以下为引用:

size_t CDebugClient::Evaluate(const char *lpExpression)
{
  DEBUG_VALUE value;

  m_hr = m_spDebugControl->Evaluate(lpExpression, DEBUG_VALUE_INT32, &value, NULL);

  return value.I32;
}

       
    CDebugClient::Evaluate 函数简单调用 IDebugControl 接口的 Evaluate 函数,完成表达式的计算工作。例如敲入 "showcontext *(esp+4)"这条命令,命令行参数 Args 的内容就是 "*(esp+4)",而 Evaluate 函数可以将这个表达式计算得到一个确定的地址。DEBUG_VALUE_INT32参数指定需要获取一个 32 位整数;DEBUG_VALUE 则是一个类似 VARIANT 的联合类型,用户保存各种可能类型的参数。
以下为引用:

size_t CDebugClient::ReadMemory(size_t offset, void *buf, size_t size) const
{
  ULONG readBytes;

#if 1
  CComQIPtr<IDebugDataSpaces> spDebugDataSpaces(m_spDebugClient);

  spDebugDataSpaces->ReadVirtual(offset, buf, size, &readBytes);
#else
  m_extensionApis.lpReadProcessMemoryRoutine(offset, buf, size, &readBytes);
#endif

  return readBytes;
}

size_t CDebugClient::ReadMemory(size_t offset, std::string& buf) const
{
  return ReadMemory(offset, const_cast<char *>(buf.c_str()), buf.size());
}


    而 ReadMemory 则比较简单,通过 IDebugDataSpaces 接口或 WdbgExts 兼容接口都能读取目标进程的虚拟内存。

    至此,编写一个 DbgEng 类型插件的必要内容已经基本上介绍完了,以后有机会再详细介绍调试接口的具体使用方法,呵呵

btw: 不要找我要可直接编译的代码。我跟小四习惯不同,只提供编写完整代码所有必要功能点的详细介绍。如果要想实际 run 起来,自己努力吧,呵呵

posted @ 2004-07-08 11:53 Flier Lu 阅读(897) 评论(0) 编辑


http://www.blogcn.com/user8/flier_lu/index.html?id=2164751&run=.04005F8

CLR 产品单元经理(Unit Manager) Jason Zander 在前几天一篇文章 Why isn't there an Assembly.Unload method? 中解释了为什么 CLR 中目前没有实现类似 Win32 API 中 UnloadLibrary 函数功能的 Assembly.Unload 方法。
    他认为之所以要实现 Assembly.Unload 函数,主要是为了回收空间和更新版本两类需求。前者在使用完 Assembly 后回收其占用资源,后者则卸载当前版本载入更新的版本。例如 ASP.NET 中对页面用到的 Assembly 程序的动态更新就是一个很好的使用示例。但如果提供了 Assembly.Unload 函数会引发一些问题:

    1.为了包装 CLR 中代码所引用的代码地址都是有效的,必须跟踪诸如 GC 对象和 COM CCW 之类的特殊应用。否则会出现 Unload 一个 Assembly 后,还有 CLR 对象或 COM 组件使用到这个 Assembly 的代码或数据地址,进而导致访问异常。而为了避免这种错误进行的跟踪,目前是在 AppDomain 一级进行的,如果要加入 Assembly.Unload 支持,则跟踪的粒度必须降到 Assembly 一级,这虽然在技术上不是不能实现,但代价太大了。

    2.如果支持 Assembly.Unload 则必须跟踪每个 Assembly 的代码使用到的句柄和对现有托管代码的引用。例如现在 JITer 在编译方法时,生成代码都在一个统一的区域,如果要支持卸载 Assembly 则必须对每个 Assembly 都进行独立编译。此外还有一些类似的资源使用问题,如果要分离跟踪技术上虽然可行,但代价较大,特别是在诸如 WinCE 这类资源有限的系统上问题比较明显。

    3.CLR 中支持跨 AppDomain 的 Assembly 载入优化,也就是 domain neutral 的优化,使得多个 AppDomain 可以共享一份代码,加快载入速度。而目前 v1.0 和 v1.1 无法处理卸载 domain neutral 类型代码。这也导致实现 Assembly.Unload 完整语义的困难性。

    基于上述问题, Jason Zander 推荐使用其他的设计方法来回避对此功能的使用。如 Junfeng Zhang 在其 BLog 上介绍的 AppDomain and Shadow Copy,就是 ASP.NET 解决类似问题的方法。

    在构造 AppDomain 时,通过 AppDomain.CreateDomain 方法的 AppDomainSetup 参数中 AppDomainSetup.ShadowCopyFiles 设置为 "true" 启用 ShadowCopy 策略;然后设置 AppDomainSetup.ShadowCopyDirectories 为复制目标目录;设置 AppDomainSetup.CachePath + AppDomainSetup.ApplicationName 指定缓存路径和文件名。
    通过这种方法可以模拟 Assembly.Unload 的语义。实现上是将需要管理的 Assembly 载入到一个动态建立的 AppDomain 中,然后通过跨 AppDomain 的透明代理调用其功能,使用 AppDomain.Unload 实现 Assembly.Unload 语义的模拟。chornbe 给出了一个简单的包装类,具体代码见文章末尾。
    
    这样做虽然在语义上能够基本上模拟,但存在很多问题和代价:
    
    1.性能:在 CLR 中,AppDomain 是类似操作系统进程的逻辑概念,跨 AppDomain 通讯就跟以前跨进程通讯一样受到诸多限制。虽然通过透明代理对象能够实现类似跨进程 COM 对象调用的功能,自动完成参数的 Marshaling 操作,但必须付出相当的代价。Dejan Jelovic给出的例子(Cross-AppDomain Calls are Extremely Slow)中,P4 1.7G 下只使用内建类型的调用大概需要 1ms。这对于某些需要被频繁调用的函数来说代价实在太大了。如他提到实现一个绘图的插件,在 OnPaint 里面画 200 个点需要 200ms 的调用代价。虽然可以通过批量调用进行优化,但跨 AppDomain 调用效率的惩罚是肯定无法逃脱的。好在据说 Whidbey 中,对跨 AppDomain 调用中的内建类型,可以做不 Marshal 的优化,以至于达到比现有实现调用速度快 7 倍以上,...,我不知道该夸奖 Whidbey 实现的好呢,还是痛骂现有版本之烂,呵呵
    
    2.易用性:需要单独卸载的 Assembly 中类型可能不支持 Marshal,此时就需要自行处理类型的管理。
    
    3.版本:在多个 AppDomain 中如何包装版本载入的正确性。
    
    此外还有安全方面问题。对普通的 Assembly.Load 来说,载入的 Assembly 是运行在载入者的 evidence 下,而这绝对是一个安全隐患,可能遭受类似 unix 下面通过溢出以 root 权限读写文件的程序来改写系统文件的类似攻击。而单独在一个 AppDomain 中载入 Assembly 就能够单独设置 CAS 权限,降低执行权限。因为 CLR 架构下的四级权限控制机制,最细的粒度只能到 AppDomain。好在据说 Whidbey 会加入对使用不同 evidence 载入 Assembly 的支持。
    
    通过这些讨论可以看到,Assembly.Unload 对于基于插件模型的程序来说,其语义的存在是很重要的。但在目前和近几个版本来说,通过 AppDomain 来模拟其语义是比较合适的选择,虽然要付出性能和易用性的问题,但能够更大程度上控制功能和安全性等方面因素。长远来说,Assembly.Unload 的实现是完全可行的,Java 中对类的卸载就是最好的例子,前面那些理由实际上都是工作量和复杂度方面的问题,并不存在无法解决的技术问题。

以下为引用:

// ObjectLoader.cs
using System;
using System.Reflection;
using System.Collections;

namespace Loader{

  /* contains assembly loader objects, stored in a hash
  * and keyed on the .dll file they represent. Each assembly loader
  * object can be referenced by the original name/path and is used to
  * load objects, returned as type Object. It is up to the calling class
  * to cast the object to the necessary type for consumption.
  * External interfaces are highly recommended!!
  * */
  public class ObjectLoader : IDisposable
  {
    // essentially creates a parallel-hash pair setup
    // one appDomain per loader
    protected Hashtable domains = new Hashtable();
    // one loader per assembly DLL
    protected Hashtable loaders = new Hashtable();

    public ObjectLoader() {/*...*/}

    public object GetObject( string dllName, string typeName, object[] constructorParms )
    {
      Loader.AssemblyLoader al = null;
      object o = null;

      try{
        al = (Loader.AssemblyLoader)loaders[ dllName ];
      } catch (Exception){}

      if( al == null )
      {
        AppDomainSetup setup = new AppDomainSetup();
        setup.ShadowCopyFiles = "true";

        AppDomain domain = AppDomain.CreateDomain( dllName, null, setup );

        domains.Add( dllName, domain );

        object[] parms = { dllName };
        // object[] parms = null;
        BindingFlags bindings = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;

        try{
          al = (Loader.AssemblyLoader)domain.CreateInstanceFromAndUnwrap(
            "Loader.dll", "Loader.AssemblyLoader", true, bindings, null, parms, null, null, null);
        } catch (Exception){
          throw new AssemblyLoadFailureException();
        }

        if( al != null )
        {
          if( !loaders.ContainsKey( dllName ) )
          {
            loaders.Add( dllName, al );
          }
          else
          {
            throw new AssemblyAlreadyLoadedException();
          }
        }
        else
        {
          throw new AssemblyNotLoadedException();
        }
      }

      if( al != null )
      {
        o = al.GetObject( typeName, constructorParms );

        if( o != null && o is AssemblyNotLoadedException )
        {
          throw new AssemblyNotLoadedException();
        }

        if( o == null || o is ObjectLoadFailureException )
        {
          string msg = "Object could not be loaded. Check that type name " + typeName +
            " and constructor parameters are correct. Ensure that type name " + typeName +
            " exists in the assembly " + dllName + ".";

          throw new ObjectLoadFailureException( msg );
        }
      }
      return o;
    }

    public void Unload( string dllName )
    {
      if( domains.ContainsKey( dllName ) )
      {
        AppDomain domain = (AppDomain)domains[ dllName ];
        AppDomain.Unload( domain );
        domains.Remove( dllName );
      }
    }

    ~ObjectLoader()
    {
      dispose( false );
    }

    public void Dispose()
    {
      dispose( true );
    }

    private void dispose( bool disposing )
    {
      if( disposing )
      {
        loaders.Clear();

        foreach( object o in domains.Keys )
        {
          string dllName = o.ToString();
          Unload( dllName );
        }
        domains.Clear();
      }
    }
  }
}


以下为引用:

// Loader.cs
using System;
using System.Reflection;

namespace Loader {
  // container for assembly and exposes a GetObject function
  // to create a late-bound object for casting by the consumer
  // this class is meant to be contained in a separate appDomain
  // controlled by ObjectLoader class to allow for proper encapsulation
  // which enables proper shadow-copying functionality.
  internal class AssemblyLoader : MarshalByRefObject, IDisposable {

    #region class-level declarations
    private Assembly a = null;
    #endregion

    #region constructors and destructors
    public AssemblyLoader( string fullPath )
    {
      if( a == null )
      {
        a = Assembly.LoadFrom( fullPath );
      }
    }

    ~AssemblyLoader()
    {
      dispose( false );
    }

    public void Dispose()
    {
      dispose( true );
    }

    private void dispose( bool disposing )
    {
      if( disposing )
      {
        a = null;
        System.GC.Collect();
        System.GC.WaitForPendingFinalizers();
        System.GC.Collect( 0 );
      }
    }
    #endregion

    #region public functionality
    public object GetObject( string typename, object[] ctorParms )
    {
      BindingFlags flags = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;

      object o = null
      ;
      if( a != null )
      {
        try
        {
          o = a.CreateInstance( typename, true, flags, null, ctorParms, null, null );
        }
        catch (Exception)
        {
          o = new ObjectLoadFailureException();
        }
      }
      else
      {
        o = new AssemblyNotLoadedException();
      }
      return o;
    }

    public object GetObject( string typename )
    {
      return GetObject( typename, null );
    }
    #endregion

  }
}


posted @ 2004-07-08 11:32 Flier Lu 阅读(1645) 评论(0) 编辑


http://www.blogcn.com/user8/flier_lu/index.html?id=2151342&run=.07A3756

 上周 MS 发布了最新的 C# 2.0 版本语言规范,其中一个很有趣的新增语法特性是 nullable types。通过这种语法,可以让一个普通内建类型的内容为空(NULL)。之所以新增这个类型,很大程度上应该是为了从语言一级对与关系型数据库的交互进行封装(估计八成这个 BT 的需求是 yukon 组提出来的,呵呵)
    与普通程序语言的内建类型不同,数据库的字段在有类型的同时,都可以单独指定是否能够为空。例如
以下为引用:

CREATE TABLE [dbo].[TestTable] (
[ID] [int] IDENTITY (1, 1) NOT NULL ,
[Name] [varchar] (20) NOT NULL ,
[Desc] [varchar] (255) NULL,
[Age] [int] NULL
}


    这样一段 TSQL 语法构造出来的表中,Desc 和 Age 字段可以存在一个空(NULL)的特殊状态。而在传统的 C++/Java 语言中,是无法通过内建类型直接表达这个语义的,而只能通过设计来模拟,如
以下为引用:

struct TestRecord
{
  int ID;
  std::string name;
  char *desc;
  int *age;

  TestRecord() : desc(NULL), age(NULL)
  {
  }

  ~TestRecord()
  {
    if(desc) delete[] desc;
    if(age) delete age;
  }

  // 其他实现函数
};

TestRecord rec = tblTest.GetRecord(...);

if(rec.desc) std::cout << rec.desc << std::endl;
if(rec.age) std::cout << *rec.age << std::endl;


    通过一个指向实际数据的指针,可以很容易模拟一个具有空语义的字段。但这种模拟需要太多薄记工作,如果要大规模使用,则需要做一个较为完整的包装模板类,如
以下为引用:

template <typename T>
class Nullable
{
private:
  T *m_value;
public:
  Nullable() : m_value(NULL)
  {
  }
  ~Nullable()
  {
    if(m_value) delete m_value;
  }
  bool hasValue(void) const { return m_value != NULL };
  operator T(void) const { return *m_value; }

  // 其他包装函数
};

// 需要针对内建类型做特化处理
template <typename T>
std::ostream& operator << (std::ostream& os, const Nullable<T>& value);

struct TestRecord
{
  int ID;
  std::string name;
  Nullable<std::string> desc;
  Nullable<int> age;
};

TestRecord rec = tblTest.GetRecord(...);

if(rec.desc.hasValue()) std::cout << rec.desc << std::endl;
if(rec.age.hasValue()) std::cout << rec.age << std::endl;


    而这种包装,对于不支持 C++ 如此灵活模板机制的 C#/Java 来说,要实现就更加复杂了。如 SourceForge 上有一个 C# 实现的 NullableTypes 项目,就是通过一个支持查询变量是否为空的 INullable 接口,和一系列底层支持类,完成类似的语义,如
以下为引用:

public sealed class TestRecord
{
  public int ID;
  public string name;
  public NullableString desc;
  public NullableInt32 age;
}

TestRecord rec = tblTest.GetRecord(...);

if(!rec.desc.IsNull) Console.WriteLn(rec.desc.Value);
if(!rec.age.IsNull) Console.WriteLn(rec.age.Value);


    语义上来说,这些模拟都是可行的,但语法上看起来实在太罗嗦了,而且也无法做到与现有类型系统的语法级无缝集成。因此 Anders Hejlsberg 决定增加倍受争议的 Nullable Types。这个语法要是搁 Java 里面,Java 社区非得为维护语法纯洁性闹翻天,呵呵,不过搁 MS 这儿则感觉是顺其自然,很能体现 MS 的实用化设计风格。毕竟 MS 系统的程序员大多已经习惯 MS 这样的修修补补。连 Java 和 C++ 都折腾出那么多所谓“增强”特性,自家的 C# 改改还用犹豫吗?呵呵
    既然 MS 在编译器一级提供了这种特性,语法上的简洁性肯定是能够很好得到保障的,如
以下为引用:

int? x = 123;
int? y = null;
if (x.HasValue) Console.WriteLine(x.Value);
if (y.HasValue) Console.WriteLine(y.Value);


    一个简单的 ? 号,表示此类型是一个 nullable type,例如 bool? 类型将有三种可能值,NULL, true 和 false,呵呵,传说中的三态变量啊 :P 而这一切实际上都是编译器做的障眼法,底层实现是通过一个泛型类型完成的,如
以下为引用:

struct Nullable<T>
{
    public bool HasValue;
    public T Value;
}


    前面那个例子实际上被编译器翻译成类似下面的伪代码
以下为引用:

Nullable<int> x = new Nullable<int>(125);
Nullable<int> y = Nullable<int>.NullValue;
Nullable<int> z =  (x.HasValue && y.HasValue) ? new Nullable<int>(x.Value + y.Value) : Nullable<int>.NullValue;


    比较幸福的是 MS 在编译器一级内建提供了 nullable 类型和普通内建类型之间的隐式转换,如
以下为引用:

int i = 123;
int? x = i;   // int --> int?
double? y = x; // int? --> double?
int? z = (int?)y; // double? --> int?
int j = (int)z;   // int? --> int


    因此在语法上这种 nullable 类型能够很轻松的融入现有 C# 语法中去,而将薄记工作交给编译器完成。

    但新增这种语法的代价,是必须要了解一些对于 nullable 的值进行计算的新规则,如
以下为引用:

int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x + y;


    对这种情况来说,x 和 y 都有可能为空,因此 z 只有在 x 和 y 都不为空的时候,才能计算得到非空值。也就是说 int? z = x + y 等价于
以下为引用:

int? z = x.HasValue && y.HasValue ? x.Value + y.Value : (int?)null;


    仔细考虑一下,实际上这种规定是非常有道理的。因为 null 这个值是一个非常特殊的值,他不是一个有效范围内的值,也不能等价于 0 或者长度为 0 的字符串,是一种特殊的存在。将一个数字与 null 相加是没有任何意义的,只能返回 null。故而上述规则成立。推广开来,对下面这种情况
以下为引用:

int? x = GetNullableInt();
int? y = x + 1;


    只有 x 非空的时候,y 才会计算得到一个非空值,否则都是 null。

    null 值带来的令人困惑的另外一个问题是 ==, !=, <, >, <=, >= 等比较操作符如何对各种组合进行操作?C# FAQ 里面有一篇文章解释了为什么比较操作符需要返回一个二值的 bool 而非三值的 bool?。

    Why don't nullable relational operators return bool? instead of bool? 

    如果要返回一个 bool? 则比较行为应该在一个三值 bool 的世界里面进行,而在这样一个世界里面,任何东西与 null 比较都会返回一个 null,也就是说 null 不等于任何东西。这就造成类似下面的语法无法发挥身处二值 bool 世界我们的预期语义
以下为引用:

void Process(int? p)
{
    if (p == null)
    {
        // do some processing...
    }
}


    必须将比较操作改成 if (!p.HasValue) 才能在三值 bool? 的世界里面正常运转,呵呵。因此设计者决定将比较操作直接返回二值 bool,使我们能够使用对引用类型进行比较相同的语法和语义对 nullable 类型进行比较。

    此外为了简化对 nullable 类型的操作,C# 2.0 还提供了一个新的 null coalescing operator (??) 运算符,用于比较值是否为空,为空则返回缺省值。使用方法如下:
以下为引用:

int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x ?? y;
int i = z ?? -1;


    通过这个运算符,可以很容易实现缺省值的语义,如
以下为引用:

string? s = GetStringValue();
Console.WriteLine(s ?? "Unspecified");


    可以看到 C# 做了相当多的工作来辅助 nullable 类型的简化使用。

    有兴趣进一步了解的朋友可以参考 C# 2.0 版本语言规范 以及下面一些讨论文章:

    Eric Gunnerson's Nullable types in C#

    TheServerSide's 3 Month Anniversary: An Auspicious Beginning

    李建忠的 Updated C# V2.0 Specifications,Nullable Types(空属类型),编程语言杂谈

btw: 上个月刚刚调了项目组,折腾一个月总算理清了大部分头绪,开始恢复更新 blog 了,呵呵

posted @ 2004-07-08 11:31 Flier Lu 阅读(933) 评论(0) 编辑


http://www.blogcn.com/user8/flier_lu/index.html?id=2042872&run=.03463D0

在上一节中简单介绍了 CLR 调试器的框架结构,其中提到 CLR 调试环境同时支持 Native 和 Managed 两种模式的调试事件。这一节将从整体上对调试事件做一个概括性的介绍。

     首先看看 CLR 通过 ICorDebugManagedCallback 回调接口提供的 Managed 调试事件。这部分的调试事件可以大致分为被动调试事件和主动调试事件:前者由 CLR 在调试程序时自动引发被动调试事件,如创建一个新的线程;后者由调试器通过 CLR 的其他调试接口,控制 CLR 调试环境完成某种调试任务,并在适当的时候引发主动调试事件,如断点和表达式计算。

     就被动调试事件来说,基本上对应于 CLR 载入运行程序的若干个步骤

     首先是动态环境的建立,分为进程、AppDomain和线程三级,并分别有对应的建立和退出调试事件:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT CreateProcess([in] ICorDebugProcess *pProcess);
  HRESULT ExitProcess([in] ICorDebugProcess *pProcess);

  HRESULT CreateAppDomain([in] ICorDebugProcess *pProcess,
                    [in] ICorDebugAppDomain *pAppDomain);
  HRESULT ExitAppDomain([in] ICorDebugProcess *pProcess,
                   [in] ICorDebugAppDomain *pAppDomain);

  HRESULT CreateThread([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugThread *thread);
  HRESULT ExitThread([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugThread *thread);

   HRESULT NameChange([in] ICorDebugAppDomain *pAppDomain,
         [in] ICorDebugThread *pThread);
   //...
 };
 



     在 CLR 的实现上,实际上是存在有物理上的 Native Thread 和逻辑上的 Managed Thread 两个概念的。进程和 Native Thread 对应着操作系统提供的相关概念,而 AppDomain 和 Managed Thread 则对应着 CLR 内部的相关抽象。上面的线程相关调试事件,实际上是 Native Thread 第一次以 Managed Thread 身份执行 Managed Code 的时候被引发的。更完整的控制需要借助后面要提及的 Native Thread 的调试事件。
     此外 AppDomain 和 Managed Thread 在创建并开始运行后,都会根据情况改名,并调用 NameChange 调试事件,让调试器有机会更新界面显示上的相关信息。

     其次是静态 Metadata 的载入和解析工作,也分为Assembly, Module和Class三级,并分别有对应的建立和退出调试事件:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT LoadAssembly([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugAssembly *pAssembly);
  HRESULT UnloadAssembly([in] ICorDebugAppDomain *pAppDomain,
                    [in] ICorDebugAssembly *pAssembly);

  HRESULT LoadModule([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugModule *pModule);
  HRESULT UnloadModule([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugModule *pModule);

  HRESULT LoadClass([in] ICorDebugAppDomain *pAppDomain,
                [in] ICorDebugClass *c);
  HRESULT UnloadClass([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugClass *c);
   //...
 };
 



     在 CLR 中,Assembly 很大程度上是一个逻辑上的聚合体,真正落实到实现上的更多的是其 Module。一个 Assembly 在载入时,可以只是保护相关 Manifest 和 Metadata,真正的代码和数据完全可以存放在不同地点的多个 Module 中。因此,在 Managed 调试事件中,明确分离了 Assembly 和 Module 的生命周期。

     然后就是对 IL 代码中特殊指令和功能的支持用调试事件:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT Break([in] ICorDebugAppDomain *pAppDomain,
             [in] ICorDebugThread *thread);

  HRESULT Exception([in] ICorDebugAppDomain *pAppDomain,
                [in] ICorDebugThread *pThread,
                [in] BOOL unhandled);

  HRESULT DebuggerError([in] ICorDebugProcess *pProcess,
                         [in] HRESULT errorHR,
                         [in] DWORD errorCode);

   HRESULT LogMessage([in] ICorDebugAppDomain *pAppDomain,
                      [in] ICorDebugThread *pThread,
                 [in] LONG lLevel,
                 [in] WCHAR *pLogSwitchName,
                 [in] WCHAR *pMessage);

  HRESULT LogSwitch([in] ICorDebugAppDomain *pAppDomain,
                     [in] ICorDebugThread *pThread,
                [in] LONG lLevel,
                [in] ULONG ulReason,
                [in] WCHAR *pLogSwitchName,
                [in] WCHAR *pParentName);

   HRESULT ControlCTrap([in] ICorDebugProcess *pProcess);

  HRESULT UpdateModuleSymbols([in] ICorDebugAppDomain *pAppDomain,
                               [in] ICorDebugModule *pModule,
                               [in] IStream *pSymbolStream);
   //...
 };
 



     Break 事件在执行 IL 指令 Break 时被引发,可被用于实现特殊的断点等功能;
     Exception 事件在代码抛出异常时,以及异常未被处理时被引发,类似于 Win32 Debug API 中的异常事件。后面介绍调试器中对异常的处理方法时再详细介绍;
     DebuggerError 事件则是在调试系统处理 Win32 调试事件发生错误时被引发;
     LogMessage 和 LogSwitch 事件分别用于处理内部类 System.Diagnostics.Log 的相关功能,类似于 Win32 API 下 OutputDebugString 函数的功能,等有机会再单独写篇文章介绍相关内容;
     ControlCTrap 事件响应用户使用 Ctrl+C 热键直接中断程序,等同于 Win32 API 下 SetConsoleCtrlHandler 函数的功能;
     UpdateModuleSymbols 事件在系统更新某个模块调试符号库的时候被引发,使调试器有机会同步状态。

     最后还省下几个主动调试事件,在调试器调用 CLR 调试接口相关功能被完成或异常时引发:
 

以下为引用:

 interface ICorDebugManagedCallback : IUnknown
 {
   //...
  HRESULT Breakpoint([in] ICorDebugAppDomain *pAppDomain,
                 [in] ICorDebugThread *pThread,
                 [in] ICorDebugBreakpoint *pBreakpoint);
   HRESULT BreakpointSetError([in] ICorDebugAppDomain *pAppDomain,
                              [in] ICorDebugThread *pThread,
                              [in] ICorDebugBreakpoint *pBreakpoint,
                              [in] DWORD dwError);

  HRESULT StepComplete([in] ICorDebugAppDomain *pAppDomain,
                  [in] ICorDebugThread *pThread,
                  [in] ICorDebugStepper *pStepper,
                  [in] CorDebugStepReason reason);

  HRESULT EvalComplete([in] ICorDebugAppDomain *pAppDomain,
                        [in] ICorDebugThread *pThread,
                        [in] ICorDebugEval *pEval);
  HRESULT EvalException([in] ICorDebugAppDomain *pAppDomain,
                         [in] ICorDebugThread *pThread,
                         [in] ICorDebugEval *pEval);

   HRESULT EditAndContinueRemap([in] ICorDebugAppDomain *pAppDomain,
                                [in] ICorDebugThread *pThread,
                                [in] ICorDebugFunction *pFunction,
                                [in] BOOL fAccurate);
   //...
 };
 



     Breakpoint 和 BreakpointSetError 在断点被触发或设置断点失败时被调用,下一节介绍断点的实现时再详细讨论;
     StepComplete 则在调试环境因为某种原因完成了一次代码步进(step)时被调用,以后介绍单步跟踪等功能实现时再详细讨论;
     EvalComplete 和 EvalException 在表达式求值完成或失败时被调用,以后介绍调试环境当前信息获取时再详细讨论;
     EditAndContinueRemap 则用于实现调试时代码编辑功能,暂不涉及。

     下面是一个比较直观的实例,显示一个简单的 CLR 调试环境在运行一个普通 CLR 程序除非相关调试事件的顺序
 

以下为引用:

 ManagedEventHandler.CreateProcess(3636)
 ManagedEventHandler.CreateAppDomain(DefaultDomain @ 3636)

 ManagedEventHandler.LoadAssembly(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ DefaultDomain)
 ManagedEventHandler.LoadModule(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ DefaultDomain)

 ManagedEventHandler.NameChange(AppDomain=cordbg)

 ManagedEventHandler.CreateThread(3944 @ cordbg)

 ManagedEventHandler.LoadAssembly(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg)
 ManagedEventHandler.LoadModule(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg)

 ManagedEventHandler.NameChange(AppDomain=cordbg.exe)

 ManagedEventHandler.LoadAssembly(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
 ManagedEventHandler.LoadModule(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)

 ManagedEventHandler.CreateThread(2964 @ cordbg.exe)

 ManagedEventHandler.UnloadModule(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg.exe)
 ManagedEventHandler.UnloadAssembly(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg.exe)

 ManagedEventHandler.UnloadModule(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
 ManagedEventHandler.UnloadAssembly(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)

 ManagedEventHandler.UnloadModule(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ cordbg.exe)
 ManagedEventHandler.UnloadAssembly(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ cordbg.exe)

 ManagedEventHandler.ExitAppDomain(cordbg.exe @ 3636)
 ManagedEventHandler.ExitThread(3944 @ cordbg.exe)
 ManagedEventHandler.ExitProcess(3636)
 



     可以看到 CLR 首先构造进程和 AppDomain;然后将系统执行所需的 mscorlib.dll 载入;接着将要执行的 Assembly 和缺省 Module 载入;并分析其外部应用(system.dll),载入之;建立一个新的 Managed Thread 执行之;最后卸载相关 Module 和 Assembly,并退出环境。

     在打印调试事件信息时值得注意的是很多调试接口都提供了类似的函数从 Unmanaged 环境中获取字符串或整数,如
 

以下为引用:

 interface ICorDebugAppDomain : ICorDebugController
 {
  HRESULT GetName([in] ULONG32 cchName,
                   [out] ULONG32 *pcchName,
                   [out, size_is(cchName),
                   length_is(*pcchName)] WCHAR szName[]);
 };

 interface ICorDebugAssembly : IUnknown
 {
  HRESULT GetName([in] ULONG32 cchName,
                   [out] ULONG32 *pcchName,
                   [out, size_is(cchName),
                   length_is(*pcchName)] WCHAR szName[]);
 };
 



     因此在实现上可以将之抽象为一个 delegate,以便共享基于尝试策略的数据获取算法,如
 
以下为引用:

 public class CorObject
 {
   protected delegate void GetStrFunc(uint cchName, out uint pcchName, IntPtr szName);

   protected string GetString(GetStrFunc func, uint bufSize)
   {
     uint size = bufSize;

     IntPtr szName = Marshal.AllocHGlobal((int)size);

     func(size, out size, szName);

     if(size > bufSize)
     {
       szName = Marshal.ReAllocHGlobal(szName, new IntPtr(size));

       func(size, out size, szName);
     }

     string name = Marshal.PtrToStringUni(szName, (int)size-1);

     Marshal.FreeHGlobal(szName);

     return name;
   }

   protected string GetString(GetStrFunc func)
   {
     return GetString(func, 256);
   }
 }
 



     这里使用 Marshal 对 Native 内存的直接操作,避免编写 unsafe 代码。使用的时候可以很简单地使用
 
以下为引用:

 public class CorAssembly : CorObject
 {
   private ICorDebugAssembly _asm;

  public CorAssembly(ICorDebugAssembly asm)
  {
     _asm = asm;
  }

   public string Name
   {
     get
     {
       return GetString(new GetStrFunc(_asm.GetName));
     }
   }
 }
 



     等到 CLR 2.0 支持泛型编程后,实现将更加方便。 :P

     这一小节,从整体上大致分析了 Managed 调试事件的分类和相关功能。具体的使用将在以后的文章中结合实际情况有针对性的介绍。至于 Win32 API 调试事件,介绍的资料就比较多了,这里就不在罗嗦,有兴趣进一步研究的朋友可以参考我以前的一个系列文章。

     Win32 调试接口设计与实现浅析 [2] 调试事件


     下一节将介绍 CLR 调试接口中断点如何实现和使用。

 to be continue...

posted @ 2004-07-08 11:31 Flier Lu 阅读(445) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1957096&run=.0E0327A

关于 NT 环境下用户态直接读写端口这码子事,本应该是95-96年 NT 架构刚刚出来时讨论的东西,现在翻出来炒现饭,实在是不得已的事情。因为前几天有朋友问起 TSS 中 IOPM 表的问题,而网上这方面的可用文章大多只是泛泛而谈,空有实现方法没有原理分析,没办法直接引用。而这些文章追述其源头基本上都是从 Dale Roberts 在 96 年 5 月发表在 Dr. Dobb's Journal 上的 Direct Port I/O and Windows NT 一文转述而来,可惜这篇文章要会员权限才能看,我等没有美刀的只能自己动手丰衣足食了。
     至于基于此原理的应用文章,网上随处可见。如有人对其做了个简单的封装 PortTalk,就足以满足大部分需求:

     PortTalk - A Windows NT I/O Port Device Driver
     PortTalk - 用于Windows NT/2000的端口驱动程序(翻译)

     以下将就其实现原理结合 NT 源码进行分析,了解相关功能可以怎样实现,以及为什么要这样实现。

     与 DOS 和 Win9x 环境不同,NT 的用户态程序是在一个严格受限的环境下运行,因此一些特殊资源如端口的访问就不能直接暴露给用户,避免发生冲突或对系统稳定性造成影响。如我们所熟知的端口操作指令 IN/OUT 等就被归为特权指令,在用户态程序调用可能会引发异常。

 

     这一限制在实现上是通过两层机制完成的:EFLAGS 标志寄存器中的 IOPL (I/O privilege level) 标志位和 TSS (Task State Segment ) 中的 IOPM (I/O permission bit map) 提供了灵活的两级控制机制。(Intel Architecture Sofware Developer's Manual V1: 12.5)
     我们知道 x86 架构下特权一般分为 0-3 四环,而 NT 环境用到的只是核心态 ring 0 和用户态 ring 3。EFLAGS 标志寄存器通过 IOPL 标志指定当前 Task 中哪些特权级别可以使用 I/O 指令。这个标志由 EFLAGS 寄存器的第 12/13 位保存,能够使用 I/O 指令的特权级别必须小于等于 IOPL 的当前值。而此标志位一般设置为 0,并只能通过 POPF 和 IRET 指令在 ring 0 进行修改(ring 3下修改不会发生异常,但没有效果)。这就保障内核能够完全限定用户态程序不能直接使用 I/O 等特权指令,但又能够通过马上要讨论的 IOPM 网开一面。(受到 IOPL 约束的 I/O sensitive 指令包括:IN, INS, OUT, OUTS, CLI, STI)
     如果当前特权级 CPL (Current Privilege Level) 大于 IOPL,则系统会进一步根据 TSS 中的 IOPM 判断是否特例允许对此端口的访问。TSS 是每任务相关的状态存储区,保存了状态 (Context) 切换所需的基本信息,如通用寄存器(EAX, ESP等等)、段选择子(CS, DS等等)、EFLAGS、EIP等可能动态改变的内容,还包括 CR3, LDT, IOPM 等静态内容。Intel 手册上定义 TSS 的最小长度为 104 字节,末尾的一个 WORD 就是 IOPM 相对于 TSS 的偏移。而操作系统一般来说对 TSS 都做了一定程度的定制,如 NT 架构下 TSS 结构(ntosinci386.h:879)大致如下:
 

以下为引用:

 typedef struct _KTSS
 {
   USHORT  Backlink;

   //...

   USHORT  Flags;

   USHORT  IoMapBase;

   KIIO_ACCESS_MAP IoMaps[IOPM_COUNT];

   KINT_DIRECTION_MAP IntDirectionMap;
 } KTSS, *PKTSS;
 



     因为当前任务的 TSS 有一个单独的 TR (Task Register) 寄存器(Intel Architecture Sofware Developer's Manual V3: 6.2.3)保存其16位段选择子和32位基址偏移,故而系统对I/O指令的处理伪代码可以表述如下:
 
以下为引用:

 typedef struct {
  unsigned limit : 16;
  unsigned baselo : 16;
  unsigned basemid : 8;
  unsigned type : 4;
  unsigned system : 1;
  unsigned dpl : 2;
  unsigned present : 1;
  unsigned limithi : 4;
  unsigned available : 1;
  unsigned zero : 1;
  unsigned size : 1;
  unsigned granularity : 1;
  unsigned basehi : 8;
 } GDTENT;

 typedef struct {
  unsigned short limit;
  GDTENT *base;
 } GDTREG;

 bool CheckIOPermission(WORD port)
 {
   if(CPL <= EFLAGS.IOPL) return true;

   GDTREG GdtReg;
   WORD TaskSeg;

   _asm cli;         // 禁止中断
   _asm sgdt GdtReg; // 获取 GDT 地址
   _asm str TaskSeg; // 获取 TSS 选择子索引

   GDTENT *pTaskGdt = GdtReg.base + (TaskSeg >> 3); // 获取 TSS 描述符地址

   KTSS *pTSS = (PVOID)(pTaskGdt->baselo | (pTaskGdt->basemid << 16) | (pTaskGdt->basehi << 24)); // 计算 TSS 基址

   char *pIOPM = ((char *)pTSS + pTSS->IoMapBase); // 计算 IOPM 基址

   size_t pos = port >> 3, idx = port & 0xF;

   if((pIOPM + pos) > (pTSS + TaskGdt->limit))
   {
     throw GeneralProtectionException();
   }

   _asm sti;

   return (pIOPM[pos] & (1 << idx)) == (1 << idx);
 }
 



     首先系统检测当前特权级别 CPL 是否小于 EFLAGS 的 IOPL;然后从 TR 寄存器中获取 TSS 选择子索引,并计算得到 TSS 描述符地址;通过 TSS 的基址和 IOPM 偏移可以得到 IOPM 地址;最后根据端口查询 IOPM 内容,判断是否允许对此端口进行操作。
     我们可以使用 windbg + livekd 工具实际看看一个系统中的相关情况:
 
以下为引用:

 // 显示 PCR
 kd> !pcr
 KPCR for Processor 0 at ffdff000:
     Major 1 Minor 1
  NtTib.ExceptionList: f460fbfc
      NtTib.StackBase: 00000000
     NtTib.StackLimit: 00000000
   NtTib.SubSystemTib: 80042000
        NtTib.Version: 2568915f
    NtTib.UserPointer: 00000001
        NtTib.SelfTib: 7ffdd000

              SelfPcr: ffdff000
                 Prcb: ffdff120
                 Irql: 00000000
                  IRR: 00000000
                  IDR: ffffffff
        InterruptMode: 00000000
                  IDT: 8003f400
                  GDT: 8003f000
                  TSS: 80042000

        CurrentThread: 826a7788
           NextThread: 00000000
           IdleThread: 80569280

            DpcQueue:

 // 显示 TSS
 kd> dd 80042000
 80042000  eb3d76f6 f460fde0 0d8b0010 00441f30
 80042010  0674c085 24f8448b 048b03eb 33026a0b
 80042020  e85051c9 fffffd6c 1474c085 50413881
 80042030  08744349 04c38347 c972fe3b 0272fe3b
 80042040  5e5fc033 8b55c35b 8b5151ec 008b0845
 80042050  4453523d f8458954 30a10a75 e900441f

 80042060  00000000 20ac0000 18000004 00000018 // IOPM 偏移, KTSS.IoMapBase

 80042070  00000000 00000000 00000000 00000000
 80042080  00000000 00000000 ffffffff ffffffff // TSS 内置 IOPM, KTSS.IoMaps[0]
 80042090  ffffffff ffffffff ffffffff ffffffff
 ...
 80044080  ffffffff ffffffff ffffffff 18000004
 80044090  00000018 00000000 00000000 00000000
 800440a0  00000000 00000000 00000000 cbb70fd9
 800440b0  75ff5051 fc4d890c 0009e6e8 06896600
 



     可以看到 TSS 的内容保存在 0x80042000 处;其 0x64 偏移内容 0x20ac 是当前 IOPM 的偏移;而 0x88 偏移处的一堆 0xFFFFFFFF 是 KTSS.IoMaps[0] 的内容,此 IOPM 表内容等会再详细解析;而 0x20ac 处正是实际使用的 IOPM 内容。

     基于此原理,Dale Roberts 提出了几种实现允许用户模式访问端口的方法,归根结底都是对 TSS 的 IOPM 偏移和内容做文章。

     有篇文章很详细的讨论了这个尝试过程,可惜是俄文的。找了几个翻译软件,最后发现还是 www.freetranslation.com提供的在线翻译比较好使,先翻译成英文再看,呵呵。

     此外一些文章也使用到相同原理,如 《NT下所有RING 3进程任意端口I/O》 一文。值得注意的是这里原文选择将 TSS 长度限制增加 0xF00,实际上限制了能够自由访问端口必须是小于 0xF00 * 8 = 30720。使用这种方法时,应该考虑到这种硬性的限制。而 0xF00 的限制,是为了保证对 TSS 长度的扩展,不会导致页错误。因为原有 TSS 长度一般是 0x20ab,在增加 0xF00 后不会导致跨页问题。TotalIO.c中对此问题描述如下:
 

以下为引用:

   Since we can safely extend the TSS only to the end of the physical memory page in which it lies, the I/O access is granted only up to port 0xf00.  Accesses beyond this port address will still generate exceptions.
 


     在实际环境中查看 TSS 的 GDT 表项方法如下(0x28 >> 3 = 5):
 
以下为引用:

 kd> rm 0x100
 Last set context
 kd> r
 Last set context:
 gdtr=8003f000   gdtl=03ff idtr=8003f400   idtl=07ff tr=0028  ldtr=0000

 kd> dd 8003f000
 8003f000  00000000 00000000 0000ffff 00cf9b00
 8003f010  0000ffff 00cf9300 0000ffff 00cffb00
 8003f020  0000ffff 00cff300 200020ab 80008b04 // TSS Limit
 8003f030  f0000001 ffc093df d0000fff 7f40f3fd
 



     TotalIO.c 中设置 TSS 长度限制的完整代码如下:
 
以下为引用:

 void SetTSSLimit(int size)
 {
  GDTREG gdtreg;
  GDTENT *g;
  short TaskSeg;

  _asm cli;       // don't get interrupted!
  _asm sgdt gdtreg;     // get GDT address
  _asm str TaskSeg;     // get TSS selector index
  g = gdtreg.base + (TaskSeg >> 3); // get ptr to TSS descriptor
  g->limit = size;     // modify TSS segment limit
 //
 //  MUST set selector type field to 9, to indicate the task is
 // NOT BUSY.  Otherwise the LTR instruction causes a fault.
 //
  g->type = 9;      // mark TSS as "not busy"
 //
 //  We must do a load of the Task register, else the processor
 // never sees the new TSS selector limit.
 //
  _asm ltr TaskSeg;     // reload task register (TR)
  _asm sti;       // let interrupts continue
 }
 



     这里把 TSS 的 type 设置为 9,表示此描述符类型为32位TSS非Busy描述符(Intel Architecture Sofware Developer's Manual V3: 3.5)。
     这种方法通过直接操作系统寄存器相关内容达到对系统任意进程受限端口的允许访问,但并不是一个完美的解决方案。相对来说通过操作系统未公开函数 Ke386SetIoAccessMap, Ke386QueryIoAccessMap 和 Ke386IoSetAccessProcess 实现独立进程的特殊端口允许访问的方法更加优雅一些。下面我们来仔细看看这几个函数的原理和使用。
     通过前面对 NT 系统中 KTSS 结构和实际内存的分析,我们可以了解:NT 环境下,每个进程单独维护了一个 TSS 内存区域,其中由 TSS 内部维护了一个全部标志位置 1 的 IOPM 表,在 TSS 末尾还维护了另外一个实际中承担端口管理工作的 IOPM 表。Ke386SetIoAccessMap 函数(ntoskei386iopm.c:80)和 Ke386QueryIoAccessMap 函数(ntoskei386iopm.c:235)就是系统提供用来读写这两个 IOPM 表的函数。而 Ke386IoSetAccessProcess 函数(ntoskei386iopm.c:318)则指定进程到底使用哪个 IOPM 表。
 
以下为引用:

 BOOLEAN Ke386QueryIoAccessMap(ULONG MapNumber, PKIO_ACCESS_MAP IoAccessMap);
 BOOLEAN Ke386SetIoAccessMap(ULONG MapNumber, PKIO_ACCESS_MAP IoAccessMap);

 BOOLEAN Ke386IoSetAccessProcess(PKPROCESS Process, ULONG MapNumber);
 



     对前两个函数来说,MapNumber指定要对哪个表进行操作。系统定义了一个 IO_ACCESS_MAP_NONE = 0 常量表示在 TSS 后面那个真实 IOPM 表,而其他的索引对应于 KTSS.IoMaps[] 数组。此数组大多数情况下只有一个表项,也就是说 MapNumber 为 0 时表示 TSS 后面那个 IOPM;为 1 时表示 TSS 内部的 KTSS.IoMaps[0]。
     Ke386QueryIoAccessMap 函数只是简单的根据 MapNumber 判断是将 IoAccessMap 内容全部置位(MapNumber = 0)、还是从 TSS 中复制对应的表 (0 < MapNumber <= IOPM_COUNT = 1)。伪代码如下:
 
以下为引用:

 #define IOPM_COUNT          1
 #define IOPM_SIZE           8192    // Size of map callers can set.

 BOOLEAN Ke386QueryIoAccessMap(ULONG MapNumber, PKIO_ACCESS_MAP IoAccessMap)
 {
   if(MapNumber > IOPM_COUNT) return FALSE;

   if(MapNumber == IO_ACCESS_MAP_NONE)
   {
     memset(IoAccessMap, -1, IOPM_SIZE);
   }
   else
   {
     void *pIOPM = &(KiPcr()->TSS->IoMaps[MapNumber-1].IoMap);

     memcpy(IoAccessMap, pIOPM, IOPM_SIZE);
   }
   return TRUE;
 }
 



     而 Ke386SetIoAccessMap 在 MapNumber 为 0 时直接返回 FALSE,因为 TSS 后的那个表是不允许修改的;对其他情况,函数将 IoAccessMap 中的内容复制回 TSS 的 IOPM 表中,并在多处理器情况下通知其他处理器重新载入 IOPM 表。伪代码如下:
 
以下为引用:

 BOOLEAN Ke386SetIoAccessMap(ULONG MapNumber, PKIO_ACCESS_MAP IoAccessMap)
 {
   if((MapNumber > IOPM_COUNT) || (MapNumber == IO_ACCESS_MAP_NONE)) return FALSE;

   void *pIOPM = &(KiPcr()->TSS->IoMaps[MapNumber-1].IoMap);

   memcpy(pIOPM, IoAccessMap, IOPM_SIZE);

   KiPcr()->TSS->IoMapBase = GetCurrentProcess()->IopmOffset;

   // 通知其他处理器重设 IOPM

   return TRUE;
 }
 



     Ke386IoSetAccessProcess 函数则简单地修改当前 TSS 的 IOPM 偏移为 MapNumber 指定的 IOPM 表偏移,并在多 CPU 情况下通知其他 CPU 重新载入 IOPM 偏移。计算偏移算法如下:
 
以下为引用:

 #define KiComputeIopmOffset(MapNumber)          
     (MapNumber == IO_ACCESS_MAP_NONE) ?         
         (USHORT)(sizeof(KTSS)) :                    
         (USHORT)(FIELD_OFFSET(KTSS, IoMaps[MapNumber-1].IoMap))

 USHORT MapOffset = KiComputeIopmOffset(MapNumber);
 



     完整的使用流程代码如下:
 
以下为引用:

 #define IOPM_SIZE           8192    // Size of map callers can set.

 typedef UCHAR   KIO_ACCESS_MAP[IOPM_SIZE];
 typedef KIO_ACCESS_MAP *PKIO_ACCESS_MAP;

 PKIO_ACCESS_MAP IOPM_local = MmAllocateNonCachedMemory(sizeof(IOPM));
 if(IOPM_local == 0)
  return STATUS_INSUFFICIENT_RESOURCES;

 Ke386QueryIoAccessMap(1, IOPM_local);

 // 修改 IOPM_Local 内容打开需要使用的端口

 Ke386SetIoAccessMap(1, IOPM_local);
 Ke386IoSetAccessProcess(PsGetCurrentProcess(), 1);
 



     具体代码可以参考 PortTalk 和 TotalIO 的源码,这里就不在罗嗦了。

posted @ 2004-07-08 11:28 Flier Lu 阅读(1110) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1934417&run=.09DD601

前两天有位朋友问到在 .NET 里面调用 Java 类方法并返回值的方法,刚好手头工作可能会需要用 JNI 来封装现有系统,于是晚上写了个调用例子,顺手对 JNI 的基本调用做了一个简单的封装。因为 JNI 的接口设计上主要面向 C++ 语言,因此决定将 JNI 的封装放在一个独立的 DLL 中进行,然后在 .NET 中通过 Interop 再做一层封装,反正这种需求下效率也不是首要考虑因素。
     JNI 的原理和基本使用方法这里就不多说了,现成的资料太多了,有兴趣的朋友可以参考 sun 的 JNI 文档以及示例。此外 Sheng Liang 的 Java Native Interface: Programmer's Guide and Specification 一书中对 JNI 做了较为全面的解析。
     JNI 的封装其实现成的有很多,如 sourceforge.net 上的 Jace 等; Java 和 .NET 互操作的封装也有一些,如 JVM-CLR Bridge 等。但一方面自己的需求与他们不太一样,另一方面也希望通过自行开发增加对这方面知识的了解,故而选择自己重新发明轮子,呵呵,权且当作是一种娱乐吧 :P

 [1] 配置并构造 JVM

     JVM 的配置与构造与 CLR 的非常类似(CLR 的相关使用方法可以参考《.Net平台下CLR程序载入原理分析 [草稿]》一文 )。用户程序通过 JRE 的 jvm.dll 提供的接口,构造并配置虚拟机,调用相应的类的方法,完成实际工作。从 JDK 1.2 开始,JRE 提供了 client/server 两个版本的 jvm.dll,一般可以在 %JRE%inclient 和 %JRE%inserver 目录下找到,和 CLR 中的 wks/srv 类似。jvm.dll 提供了 JNI_CreateJavaVM 函数用于构造虚拟机,对应于 CLR 中 mscoree.dll 提供的 CorBindToRuntimeEx 函数。此函数的定义可以在 %JDK%includejni.h 头文件里面找到,静态链接可以通过 %JDK%libjvm.lib 库文件完成,动态连接则可以根据选用 JRE 的虚拟机类型从相应 jvm.dll 中使用 GetProcAddress 函数获得入口。
 

以下为引用:

 _JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);
 


     pvm 参数返回虚拟机的对象指针,penv 参数返回当前线程执行环境的对象指针,args 参数传入虚拟机构造时使用的配置参数。
     一般来说,每个进程同时只能启动一个 JVM 实例,这在很多 JVM 实现中是硬编码在虚拟机构造代码中。因此使用者应该通过 singleton 模式保障全局 JVM 实例的唯一性,或者通过 JNI_GetCreatedJavaVMs 函数获取已经构造的虚拟机对象指针。而 JavaVM::DestroyJavaVM 方法,大多数情况下是不起作用的,只是需要在程序结束是礼节性的调用一下,不要指望通过它真正卸载 JVM,否则再次调用 JNI_CreateJavaVM 函数时可能会发生错误。一个简单的封装如下:
 
以下为引用:

 namespace JBridge
 {
  public class JStub
  {
     [DllImport("JvmStub.dll")]
     public static extern IntPtr CreateJVM();

     [DllImport("JvmStub.dll")]
     public static extern void DestroyJVM(IntPtr vm);
   }
   
   public class JVM 
   {
     static private IntPtr _vm = IntPtr.Zero;    

     private JVM()
     {
     }

     ~JVM()
     {
       if(_vm != IntPtr.Zero)
       {
         JStub.DestroyJVM(_vm);
         _vm = IntPtr.Zero;
       }      
     }

     public static IntPtr getInstance()
     {
       if(_vm == IntPtr.Zero)
       {
         lock(typeof(JVM))
         {
           if(_vm == IntPtr.Zero)
           {
             _vm = JStub.CreateJVM();
           }
         }
       }
       return _vm;
     }
   }
 }
 


    
     而在一个创建了 JVM 的进程中,并不是所有线程都能够直接使用 JVM 的,需要通过 JavaVM::AttachCurrentThread 方法将当前线程挂接到 JVM 环境上,并获取当前线程执行环境的对象指针 JNIEnv *pEnv;在无需处理 JVM 调用时,应该调用 JavaVM::DetachCurrentThread 方法析构 JVM 为当前线程准备的数据;而 JNI_CreateJavaVM 函数在构造 JVM 时缺省返回调用线程的执行环境对象指针。
 
以下为引用:

 struct JavaVM_
 {
   jint DestroyJavaVM()
   {
     // ...
   }
   jint AttachCurrentThread(void **penv, void *args)
   {
     // ...
   }
   jint DetachCurrentThread()
   {
     // ...
   }
 }
 


     最后的 args 参数是配置虚拟机以何种方式启动。
 
以下为引用:

 #define JNI_VERSION_1_1 0x00010001
 #define JNI_VERSION_1_2 0x00010002
 #define JNI_VERSION_1_4 0x00010004

 typedef struct JavaVMInitArgs {
   jint version;
   jint nOptions;
   JavaVMOption *options;
   jboolean ignoreUnrecognized;
 } JavaVMInitArgs;
 



     字段 version 指定虚拟机使用的兼容版本,如 jni.h 定义的 JNI_VERSION_1_2;
     字段 nOptions 和 options 指定虚拟机使用哪些配置。
 
以下为引用:

 typedef struct JavaVMOption {
   char *optionString;
   void *extraInfo;
 } JavaVMOption;
 


     配置信息以字符串形式保存在 JavaVMOption::optionString 中,由 JVM 进行具体解析。常用的配置有诸如显示 JNI 调试信息(-verbose:jni);对 JNI 调用进行额外检查(-Xcheck:jni);以及指定 Java 类(-Djava.class.path=???)、JNI 本地库(-Djava.library.path=???)、初始化类库(-Xbootclasspath:???)等等路径。
     一个实际的配置和构造 JVM 示例代码如下:
 
以下为引用:

 JavaVMOption opts[5];

 opts[0].optionString = "-Djava.class.path=F:\Study\Java\JNI\Prompt";
 opts[1].optionString = "-Djava.library.path=F:\Study\Java\JNI\Prompt";
 opts[2].optionString = "-verbose:jni";
 opts[3].optionString = "-Xcheck:jni";
 opts[4].optionString = "-Xbootclasspath:E:\Borland\JBuilderX\jdk1.4\jre\lib\rt.jar";

 JavaVMInitArgs args;

 args.version = JNI_VERSION_1_2;
 args.options = opts;
 args.nOptions = sizeof(opts) / sizeof(opts[0]);
 args.ignoreUnrecognized = JNI_TRUE;

 Check(JNI_CreateJavaVM(&m_jvm, reinterpret_cast<void **>(static_cast<JNIEnv **>(m_env)), &args));
 


posted @ 2004-07-08 11:28 Flier Lu 阅读(2028) 评论(3) 编辑


http://www.blogcn.com/user8/flier_lu/index.html?id=1894812&run=.0AA0EFB

 如 Don Box 在《.NET本质论 第1卷:公共语言运行库》一书的第10章中介绍, CLR 调试框架是一个由 CLR 提供的,面向工具开发商的,支持调试功能的最小功能集。与 JVM 的 JDI (Java Debug Interface)不同,CLR 调试框架不仅仅关注于虚拟机一级的调试,同时也提供了 Native 一级调试的统一接口。使得现有工具开发商能够以最小代价移植并支持 CLR 调试功能。而对 CLR 调试更高层次或更细粒度的支持,则是由前面提到的 Profiling API 完成。
     CLR 调试接口主要通过 mscordbi.dll 提供的 ICorDebug 接口,让调试器通过进程内或进程外方式,对被调试 CLR 进行监控。而 ICorDebug 接口可以通过 .NET Framework SDK 中 includecordebug.idl 或 includecordebug.h 直接使用。对 C#/Delphi 也可以直接 reference/import 在 SDK 的 lib 目录下的 cordebug.tlb 类型库,获得调用包装类。下面示例将都使用 C# 作为描述语言。
     在使用时,可以直接获取ICorDebug接口,并调用其Initialize/Terminate方法进行初始化和析构操作,框架代码如下:
 
以下为引用:

 using CORDBLib;

 namespace cordbg
 {
  public class Debugger : IDisposable
  {
    private ICorDebug _dbg;

    public void Run()
     {
       _dbg = new CorDebugClass();

       try
       {
         _dbg.Initialize();

         // 构造调试环境

         // 处理调试事件
       }
       finally
       {
         _dbg.Terminate();
       }
     }
     ...
   }
  [MTAThread]
  static void Main(string[] args)
  {
    using(Debugger dbg = new Debugger())
    {
      dbg.Run();
    }
  }
 }
 



     注意 CLR 调试环境必须在 MTA 的线程套间上下文(Thread Apartment Context)中运行,因此必须将入口函数的 STAThread 属性改成 MTAThread,否则会在调试接口调用回调函数时出现异常。对应于 COM 中的 CoInitializeEx(NULL, COINIT_MULTITHREADED) 调用。
     在创建了 ICorDebug 调试接口后,需要针对托管和非托管调试事件,提供调试事件回调接口。可以将实现了调试事件接口 ICorDebugManagedCallback/ICorDebugUnmanagedCallback 的实例,使用 ICorDebug 接口的 SetManagedHandler/SetUnmanagedHandler 方法,挂接到调试系统上,在适当的时候由调试系统回调,通知调试器有调试事件发生。实现上可以通过 ManagedEventHandler/UnmanagedEventHandler 两个单独的类,抽象出对托管和非托管调试事件的处理机制,将之挂接到调试器上,如:
 
以下为引用:

 namespace cordbg
 {
  public class DebugEventHandler
  {
     protected Debugger _dbg;

   public DebugEventHandler(Debugger dbg)
   {
       this._dbg = dbg;
   }
  }

  public class ManagedEventHandler : DebugEventHandler, ICorDebugManagedCallback
  {
     public ManagedEventHandler(Debugger dbg) : base(dbg)
     {
     }

     // 实现 ICorDebugManagedCallback 接口
   }

  public class UnmanagedEventHandler : DebugEventHandler, ICorDebugUnmanagedCallback
  {
     public UnmanagedEventHandler(Debugger dbg) : base(dbg)
     {
     }

     // 实现 ICorDebugUnmanagedCallback 接口
   }

  public class Debugger : IDisposable
  {
    public void Run()
     {
       //...

       _dbg.SetManagedHandler(new ManagedEventHandler(this));
       _dbg.SetUnmanagedHandler(new UnmanagedEventHandler(this));

       //...
     }
   }
 }
 



     在准备好了调试事件处理器后,就可以根据需要,创建或者附加到目标调试进程上。ICorDebug 提供了 CreateProcess 方法对 Win32 API 中 CreateProcess 函数进行了包装。
 
以下为引用:

 public abstract interface ICorDebug
 {
   public abstract new void CreateProcess (
     string lpApplicationName,
     string lpCommandLine,
     _SECURITY_ATTRIBUTES lpProcessAttributes,
     _SECURITY_ATTRIBUTES lpThreadAttributes,
     int bInheritHandles,
     uint dwCreationFlags,
     IntPtr lpEnvironment,
     System.String lpCurrentDirectory,
     uint lpStartupInfo,
     uint lpProcessInformation,
     CorDebugCreateProcessFlags debuggingFlags,
     ICorDebugProcess ppProcess)
 }

 BOOL CreateProcess(
   LPCTSTR lpApplicationName,
   LPTSTR lpCommandLine,
   LPSECURITY_ATTRIBUTES lpProcessAttributes,
   LPSECURITY_ATTRIBUTES lpThreadAttributes,
   BOOL bInheritHandles,
   DWORD dwCreationFlags,
   LPVOID lpEnvironment,
   LPCTSTR lpCurrentDirectory,
   LPSTARTUPINFO lpStartupInfo,
   LPPROCESS_INFORMATION lpProcessInformation
 );
 



     可以看到这两个函数的参数基本上是一一对应的,只不过ICorDebug.CreateProcess函数多了一个输入debuggingFlags参数指定调试标志和一个输出ppProcess参数返回创建进程的控制接口。
     两个 _SECURITY_ATTRIBUTES 类型的安全属性,一般来说可以设置为空,使用缺省设置。
 
以下为引用:

 _SECURITY_ATTRIBUTES sa = new _SECURITY_ATTRIBUTES();

 sa.nLength = (uint)Marshal.SizeOf(sa);
 sa.bInheritHandle = Win32.BOOL.FALSE;
 sa.lpSecurityDescriptor = IntPtr.Zero;
 



     值得注意的是 dwCreationFlags 指定了创建进程是否支持 Native 模式的调试,也就是前面 SetUnmanagedHandler 方法调用的接口是否起作用。可以根据情况如命令行选项决定是否支持 Native 调试模式,如
 
以下为引用:

 namespace Win32
 {
   public struct CreationFlag
   {
     public const uint DEBUG_PROCESS               = 0x00000001;
     public const uint DEBUG_ONLY_THIS_PROCESS     = 0x00000002;

     public const uint CREATE_SUSPENDED            = 0x00000004;

     public const uint DETACHED_PROCESS            = 0x00000008;

     public const uint CREATE_NEW_CONSOLE          = 0x00000010;

     public const uint NORMAL_PRIORITY_CLASS       = 0x00000020;
     public const uint IDLE_PRIORITY_CLASS         = 0x00000040;
     public const uint HIGH_PRIORITY_CLASS         = 0x00000080;
     public const uint REALTIME_PRIORITY_CLASS     = 0x00000100;

     public const uint CREATE_NEW_PROCESS_GROUP    = 0x00000200;
     public const uint CREATE_UNICODE_ENVIRONMENT  = 0x00000400;

     public const uint CREATE_SEPARATE_WOW_VDM     = 0x00000800;
     public const uint CREATE_SHARED_WOW_VDM       = 0x00001000;
     public const uint CREATE_FORCEDOS             = 0x00002000;

     public const uint BELOW_NORMAL_PRIORITY_CLASS = 0x00004000;
     public const uint ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000;

     public const uint CREATE_BREAKAWAY_FROM_JOB   = 0x01000000;
   }
 }

 namespace cordbg
 {
  public class Debugger : IDisposable
  {
    private void Run()
     {
       //...

       uint dwCreationFlag = CreationFlag.CREATE_NEW_CONSOLE;

       if(Options.NativeMode)
       {
         dwCreationFlag |= CreationFlag.DEBUG_ONLY_THIS_PROCESS | CreationFlag.DEBUG_PROCESS;
       }

       //...
     }
   }
 }
 



     比较麻烦的是指定启动参数的 lpStartupInfo 参数和返回进程信息的 lpProcessInformation 参数。C# 在导入 cordebug.tlb 类型库时,都没有处理这两个类型,必须自己定义之:
 
以下为引用:

 [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
 public struct STARTUPINFO
 {
   public uint    cb;
   public string  lpReserved;
   public string  lpDesktop;
   public string  lpTitle;
   public uint    dwX;
   public uint    dwY;
   public uint    dwXSize;
   public uint    dwYSize;
   public uint    dwXCountChars;
   public uint    dwYCountChars;
   public uint    dwFillAttribute;
   public uint    dwFlags;
   public ushort  wShowWindow;
   public ushort  cbReserved2;
   public IntPtr  lpReserved2;
   public IntPtr  hStdInput;
   public IntPtr  hStdOutput;
   public IntPtr  hStdError;
 }

 [StructLayout(LayoutKind.Sequential)]
 public struct PROCESS_INFORMATION
 {
   public IntPtr hProcess;
   public IntPtr hThread;
   public uint   dwProcessId;
   public uint   dwThreadId;
 }
 



     使用的时候则需要先在堆栈中构造此结构的值类型对象,然后通过 unsafe 形式指针,或者 Marshal 手工处理将之转换为地址。这里为了避免使用较为 dirty 的 unsafe 方式,通过 Marshal.AllocHGlobal 分配全局内存;然后调用 Marshal.StructureToPtr 将结构复制到内存中;调用 CreateProcess 时使用此内存的地址;调用返回后使用 Marshal.PtrToStructure 从内存中获得结构的内容;最后调用 Marshal.FreeHGlobal 释放全局内存。简要代码如下:
 
以下为引用:

 //...

 STARTUPINFO si = new STARTUPINFO(); // 构造时所有字段已清零

 si.cb = (uint)Marshal.SizeOf(si);

 PROCESS_INFORMATION pi = new PROCESS_INFORMATION();

 IntPtr ppi = Marshal.AllocHGlobal(Marshal.SizeOf(pi)),
        psi = Marshal.AllocHGlobal(Marshal.SizeOf(si));

 Marshal.StructureToPtr(si, psi, true);
 Marshal.StructureToPtr(pi, ppi, true);

 _dbg.CreateProcess(Options.FileInfo.FullName, Options.CommandLine,
   ref sa, ref sa, BOOL.FALSE, dwCreationFlag, IntPtr.Zero,
   Options.CurrentDirectory, (uint)psi.ToInt32(), (uint)ppi.ToInt32(),
   CorDebugCreateProcessFlags.DEBUG_NO_SPECIAL_OPTIONS, out _proc);

 pi = (PROCESS_INFORMATION)Marshal.PtrToStructure(ppi, typeof(PROCESS_INFORMATION));

 Marshal.FreeHGlobal(ppi);
 Marshal.FreeHGlobal(psi);

 Native.CloseHandle(pi.hProcess);

 //...
 



     而将调试器附加到现有进程上则相对简单得多,接口方法如下:
 
以下为引用:

 public abstract interface ICorDebug
 {
   public abstract new void DebugActiveProcess(uint id, int win32Attach, ICorDebugProcess ppProcess)
 }

 BOOL DebugActiveProcess(
   DWORD dwProcessId
 );
 



     与 Win32 API 的 DebugActiveProcess 相比,ICorDebug.DebugActiveProcess 增加的 win32Attach 指定是否允许 Native 模式调试,ppProcess 返回目标调试进程的控制接口。

     以上简要介绍了 CLR 调试接口在使用时如何构造调试环境,以及对调试目标进程的创建和附加的方法。下一节将整体上对托管和非托管的各种调试事件做一个介绍,然后再针对不同的调试功能开始详细介绍。

 btw: 五一回去玩了一周,心都散了,呵呵。回来后公司又一直在装修,拖到今天总算痛定思痛开始恢复 BLog 更新了 :P

posted @ 2004-07-08 11:27 Flier Lu 阅读(492) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1819166&run=.08F07F8

折腾 CLR 调试接口也有几周了,可是在将 C++ 代码移植到 C# 时总是有问题,直到刚刚才灵机一动有所突破,呵呵,总算可以开始写这个系列文章了。:D
     虽然需要实际用到 CLR 调试接口的人可能非常少,但通过对 CLR 调试接口和 Profiler 接口的了解,可以从多个截面加深对 CLR 架构的认识,而且灵活使用这两个接口可以开发出非常强大的辅助开发工具来。:D
     目前关于 CLR 调试接口方面的资料比较少,较为详细的除了.NET Framework SDK自带的两个文档(Tool Developers Guidedocs目录下的Debug.doc和DebugRef.doc)和一个例子(Tool Developers GuideSamplesdebugger)外,就只有 MSDN 杂志的上两篇文章,以及 Jon Shute 的一个 Debuggers under .NET 系列文章了。

     .NET Framework SDK 自带的 Debug.doc 文档从整体结构上介绍了 CLR 调试接口的架构;DebugRef.doc 则详细地介绍了具体的使用方法,可惜其中纰漏不少,大概是最终发布特性没有跟文档同步吧,呵呵。而 samples 目录下的那个例子,则是详尽地演示了大部分调试接口功能的使用,极具参考价值。

     MSDN 杂志2002年第11期中 Mike Pellegrino 的 Improve Your Understanding of .NET Internals by Building a Debugger for Managed Code 一文可以说是目前最为全面的调试接口介绍文章了,而且给出了详细的例子。而2001年第12期中 Matt Pietrek 的 Under the Hood 专栏文章 The .NET Profiling API and the DNProfiler Tool 虽然介绍的是 .NET Profiling API,但与调试接口环环相扣、相辅相成,是理解 CLR 架构的必读文章。

     Jon Shute老兄是 SharpDevelop 项目 CLR 调试器部分的负责人,据他说已经有一个包装好的C#版本调试器接口了,呵呵,值得期待啊。:D

     Debuggers under .NET part 1
     Debuggers under .NET part 2
     Debugging under .NET part 3

     上面提到的 SharpDevelop 项目提供了一个完全开源的 C# 开发环境,虽然目前还不太成熟,但也已初具规模了。和 ASP.NET 的 Web Matrix 都是非常优秀的 .NET 开发环境免费项目。清华出版社翻译的 《C#软件项目开发全程剖析——全面透视SharpDevelop软件的开发内幕》 一书则较为全面地介绍了 SharpDevelop 的设计与开发流程。虽然没有很深入的技术内容,但对程序的设计和开发周期有很全面的介绍,对初涉 .NET 架构程序设计和开发的朋友有相当的参考价值。

     

posted @ 2004-07-08 11:25 Flier Lu 阅读(472) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1745407&run=.0F766FA

再回头看前面那个 C# 代码的例子,在 JIT 完成之后:
 
以下为引用:

 .method private hidebysig static void  Main(string[] args) cil managed
 // SIG: 00 01 01 1D 0E
 {
   .entrypoint
   .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
   // Method begins at RVA 0x2120
   // Code size       47 (0x2f)
   .maxstack  1
   .locals init ([0] class flier.Base b,
            [1] class flier.Base d,
            [2] class flier.IFoo i)
   IL_0000:  /* 73   | (06)000007       */ newobj     instance void flier.Base::.ctor()
   IL_0005:  /* 0A   |                  */ stloc.0
   IL_0006:  /* 73   | (06)00000B       */ newobj     instance void flier.Derived::.ctor()
   IL_000b:  /* 0B   |                  */ stloc.1
   IL_000c:  /* 06   |                  */ ldloc.0
   IL_000d:  /* 6F   | (06)000003       */ callvirt   instance void flier.Base::CallFromObjBase()
   IL_0012:  /* 07   |                  */ ldloc.1
   IL_0013:  /* 6F   | (06)000003       */ callvirt   instance void flier.Base::CallFromObjBase()
   IL_0018:  /* 07   |                  */ ldloc.1
   IL_0019:  /* 6F   | (06)000004       */ callvirt   instance void flier.Base::CallFromObjDerived()
   IL_001e:  /* 06   |                  */ ldloc.0
   IL_001f:  /* 0C   |                  */ stloc.2
   IL_0020:  /* 08   |                  */ ldloc.2
   IL_0021:  /* 6F   | (06)000001       */ callvirt   instance void flier.IFoo::CallFromIntfBase()
   IL_0026:  /* 07   |                  */ ldloc.1
   IL_0027:  /* 0C   |                  */ stloc.2
   IL_0028:  /* 08   |                  */ ldloc.2
   IL_0029:  /* 6F   | (06)000002       */ callvirt   instance void flier.IFoo::CallFromIntfDerived()
   IL_002e:  /* 2A   |                  */ ret
 } // end of method EntryPoint::Main

 0:000> !ip2md 06d900a7
 MethodDesc: 0x00975070
 Jitted by normal JIT
 Method Name : [DEFAULT] Void flier.EntryPoint.Main(SZArray String)
 MethodTable 975088
 Module: 167d98
 mdToken: 0600000c (D:TempCallItCallItinDebugCallIt.exe)
 Flags : 10
 Method VA : 06d90058

 0:000> u 06d90058
 06d90058 55               push    ebp
 06d90059 8bec             mov     ebp,esp
 06d9005b 83ec10           sub     esp,0x10
 06d9005e 57               push    edi
 06d9005f 56               push    esi
 06d90060 53               push    ebx
 06d90061 894dfc           mov     [ebp-0x4],ecx
 06d90064 c745f800000000   mov     dword ptr [ebp-0x8],0x0
 06d9006b 33f6             xor     esi,esi
 06d9006d 33ff             xor     edi,edi

 // newobj     instance void flier.Base::.ctor()
 06d9006f b9d8519700       mov     ecx,0x9751d8          // 类 flier.Base 的方法表
 06d90074 e89f1fbdf9       call    00962018
 06d90079 8bd8             mov     ebx,eax
 06d9007b 8bcb             mov     ecx,ebx
 06d9007d ff1520529700     call    dword ptr [00975220]  // call flier.Base::.ctor()
 06d90083 895df8           mov     [ebp-0x8],ebx         // stloc.0

 // newobj     instance void flier.Derived::.ctor()
 06d90086 b988529700       mov     ecx,0x975288          // 类 flier.Derived 的方法表
 06d9008b e8881fbdf9       call    00962018
 06d90090 8bd8             mov     ebx,eax
 06d90092 8bcb             mov     ecx,ebx
 06d90094 ff15d8529700     call    dword ptr [009752d8]  // call flier.Derived::.ctor()
 06d9009a 8bf3             mov     esi,ebx               // stloc.1

 06d9009c 8b4df8           mov     ecx,[ebp-0x8]         // ldloc.0
 06d9009f 3909             cmp     [ecx],ecx
 06d900a1 ff151c529700     call    dword ptr [0097521c]  // callvirt   instance void flier.Base::CallFromObjBase()

 06d900a7 8bce             mov     ecx,esi               // ldloc.1
 06d900a9 3909             cmp     [ecx],ecx
 06d900ab ff151c529700     call    dword ptr [0097521c]  // callvirt   instance void flier.Base::CallFromObjBase()

 06d900b1 8bce             mov     ecx,esi               // ldloc.1
 06d900b3 8b01             mov     eax,[ecx]
 06d900b5 ff5038           call    dword ptr [eax+0x38]  // callvirt   instance void flier.Base::CallFromObjDerived()

 06d900b8 8b7df8           mov     edi,[ebp-0x8]         // ldloc.0
 06d900bb 8bcf             mov     ecx,edi               // stloc.2
 06d900bd 8b01             mov     eax,[ecx]
 06d900bf 8b400c           mov     eax,[eax+0xc]
 06d900c2 8b402c           mov     eax,[eax+0x2c]
 06d900c5 ff10             call    dword ptr [eax]       // callvirt   instance void flier.IFoo::CallFromIntfBase()

 06d900c7 8bfe             mov     edi,esi               // ldloc.1
 06d900c9 8bcf             mov     ecx,edi               // stloc.2
 06d900cb 8b01             mov     eax,[ecx]
 06d900cd 8b400c           mov     eax,[eax+0xc]
 06d900d0 8b402c           mov     eax,[eax+0x2c]
 06d900d3 ff5004           call    dword ptr [eax+0x4]   // callvirt   instance void flier.IFoo::CallFromIntfDerived()

 06d900d6 90               nop
 06d900d7 5b               pop     ebx
 06d900d8 5e               pop     esi
 06d900d9 5f               pop     edi
 06d900da 8be5             mov     esp,ebp
 06d900dc 5d               pop     ebp
 06d900dd c3               ret
 


     除了刚刚分析过的 call 和对虚函数的 callvirt 指令外,这里又多出一种对接口虚函数进行调用的操作。
 

以下为引用:

 06d900bb 8bcf             mov     ecx,edi               // stloc.2
 06d900bd 8b01             mov     eax,[ecx]             // 载入对象地址指向对象结构头部(04aa1b4c)字段指向的类型信息地址
 06d900bf 8b400c           mov     eax,[eax+0xc]         // 载入全局接口偏移量表基址
 06d900c2 8b402c           mov     eax,[eax+0x2c]        // 获取 IFoo 接口映射表偏移量
 06d900c5 ff10             call    dword ptr [eax]       // callvirt   instance void flier.IFoo::CallFromIntfBase()
 


     使用 WinDbg 动态跟踪到上述指令处
 
以下为引用:

 0:000> !dumpstackobjects
 ESP/REG  Object   Name
 ebx      04aa1b74 flier.Derived
 ecx      04aa2804 System.IO.TextWriter/SyncTextWriter
 esi      04aa1b74 flier.Derived
 edi      04aa1b68 flier.Base
 0012f6a0 04aa1b68 flier.Base
 0012f6a4 04aa1b4c System.Object[]
 0012f6d8 04aa1b4c System.Object[]
 0012f928 04aa1b4c System.Object[]
 0012f92c 04aa1b4c System.Object[]
 


     edi 指向 flier.Base 类型的对象实例(0x04aa1b68)
 
以下为引用:

 0:000> !dumpobj 04aa1b68
 Name: flier.Base
 MethodTable 0x009751d8
 EEClass 0x06c6334c
 Size 12(0xc) bytes
 mdToken: 02000003  (D:TempCallItCallItinDebugCallIt.exe)

 0:000> dd 04aa1b68
 04aa1b68  009751d8 00000000 00000000 00975288
 04aa1b78  00000000 80000000 79b7daf8 00000015
 



     而此对象的偏移 0 处保存着此对象的类型信息地址(0x009751d8)
 
以下为引用:

 0:000> !dumpmt 009751d8
 EEClass : 06c6334c
 Module : 00167d98
 Name: flier.Base
 mdToken: 02000003  (D:TempCallItCallItinDebugCallIt.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 1
 Interface Map : 00975228
 Slots in VTable : 9

 0:000> dd 009751d8
 009751d8  00080000 0000000c 06c6334c 0097bff0
 009751e8  00120001 00167d98 0008ffff 00975228
 


     类型信息的 0xC 偏移处是全局接口偏移量表的入口基址 (0x0097bff0)
 

以下为引用:

 0:000> dd 0097bff0
 0097bff0  ???????? ???????? ???????? ????????
 0097c000  00000000 0097c000 00004000 00000000
 0097c010  00000000 000003e8 00000001 00975214
 0097c020  009752cc 00000000 00000000 00000000
 


     而 IFoo 接口的物理地址就在此偏移量表的 0x2C 偏移处(0x00975214)。这个地址是直接指向 flier.Base 类的虚方法表。
 
以下为引用:

 0:000> !dumpmt -md 009751d8
 EEClass : 06c6334c
 Module : 00167d98
 Name: flier.Base
 mdToken: 02000003  (D:TempCallItCallItinDebugCallIt.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 1
 Interface Map : 00975228
 Slots in VTable : 9
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 79b7c4eb 79b7c4f0    None   [DEFAULT] [hasThis] String System.Object.ToString()
 79b7c473 79b7c478    None   [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
 79b7c48b 79b7c490    None   [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
 79b7c52b 79b7c530    None   [DEFAULT] [hasThis] Void System.Object.Finalize()
 0097519b 009751a0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromObjDerived()
 009751ab 009751b0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase()
 009751bb 009751c0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromIntfDerived()
 0097518b 00975190    None   [DEFAULT] [hasThis] Void flier.Base.CallFromObjBase()
 009751cb 009751d0    None   [DEFAULT] [hasThis] Void flier.Base..ctor()

 0:000> dd 009751d8
 009751d8  00080000 0000000c 06c6334c 0097bff0
 009751e8  00120001 00167d98 0008ffff 00975228
 009751f8  00000000 00000009 79b7c4eb 79b7c473
 00975208  79b7c48b 79b7c52b 0097519b 009751ab
 00975218  009751bb 0097518b 009751cb 00000000
 00975228  00975138 00050001 00000000 00000000
 00975238  00975288 00000000 00000003 00000000
 00975248  e8000008 ff7d9110 00000009 c00020c4
 


     0x0097519b 就是最后 flier.Base.CallFromObjDerived() 函数的入口地址。因此对于接口进行调用的 callvirt 指令,实际上是遵循以下的 dispatch 路线完成调用的:

     ObjectPtr -> Object -> Class -> Global Interface Map Table -> Class Method Table

     具体的结构图请参考《本质论》167面的图 (6.5 - 0.1), -_-b

     至此,CLR 中最常见的三种函数调用方式就大致分析完毕,以后有机会在继续分析其他的如jmp、间接调用和 tail call等方式的实现。

 btw: BLogCN限制一篇帖子只能12K实在太过分了,害得我一篇文章拆成三盘发 :( 
         这也就算了,居然还说我的文章里面有敏感词语,真是 $#@%$#       

posted @ 2004-07-08 11:24 Flier Lu 阅读(530) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1745373&run=.00B7C29

在了解了方法表的物理结构后,我们接着分析方法的动态调用机制。

     从方法的调用类型来分,CLR支持直接调用、间接调用和很少见的 tail call 模式。

     直接调用最为常见,又可分为使用虚方法表的 callvirt 指令和不使用虚方法表的 call 和 jmp 指令。
     间接调用稍微少见,通过 ldftn/calli 和 ldvirtftn/calli 两组指令,从栈中获取方法描述 (Method Desc),语义上等同于 call/callvirt 指令。
     tail call 调用更为少见,类似于 jmp,但是作为前缀指令附加在 call/calli/callvirt 指令上的。

     下面我们对最常见的直接调用方式做一个简单的分析,首先看看一个例子程序 Virt_not.il:
 

以下为引用:

 .assembly extern mscorlib { }
 .assembly virt_not { }
 .module virt_not.exe

 .class public A
 {
  .method public specialname void .ctor() { ret }
  .method public void Foo()
  {
   ldstr "A::Foo"
   call void [mscorlib]System.Console::WriteLine(string)
   ret
  }
  .method public virtual void Bar()
  {
   ldstr "A::Bar"
   call void [mscorlib]System.Console::WriteLine(string)
   ret
  }
  .method public virtual void Baz()
  {
   ldstr "A::Baz"
   call void [mscorlib]System.Console::WriteLine(string)
   ret
  }
 }

 .class public B extends A
 {
  .method public specialname void .ctor() { ret }
  .method public void Foo()
  {
   ldstr "B::Foo"
   call void [mscorlib]System.Console::WriteLine(string)
   ret
  }
  .method public virtual void Bar()
  {
   ldstr "B::Bar"
   call void [mscorlib]System.Console::WriteLine(string)
   ret
  }
  .method public virtual newslot void Baz()
  {
   ldstr "B::Baz"
   call void [mscorlib]System.Console::WriteLine(string)
   ret
  }
 }

 .method public static void Exec()
 {
  .entrypoint
  newobj instance void B::.ctor() // create instance of derived class
  castclass class A  // cast it to base class

  dup    // we need 3 instance pointers
  dup    // on stack for 3 calls

  call instance void A::Foo()
  callvirt instance void A::Bar()
  callvirt instance void A::Baz()

  ret
 }
 



     上述代码是使用 IL 汇编直接编写,其 Exec 函数将被编译成 IL 代码如下:
 
以下为引用:

 .method public static void  Exec() cil managed
 // SIG: 00 00 01
 {
   .entrypoint
   // Method begins at RVA 0x209c
   // Code size       28 (0x1c)
   .maxstack  8
   IL_0000:  /* 73   | (06)000006       */ newobj     instance void B::.ctor()
   IL_0005:  /* 74   | (1B)000001       */ castclass  class A
   IL_000a:  /* 25   |                  */ dup
   IL_000b:  /* 25   |                  */ dup
   IL_000c:  /* 28   | (06)000003       */ call       instance void A::Foo()
   IL_0011:  /* 6F   | (06)000004       */ callvirt   instance void A::Bar()
   IL_0016:  /* 6F   | (06)000005       */ callvirt   instance void A::Baz()
   IL_001b:  /* 2A   |                  */ ret
 } // end of method 'Global Functions'::Exec
 


     可以看到直接调用时 call 和 callvirt 指令,都是以方法的 Token 为参数的。但不同之处在于实现上,call指令使用类型的方法表,而 callvirt 使用对象的方法表。
     在 WinDbg 载入 Virt_not.exe 后,可以在 Exec 被 JIT 编译后,使用 !ip2md 命令查看其方法描述信息,如
 
以下为引用:

 0:000> g; !clrstack
 Breakpoint 0 hit
 Thread 0
 ESP       EIP
 0012f694  791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void A.Foo()
 0012f6a4  06d90088 [DEFAULT] Void Exec()
 0012f9b0  791da717 [FRAME: GCFrame]
 0012fa94  791da717 [FRAME: GCFrame]

 0:000> !ip2md 06d90088
 MethodDesc: 0x00975070
 Jitted by normal JIT
 Method Name : [DEFAULT] Void Exec()
 MethodTable 975078
 Module: 15cd20
 mdToken: 06000001 (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 Flags : 10
 Method VA : 06d90058
 



     反汇编 Exec 方法的代码如下:
 
以下为引用:

 0:000> u 06d90058
 06d90058 55               push    ebp
 06d90059 8bec             mov     ebp,esp

 // newobj instance void B::.ctor()
 06d9005b 56               push    esi
 06d9005c b9a8519700       mov     ecx,0x9751a8 // 类 B 的方法表地址
 06d90061 e8b21fbdf9       call    00962018
 06d90066 8bf0             mov     esi,eax

 06d90068 8bce             mov     ecx,esi
 06d9006a ff15ec519700     call    dword ptr [009751ec]

 // castclass class A
 06d90070 8bd6             mov     edx,esi
 06d90072 b900519700       mov     ecx,0x975100 // 类 A 的方法表地址
 06d90077 e8a00b4672       call    mscorwks!JIT_ChkCastClass (791f0c1c)

 06d9007c 8bf0             mov     esi,eax      // 对象地址
 06d9007e 90               nop
 06d9007f 90               nop

 // call       instance void A::Foo()
 06d90080 8bce             mov     ecx,esi
 06d90082 ff1544519700     call    dword ptr [00975144]

 // callvirt   instance void A::Bar()
 06d90088 8bce             mov     ecx,esi
 06d9008a 8b01             mov     eax,[ecx]
 06d9008c ff5038           call    dword ptr [eax+0x38]

 // callvirt   instance void A::Baz()
 06d9008f 8bce             mov     ecx,esi
 06d90091 8b01             mov     eax,[ecx]
 06d90093 ff503c           call    dword ptr [eax+0x3c]

 06d90096 90               nop
 06d90097 5e               pop     esi
 06d90098 5d               pop     ebp
 06d90099 c3               ret
 



     可以看到 call 指令是通过一个绝对地址的间接寻址调用函数的,此调用指向代码如下:
 
以下为引用:

 0:000> dd 00975144
 00975144  009750d3 00000000 00000000 00000000

 0:000> u 009750d3
 009750d3 e808857dff       call    0014d5e0

 0:000> u 0014d5e0
 0014d5e0 52               push    edx
 0014d5e1 68f0301b79       push    0x791b30f0
 0014d5e6 55               push    ebp
 0014d5e7 53               push    ebx
 0014d5e8 56               push    esi
 0014d5e9 57               push    edi
 0014d5ea 8d742410         lea     esi,[esp+0x10]
 0014d5ee 51               push    ecx
 0014d5ef 52               push    edx
 0014d5f0 648b1d2c0e0000   mov     ebx,fs:[00000e2c]
 0014d5f7 8b7b08           mov     edi,[ebx+0x8]
 0014d5fa 897e04           mov     [esi+0x4],edi
 0014d5fd 897308           mov     [ebx+0x8],esi
 0014d600 56               push    esi
 0014d601 e844940879       call    mscorwks!PreStubWorker (791d6a4a)
 0014d606 897b08           mov     [ebx+0x8],edi
 



     呵呵,这不正是上次分析的调用JIT的包装代码吗?

     在进行了 JIT 之后,上面的 Exec 代码调用 A::Foo 方法体被JIT修改为:
 

以下为引用:

 0:000> dd 975144
 00975144  009750d3 00000000 00000000 00000000

 0:000> u 009750d3
 009750d3 e9f8af4106       jmp     06d900d0

 0:000> !ip2md 06d900d0
 MethodDesc: 0x009750d8
 Jitted by normal JIT
 Method Name : [DEFAULT] [hasThis] Void A.Foo()
 MethodTable 975100
 Module: 15cd20
 mdToken: 06000003 (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 Flags : 0
 Method VA : 06d900d0
 



     也就是说 call 指令实际上是直接对 JIT 后的 A::Foo 方法体的代码进行了调用。

     而 callvirt 指令则使用两段的间接寻址来调用方法。
 

以下为引用:

 // callvirt   instance void A::Bar()
 06d90088 8bce             mov     ecx,esi
 06d9008a 8b01             mov     eax,[ecx]
 06d9008c ff5038           call    dword ptr [eax+0x38]
 


     这里的 esi 是指向对象的指针,而对象结构的第一个 DWORD 保存指向实际类型方法表的指针,也就是《本质论》中所说的 RuntimeTypeHandle (具体分析请参看我以前的一篇文章《Type, RuntimeType and RuntimeTypeHandle 》)。而方法表的 0x38 偏移处内容如下:
 
以下为引用:

 0:000> !dumpmt -md 00975100
 EEClass : 06c63344
 Module : 0015cd20
 Name: A
 mdToken: 02000002  (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 0
 Interface Map : 0097514c
 Slots in VTable : 8
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 79b7c4eb 79b7c4f0    None   [DEFAULT] [hasThis] String System.Object.ToString()
 79b7c473 79b7c478    None   [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
 79b7c48b 79b7c490    None   [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
 79b7c52b 79b7c530    None   [DEFAULT] [hasThis] Void System.Object.Finalize()
 009750e3 009750e8    None   [DEFAULT] [hasThis] Void A.Bar()
 009750f3 009750f8    None   [DEFAULT] [hasThis] Void A.Baz()
 009750c3 009750c8    None   [DEFAULT] [hasThis] Void A..ctor()
 009750d3 009750d8    None   [DEFAULT] [hasThis] Void A.Foo()

 0:000> dd 00975100
 00975100  00080000 0000000c 06c63344 00000000
 00975110  00120000 0015cd20 0006ffff 0097514c
 00975120  00000000 00000008 79b7c4eb 79b7c473
 00975130  79b7c48b 79b7c52b 009750e3 009750f3
 00975140  009750c3 009750d3 00000000 00000000
 



     可以看到 00975100+0x38 正好是 A.Bar() 方法的入口地址
 
以下为引用:

 0:000> u 009750e3
 009750e3 e8f8847dff       call    0014d5e0

 0:000> u 14d5e0
 0014d5e0 52               push    edx
 ...
 0014d600 56               push    esi
 0014d601 e844940879       call    mscorwks!PreStubWorker (791d6a4a)
 0014d606 897b08           mov     [ebx+0x8],edi

 0:000> !dumpmd 009750e8
 Method Name : [DEFAULT] [hasThis] Void A.Bar()
 MethodTable 975100
 Module: 15cd20
 mdToken: 06000004 (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 Flags : 0
 IL RVA : 0000205e
 



     因此 callvirt 指令实际上是使用变量实际保存对象的类型的方法表在进行调用,也就是我们所说的虚函数语义。

posted @ 2004-07-08 11:22 Flier Lu 阅读(614) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1702100&run=.0A2F3E7

虽然也算是把 TCP/IP Illustrated 三卷大致翻看过一遍,但现在回想起来读的真是很不认真,往往只是对自己感兴趣的地方仔细研究,麻烦的地方点到为止。这次因为那个号称 TCP 原理上存在的漏洞,又涨了一些经验值,原来发送 SYN 包也是可以导致重置 TCP 连接的。
     具体漏洞描述可参考 Vulnerabilities in TCP,因为已经看了一些公司里老大写的内部文档,我就不好对其多做评论了,呵呵
     其原理大概是通过监听、猜测或尝试,向被攻击连接的一方机器发送一个序列号在已建立连接 TCP 窗口中的 SYN 包,导致接收端以为连接无效而重置连接,并向另一端发送 RST 包切断连接。

     这比传统的向两端发送多个 RST 包试图切断 TCP 连接的方式要优雅得多。因为 TCP 连接在 ESTABLISHED 状态时收到 RST 包后,直接清理队列并删除 TCB,连接进入 CLOSED 状态。
 

以下为引用:

 second check the RST bit,

 ...

   ESTABLISHED
   FIN-WAIT-1
   FIN-WAIT-2
   CLOSE-WAIT

     If the RST bit is set then, any outstanding RECEIVEs and SEND
     should receive "reset" responses.  All segment queues should be
     flushed.  Users should also receive an unsolicited general
     "connection reset" signal.  Enter the CLOSED state, delete the
     TCB, and return.
 



     如果切断不够及时(通常情况下如此),会导致接收到切断用 RST 包后,又在 CLOSED 状态收到数据(下图中第二行的 B)或对方发来的 RST 包(下图中第四行的 A),而这种异常情况可以被检测软件发现 :P
 
以下为引用:

               C     RST   --> B (ESTABLISHED)
 (ESTABLISHED) A     DATA  --> B (CLOSED)
 (ESTABLISHED) A <-- RST       C
      (CLOSED) A <-- RST       B (CLOSED)
 


     RFC 793 中对 CLOSED 状态的处理流程描述如下:
 
以下为引用:

 If the state is CLOSED (i.e., TCB does not exist) then

   all data in the incoming segment is discarded.  An incoming
   segment containing a RST is discarded.  An incoming segment not
   containing a RST causes a RST to be sent in response.  The
   acknowledgment and sequence field values are selected to make the
   reset sequence acceptable to the TCP that sent the offending
   segment.
 



     简单的说就是在 CLOSED 状态收到数据包则回应 RST 包(有的操作系统屏蔽了此功能);收到 RST 包直接丢弃。
     而通过 SYN 包则较难检测并且至少在一端不存在此问题(可以选择) :D

     对上述漏洞所处的 ESTABLISHED 状态处理 SYN 包的流程,RFC 793 中是如下描述的(rfc793.txt:4394):
 

以下为引用:

 fourth, check the SYN bit,

   SYN-RECEIVED
   ESTABLISHED STATE
   FIN-WAIT STATE-1
   FIN-WAIT STATE-2
   CLOSE-WAIT STATE
   CLOSING STATE
   LAST-ACK STATE
   TIME-WAIT STATE

     If the SYN is in the window it is an error, send a reset, any
     outstanding RECEIVEs and SEND should receive "reset" responses,
     all segment queues should be flushed, the user should also
     receive an unsolicited general "connection reset" signal, enter
     the CLOSED state, delete the TCB, and return.

     If the SYN is not in the window this step would not be reached
     and an ack would have been sent in the first step (sequence
     number check).
 



     如果在 ESTABLISHED 状态收到一个在窗口内的 SYN 包则是一个错误,需要发送 RST 包响应并且清空所有队列。而用户将收到一个主动的“连接已重置”的信号,并进入 CLOSED 状态,TCB 被删除后返回。

     翻阅了一下FreeBSD 5/Linux 2.4.26 和NT 4的TCP部分代码,在FreeBSD中很容易就找到了这种情况的处理代码(sys etinet cp_input.c:1697):
 

以下为引用:

 /*
  * If a SYN is in the window, then this is an
  * error and we send an RST and drop the connection.
  */
 if (thflags & TH_SYN) {
  tp = tcp_drop(tp, ECONNRESET);
  rstreason = BANDLIM_UNLIMITED;
  goto drop;
 }
 


     注释里面也说的很清楚,如果一个 SYN 包在 TCP 窗口中,这就是一个需要发送 RST 包并丢弃连接的错误。
     
     在 Linux 下的处理 ESTABLISHED 状态的 tcp_rcv_established 函数(netipv4 cp_input.c:3589)中也对 SYN 包做了相同的处理:
 
以下为引用:

 int tcp_rcv_established(...)
 {
   // ...
   
  if(th->rst) {
   tcp_reset(sk);
   goto discard;
  }

  tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq);

  if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
   TCP_INC_STATS_BH(TcpInErrs);
   NET_INC_STATS_BH(TCPAbortOnSyn);
   tcp_reset(sk);
   return 1;
  }
  
  // ...
 }
 


    
     而 NT 下也有类似的处理代码(ntos di cpip cp cp.rcv.c:2829):
 
以下为引用:

 // At this point, the segment is in our window and does not overlap
 // on either end. If it's the next sequence number we expect, we can
 // handle the data now. Otherwise we'll queue it for later. In either
 // case we'll handle RST and ACK information right now.

 // 按 RFC 793,首先检查 RST 位
 // ...

 // 接着检查 SYN 位
 if (RcvInfo.tri_flags & TCP_FLAG_SYN)
 {
   // ...

   SendRSTFromHeader(TCPH, Size, Src, Dest, OptInfo);  // 发送 RST 包切断连接

   // ...

   return IP_SUCCESS;
 }
 



     可以看到 FreeBSD, Linux 和 NT 的上述代码都是完全符合 RFC 要求的。虽然这个漏洞目前很难被利用,但通过构造 SYN 包切断 TCP 连接的思路还是非常值得借鉴的 :P

     而实际上通过阅读 RFC 和 FreeBSD/NT 相关源码,发现可能引起重置 TCP 连接的原因远远不止直接收到 RST 包或上述这种对 SYN 包的特殊处理。通过直接发送 RST 包只是主动重置的步骤,而还有很多情况可能引发被动重置。RFC 793 的 Reset Generation (rfc793.txt:2308) 中有非常详细的描述。但可惜的是这些重置发生的状态,都不是最容易被利用的 ESTABLISHED 状态,呵呵。
     
 btw: 匆匆忙忙写完,估计问题挺多,呵呵,欢迎指正 :D

posted @ 2004-07-08 11:14 Flier Lu 阅读(2624) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1745355&run=.0A0B923

 Don Box《.NET本质论 第1卷:公共语言运行库》的第6章里,详细地解说了 CLR 中方法地调用机制的原理;qqchen在其 BLog 上也有一篇不错的介绍 CLR 中方法调用分类的文章《CLR Drilling Down: The Overhead of Method Calls 》。但因为他们文章的目的不同,故而没有足够深入到让我满足的内部细节,呵呵,只好自己接着分析。:D

     我在《用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程》一文中介绍了如何使用 WinDbg 跟踪 Don Box 所描述的 JIT 过程。本文中将使用前文所介绍的 WinDbg 功能进一步分析 CLR 中方法的调用机制。

     首先我们来看一个简单的例子,其中有两个类和一个接口的定义,并使用了几种不同的调用类型进行方法调用:
 

以下为引用:

 using System;

 namespace flier
 {
   public interface IFoo
   {
     void CallFromIntfBase();
     void CallFromIntfDerived();
   }

   public class Base : IFoo
   {
     public void CallFromObjBase()
     {
       System.Console.WriteLine("Base.CallFromObjBase");
     }

     public virtual void CallFromObjDerived()
     {
       System.Console.WriteLine("Base.CallFromObjDerived");
     }

     public void CallFromIntfBase()
     {
       System.Console.WriteLine("Base.IFoo.CallFromIntfBase");
     }
     public virtual void CallFromIntfDerived()
     {
       System.Console.WriteLine("Base.IFoo.CallFromIntfDerived");
     }
   }

   public class Derived : Base, IFoo
   {
     public new void CallFromObjBase()
     {
       System.Console.WriteLine("Derived.CallFromObjBase");
     }

     public override void CallFromObjDerived()
     {
       System.Console.WriteLine("Derived.CallFromObjDerived");
     }

     public override void CallFromIntfDerived()
     {
       System.Console.WriteLine("Derived.IFoo.CallFromIntfDerived");
     }
   }

  class EntryPoint
  {
   [STAThread]
   static void Main(string[] args)
   {
       Base b = new Base(),
            d = new Derived();

       b.CallFromObjBase();

       d.CallFromObjBase();
       d.CallFromObjDerived();

       IFoo i = (IFoo) b;

       i.CallFromIntfBase();

       i = (IFoo)d;

       i.CallFromIntfDerived();
   }
  }
 }
 



     将之编译成 CallIt.exe 后用 WinDbg 启动调试之。进入调试后,可以使用 sos 的 !name2ee 命令查看指定类型的当前状态,如:
 
以下为引用:

 0:000> !name2ee CallIt.exe flier.Derived
 --------------------------------------
 MethodTable: 00975288
 EEClass: 06c63414
 Name: flier.Derived
 


     使用 !dumpclass 命令进一步查看类型详细信息:
 
以下为引用:

 0:000> !dumpclass 06c63414
 Class Name : flier.Derived
 mdToken : 02000004 ()
 Parent Class : 06c6334c
 ClassLoader : 0015ee08
 Method Table : 00975288
 Vtable Slots : 9
 Total Method Slots : b
 Class Attributes : 100001 :
 Flags : 1000003
 NumInstanceFields: 0
 NumStaticFields: 0
 ThreadStaticOffset: 0
 ThreadStaticsSize: 0
 ContextStaticOffset: 0
 ContextStaticsSize: 0
 


     可以发现 Derived 类型有 11 个 Method Slot,但只有 9 个 Vtable Slot。使用 !dumpmt 进一步查看之:
 
以下为引用:

 0:000> !dumpmt -md 00975288
 EEClass : 06c63414
 Module : 00167d98
 Name: flier.Derived
 mdToken: 02000004  (D:TempCallItCallItinDebugCallIt.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 1
 Interface Map : 009752e0
 Slots in VTable : 11
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 79b7c4eb 79b7c4f0    None   [DEFAULT] [hasThis] String System.Object.ToString()
 79b7c473 79b7c478    None   [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
 79b7c48b 79b7c490    None   [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
 79b7c52b 79b7c530    None   [DEFAULT] [hasThis] Void System.Object.Finalize()
 0097525b 00975260    None   [DEFAULT] [hasThis] Void flier.Derived.CallFromObjDerived()
 009751ab 009751b0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase()
 0097526b 00975270    None   [DEFAULT] [hasThis] Void flier.Derived.CallFromIntfDerived()
 // 以下开始为 IFoo 接口方法表
 009751ab 009751b0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase()
 0097526b 00975270    None   [DEFAULT] [hasThis] Void flier.Derived.CallFromIntfDerived()
 // 以下开始为非虚方法表
 0097524b 00975250    None   [DEFAULT] [hasThis] Void flier.Derived.CallFromObjBase()
 0097527b 00975280    None   [DEFAULT] [hasThis] Void flier.Derived..ctor()
 


     可以看到正如 Don Box 在书中所说,类型的方法表是分为虚方法表和非虚方法表两部分的。前面 9 个 Method Slot 组成 Derived 的 VTable,后两个 Slot 保存非虚方法。检查 Base 类的情况也是类似:
 
以下为引用:

 0:000> !name2ee CallIt.exe flier.Base
 --------------------------------------
 MethodTable: 009751d8
 EEClass: 06c6334c
 Name: flier.Base

 0:000> !dumpclass 06c6334c
 Class Name : flier.Base
 mdToken : 02000003 ()
 Parent Class : 79b7c3c8
 ClassLoader : 0015ee08
 Method Table : 009751d8
 Vtable Slots : 7
 Total Method Slots : 9
 Class Attributes : 100001 :
 Flags : 1000003
 NumInstanceFields: 0
 NumStaticFields: 0
 ThreadStaticOffset: 0
 ThreadStaticsSize: 0
 ContextStaticOffset: 0
 ContextStaticsSize: 0

 0:000> !dumpmt -md 009751d8
 EEClass : 06c6334c
 Module : 00167d98
 Name: flier.Base
 mdToken: 02000003  (D:TempCallItCallItinDebugCallIt.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 1
 Interface Map : 00975228
 Slots in VTable : 9
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 79b7c4eb 79b7c4f0    None   [DEFAULT] [hasThis] String System.Object.ToString()
 79b7c473 79b7c478    None   [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
 79b7c48b 79b7c490    None   [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
 79b7c52b 79b7c530    None   [DEFAULT] [hasThis] Void System.Object.Finalize()
 0097519b 009751a0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromObjDerived()
 // 以下开始为 IFoo 接口方法表
 009751ab 009751b0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase()
 009751bb 009751c0    None   [DEFAULT] [hasThis] Void flier.Base.CallFromIntfDerived()
 // 以下开始为非虚方法表
 0097518b 00975190    None   [DEFAULT] [hasThis] Void flier.Base.CallFromObjBase()
 009751cb 009751d0    None   [DEFAULT] [hasThis] Void flier.Base..ctor()
 



     而对于每个接口,实际上 CLR 是单独维护了一个方法表的。如 Base 类的方法表中指出,地址 0x009752e0 处有一个接口方法映射表,查看其内容如下:
 
以下为引用:

 0:000> dd 0x009752e0
 009752e0  00975138 00070001 00000000 00000000
 


     每个接口映射表表项由2个DWORD组成,头一个DWORD就是接口方法表的地址。
 
以下为引用:

 0:000> !dumpmt -md 00975138
 EEClass : 06c633b0
 Module : 00167d98
 Name: flier.IFoo
 mdToken: 02000002  (D:TempCallItCallItinDebugCallIt.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 0
 Interface Map : 0097516c
 Slots in VTable : 2
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 009750eb 009750f0    None   [DEFAULT] [hasThis] Void flier.IFoo.CallFromIntfBase()
 00975113 00975118    None   [DEFAULT] [hasThis] Void flier.IFoo.CallFromIntfDerived()
 


     比较一下就会发现,Base 和 Derived 类的接口映射表指向的接口方法表都是一样的。
 
以下为引用:

 0:000> dd 009752e0
 009752e0  00975138 00070001 00000000 00000000

 0:000> dd 00975228
 00975228  00975138 00050001 00000000 00000000
 



     只是接口映射表表项第2个 DWORD 的高 WORD 指名此接口在原方法表中的起始索引(Base 为 5,Derived 为 7)不同。这正符合《本质论》中167页那张图所示的接口映射表结构。

posted @ 2004-07-08 11:13 Flier Lu 阅读(604) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1733932&run=.09D4C2F

转眼折腾 .NET 这套东西也有好几年了,从最开始与几个朋友互相转发讨论那聊聊无几的英文介绍文章、到抱着鸟语SDK文档和几十M的设计手册狂啃,再到现在铺天盖地的 .NET 方向的书籍,呵呵,不得不感叹世界发展之快 :D
     不过折腾到现在,抛开纯粹应用层面和技巧性的书籍不谈,就 .NET 方向较为深入的书籍实在不多。就好像 Windows 发展这么多年,真正常备案头的除了 MSDN 以外,其实也就不超过 5 本。
     下面把我觉得有志于研究 .NET 架构的朋友必备的几本书籍大致列一下,顺便把电子版的共享出来,以免后来者遭遇我以前学习时无人讨论无资料可看的窘境 :P

     首先 Jeffrey Richter 的 Applied Microsoft .NET Framework Programming是绝对必备的,呵呵。虽然这本书不是那种 interlnal 的书籍,但是其覆盖的知识点之全,以及解释时的度的把握之准确,非 Jeffrey Richter 不能完成。清华出版社翻译出版了此书的中文版,质量还算过得去,呵呵。如果英文好的朋友,建议直接阅读影印版或者电子版。

 
  Applied Microsoft .NET Framework Programming [/url]

 
 
 Microsoft .NET框架程序设计(修订版) 

 
  Applied Microsoft .NET Framework Programming(影印版)  

     其次当属 Don Box 的 Essential .NET, Volume I: The Common Language Runtime一书。从 COM 到 .NET 再到 SOAP, Don Box 一直是走在 MS 技术最前沿的人。如果说 Jeffrey Richter 的 Applied Microsoft .NET Framework Programming 较为偏重使用,则此书更为偏重实现和原理,是在读完前面那本之后,进一步了解 CLR 原理的最佳途径。电力出版社翻译出版了此书的中文版,翻译质量只能说尚可,需要对照英文原版一起看,呵呵
     
 
  Essential .NET, Volume I: The Common Language Runtime [/url]    

 
 
 .NET本质论 第1卷:公共语言运行库
 

     在通读上面两本书之后,你会发现要真正理解 CLR 的原理,Metadata 结构和 IL 代码的了解是必不可少的。而 Serge Lidin 的 Inside Microsoft .NET IL Assembler正是弥补了这个问题。书中对静态的 Metadata 和动态的 IL 执行机制做了非常细致的分析,可以说是真正深入理解 CLR 机制的必备书籍。机械工业出版社翻译出版了此书的中文版,翻译质量还行。
     
 
  Inside Microsoft .NET IL Assembler [/url]

 
 
 Microsoft.NET IL汇编语言程序设计 

     在完全理解这三本书的内容之后,如果还是吃不饱,呵呵,那就只能进一步研究 CLI 规范的内容了。.NET Framework SDK 的 Tool Developers Guidedocs 目录下有非常详细的设计文档,其中 Partition I Architecture 是结构性的介绍,必读;Partition II Metadata 介绍了 Metadata 的静态结构,是理解 CLR 核心结构组织的基础;其他的文档基本上都是针对某个方面的,可以根据兴趣选择性阅读。
     
     The Common Language Infrastructure Annotated Standard一书是阅读 CLI 规范的最好手册,针对 CLI 规范中疑难或者设计实现方法的很多地方做了非常详细的解释。

     
  The Common Language Infrastructure Annotated Standard [/url]


     而 
 Shared Source CLI Essentials  一书则是针对 CLI 规范的一个示范性实现 SSCLI (Rotor) 进行针对性的分析,非常有参考价值。只可惜现在国内还没有引进此书,也弄不到电子版 :(
     
     以上介绍的都是针对 .NET 方向通用实现层面较为深入的书籍。此外还有不少针对某个特殊应用的好书,如 Microsoft .NET RemotingMicrosoft ADO.NET等等。
     
 
  Microsoft .NET Remoting [/url]

 
 
 Microsoft ADO .NET [/url]

     只是临时整理了一下,难免有所遗漏,欢迎大家补充 :D

 btw: 因为这些电子版书籍的分发涉及到版权问题,请下载者勿用于商业用途,仅供学习研究。短期内可以通过下面的连接下载:

 Applied Microsoft .NET Framework Programming
 Essential .NET, Volume I: The Common Language Runtime
 Inside Microsoft .NET IL Assembler
 The Common Language Infrastructure Annotated Standard
 

posted @ 2004-07-08 11:12 Flier Lu 阅读(10743) 评论(37) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1678453&run=.05A880F

本来想按照 sos 的帮助文件上命令的分类逐步介绍 WinDbg 下使用 sos 调试 CLR 程序,但发现这样实在不够直观。索性改成根据我分析 CLR 的实际案例,step by step 介绍功能,这样结构上虽然混乱一点,但更加直观,也易于上手 :P

     前面两篇文章里面分别介绍了 WinDbg 的调试配置和线程的基本概念,这篇文章将针对 JIT 编译对象方法的流程进行分析,逐步介绍如何使用 WinDbg 调试 CLR 程序。

     用WinDbg探索CLR世界 [1] - 安装与环境配置
     用WinDbg探索CLR世界 [2] - 线程

     首先写一个简单的例子程序 demo.cs 并编译为 demo.exe,使用配置好的 WinDbg 打开之:
 

以下为引用:

 using System;

 namespace flier
 {
   class EntryPoint
   {
     public void m1()
     {
       System.Console.Write("EntryPoint.m1()");
     }

     public void m2()
     {
       System.Console.Write("EntryPoint.m2()");
     }

     public static void Main()
     {
       EntryPoint ep = new EntryPoint();

       ep.m1();
       ep.m2();
     }
   }
 }
 



     WinDbg 会在载入 demo.exe 后中断执行。此时可以使用 .load sos 命令加载 sos.dll 命令扩展,并用 .chain 验证加载是否成功;然后用 ld demo 命令加载 demo.exe 的调试符号文件,用 lm 命令验证加载是否成功。
     然后用 ld kernel32 加载 Kernel32 的调试符号文件,并用 bp kernel32!LoadLibraryExW "du poi(esp+4)" 命令在载入 DLL 的函数入口加上断点。接下来就是一路 g 指令,直到 mscorwks.dll 被加载。这个 mscorwks.dll 就是类似 JVM 中 jvm.dll 的虚拟机实现代码,我们要了解的大部分功能都在其中。详细的解释可以参看我以前的一篇文章《.Net平台下CLR程序载入原理分析》

     在 mscorwks.dll 被载入后用 ld mscorwks 命令载入其调试符号库,就可以正式开始我们的探索工作了 :D

     目前使用到的 WinDbg 命令如下

以下为引用:

 .load sos     // 加载 sos 调试扩展模块,可使用 .chain 命令验证

 ld demo       // 加载 demo.exe 调试符号库,可使用 lm 命令验证

 ld kernel32   // 加载 kernel32.exe 调试符号库

 bp kernel32!LoadLibraryExW "du poi(esp+4)" // 设置断点监视何时 mscorwks.dll 被载入

 g             // 执行直到 mscorwks.dll被加载

 bd 0          // 清除前面设置的断点,开始对 mscorwks.dll 进行处理

 ld mscorwks   // 加载 mscorwks.dll 调试符号库
 


     Don Box 在《.NET本质论 第1卷:公共语言运行库》的第六章介绍了方法调用的内部实现流程。其中提到方法表在 JIT 之前,保存的都是 call mscorwks.dll!PreStubWorker 调用,直到第一次使用时,才会对目标 IL 代码进行 JIT 编译,并调用之。因此我们第一步可以在此函数上设置断点(bp mscorwks!PreStubWorker),看看系统是如何调用此函数的。
 

以下为引用:

 0:000> bp mscorwks!PreStubWorker
 0:000> g
 ModLoad: 70ad0000 70bb6000   E:WINDOWSWinSxS†_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.100.0_x-ww_8417450Bcomctl32.dll
 ModLoad: 79780000 79980000   e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll
 ModLoad: 79980000 79ca6000   e:windowsassembly ativeimages1_v1.1.4322mscorlib.0.5000.0__b77a5c561934e089_ed6bc96cmscorlib.dll
 ModLoad: 79510000 79523000   E:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorsn.dll
 Breakpoint 1 hit
 eax=0012f7c0 ebx=00148c60 ecx=04aa112c edx=00000004 esi=0012f784 edi=0012f9a8
 eip=791d6a4a esp=0012f764 ebp=0012f79c iopl=0         nv up ei pl zr na po nc
 cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
 mscorwks!PreStubWorker:
 791d6a4a 55               push    ebp
 


     断点被激活就代表函数被调用。我们先使用 k 看看函数被调用时的上下文环境。
 
以下为引用:

 0:000> k
 ChildEBP RetAddr
 0012f760 0014930e mscorwks!PreStubWorker
 WARNING: Frame IP not in any known module. Following frames may be wrong.
 0012f79c 791da434 0x14930e
 0012f8b4 791dd2ec mscorwks!MethodDesc::CallDescr+0x1b6
 0012f96c 79240405 mscorwks!MethodDesc::Call+0xc5
 0012fa18 79240520 mscorwks!AppDomain::InitializeDomainContext+0x10f
 0012fa7c 7923d744 mscorwks!SystemDomain::InitializeDefaultDomain+0x11c
 0012fd60 791c6e73 mscorwks!SystemDomain::ExecuteMainMethod+0x120
 0012ffa0 791c6ef3 mscorwks!ExecuteEXE+0x1c0
 0012ffb0 7880a53e mscorwks!_CorExeMain+0x59
 0012ffc0 77e1f38c mscoree!_CorExeMain+0x30 [f:dd dpclrsrcdllsshimshim.cpp @ 5426]
 0012fff0 00000000 KERNEL32!BaseProcessStart+0x23
 


     这里可以看到从 mscoree!_CorExeMain 一路执行下来的步骤,而那个警告说明这个 stack frame 不在任意一个已知模块中。这是很正常的,因为这个栈帧实际上是指向由 JIT 动态生成的代码。我们监视的 mscorwks!PreStubWorker 函数只是作为方法表中函数的入口 stub,系统启动时还会通过其他方式调用 JIT 完成代码的编译执行。
     接下来用 SOS 的 !clrstack 命令看看 CLR 的调用堆栈,显示如下:
 
以下为引用:

 0:000> !clrstack
  succeeded
 Loaded Son of Strike data table version 5 from "E:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorwks.dll"
 Thread 0
 ESP       EIP
 0012f784  791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void System.AppDomain.SetupDomain(ValueClass System.LoaderOptimization,String,String)
 0012f9a8  791d6a4a [FRAME: GCFrame]
 0012fad0  791d6a4a [FRAME: DebuggerClassInitMarkFrame]
 0012fa94  791d6a4a [FRAME: GCFrame]
 


     如果需要更为详细的详细,可以使用 -p, -l 或 -r 参数分别显示参数、局部变量和寄存器,当然前两者需要调试符号库的支持才行。

     如此一路 g; !clrstack 执行下去,直到 flier.EntryPoint.m1 函数需要被处理为止:
 

以下为引用:

 0:000> !clrstack
 Thread 0
 ESP       EIP
 0012f68c  791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void flier.EntryPoint.m1()
 0012f69c  06d90080 [DEFAULT] Void flier.EntryPoint.Main()
 0012f9b0  791da717 [FRAME: GCFrame]
 0012fa94  791da717 [FRAME: GCFrame]
 


     此时用 !dumpstackobjects 命令可以查看当前线程堆栈中使用的所有对象
 
以下为引用:

 0:000> !dumpstackobjects
 ESP/REG  Object   Name
 ecx      04aa1a90 flier.EntryPoint
 0012f678 04aa1a90 flier.EntryPoint
 0012f67c 04aa1a90 flier.EntryPoint
 0012f680 04aa1a90 flier.EntryPoint
 


     这里的 flier.EntryPoint 对象地址 0x04aa1a90 就是我们要分析的对象在内存中的位置。

     这一阶段使用到的 WinDbg 命令如下:
 

以下为引用:

 bp mscorwks!PreStubWorker // 设置代码断点

 g                         // 继续运行至断点

 k                         // 查看函数调用时的 Native 堆栈调用

 !clrstack                 // 查看函数调用时的 CLR 堆栈调用

 !dumpstackobjects         // 查看线程堆栈中使用到的所有对象
 


     知道地址后,就可以用 !dumpobj 命令查看对象的详细信息
 

以下为引用:

 0:000> !dumpobj 04aa1a90
 Name: flier.EntryPoint
 MethodTable 0x009750a8
 EEClass 0x06c632e8
 Size 12(0xc) bytes
 mdToken: 02000002  (D:Tempdemo.exe)
 


     信息包括对象的类型名字(Name)和类型信息的地址(EEClass),以及对象的大小(Size)和 Token (mdToken),而方法表 (MethodTable) 正是我们分析方法调用的目标。我们可以用 !dumpclass 命令先进一步查看对象的类型信息:
 
以下为引用:

 0:000> !dumpclass 0x6c632e8
 Class Name : flier.EntryPoint
 mdToken : 02000002 ()
 Parent Class : 79b7c3c8
 ClassLoader : 00153850
 Method Table : 009750a8
 Vtable Slots : 4
 Total Method Slots : 8
 Class Attributes : 100000 :
 Flags : 1000003
 NumInstanceFields: 0
 NumStaticFields: 0
 ThreadStaticOffset: 0
 ThreadStaticsSize: 0
 ContextStaticOffset: 0
 ContextStaticsSize: 0
 


     可以发现其信息与对象信息有很多符合之处,正如 Don Box 所说,一个对象引用指向一个类型 EEClass 实例,而方法表为类型所有,其对象共有。我们可以使用 !dumpmt 命令进一步查看方法表的信息,-md 参数表示需要查看每个方法描述 (MethodDesc):
 
以下为引用:

 0:000> !dumpmt -md 0x09750a8
 EEClass : 06c632e8
 Module : 0014e090
 Name: flier.EntryPoint
 mdToken: 02000002  (D:Tempdemo.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 0
 Interface Map : 009750f4
 Slots in VTable : 8
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 79b7c4eb 79b7c4f0    None   [DEFAULT] [hasThis] String System.Object.ToString()
 79b7c473 79b7c478    None   [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
 79b7c48b 79b7c490    None   [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
 79b7c52b 79b7c530    None   [DEFAULT] [hasThis] Void System.Object.Finalize()
 0097506b 00975070    None   [DEFAULT] [hasThis] Void flier.EntryPoint.m1()
 0097507b 00975080    None   [DEFAULT] [hasThis] Void flier.EntryPoint.m2()
 0097508b 00975090    None   [DEFAULT] Void flier.EntryPoint.Main()
 0097509b 009750a0    None   [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()
 


     可以看到方法表中共有8个表项,其中前4个已经绑定到使用 ngen 预编译好的静态函数上
 
以下为引用:

 0:000> u 79b7c4eb
 mscorlib_79980000+0x1fc4eb:
 79b7c4eb e8909cfeff       call    mscorlib_79980000+0x1e6180 (79b66180)
 79b7c4f0 0000             add     [eax],al
 79b7c4f2 0080d86206c0     add     [eax+0xc00662d8],al
 79b7c4f8 06               push    es
 79b7c4f9 00fc             add     ah,bh
 79b7c4fb e8809cfeff       call    mscorlib_79980000+0x1e6180 (79b66180)
 79b7c500 07               pop     es
 79b7c501 0010             add     [eax],dl
 


     后四个则作为可被覆盖的虚方法在方法表中,这也是为什么在查看类型信息时 Vtable Slots = 4 而 Total Method Slots = 8 的原因。

     对方法表的每个项目,可以用 !DumpMD 命令查看详细描述,如
 

以下为引用:

 0:000> !DumpMD 0x00975070
 Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint.m1()
 MethodTable 9750a8
 Module: 14e090
 mdToken: 06000001 (D:Tempdemo.exe)
 Flags : 0
 IL RVA : 00002050
 


     IL RVA 说明此方法的 IL 代码相对虚拟地址(IL RVA),也就是说此方法还没有被 JIT,仍以 IL 代码形式存在。对于已经完成 JIT 的方法,将显示其 JIT 后函数体代码的虚拟地址(Method VA):
 
以下为引用:

 0:000> !DumpMD 0x009750a0
 Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()
 MethodTable 9750a8
 Module: 14e090
 mdToken: 06000004 (D:Tempdemo.exe)
 Flags : 0
 Method VA : 06d900a8
 

     这一阶段使用到的 WinDbg 命令如下:
 

以下为引用:

 !dumpobj 04aa1a90     // 查看对象的详细信息

 !dumpclass 0x6c632e8  // 查看类型的详细信息

 !dumpmt -md 0x09750a8 // 查看方法表的详细信息

 !dumpmd 0x00975070    // 查看方法表项的方法描述的详细信息

 u 0x79b7c4eb          // 反汇编指定地址的指令
 


     我们反汇编一下 !DumpMT 命令列出的几个方法,就会发现正如 Don Box 所说,已经被 JIT 的代码指向一个jmp指令,直接跳转到编译后的方法体,如:
 

以下为引用:

 0:000> u 0097509b
 0097509b e908b04106       jmp     06d900a8
 


     而没有被 JIT 的函数,则指向一个call指令,调用一个 prolog 代码,间接调用 mscorwks!PreStubWorker 函数完成实际 JIT 工作,如:
 
以下为引用:

 0:000> u 0x0097506b
 0097506b e878427dff       call    001492e8

 0:000> u 0x0097507b
 0097507b e868427dff       call    001492e8
 



     这个 prolog 代码很简单,负责构造 mscorwks!PreStubWorker 所需的调用堆栈
 
以下为引用:

 0:000> u 0x001492e8
 001492e8 52               push    edx
 001492e9 68f0301b79       push    0x791b30f0
 001492ee 55               push    ebp
 001492ef 53               push    ebx
 001492f0 56               push    esi
 001492f1 57               push    edi
 001492f2 8d742410         lea     esi,[esp+0x10]
 001492f6 51               push    ecx
 001492f7 52               push    edx
 001492f8 648b1d2c0e0000   mov     ebx,fs:[00000e2c]
 001492ff 8b7b08           mov     edi,[ebx+0x8]
 00149302 897e04           mov     [esi+0x4],edi
 00149305 897308           mov     [ebx+0x8],esi
 00149308 56               push    esi
 00149309 e83cd70879       call    mscorwks!PreStubWorker (791d6a4a)
 0014930e 897b08           mov     [ebx+0x8],edi
 00149311 894604           mov     [esi+0x4],eax
 00149314 5a               pop     edx
 00149315 59               pop     ecx
 00149316 5f               pop     edi
 00149317 5e               pop     esi
 00149318 5b               pop     ebx
 00149319 5d               pop     ebp
 0014931a 83c404           add     esp,0x4
 0014931d 8f0424           pop     [esp]
 00149320 c3               ret
 


     而这段 prolog 代码是由类似 ROTOR 中的 GeneratePrestub 函数(vmi386cgenx86.cpp:1829) 动态生成的,完成对 PreStubWorker 函数调用的封装。而 PreStubWorker 函数会调用 JIT 完成真正的函数编译工作,并将方法表的入口改为指向编译后函数体的 jmp 指令。具体的流程请参考Don Box 在《.NET本质论 第1卷:公共语言运行库》的第六章中的介绍,这里就不再罗嗦了。以后有机会再写篇文章详细分析一下 JIT 的工作流程。

     在 JIT 处理 flier.EntryPoint.m1 时,用 g 命令执行,再回头来分析 m1 函数的入口,就会发现如前面所述,调用 JIT 过程的 call 指令变成了直接调用 Native 函数体的 jmp 指令。:D


     这一小节,我们介绍了使用 WinDbg 跟踪调试 CLR 程序的一遍流程,并了解了对堆栈、对象和类信息进行分析的 SOS 命令,希望大家能够借此开始探索 CLR 内部世界的旅程。 :P

     Jason Zander在其 BLog 的一篇文章,SOS Debugging with the CLR (Part 1),里面也详细介绍了使用 WinDbg 和 SOS 调试 CLR 程序的部分方法,值得一看。
     关于 WinDbg 的基本使用方法,CodeProject.com上有一个系列文章介绍
     
     Debug Tutorial Part 1: Beginning Debugging Using CDB and NTSD
     Debug Tutorial Part 2: The Stack
     Debug Tutorial Part 3: The Heap
     Debug Tutorial Part 4: Writing WINDBG Extensions

posted @ 2004-07-08 11:11 Flier Lu 阅读(1217) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1670460&run=.0FB98AB

CLR中通过预定义属性(Attribute)为值类型结构的定义提供了很大的灵活性,基本上可以很灵活地处理绝大部分原有Win32 API和COM接口的定义。
     
     对没有显式指定的类和结构,C#及其它编译器有权利任意更改字段定义顺序,以优化占用空间和访问时间。例如一个结构
 
以下为引用:

 struct A
 {
   short a;
   int b;
   byte c;
   byte d;
 }
 

    
     在缺省情况下使用的是LayoutKind.Auto模式,编译器可以任意重排字段顺序,如
 
以下为引用:

 struct A
 {
   int b;
   short a;
   byte c;
   byte d;
 }
 

 
     以保障字节对齐,在不增加填补(padding)空间的同时提高访问效率。
     但这样一来就无法与C++/COM等定义的结构兼容,因此需要显式指定排列方式为LayoutKind.Sequential或LayoutKind.Explicit。前者要求编译器保障内存中字段顺序与定义的相同,按照StructLayoutAttribute.Pack进行对齐;后者则通过FieldOffsetAttribute预定义属性显式定义每个字段的位置,可以用于模拟Union语义。
     例如前面例子可以强制要求结构保持字段顺序同时按1字节对齐
 
以下为引用:

 [StructLayout(LayoutKind.Sequential, Pack=1)]
 struct A
 {
   short a;
   int b;
   byte c;
   byte d;
 }
 

   
     而通过 LayoutKind.Explicit 模式则可以模拟 Union 语义
 
以下为引用:

 union MyUnion
 {
     int number;
     double d;
 }
 

    
 
以下为引用:

 [ StructLayout( LayoutKind.Explicit )]
 public struct MyUnion 
 {
    [ FieldOffset( 0 )]
    public int i;
    [ FieldOffset( 0 )]
    public double d;
 }
 

    
     但这里的 Union 语义受到一定的限制:在被 Union 语义使用的字段或结构中,必须很小心地使用引用类型或 Marshal 形式的字段,因为 CLR 不允许值类型和引用类型在内存布局上重叠。准确的说是 LayoutKind.Explicit 这种内存布局中不能有引用类型字段与值类型字段重叠,而引用类型字段与引用类型字段,或者值类型字段与值类型字段完全重叠都是允许的。
     
     实际上 C# 中提供了很多 syntax sweet 辅助用户处理常见数据结构,例如对数组和字符串的处理就提供了很全面的支持,如
 
以下为引用:

 typedef struct _MYSTRSTRUCT
 {
    wchar_t* buffer;
    UINT size; 
 } MYSTRSTRUCT;

 struct UnmanagedInformation {
   int num;
   char* string;
   int array[32];
 };
 


    
     可以通过 PInvoke 内建支持很容易转换为 Managed 结构定义
 
以下为引用:

 [ StructLayout( LayoutKind.Sequential, CharSet=CharSet.Unicode )]
 public struct MyStrStruct 
 {  
    public String buffer;
    public int size;
 }

 [StructLayout( LayoutKind.Sequential, CharSet=CharSet.Ansi )]
 struct ManagedInformation {
   public int num;
   public string str;
   [MarshalAs (UnmanagedType.ByValArray, SizeConst=32)]
   public int[] array[];
 }
 


    
     这种特殊的语法现象,实际上是使用引用类型的语法来操作值类型的语义。例如上面通过 [MarshalAs (UnmanagedType.ByValArray, SizeConst=32)] 定义的数组,在 CLR 中语法上是作为一个引用类型存在的。
 
以下为引用:

 .field public  marshal( fixed array [32]) int32[] 'array'
 

    
     使用的时候同样需要先 info.array = new int[32]; 然后才能使用;但同时它在语义上定义了 ManagedInformation 结构中的一个连续内存区域。
 
以下为引用:

 void test2()
 {
   ManagedInformation info = new ManagedInformation();

   info.num = 3;
   info.str = "Test";
   info.array = new int[32];
   for(int i=0; i<info.array.Length; i++)
     info.array[i] = i+1;

   IntPtr pInfo = Marshal.AllocHGlobal(Marshal.SizeOf(info));
   Marshal.StructureToPtr(info, pInfo, false);
   Marshal.FreeHGlobal(pInfo);
 }
 


    
     从上面的例子可以看到 info.str 和 info.array 的使用完全和引用类型一致,但在执行完 Marshal.StructureToPtr 后,在调试器的数据窗口直接查看 pInfo 指向的内存区域,就会发现 info.str 保存了一个指向 "Test" 字符串的 Unmanaged 指针,而 info.array 是一个固定长度的连续地址数组。
     
     在平时这种 syntax sweet 可以大大简化用户的操作,但是在使用 Union 语义的时候就不成了,因为这种用引用类型对值类型的包装,是无法与其他值类型字段在内存上重叠的。做这样限定的原因,可能是因为对 GC 来说需要确定地知道某个字段是否为引用类型,CLR不能给 GC 一个具有二义性的语义。而 Marshal 的字段实际上也是一个引用字段,故而受到同样的限制。因为当一个引用类型字段与值类型字段重叠的时候就会出现二义性的问题;而值类型字段互相重叠(GC看来此内存还是保存一个值类型),或者引用类型字段互相重叠不存在二义性的问题。
     
     也就是说类似 DEBUG_EVENT 的结构,是无法在C#中直接通过正常语法定义的。
 
以下为引用:

 typedef struct _EXCEPTION_RECORD {  
   DWORD ExceptionCode;  
   DWORD ExceptionFlags;  
   struct _EXCEPTION_RECORD* ExceptionRecord;  
   PVOID ExceptionAddress;  
   DWORD NumberParameters;  
   ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

 typedef struct _EXCEPTION_DEBUG_INFO {  
   EXCEPTION_RECORD ExceptionRecord;  
   DWORD dwFirstChance;
 } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

 typedef struct _DEBUG_EVENT {  
   DWORD dwDebugEventCode;  
   DWORD dwProcessId;  
   DWORD dwThreadId;  
   union {    
     EXCEPTION_DEBUG_INFO Exception;    
     CREATE_THREAD_DEBUG_INFO CreateThread;    
     CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;    
     EXIT_THREAD_DEBUG_INFO ExitThread;    
     EXIT_PROCESS_DEBUG_INFO ExitProcess;    
     LOAD_DLL_DEBUG_INFO LoadDll;    
     UNLOAD_DLL_DEBUG_INFO UnloadDll;    
     OUTPUT_DEBUG_STRING_INFO DebugString;    
     RIP_INFO RipInfo;  
   } u;
 } DEBUG_EVENT, *LPDEBUG_EVENT;
 


    
     如果定义成类似上面 Union 的形式,如
 
以下为引用:

 [StructLayout(LayoutKind.Sequential)]
   public struct EXCEPTION_RECORD 
 {
   public const int EXCEPTION_MAXIMUM_PARAMETERS = 15;

   public int ExceptionCode;
   public uint ExceptionFlags;
   public IntPtr ExceptionRecord;
   public IntPtr ExceptionAddress;
   public uint NumberParameters;

   [MarshalAs(UnmanagedType.ByValArray, SizeConst=EXCEPTION_MAXIMUM_PARAMETERS)]
   public uint[] ExceptionInformation;
 }

 [StructLayout(LayoutKind.Sequential)]
   public struct EXCEPTION_DEBUG_INFO 
 {
   public EXCEPTION_RECORD ExceptionRecord;
   public uint dwFirstChance;
 } 

 [StructLayout(LayoutKind.Explicit)]
 public struct DEBUG_EVENT
 {
   [FieldOffset(0)] public uint dwDebugEventCode;
   [FieldOffset(4)] public uint dwProcessId;
   [FieldOffset(8)] public uint dwThreadId;
   
   [FieldOffset(12)] public EXCEPTION_DEBUG_INFO Exception;      
   [FieldOffset(12)] public CREATE_THREAD_DEBUG_INFO CreateThread;
   [FieldOffset(12)] public CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
   [FieldOffset(12)] public EXIT_THREAD_DEBUG_INFO ExitThread;
   [FieldOffset(12)] public EXIT_PROCESS_DEBUG_INFO ExitProcess;
   [FieldOffset(12)] public LOAD_DLL_DEBUG_INFO LoadDll;
   [FieldOffset(12)] public UNLOAD_DLL_DEBUG_INFO UnloadDll;
   [FieldOffset(12)] public OUTPUT_DEBUG_STRING_INFO DebugString;
   [FieldOffset(12)] public RIP_INFO RipInfo;
 }
 


    
     则在动态构造 DEBUG_EVENT 时会引发异常,在静态构造时程序进入函数前就异常。异常信息如下:
 
以下为引用:

 An unhandled exception of type 'System.TypeLoadException' occurred in Debugger.exe

 Additional information: 未能从程序集 Debugger, Version=1.0.1572.568, Culture=neutral, PublicKeyToken=null 中加载类型 DEBUG_EVENT,因为它在 16 偏移位置处包含一个对象字段,该字段已由一个非对象字段不正确地对齐或重叠。
 


    
     最可恨的是如果静态构造时,程序根本就在构造函数堆栈时歇菜了,不进入函数体也不抛出异常。害得我晚上调试时以为是自己程序的问题,把代码改的乱七八糟,最后只能靠屏蔽代码发现这个问题 :( 
     例如下面的简单代码会导致函数无法进入但也没有异常抛出,就看到 CPU 到了 100%,几秒钟后就一切正常,但函数并没有被调用。
 
以下为引用:

 class Test
 {
   void DoSomething()
   {
     Win32.DEBUG_EVENT evtHeader;
   }
 }
 

    
     直接查看 IL 代码也没有什么异常用法,问题可能出在 JIT 载入类型的时候 :( 不知道这算不算 .NET Framework 1.1 的一个 bug。IL 代码如下,很简单,问题应该出在载入类型构造堆栈的时候
 
以下为引用:

 .method public hidebysig newslot virtual final 
         instance void  DebugEvent(unsigned int32 pDebugEvent,
                                   int32 fOutOfBand) cil managed
 {
   // Code size       2 (0x2)
   .maxstack  0
   .locals init ([0] valuetype Debugger.Utils.Win32/DEBUG_EVENT evtHeader)
   IL_0000:  nop
   IL_0001:  ret
 } // end of method UnmanagedEventListener::DebugEvent
 

    
     MSDN上举例时使用的方式是为 Union 语义的每种情况定义一个单独的结构,但这样代码太过繁琐。我现在采用的是通过设计的方式回避这个问题,根据DEBUG_EVENT.dwDebugEventCode动态判断其后字段的内容,再手工转换。
     
     我首先定义一个DEBUG_EVENT_HEADER类型,然后在代码中先转换这一部分内容
 
以下为引用:

 public class Win32 
 {
   [StructLayout(LayoutKind.Sequential)]
     public struct DEBUG_EVENT_HEADER
   {
     public uint dwDebugEventCode;
     public uint dwProcessId;
     public uint dwThreadId;
   }
 }

 // ...
     
 public class UnmanagedEventListener
 {
   public void DebugEvent(uint pDebugEvent, int fOutOfBand)
   {
     IntPtr pEvent = new IntPtr(pDebugEvent);
     
     Win32.DEBUG_EVENT_HEADER evtHeader = (Win32.DEBUG_EVENT_HEADER)Marshal.PtrToStructure(
       new IntPtr(pDebugEvent), typeof(Win32.DEBUG_EVENT_HEADER));
      
    // ...
   }
 }

 // ...
 



     这里的 pDebugEvent 参数是一个指针,我使用 new IntPtr(pDebugEvent) 将之转换为 Managed 指针,传递给 Marshal.PtrToStructure 函数,从一个 Unmanaged 内存指针读入一个结构到 Managed 值类型对象。然后就可以根据事件类型进行处理,如
 
以下为引用:

 public class UnmanagedEventListener
 {
   public void DebugEvent(uint pDebugEvent, int fOutOfBand)
   {
     // ...
     
     IntPtr pEventData = new IntPtr(pDebugEvent + Marshal.SizeOf(evtHeader));
     
     switch(evtHeader.dwDebugEventCode)
     {
       case Win32.CREATE_PROCESS_DEBUG_EVENT:
       {
         ProcessEvent(ctxt, (Win32.CREATE_PROCESS_DEBUG_INFO)
           Marshal.PtrToStructure(pEventData, typeof(Win32.CREATE_PROCESS_DEBUG_INFO)));          
         break;
       }
       case Win32.EXIT_PROCESS_DEBUG_EVENT:
       {
         ProcessEvent(ctxt, (Win32.EXIT_PROCESS_DEBUG_INFO)
         Marshal.PtrToStructure(pEventData, typeof(Win32.EXIT_PROCESS_DEBUG_INFO)));
         break;
       }     
       // ... 
     } 
     // ...
   } 
   // ...
 }
 

    
     这样一来就可以通过指针操作模拟 Union 语义,但远不如直接用属性定义看起来舒服,呵呵
     
     一个稍微好一点的解决方法是自定义一些属性,标记 DEBUG_EVENT 结构里那些 Union 中的结构,然后通过 Reflection 完成读取和转换工作,代码比较繁琐,这里就不一一列举了。
     
     但是实际上在传统C++语言中这种用法是大量存在,例如在处理复杂网络协议和文件结构的程序里面,多层 Union 加上数组的结构是无法避免的。而 CLR 在处理结构的时候,实际上可以通过进一步将之细分为运行时内存布局和存储时内存布局来解决这个问题,以免用户通过较为 dirty 的方式模拟这个语义。但可惜到最新的 .NET Framework 2.0 都还是存在这个限制。
     
     
     关于结构类型定义的方法下面这篇文章里面有非常详细的讲解

     Everything you (n)ever wanted to know about Marshalling (and were afraid to ask!)
     
     此外有个 Wiki 项目 PInvoke.NET 也还不错,可以直接查询到很多 Win32 API 的定义。不过最终解决方法还是写个可以编译 .h/.idl 文件到 CLR 格式定义的编译器出来,不知道谁搞定做这个造福全人类的东东啊,呵呵

 btw: 感谢MSN上的朋友 笨笨 (-_-b)和水木网友 Nineteen 提示我找出真正问题所在 :P

posted @ 2004-07-08 11:09 Flier Lu 阅读(1022) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1652203&run=.002A442

记得在VS2003刚刚发布的时候,曾经给VS项目经理写过一封信询问下个版本中是否会加入关于Refactoring的支持,但是那位老兄的说法是这部分功能应该由第三方软件而不是MS完成,呵呵。不过随着主流IDE开发环境如Eclipse/JBuilder等都增加了重构的支持功能,VS2005也加入了相应的功能。同时一些第三方厂商也提供了针对VS2002/VS2003的重构支持。
     刚刚在纯朴的狗熊的主页上看到对VS.NET下重构工具的介绍文章《.NET下的重构》。于是下载了一个C# Refactoring Tool试用了一下,感觉虽然功能不如Java的开发环境下那么强大,但是基本功能点还是足够了,非常不错,决定留用了,呵呵 :D 
 
以下为引用:

 C# Refactoring Tool version 1.5.1 announced 

 Features 

   Refactorings 
     Encapsulate Field updated 
     Extract Method 
     Inline Temp 
     Introduce Explaining Variable 
     Move Member (Field, Method) updated 
     Rename (Field, Method, Property, Local Variable, Parameter, Type, Namespace) updated 
     Replace Magic Literal with Symbolic Constant updated 
     Replace Temp with Query 
     
   Integrated with MS Visual Studio .NET (as Add-in)  
   Undo/Redo in MS Visual Studio .NET   
   Solution Level Processing 
 


    
     可惜要注册,没有$奉上只好顺手剁了,用用先 :P
     
     虽然程序本身写的还不错,但感觉作者的注册机制做的真是很搞笑,呵呵
     用VC7写了一个 Unmanaged 的 DLL (DotNetRefactoring.Verify.dll)用于完成实际注册码解码工作,同时用一个 Managed C++ 的 DLL (DotNetRefactoring.VerifyWrap.dll)对其功能进行封装。但因为封装得太好,导致只要把 DotNetRefactoring.Verify.dll 删掉或者随便用个DLL替换,都会导致验证失败,但是无法弹出警告对话框。同时只需要写一个具有相同导出函数的 Unmanaged DLL,就可以“完成”注册码的解码工作...-_-b
     只需要先在注册表的 HKEY_LOCAL_MACHINESOFTWARE.NET RefactoringC# Refactoring Tool 下面增加一个名为 Key 的字符串,如
 
以下为引用:

 Windows Registry Editor Version 5.00

 [HKEY_LOCAL_MACHINESOFTWARE.NET RefactoringC# Refactoring Tool]
 "Key"="Flier Lu"
 


    
     然后用VC7写一个 Unmanaged Win32 DLL,导出一个类,然后替换 DotNetRefactoring.Verify.dll 即可。(仅供试用,请勿用于非法用途 :P)
 
以下为引用:

 // DotNetRefactoring.Verify.cpp : Defines the entry point for the DLL application.
 //

 #include "stdafx.h"

 #include <stdlib.h>

 BOOL APIENTRY DllMain( HANDLE hModule, 
                        DWORD  ul_reason_for_call, 
                        LPVOID lpReserved)
 {
   return TRUE;
 }

 #ifdef DOTNETREFACTORINGVERIFY_EXPORTS
 #define DOTNETREFACTORINGVERIFY_API __declspec(dllexport)
 #else
 #define DOTNETREFACTORINGVERIFY_API __declspec(dllimport)
 #endif

 // This class is exported from the DotNetRefactoring.Verify.dll
 class DOTNETREFACTORINGVERIFY_API CVerify {
 public:
   static char *decryptString(const char *msg);

   CVerify& operator=(const CVerify& v);

   static void remove(char *msg);
 };

 char *CVerify::decryptString(const char *msg)
 {
   char buf[1024] = "Licensed to ";

   strcat(buf, msg);

   return strdup(buf);
 }

 CVerify& CVerify::operator=(const CVerify& v)
 {
   return *this;
 }

 void CVerify::remove(char *msg)
 {
   free(msg);
 }
 


posted @ 2004-07-08 11:07 Flier Lu 阅读(650) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1635813&run=.06FE977

 Vijay 在其 BLog 上的一篇文章 A .NET Riddle 中提出了一个有趣的问题:
 
以下为引用:

     What is the only Type in .NET which has only a non-public constructor and the method body of which throws up a NotImplemented Exception. If so how is the type instance created then ?
 


     答案就是 System.RuntimeType 类型。Vijay 在接下来的一篇文章 .NET Riddle Redux - The Answer 中解释了为什么如此,并且介绍了 Rotor 中 RuntimeType 类型获得的方法。
     Rotor 中 System.RuntimeType 类型的构造函数(bclsystemRuntimeType.cs:59)代码如下:
 
以下为引用:

 internal sealed class RuntimeType : Type, ISerializable, ICloneable
 {
   // Prevent from begin created
   internal RuntimeType()
   {
     throw new NotSupportedException(Environment.GetResourceString(ResId.NotSupported_Constructor));
   }
 }
 


     正如前面那个谜语中所说,有一个内部构造函数但函数体直接抛出异常,因此不能直接构造。唯一能够获得此类型实例的方式是通过Object.GetType()等函数,由 CLR 创建。

     下面我们简单介绍一下,Type、RuntimeType 和 RuntimeTypeHandle 三个类型之间的关系。

     简单的说:Type 是一个表示类型的抽象类;RuntimeType 是 Type 针对载入类型信息的具体实现;RuntimeTypeHandle 则是类型唯一的抽象句柄。
 

以下为引用:

 namespace System
 {
   public struct RuntimeTypeHandle : ISerializable
   {
     private IntPtr m_ptr;

     //...
   }

   public abstract class Type : MemberInfo, IReflect
   {
     //...

     public abstract RuntimeTypeHandle TypeHandle
     {
       get;
     }

     public static RuntimeTypeHandle GetTypeHandle(Object o)
     {
       return RuntimeType.InternalGetTypeHandleFromObject(o);
     }

     public static Type GetTypeFromHandle(RuntimeTypeHandle handle)
     {
       return RuntimeType.GetTypeFromHandleImpl(handle);
     }

     //...
   }

   internal sealed class RuntimeType : Type, ISerializable, ICloneable
   {
     //...

     public override RuntimeTypeHandle TypeHandle
     { get { return InternalGetClassHandle(); } }

     [MethodImplAttribute(MethodImplOptions.InternalCall)]
     private extern RuntimeTypeHandle InternalGetClassHandle();

     [MethodImplAttribute(MethodImplOptions.InternalCall)]
     internal extern static RuntimeTypeHandle InternalGetTypeHandleFromObject(Object o);

     [MethodImplAttribute(MethodImplOptions.InternalCall)]
     public static extern Type GetTypeFromHandleImpl(RuntimeTypeHandle handle);

     //...
   }
 }
 



     Type 使用最为广泛,提供了完整的类型信息获取的接口,可以通过 C# 的 typeof 关键字或者 Object.GetType() 函数从类型和对象直接获取。但 Type 只是一个抽象类,需要通过子类实现其部分功能。除了常见的 RuntimeType 子类外,还有 System.Reflection 名字空间下的 TypeDelegator 和 System.Reflection.Emit 名字空间下的 EnumBuilder、TypeBuilder 和 SymbolType 从其直接继承。
     RuntimeType 是 System 名字空间的内部类,用于实现对普通类型的运行时信息提供代码。Type中很多抽象函数如 Type.GetMethodImpl 都是在 RuntimeType 中实现的。与 TypeBuilder 等不同,RuntimeType 是类型的运行时表现。
     RuntimeTypeHandle 是运行时类型的抽象句柄,结构内部实际上是一个 Unamanged 指针。可以使用 Type.GetTypeFromHandle() 函数、Type.TypeHandle 属性和 Type.GetTypeHandle() 函数,完成在类型句柄、类型和对象之间的双向转换。例如:
 
以下为引用:

 MyClass1 cls = new MyClass1();

 Debug.Assert(cls.GetType().TypeHandle.Equals(Type.GetTypeHandle(cls)));
 Debug.Assert(Type.GetTypeFromHandle(cls.GetType().TypeHandle) == typeof(MyClass1));
 



     从 Rotor 的实现代码上来看,Type 类型的实际构造基本上都是由 RuntimeType 类型通过 COMClass 类(vmComClass.cpp:260)完成的;而 RuntimeTypeHandle 则是类型方法表 MethodTable 类(vmClass.h:293)的指针的简单包装。
     获取 RuntimeTypeHandle 的函数 Type.GetTypeHandle() 实际上调用的是 RuntimeType.InternalGetTypeHandleFromObject 函数(bclsystemRuntimeType.cs:592);而此函数的内部实现被绑定(vmECall.cpp:515)到 COMClass::GetTHFromObject 函数(vmComClass.cpp:255)上;最终只是简单地返回对象的 MethodTable 指针。伪代码如下
 
以下为引用:

 void * COMClass::GetTHFromObject(Object* obj)
 {
   if(obj==NULL)
     FCThrowArgumentNull(L"obj");

   return obj->GetMethodTable();
 }
 



     因此在 Unmanaged C++ 中实际上可以直接通过 GetTypeHandle 返回的 RuntimeTypeHandle.Data 转换为地址指针,访问 MethodTable 类的数据。例如 MethodTable 第二个 DWORD 中保存着此对象的基本大小,可以在 VS.NET 调试环境的内存窗口中查看到。
     
     直接从类型获取 RuntimeTypeHandle 的 Type.InternalGetClassHandle 函数(bclsystemRuntimeType.cs:589),内部实现被绑定(vmECall.cpp:528)到 COMClass::GetClassHandle 函数(vmComClass.cs:1434)上,直接从类型的内部包装类 ReflectClass 类(vmReflectorWrap.h:212)中获取初始化时提供的类型句柄(MethodTable指针)。伪代码如下
 
以下为引用:

 void * COMClass::GetClassHandle(ReflectClassBaseObject* refThisUNSAFE)
 {  
   REFLECTCLASSBASEREF refThis = (REFLECTCLASSBASEREF) refThisUNSAFE;

   ReflectClass* pRC = (ReflectClass*) refThis->GetData();  

   TypeHandle ret = pRC->GetTypeHandle();
   
   return ret.AsPtr();
 }
 



     从 RuntimeTypeHandle 反向查询 Type 的 Type.GetTypeFromHandle 函数,实现上调用 RuntimeType.GetTypeFromHandleImpl 函数(bclsystemRuntimeType.cs:596),并被绑定(vmECall.cpp:529)到 COMClass::GetClassFromHandle 函数(vmComClass.cpp:1451)上。
     GetClassFromHandle 函数首先试图从 RuntimeTypeHandle 实例指向的方法表中,调用 MethodTable::GetExistingExposedClassObject 函数(vmClass.h:488)获取已经建立的类型信息包装类;如果第一次使用此类型,则调用 TypeHandle::CreateClassObj 函数(vmClass.cpp:12486)创建类型信息包装类实例。伪代码如下:
 
以下为引用:

 Object * COMClass::GetClassFromHandle(LPVOID handle)
 {
   if(handle == 0)
     FCThrowArgumentEx(kArgumentException, NULL, L"InvalidOperation_HandleIsNotInitialized");

   TypeHandle typeHnd(handle);

   if(!typeHnd.IsTypeDesc())
   {
     MethodTable *pMT = typeHnd.GetMethodTable();

     if (pMT)
     {
       OBJECTREF o = pMT->GetExistingExposedClassObject(); // 获取已经建立的类型信息包装类
       if (o != NULL)
       {
         return (OBJECTREFToObject(o));
       }
     }
   }

   return typeHnd.CreateClassObj(); // 创建类型信息包装类实例
 }
 


posted @ 2004-07-08 11:07 Flier Lu 阅读(789) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1620823&run=.0C124A1

  OFBiz项目的主要人员之一David Jones在一篇讨论中,Re: double-checked synch, Entity Engine & Struts,讨论了为什么不在OFBiz中使用Struts和Double Checked Locking模式。
     Double Checked Locking 模式是一种非常有效的对 Singleton 模式的优化方法。
     
     一般来说,Singleton模式可以通过如下代码简单实现:
 
以下为引用:

 // Single threaded version
 class Foo 
 { 
   private Helper helper = null;
   public Helper getHelper() {
     if (helper == null) 
         helper = new Helper();
     return helper;
     }
   // other functions and members...
 }
 

    
     而为了支持多线程时可能存在的并发访问,需要进行锁定保护
 
以下为引用:

 // Correct multithreaded version
 class Foo 
 { 
   private Helper helper = null;
   public synchronized Helper getHelper() {
     if (helper == null) 
         helper = new Helper();
     return helper;
     }
   // other functions and members...
 }
 

    
     这样的锁定实际上只在第一次访问getHelper函数并初始化Helper对象这个时间段有效,其他时间都是冗余的。因此在C++/C#等语言中大量使用了 Double-Checked Locking 模式进行优化,如
 
以下为引用:

 // Broken multithreaded version
 // "Double-Checked Locking" idiom
 class Foo 
 { 
   private Helper helper = null;
   public Helper getHelper() {
     if (helper == null) 
       synchronized(this) {
         if (helper == null) 
           helper = new Helper();
       }    
     return helper;
     }
   // other functions and members...
 }
 

    
     具体的模式原理和分析,可以参考关于ACE中此模式使用的文章。
     
     ACE中的Double Checked Locking 模式
     
     但在 Java 中,这种使用方法实际上是错误的
     
     The "Double-Checked Locking is Broken" Declaration
     
     这是因为 Java 语言规范和 C++/C# 等不同,是一个非常灵活的规范。Java 编译器可以自由地重排变量的初始化和访问顺序,以提高运行时效率;同时 Java 中的变量访问是可以被自动缓存到寄存器的,这也导致潜在的 JIT 编译器相关的依赖性错误。每个编译器可能有不同的实现方法,同样的代码在不同编译器和执行环境下可能有不同的表现,呵呵
     关于 Java 基于编译器的变量重排和内存模型的相关知识可以参考
     
     Synchronization and the Java Memory Model
     
     就目前来说,一种可行的解决办法是使用线程局部存储来保存 Singleton 实例,避免 JIT 对其进行优化
 
以下为引用:

 class Foo 
 {
  /** If perThreadInstance.get() returns a non-null value, this thread
  has done synchronization needed to see initialization of helper */

   private final ThreadLocal perThreadInstance = new ThreadLocal();  
   private Helper helper = null;
   
   public Helper getHelper() 
   {
      if (perThreadInstance.get() == null) createHelper();
      
      return helper;
   }
   
   private final void createHelper() 
   {
      synchronized(this) 
      {
          if (helper == null)
              helper = new Helper();
      }
   // Any non-null value would do as the argument here
      perThreadInstance.set(perThreadInstance);
   }
 }
 


    
     而要彻底解决这个问题,还是得等 JSR 133 提供的volatile 关键字,用于修饰变量的可变性,强制屏蔽编译器和 JIT 的优化工作。
 
以下为引用:

 // Works with acquire/release semantics for volatile
 // Broken under current semantics for volatile
 class Foo 
 {
   private volatile Helper helper = null;
   public Helper getHelper() 
   {
     if (helper == null) 
     {
       synchronized(this) 
       {
         if (helper == null)
           helper = new Helper();
       }
     }
     return helper;
   }
 }
 

   
     更详尽的资料可以参考 The Java Memory Model

posted @ 2004-07-08 11:06 Flier Lu 阅读(719) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1577440&run=.0999083

让我们回过头来看看P4架构下的Cache结构。

     The IA-32 Intel Architecture Software Developer's Manual, Volume 3: System Programming Guide

     Intel的系统变成手册中第十章介绍了IA32架构下的内存缓存控制。因为CPU速度和内存速度的巨大差距,CPU厂商通过在CPU中内置和外置多级缓存提高频繁使用数据的访问速度。一般来说,在CPU和内存之间存在L1, L2和L3三级缓存(还有几种TLB缓存在此不涉及),每级缓存的速度有一个数量级左右的差别,容量也有较大差别(实际上跟$有关,呵呵),而L1缓存更是细分为指令缓存和数据缓存,用于不同的目的。就P4和Xeon的处理器来说,L1指令缓存由Trace Cache取代,内置在NetBust微架构中;L1数据缓存和L2缓存则封装在CPU中,根据CPU档次不同,分别在8-16K和256-512K之间;而L3缓存只在Xeon处理器中实现,也是封装在CPU中,512K-1M左右。
     可以通过查看CPU信息的软件如CPUInfo查看当前机器的缓存信息,如我的系统为:
     P4 1.7G, 8K L1 Code Cache, 12K L1 Data Cache, 256K L2 Cache。

     而缓存在实现上是若干行(slot or line)组成的,每行对应内存中的一个地址上的连续数据,由高速缓存管理器控制读写中的数据载入和命中。其原理这里不多罗嗦,有兴趣的朋友可以自行查看Intel手册。需要知道的就是每个slot的长度在P4以前是32字节,P4开始改成64字节。而对缓存行的操作都是完整进行的,哪怕只读一个字节也需要将整个缓存行(64字节)全部载入,后面的优化很大程度上基于这些原理。

     就缓存的工作模式来说,P4支持的有六种之多,这里就不一一介绍了。对我们优化有影响的,实际上就是写内存时缓存的表现。最常见的WT(Write-through)写通模式在写数据到内存的同时更新数据到缓存中;而WB(Write-back)写回模式,则直接写到缓存中,暂不进行较慢的内存读写。这两种模式在操作频繁操作(每秒百万次这个级别)的内存变量处理上有较大性能差别。例如通过编写驱动模块操作MTRR强行打开WB模式,在Linux的网卡驱动中曾收到不错的效果,但对内存复制的优化帮助不大,因为我们需要的是完全跳过对缓存的操作,无论是缓存定位、载入还是写入。

     好在P4提供了MOVNTQ指令,使用WC(Write-combining)模式,跳过缓存直接写内存。因为我们的写内存操作是纯粹的写,写入的数据一定时间内根本不会被使用,无论使用WT还是WB模式,都会有冗余的缓存操作。优化代码如下:
 

以下为引用:

 global _fast_memcpy7

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy7:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]              ; number of QWORDS (8 bytes) assumes len / CACHEBLOCK is an integer
   shr ecx, 3

   lea esi, [esi+ecx*8]        ; end of source
   lea edi, [edi+ecx*8]        ; end of destination
   neg ecx                     ; use a negative offset as a combo pointer-and-loop-counter

 .copyloop:
   movq mm0, qword [esi+ecx*8]
   movq mm1, qword [esi+ecx*8+8]
   movq mm2, qword [esi+ecx*8+16]
   movq mm3, qword [esi+ecx*8+24]
   movq mm4, qword [esi+ecx*8+32]
   movq mm5, qword [esi+ecx*8+40]
   movq mm6, qword [esi+ecx*8+48]
   movq mm7, qword [esi+ecx*8+56]

   movntq qword [edi+ecx*8], mm0
   movntq qword [edi+ecx*8+8], mm1
   movntq qword [edi+ecx*8+16], mm2
   movntq qword [edi+ecx*8+24], mm3
   movntq qword [edi+ecx*8+32], mm4
   movntq qword [edi+ecx*8+40], mm5
   movntq qword [edi+ecx*8+48], mm6
   movntq qword [edi+ecx*8+56], mm7

   add ecx, 8
   jnz .copyloop

   sfence ; flush write buffer
   emms

   pop edi
   pop esi

   ret
 



     写内存的movq指令全部改为movntq指令,并在复制操作完成后,调用sfence刷新写缓存,因为缓存中内容可能已经失效了。这样一来在写内存外的载入缓存操作,以及缓存本身的操作都被省去,大大减少了冗余内存操作。按AMD的说法能有60%的性能提升,我实测也有50%左右明显的性能提升。
     movntq和sfence等指令可以参考Intel的指令手册:

     The IA-32 Intel Architecture Software Developer's Manual, Volume 2A: Instruction Set Reference, A-M
     The IA-32 Intel Architecture Software Developer's Manual, Volume 2B: Instruction Set Reference, N-Z


     在优化完写内存后,同样可以通过对读内存的操作进行优化提升性能。虽然CPU在读取数据时,会有一个自动的预读优化,但在操作连续内存区域时显式要求CPU预读数据,还是可以明显地优化性能。
 

以下为引用:

 global _fast_memcpy8

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy8:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]              ; number of QWORDS (8 bytes) assumes len / CACHEBLOCK is an integer
   shr ecx, 3

   lea esi, [esi+ecx*8]        ; end of source
   lea edi, [edi+ecx*8]        ; end of destination
   neg ecx                     ; use a negative offset as a combo pointer-and-loop-counter

 .writeloop:
   prefetchnta [esi+ecx*8 + 512] ; fetch ahead by 512 bytes

   movq mm0, qword [esi+ecx*8]
   movq mm1, qword [esi+ecx*8+8]
   movq mm2, qword [esi+ecx*8+16]
   movq mm3, qword [esi+ecx*8+24]
   movq mm4, qword [esi+ecx*8+32]
   movq mm5, qword [esi+ecx*8+40]
   movq mm6, qword [esi+ecx*8+48]
   movq mm7, qword [esi+ecx*8+56]

   movntq qword [edi+ecx*8], mm0
   movntq qword [edi+ecx*8+8], mm1
   movntq qword [edi+ecx*8+16], mm2
   movntq qword [edi+ecx*8+24], mm3
   movntq qword [edi+ecx*8+32], mm4
   movntq qword [edi+ecx*8+40], mm5
   movntq qword [edi+ecx*8+48], mm6
   movntq qword [edi+ecx*8+56], mm7

   add ecx, 8
   jnz .writeloop

   sfence ; flush write buffer
   emms

   pop edi
   pop esi

   ret
 



     增加一个简单的prefetchnta指令,提示CPU在处理当前读取内存操作的同时,预读前面512字节处的一个缓存行64字节内容。这样一来又可以有10%左右的性能提升。
     最后,对正在处理的内存,可以通过显式的内存读取操作,强制性要求其载入到缓存中,因为prefetchnta指令还只是一个提示,可以被CPU忽略。这样可以再次获得60%左右的性能提示,我实测没有这么高,但是也比较明显。
 
以下为引用:

 global _fast_memcpy9

 %define param       esp+12+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 %define CACHEBLOCK 400h

 _fast_memcpy9:
   push esi
   push edi
   push ebx

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]              ; number of QWORDS (8 bytes) assumes len / CACHEBLOCK is an integer
   shr ecx, 3

   lea esi, [esi+ecx*8]        ; end of source
   lea edi, [edi+ecx*8]        ; end of destination
   neg ecx                     ; use a negative offset as a combo pointer-and-loop-counter

 .mainloop:
   mov eax, CACHEBLOCK / 16    ; note: .prefetchloop is unrolled 2X
   add ecx, CACHEBLOCK         ; move up to end of block

 .prefetchloop:
   mov ebx, [esi+ecx*8-64]     ; read one address in this cache line...
   mov ebx, [esi+ecx*8-128]    ; ... and one in the previous line
   sub ecx, 16                 ; 16 QWORDS = 2 64-byte cache lines
   dec eax
   jnz .prefetchloop

   mov eax, CACHEBLOCK / 8

 .writeloop:
   prefetchnta [esi+ecx*8 + 512] ; fetch ahead by 512 bytes

   movq mm0, qword [esi+ecx*8]
   movq mm1, qword [esi+ecx*8+8]
   movq mm2, qword [esi+ecx*8+16]
   movq mm3, qword [esi+ecx*8+24]
   movq mm4, qword [esi+ecx*8+32]
   movq mm5, qword [esi+ecx*8+40]
   movq mm6, qword [esi+ecx*8+48]
   movq mm7, qword [esi+ecx*8+56]

   movntq qword [edi+ecx*8], mm0
   movntq qword [edi+ecx*8+8], mm1
   movntq qword [edi+ecx*8+16], mm2
   movntq qword [edi+ecx*8+24], mm3
   movntq qword [edi+ecx*8+32], mm4
   movntq qword [edi+ecx*8+40], mm5
   movntq qword [edi+ecx*8+48], mm6
   movntq qword [edi+ecx*8+56], mm7

   add ecx, 8
   dec eax
   jnz .writeloop

   or ecx, ecx ; assumes integer number of cacheblocks
   jnz .mainloop

   sfence ; flush write buffer
   emms

   pop ebx
   pop edi
   pop esi

  ret
 



     至此,一个完整的内存复制函数的优化流程就结束了,通过对缓存的了解和使用,一次又一次地超越自己,最终获得一个较为令人满意地结果。(号称300%性能提示,实测175%-200%,也算相当不错了)

     在编写测试代码的时候需要注意两点:

     一是计时精度的问题,需要使用高精度的物理计数器,避免误差。推荐使用rdtsc指令,然后根据CPU主频计算时间。CPU主频可以通过高精度计时器动态计算,我这儿偷懒直接从注册表里面读取了 :P
     代码如下:
 

以下为引用:

 #ifdef WIN32
 typedef __int64 uint64_t;
 #else
 #include <stdint.h>
 #endif

   bool GetPentiumClockEstimateFromRegistry(uint64_t& frequency)
   {
     HKEY hKey;

     frequency = 0;

     LONG rc = ::RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Hardware\Description\System\CentralProcessor\0", 0, KEY_READ, &hKey);

     if(rc == ERROR_SUCCESS)
     {
       DWORD cbBuffer = sizeof (DWORD);
       DWORD freq_mhz;

       rc = ::RegQueryValueEx(hKey, "~MHz", NULL, NULL, (LPBYTE)(&freq_mhz), &cbBuffer);

       if (rc == ERROR_SUCCESS)
         frequency = freq_mhz * MEGA;

       RegCloseKey (hKey);
     }

     return frequency > 0;
   }

   void getTimeStamp(uint64_t& timeStamp)
   {
 #ifdef WIN32
     __asm
     {
       push  edx
         push  ecx
         mov   ecx, timeStamp
         //_emit 0Fh // RDTSC
         //_emit 31h
         rdtsc
         mov   [ecx], eax
         mov   [ecx+4], edx
         pop   ecx
         pop   edx
     }
 #else
     __asm__ __volatile__ ("rdtsc" : "=A" (timeStamp));
 #endif
   }
 


     二是测试内存复制的缓冲区的大小,如果缓冲区过小,第一次拷贝两个缓冲区时就会导致所有数据都被载入L2缓存中,得出比普通内存操作高一个数量级的数值。例如我的L2缓冲为256K,如果我用两个128K的缓冲区对着拷贝,无论循环多少次,速度都在普通内存复制的10倍左右。因此设置一个较大的值是必要的。


 btw: 不早了,先草草写到这里,明天再润色吧 :D

posted @ 2004-07-08 11:05 Flier Lu 阅读(1889) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1618917&run=.022BCC2

在本系列文章的前两篇文章中,简要地介绍了 Win32 调试接口中用户态调试器结构和调试事件的相关知识

     Win32 调试接口设计与实现浅析 [1] 用户态调试器结构初探
     Win32 调试接口设计与实现浅析 [2] 调试事件

     在这一小节中,将进一步展开分析 NT 系统核心中对上述功能提供支持的调试子系统的创建过程。

     从前面两个小节我们可以了解到,Win32 调试接口对用户态调试器来说,实际上绝大多数工作都是通过一个调试界面端口"DbgUiApiPort"完成的。用户态调试器通过此端口完成对调试子系统的挂接,进而接收和处理调试事件。因此,对调试子系统的分析也将从此端口开始。

     首先来看看调试子系统以及调试界面和调试服务端口的创建过程。

     在《Windows 2000 内部揭密》的第四章中,Solomon详细地介绍了 NT 系统启动的整个过程。其中SMSS (Session Manager) 是启动程序 NTLDR 载入运行的第一个本机(Native)应用程序(不使用 Win32 子系统的 API),其被作为操作系统一部分受到信任,完成系统初始化工作。Win32 子系统 CSRSS(Client-Server Runtime SubSystem)和系统登陆进程 WinLogon 在 SMSS 初始化工作完成后被载入执行,以实际完成接受用户登陆运行的工作。其中 SMSS 系统初始化的工作就包括对调试子系统的初始化。一个启动后的进程树实例如下:
 

以下为引用:

 System(4)
     smss.exe(388)
         csrss.exe(436)
         winlogon.exe(460)
             services.exe(504)
             lsass.exe(516)
 


     smss.exe的入口函数(smserversmss.c:28)首先检查从 NTLDR 通过命令行传入的参数中是否有调试参数,如果有则将之分析后放入 SmpDebug 全局变量(smserversmsrvp.h:82)中;然后调用 SmpInit 函数(smserversminit.c:683)初始化Session Manager。
     SmpInit 函数在完成初始化工作、构造 SMSS 服务端口 "SmApiPort" 和两个用于处理向 SMSS 发送服务请求的线程后,会调用 SmpLoadDataFromRegistry 函数(smserversminit.c:934)从注册表 HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession Manager 键中载入 Session Manager 的相关参数。
     Session Manager配置注册表键下SubSystems子键的Required、Optional和Kmode三个键,定义了系统支持的子系统类型。通常情况下,Required包括Debug和Windows子系统;Optional包括可选的Posix子系统;Kmode定义核心子系统Windows在核心态的实现win32k.sys。而子系统名字又进一步指向实现子系统的可执行文件映象。一个典型的设置如下:
 
以下为引用:

 HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerSubSystems

 Required = "Debug Windows"
 Optional = "Posix"

 Kmode = "%SystemRoot%system32win32k.sys"

 Debug = ""

 Windows = "%SystemRoot%system32csrss.exe ObjectDirectory=Windows SharedSection=1024,3072,512 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserServerDllInitialization,3 ServerDll=winsrv:ConServerDllInitialization,2 ProfileControl=Off MaxRequestThreads=16"

 Posix = "%SystemRoot%system32psxss.exe"
 



     其中Required中包括的子系统,SMSS将自动载入并初始化之。值得注意的是SubSystems子键中的Debug与Windows、Posix等其他子系统不同,并没有指向实际的可执行文件。因为调试子系统是由 SmpLoadDataFromRegistry 函数根据子系统名字是否为 debug,判断在调用执行载入子系统命令的 SmpExecuteCommand 函数(smserversminit.c:3235)时,是否带 SMP_DEBUG_FLAG 标志,表示当前需要载入的是调试子系统。而 SmpExecuteCommand 函数入口处一旦发现标志参数包含 SMP_DEBUG_FLAG 标志,就立刻调用 SmpLoadDbgSs 函数(smserversmdbg.c:108)实际载入调试子系统并直接返回,不再进一步解析和执行命令。

     完整的初始化调试子系统函数调用流程如下:
 

以下为引用:

   main                    (smserversmss.c:28)
   SmpInit                 (smserversminit.c:683)
   SmpLoadDataFromRegistry (smserversminit.c:934)
   SmpExecuteCommand       (smserversminit.c:3235)
   SmpLoadDbgSs            (smserversmdbg.c:108)
 


     Win2003下处理子系统载入的部分代码被放入一个独立的 SmpLoadSubSystemsForMuSession 函数中,而 debug 子系统则改为在每个Session中载入。也就是说传入 SmpExecuteCommand 函数的 SMP_DEBUG_FLAG 标志会导致此函数直接退出。<TBD>

   NT 下 SmpLoadDbgSs 函数中分别对调试子系统中用户态和核心态调试时间响应端口和处理线程做了初始化。伪代码如下:
 

以下为引用:

 NTSTATUS SmpLoadDbgSs(IN PUNICODE_STRING DbgSsName)
 {
   NTSTATUS st = DbgpInit(); // 初始化用户态调试器环境

   if(!NT_SUCCESS(st)) return st;

   st = DbgSsInitialize(...); // 初始化核心态调试器环境

   SmpDbgSsLoaded = TRUE; // 调试子系统已经成功载入

   return STATUS_SUCCESS;
 }
 



     DbgInit 函数(smserverdbginit.c:26)中首先完成对应用程序线程Hash表的初始化;然后构造一个具有所有访问权限的安全描述符;使用此安全描述符创建两个LPC端口对象"DbgSsApiPort"和"DbgUiApiPort",分别被用户态调试器用于连接调试服务和调试界面;最后创建两个线程分别处理这两个端口上的调试事件,线程由 DbgpSsApiLoop 函数(smserverdbgloop.c:123)和 DbgpUiApiLoop 函数(smserverdbgloop.c:288)完成实际事件处理工作。

     DbgSsInitialize 函数(ntosdlldllssstb.c:429)中则先建立与用户态调试器的调试服务端口的链接;然后初始化核心态调试器调试服务的相关全局变量;最后创建用户线程使用DbgSspSrvApiLoop函数(ntosdlldllssstb.c:737)处理核心调试事件。
     其中核心态调试器的相关实现本节暂且不涉及,等后面具体讨论核心态调试器的原理时再详细分析。

     下一节中将详细分析调试子系统中调试服务端口和调试界面端口的事件处理线程工作流程,以及如何与用户态调试器配合完成调试工作。

posted @ 2004-07-08 11:03 Flier Lu 阅读(678) 评论(2) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1603607&run=.0F06293

随着应用安全性逐渐受到重视,这方面的书籍也越来越多,刚刚整理了一下自己手头关于.NET安全编程方面的书,发现有几本还是很不错的,顺便推荐一把。

     O'Reilly 的书始终是有品质保障的,Programming .NET Security也非常不错,amazon上4.5星评价。虽然从销售量来看好像不如其他几本,但从我购买的十几本 O'Reilly 书籍的平均水平来看,肯定还是值得一读的。
     从内容上,全书700多页分为5部分,原理、安全、加密、框架和手册,覆盖面还是很全的,而且结构设置比较合理,有大局观,比较适合开发人员阅读、查阅。
 

以下为引用:

 

 Programming .NET Security

 By Adam Freeman, Allen Jones

 Publisher : O'Reilly
 Pub Date : June 2003
 ISBN : 0-596-00442-7
 Pages : 714

     With the spread of web-enabled desktop clients and web-server based applications, developers can no longer afford to treat security as an afterthought. It's one topic, in fact, that .NET forces you to address, since Microsoft has placed security-related features at the core of the .NET Framework. Yet, because a developer's carelessness or lack of experience can still allow a program to be used in an unintended way, Programming .NET Security shows you how the various tools will help you write secure applications.

 Part I:  Fundamentals

     Discusses the need for security and the approaches to adopt when developing secure software. These chapters also discuss assemblies and application domains—two fundamental building blocks of .NET applications that play a crucial role in the creation of secure software:

   Chapter 1.  Security Fundamentals
   Chapter 2.  Assemblies
   Chapter 3.  Application Domains
   Chapter 4.  The Lifetime of a Secure Application

 Part II:  .NET Security

     Contains information about the security-related features provides by the .NET runtime. These chapters describe how the runtim enforces application security and how you can manipulate, customize, and extend runtime security to meet your own security requirements:

   Chapter 5.  Introduction to Runtime Security
   Chapter 6.  Evidence and Code Identity
   Chapter 7.  Permissions
   Chapter 8.  Security Policy
   Chapter 9.  Administering Code-Access Security
   Chapter 10.  Role-Based Security
   Chapter 11.  Isolated Storage

 Part III:  .NET Cryptography

     Provides a description of modern cryptographic techniques and details the implementation of these techniques provided by the .NET Framework class library. These chapters demonstrate the use of each implementationand show you how to extend the functionality of the .NET class library by implementing your own cryptographic algorithms:

   Chapter 12.  Introduction to Cryptography
   Chapter 13.  Hashing Algorithms
   Chapter 14.  Symmetric Encryption
   Chapter 15.  Asymmetric Encryption
   Chapter 16.  Digital Signatures
   Chapter 17.  Cryptographic Keys

 Part IV:  .NET Application Frameworks

     Discusses other aspects of .NET Framework security not specifically related to runtime security of cryptography. These include ASP.NET application security, integration with the security-related features of Enterprise Services (COM+), and the use of the Windows Event Log for recording security events:

   Chapter 18.  ASP.NET Application Security
   Chapter 19.  COM+ Security
   Chapter 20.  The Event Log Service

 Part V:  API Quick Reference

     Provides a quick reference to all types defined in the security-related namespaces of the .NET Framework base clase library:

   Chapter 21.  How to Use This Quick Reference
   Chapter 22.  Converting from C# to VB Syntax
   Chapter 23.  The System.Security Namespace
   Chapter 24.  The System.Security.Cryptography Namespace
   Chapter 25.  The System.Security.Cryptography.X509Certificates Namespace
   Chapter 26.  The System.Security.Cryptography.Xml Namespace
   Chapter 27.  The System.Security.Permissions Namespace
   Chapter 28.  The System.Security.Policy Namespace
   Chapter 29.  The System.Security.Principal Namespace
 


     Addison Wesley出版的.NET Framework Security 则并不仅仅面向开发人员,因此从原理到配置到编程都有提及,而且有一些其他书籍没有提及的特色章节,例如讨论了Hosting Managed Code时的安全问题等等。Amazon上4星评价,销量是最大的。

以下为引用:

 

 .NET Framework Security

 By Brian A. LaMacchia, Sebastian Lange, Matthew Lyons, Rudi Martin, Kevin T. Price

 Publisher : Addison Wesley
 Pub Date : April 24, 2002
 ISBN : 0-672-32184-X
 Pages : 816
 Slots : 2

     .NET Framework Security provides the ultimate high-end comprehensive reference to all of the new security features available in .NET. Through extensive code samples and step-by-step walkthroughs of configuration techniques, the reader is taken deep into the world of secure applications. Demonstrations of creating custom procedures and a full explanation of each aspect separate this book from many other "lecture books." Many of the concepts expressed in this book are not only viable in .NET, but on the Internet in general. These factors combined make this the one reference that every developer and system administrator should have.

 Part I.  Introduction to the .NET Developer Platform Security

   Chapter 1.  Common Security Problems on the Internet
   Chapter 2.  Introduction to the Microsoft .NET Developer Platform
   Chapter 3.  .NET Developer Platform Security Solutions

 Part II: Code Access Security Fundamentals

   4 User-and Code-Identity–Based Security: Two Complementary Security Paradigms
   5 Evidence: Knowing Where Code Comes From
   6 Permissions: The Workhorse of Code Access Security
   7 Walking the Stack
   8 Membership Conditions, Code Groups, and Policy Levels: The Brick and Mortar of Security Policy
   9 Understanding the Concepts of Strong Naming Assemblies
   10 Hosting Managed Code
   11 Verification and Validation: The Backbone of .NET Framework Security
   12 Security Through the Lifetime of a Managed Process: Fitting It All Together

 Part III: ASP.NET and Web Services Security Fundamentals

   13 Introduction to ASP.NET Security
   14 Authentication: Know Who Is Accessing Your Site
   15 Authorization: Control Who Is Accessing Your Site
   16 Data Transport Integrity: Keeping Data Uncorrupted

 Part IV: .NET Framework Security Administration

   17 Introduction: .NET Framework Security and Operating System Security
   18 Administering Security Policy Using the .NET Framework Configuration Tool
   19 Administering .NET Framework Security Policy Using Scripts and Security APIs
   20 Administering an IIS Machine Using ASP.NET
   21 Administering Clients for .NET Framework Mobile Code
   22 Administering Isolated Storage and Cryptography Settings in the .NET Framework

 Part V: .NET Framework Security for Developers

   23 Creating Secure Code: What All .NET Framework Developers Need to Know
   24 Architecting a Secure Assembly
   25 Implementing a Secure Assembly
   26 Testing a Secured Assembly
   27 Writing a Secure Web Site Using ASP.NET
   28 Writing a Secure Web Application in the .NET Development Platform
   29 Writing a Semi-Trusted Application
   30 Using Cryptography with the .NET Framework: The Basics
   31 Using Cryptography with the .NET Framework: Advanced Topics
   32 Using Cryptography with the .NET Framework: Creating and Verifying XML Digital Signatures
 


     Sybex的.NET Development Security Solutions相对来说平和一些,没有太多可圈可点之处,但也算中规中矩。不过好处是有电子工业出版社翻译的中文版《.NET开发安全解决方案应用编程》可以看,呵呵

以下为引用:

 

 .NET Development Security Solutions

 by John Paul Mueller   ISBN:0782142664

 Sybex ? 2003 (471 pages)

     This guide leads you through the differences in Studio in the .NET framework that didn't appear in older versions of Visual Studio, helps you understand the new rules for .NET security, and helps you fix problems created by holes in the .NET security.

 Part I - Introduction to .NET Security
   Chapter 1 - Understanding .NET Security
   Chapter 2 - .NET Framework Security Overview
   Chapter 3 - Avoiding Common Errors and Traps
 Part II - Desktop and LAN Security
   Chapter 4 - .NET Role-Based Security Techniques
   Chapter 5 - Policies and Code Groups in Detail
   Chapter 6 - Validation and Verification Issues
   Chapter 7 - .NET Cryptographic Techniques
   Chapter 8 - LAN Security Requirements
 Part III - Web-based Security
   Chapter 9 - Web Server Security
   Chapter 10 - Web Data Security
   Chapter 11 - Securing XML and Web Services
 Part IV - Other Security Topics
   Chapter 12 - Active Directory Security
   Chapter 13 - Wireless Device Security
   Chapter 14 - Win32 API Overview
   Chapter 15 - Win32 API Advanced Techniques
 


     Prentice Hall PTR出版的.NET Security and Cryptography一书则较为偏向于.NET架构下的密码学相关的原理和使用,虽然也有提及安全,但显然不是其重点。
 

以下为引用:

 

 .NET Security and Cryptography

 By Peter Thorsteinson, G. Gnana Arun Ganesh

 Publisher : Prentice Hall PTR
 Pub Date : August 18, 2003
 ISBN : 0-131-00851-X
 Pages : 496

 Chapter One.    .NET Cryptography and Security
 Chapter Two.    Fundamentals of Cryptography
 Chapter Three.  Symmetric Cryptography
 Chapter Four.   Asymmetric Cryptography
 Chapter Five.   Digital Signatures
 Chapter Six.    XML Cryptography
 Chapter Seven.  .NET User-Based Security
 Chapter Eight.  .NET Code Access Security
 Chapter Nine.   ASP.NET Security
 Chapter Ten.    Web Services Security
 


     此外还有一本清华大学出版社引进的.NET Security Programming,因为手头没有电子版,只能凭其目录和amazon上的评价大概了解,感觉内容有些杂,而且没有太多能够强烈吸引我的章节标题,呵呵。

以下为引用:

 

 原书名:    .NET Security Programming
 原出版社:  John Wiley & sons,Inc.
 作者:      (美)Donis Marshall
 译者:      余波 张立浩
 书号:      7-302-07252-3
 页码:      238
 市场价:     ¥35.00
 开本:      16开
 丛书名:
 出版社:    清华大学出版社
 出版日期:  2003-10-1
 


     关于ASP.NET安全方面的问题,上述书籍中虽都有提及,但不够详细,不如专门去买本ASP.NET方面的安全书籍来看,这里就不一一列举了。

 短期内上述书籍可以从以下地址下载:

 Programming .NET Security
 .NET Framework Security
 .NET Development Security Solutions
 .NET Security and Cryptography

posted @ 2004-07-08 11:00 Flier Lu 阅读(1345) 评论(3) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1601113&run=.01EAE25

  Keith Brown在4月份的MSDN杂志上发表了一篇讨论.NET下安全性的文章,Beware of Fully Trusted Code,其中详细讨论了让 Managed Code 允许在 Fully Trusted 模式下的危害。真是不看不知道,呵呵,现在我是深深感到了 Fully Trusted Code 的可怕。:D

     对一个类型的私有成员变量来说,其私有性的保护实际上只是编译期的
 

以下为引用:

 class DiskQuota {
 private:
   long MinBytes;
   long MaxBytes;
 };
 

    
     以上类型的私有成员变量实际上可以简单的通过unsafe代码中的指针操作访问
 
以下为引用:

 void EvilCode(DiskQuota* pdq) {
    // use pointer arithmetic to index
    // into the object wherever we like!
    ((long*)pdq)[1] = MAX_LONG;
 }    
 

    
     好在CLR能够通过类型系统完整性检测,一定程度上解决这类有意或缓冲区溢出的问题,除非在程序中显式的要求CLR关闭此类检测。如
 
以下为引用:

 // evilcode.cs
 using System.Security.Permissions;

 [assembly: SecurityPermission(
    SecurityAction.RequestMinimum,
    Flags=SecurityPermissionFlag.SkipVerification)]

 // your evil code goes here
 


    
     麻烦的是一旦使用/unsafe参数编译C#程序,或者使用Managed C++编写程序,这个权限就被隐式地设置为最低,以保障代码地正确执行。虽然新版本的C#编译器将提供参数选项打开此权限,但最根本的解决方法还是得通过.NET安全策略来完成。可惜在缺省状态下,本地硬盘上的Managed程序都是在 Fully Trusted 模式下运行的。
     可以通过控制面板的管理工具中的"Microsft .NET Framework 1.1配置"工具,在 我的电脑运行库安全策略计算机代码组All_Code 上通过 编辑代码组属性 看到当前的安全策略设置。安全策略一般分为企业、计算机和用户三级,每级又可根据一定的代码组条件分为多个代码组,设置不同的权限集。
     我们接下来看看 Fully Trusted 权限的“威力”所在吧 :P
     
     首先是所谓的私有方法,众所周知它们是可以通过Reflection被直接调用的,呵呵,如下:    
 
以下为引用:

 using System;
 using System.Reflection;

 class EvilCodeWithFullTrust
 {
   static void CallPrivateMethod(object o, string methodName) {
     Type t = o.GetType();
     MethodInfo mi = t.GetMethod(methodName,
        BindingFlags.NonPublic |
        BindingFlags.Instance);
     mi.Invoke(o, null);
   }
   static void Main() {
     CallPrivateMethod(new NuclearReactor(), "Meltdown");
     }
 }
 


    
     比较好的解决方法是通过检测调用者是否被信任,来保障私有函数不被滥用,如使用StrongNameIdentityPermission属性限制调用者所在assembly必须有相同的public key,可以参见我以前的一篇 BLog 文章《关于信任粒度的讨论》
 
以下为引用:

 public class NuclearReactor {
   [StrongNameIdentityPermission(
     SecurityAction.LinkDemand,
     PublicKey="002400000...")]
   private void Meltdown() {
     // calling assembly must have specified public key!
   }
 }
 

    
     这样可以禁止直接通过Reflection访问私有成员函数或变量。但如果调用者代码具有 Fully Trusted 权限的话,它可以直接使用命令行工具 caspol -s off 或者通过代码,完全关闭 StrongNameIdentityPermission 进行的检测,呵呵
 
以下为引用:

 using System.Security;
 class EvilCodeWithFullTrust {
   static void Main() {
     SecurityManager.SecurityEnabled = false;
     // now call Meltdown via reflection!
   }
 }
 

    
     改变SecurityManager.SecurityEnabled的值需要SecurityPermissionFlag.ControlPolicy权限,对应于配置中安全性下的允许策略控制权限, Fully Trusted 情况下是打开的 :(
     
     既然系统自动的调用者验证可以被跳过,是否能通过程序手工进行验证呢?以下代码在调用不可完全信任的插件之前,通过降低权限来限定插件的能力,可以说这是一种非常好的安全编程习惯。
 
以下为引用:

 using System.Security;
 using System.Security.Permissions;

 class WellMeaningCode {
   public void CallPlugIn(EvilCode plugin) {
     // put a CAS modifier on the stack that denies all file system access
     new FileIOPermission(
       PermissionState.Unrestricted).Deny();
     plugin.DoWork();
     CodeAccessPermission.RevertDeny();
   }
 }
 


    
     但是在 Fully Trusted 权限下还是无能为力,直接对PermissionSet.Assert函数的调用使得权限检查再一次被跳过。
 
以下为引用:

 class EvilCodeWithFullTrust {
   void DoWork() {
     new PermissionSet(
       PermissionState.Unrestricted).Assert();
     // happily access the file system
     // regardless of the caller's deny!
       }
 }
 

     前面提到配置工具中对安全策略的配置有企业、机器和用户三层,实际上还有AppDomain这一层,可以通过AppDomain.SetAppDomainPolicy载入单独的安全策略限定其后需要执行的代码,ASP.NET就是通过这种方法限定执行代码的权限。但是具有 Fully Trusted 权限的代码可以打破 AppDomain 边界的限制,并可以通过调用其他 Unmanaged Code 实现对其他 AppDomain 侵入。好在可以通过修改 machine.config 文件让 ASP.NET 运行在较低的权限集中,如
 

以下为引用:

 <configuration>
   <system.web>
     <securityPolicy>
       <trustLevel name="Full" policyFile="internal" />
       <trustLevel name="High" policyFile="web_hightrust.config" />
       <trustLevel name="Medium" policyFile="web_mediumtrust.config" />
       <trustLevel name="Low" policyFile="web_lowtrust.config" />
       <trustLevel name="Minimal" policyFile="web_minimaltrust.config" />
     </securityPolicy>
     <!--  level="[Full|High|Medium|Low|Minimal]" -->
     <trust level='Medium'/>
   </system.web>
 </configuration>
 

    
     但因为很多程序员没有耐心完成最小权限集的调整工作,导致大部分情况下为了省事将缺省权限设置为 Fully Trusted,导致上述各种精心设计的安全检测都形同虚设。再好的技术如果没有相应的管理来保障,只是摆设而已。
     解决这个问题,一方面需要程序员对其代码的权限做限定和调整,另一方面需要开发厂商提供静态和动态的权限分析工具辅助,最后还得配合传统的NT权限设置来把关,毕竟CLR还是运行在NT的用户的权限集下。
     
     有兴趣的朋友可以进一步阅读这篇文章:Writing managed code for semi-trusted environment
     
     此外 Brown 还是 Programming Windows Security 一书的作者,他在书中详细介绍了 Windows 环境下的安全子系统的运行机制和使用方法,并一定程度上涉及到了网络认证、COM+和Internet环境下的安全性问题,非常详尽值得一读。此书已经由电力出版社翻译出版 《Windows安全性编程》
     

posted @ 2004-07-08 10:58 Flier Lu 阅读(1442) 评论(0) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1587099&run=.0C9B086

    MS的Fusion组成员Junfeng Zhang在其中文版本BLog上发表了一篇文章,CLR Loader and Java Class Loader Compared,简要地比较了CLR Loader和Java Class Loader。得出的结论是Java Class Loader在CLR中并不存在等价的东西,而且他在分析过程中详细地评述了Java Class Loader 的弱点,非常精彩,呵呵,感谢 Zhang 的努力 :)

     不过就我个人观点来看,CLR和JVM只是通过两个不同的思路实现了相同的概念,各有千秋。
     
     Java Class Loader与CLR Loader相比,实际上最大的缺陷就是对类型和版本的控制粒度不足,如此一来就造成Zhang所说的类型载入上的共享、安全和版本控制问题。这个问题一方面因为JVM设计目标就和CLR有所不同;另一方面可能因为Sun没有经历过MS的Dll Hell,处理这种问题经验不足和重视程度不够,呵呵
     从设计目标和指导思想来说,JVM的结构可以归结于两个S:Simple and Small。毕竟JVM最初是为嵌入家电系统设计的,各种资源都非常有限,而对不同来源不同版本类型的side by side运行,可以说基本上没有什么需求。分析过Java Class和CLR Metadata格式的朋友就会发现,两者结构上的复杂度可以说相差几个数量级,呵呵。而这种Simple and Small的设计思路,可以说是Java早期成功的最大助力,但也随着Java的飞速发展成为其硬伤之一。
     
     与此同时,JVM实现结构的简单也促使Java在设计上肯下功夫,通过设计上的复杂性和灵活性来弥补实现上的简单带来的缺陷。例如 Zhang 所说JVM Class Loader的很多问题,除了不同Class Loader之间类型共享外,其实都可以通过定制Class Loader来实现,而且我记得已经有类似的解决方案,只是并没有归纳进标准中去。而类型共享和安全方面的一些问题,就我的理解,JVM中Class Loader实际上有点象一个为类型和安全定制的轻量级AppDomain,是一个独立的隔离域。因此并不能完全将两个Class Loader的类型之间的问题,等同于一个AppDomain的不同类型之间的问题来比较看待。
     而对于CLR来说,功能点一级的实现可以说比较完美了,长期以来对Dll Hell和类型共享的强烈需求,促使MS从COM到CLR一直在完善版本和类型控制系统。但是结构上感觉还是有些松散,缺乏一个类似Class Loader的组件,将分散在多个基础类上的功能聚合到一起。CLR把太多的细节隐藏在底层实现中,甚至过于依赖对实现的控制,秉承了MS作为操作系统厂商一贯的风格,呵呵。
     
     这也体现了Java和MS两大阵营的风格不同:前者注重于开放性的设计;后者注重于实用性的实现。
     
     如果Java能将CLR的版本和签名吸收进去,CLR多提供一些面向应用而非面向实现的工具类,这个世界将会美好得多 :P

 btw: 技术方面的观点近期补上 :)

posted @ 2004-07-08 10:57 Flier Lu 阅读(931) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1558534&run=.0F8ED5D

 Raymond Chen完成了一个非常出色的系列文章,讨论了从16bit x86架构到 IA64/AMD64、从CISC到RISC的各种调用约定(calling convention)。加上其后的讨论,让我们能够从头到尾对调用约定的发展有所了解。

     The history of calling conventions, part 1中讨论了16bit的x86架构下的调用约定。这些调用约定都需要对 BP, SI 和 DI 寄存器进行保护,并通过 AX 或 DX:AX 返回 16/32 位返回值。
 

以下为引用:

 调用约定    语言      参数压栈顺序  清理堆栈方  使用寄存器
 __cdecl     C           从右到左      调用者
 __pascal    Pascal      从左到右     被调用者
 __fortran   Fortran     从左到右     被调用者
 __fastcall  C/Delphi    从右到左      调用者      DX, CX (MS)
                                                                          AX, DX, CX (Borland)
 


     因为Pascal调用由被调用者清理堆栈,可以让调用函数者省下3个字节的代码,所以被Windows早期版本定为API的标准调用约定,并一直沿用下来。这让我想起那个罗马时代的两匹马屁股宽度,决定了现代火箭推动器宽度的笑话 :D 虽然这个笑话没什么真实性,但Windows下这种问题的确存在 :P

     The history of calling conventions, part 2中讨论了RISC架构下的一些常见处理器上的调用约定,如Alpha AXP、MIPS R4000、PowerPC。
     可以发现这些RISC处理器,包括后面要讨论的IA64处理器,基本上都是通过寄存器而非堆栈来传递参数的,毕竟RISC的寄存器多啊,呵呵,羡慕的说。而且这些RISC处理器一般硬件上就限定了唯一的调用约定,也就是传递参数的寄嫫鞯墓潭ㄐ蛄小?

     The history of calling conventions, part 3中讨论了我们比较熟悉的32位 x86 架构下的调用约定。这些调用约定都需要对EDI, ESI, EBP 和 EBX 寄存器进行保护,并通过 EAX 或 EDX:EAX 返回 32/64 位返回值。
 

以下为引用:

 调用约定    参数压栈顺序  清理堆栈方  使用寄存器    函数名编码方式
 __cdecl       从右到左      调用者                    加 '_' 前缀
 __stdcall     从右到左     被调用者                   加 '_' 前缀,'@' 后缀加上参数字节数字
 __fastcall    从右到左     被调用者   ECX, EDX (MS)   加 '@' 前缀,'@' 后缀加上参数字节数字
                                     EAX, ECX, EDX (Borland)
 thiscall      从右到左      被调用者   ECX (this)      C++编译器相关编码算法
 


     MSDN上有一个非常详细的调用示例图
 
以下为引用:

 The following example shows the results of making a function call using various calling conventions.
 This example is based on the following function skeleton. Replace calltype with the appropriate calling convention.

 void    calltype MyFunc( char c, short s, int i, double f );
 .
 .
 .
 void    MyFunc( char c, short s, int i, double f )
     {
     .
     .
     .
     }
 .
 .
 .
 MyFunc ('x', 12, 8192, 2.7183);


 __cdecl
 The C decorated function name is “_MyFunc.”

 The__cdecl calling convention
 

 __stdcall and thiscall
 The C decorated name (__stdcall) is “_MyFunc@20.” The C++ decorated name is proprietary.

 The __stdcall and thiscall calling conventions
 

 __fastcall
 The C decorated name (__fastcall) is “@MyFunc@20.” The C++ decorated name is proprietary.

 The__fastcall calling convention
 
 


     The history of calling conventions, part 4: ia64中讨论了Itanium中128个整数寄存器如何被函数调用使用。
     其中r0到r32作为全局寄存器不参与共享,胜于96个寄存器用于局部使用。例如调用一个函数时用到六个参数(r32-r37)和三个返回参数寄存器(r38, r39, r40),则寄存器r41-r127可以被使用。然后在此函数中再调用一个函数,参数就从r38开始存放,如三个参数r38, r39, r40。当调用call后,CPU会做一个保护操作,将原本上一层函数调用中使用的参数寄存器r32-r37保存到一个独立的寄存器堆栈中,而将此次函数调用的参数寄存器移动到开始处,r32 = r38, r33 = r39, r34 = r40,因此对每个函数本身来说,其调用参数永远是从r32开始的。而因为此过程中根本不使用堆栈,也就不存在x86架构下清理堆栈的问题。而返回值直接通过一个预定义寄存器r8返回,因此也不存在缓冲区溢出的问题了。
     其中两个比较特殊的地方是:
     首先,堆栈的头16个字节是可以自由使用的,而且对其上层调用者无意义,这个区域也被称为red zone。类似于x86架构下堆栈指针后面的空间。
     其次,IA64下的函数指针不是直接指向函数代码头部,而是指向一个函数描述结构,此结构的第一个64位值指向实际函数入口地址。这种机制被很多RISC结构的系统采用,如PPC。而且

     而对于浮点数参数也使用了类似的机制,f0-f7被保留给全局使用,第一个浮点参数从f8开始。例如一个函数有四个参数,第三个参数为浮点数,则传入函数的有效参数寄存器是r32, r33, r35和f8,中间的r34被保留。

     The history of calling conventions, part 5: amd64中讨论了AMD64下的调用约定。为最大限度兼容32位x86架构,AMD64在把通用寄存器扩展为rax, rbx之后,另外新增了8个64位存器r8-r15。
     函数调用时,头4个参数通过rcx, rdx, r8和r9寄存器传递,其他参数还是通过堆栈传递,堆栈中为通过寄存器传递的参数保留空间。而小于64位的参数不会被自动扩展用0填充高位,使用时需要显式清空无效的高位。返回值通过rax寄存器返回,大于64位时通过堆栈保存并返回指针。而堆栈的清除工作由函数调用者完成。为保障效率,堆栈必须是16字节对其的,如压栈8字节数据还需要进行补齐工作。

     例如下面的函数调用的汇编代码和堆栈情况如下
 

以下为引用:

 void SomeFunction(int a, int b, int c, int d, int e);
 void CallThatFunction()
 {
     SomeFunction(1, 2, 3, 4, 5);
 }

 mov     dword ptr [rsp+0x20], 5     ; output parameter 5
 mov     r9d, 4                      ; output parameter 4
 mov     r8d, 3                      ; output parameter 3
 mov     edx, 2                      ; output parameter 2
 mov     ecx, 1                      ; output parameter 1
 call    SomeFunction                ; Go Speed Racer!


 xxxxxxx8 .. rest of stack (minus arg area) ..
 xxxxxxx0 (arg5)
 xxxxxxx8 (arg4 spill)
 xxxxxxx0 (arg3 spill)
 xxxxxxx8 (arg2 spill)
 xxxxxxx0 (arg1 spill) <- ".. rest of stack .." from first diagram
 xxxxxxx8 return address <- RSP upon entry to callee
 



     虽然AMD64的返回值仍然通过堆栈传递,存在被缓冲区溢出的可能性,但因为新架构实现了支持堆栈部分内存不可执行的保护特性,所以想通过溢出获取权限的技术难度大大增加。加上操作系统如Win2003在堆栈溢出上的保护代码,以后堆栈溢出的攻击手段将被大大削弱。

     最后Raymond Chen还在What can go wrong when you mismatch the calling convention? 中讨论了错误使用调用约定的潜在问题。其中也提到了RunDll32.exe的使用,具体情况可以参考我一起的一篇BLog《RunDll32 的使用方法与实现原理》

     再次感谢Raymond Chen为我们带来了如此精彩的一系列讨论。 :P

posted @ 2004-07-08 10:57 Flier Lu 阅读(1573) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1577430&run=.0556816

  在复杂的底层网络程序中,内存拷贝、字符串比较和搜索操作很容易成为性能瓶颈所在。编译器自带的此类函数虽然做了一些通用性的优化工作,但因为在使用指令集方面受到兼容性的约束,远远没有达到最大限度利用硬件能力的地步。而通过针对特定硬件平台的优化,可以大大提高此类操作的性能。下面我将以P4平台下内存拷贝操作为例,根据AMD提供的一份优化文档中的例子,简要介绍一下如何通过特定指令集,优化内存带宽的使用。虽然因为硬件限制没有达到AMD文档中所说memcpy函数300%的性能提升,但在我机器上实测也有%175-%200的明显性能提升(此数据可能根据机器情况不同)。

     Optimizing Memory Bandwidth from AMD

     按照众所周知的“摩尔”定律,CPU的运算速度每18个月翻一翻,但与此同时内存和外存(硬盘)的速度并无法达到同步增长。这就造成高速CPU与相对低速的内存和外设之间的不同步发展,成为很多程序的瓶颈所在。而如何最大限度提升对现有硬件的利用程度,是算法以下层面优化的主要途径。对内存拷贝操作来说,了解和合理使用Cache是最关键的一点。为追求性能,我们将以牺牲兼容性为代价,因此以下讨论和代码都以P4及以上级别CPU为主,AMD芯片虽然实现上有所区别,但在指令集和整体结构上相同。

     首先我们来看一个最简单的memcpy的汇编实现:
 

以下为引用:

 ;
 ; Flier Lu (flier@nsfocus.com)
 ;
 ; nasmw.exe -f win32 fastmemcpy.asm -o fastmemcpy.obj
 ;
 ; extern "C" {
 ;   extern void fast_memcpy1(void *dst, const void *src, size_t size);
 ; }
 ;
 cpu p4

 segment .text use32

 global _fast_memcpy1

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy1:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]

   rep movsb

   pop edi
   pop esi
   ret
 



     这里我为了代码可移植性,使用的是NASM格式的汇编代码。NASM是一个非常出色的开源汇编编译器,支持各种平台和中间格式,被开源项目广泛使用,这样可以避免同时使用 VC 的嵌入式汇编和 GCC 中麻烦的 unix 风格 AT&T 格式汇编 :P

     代码初始的cpu p4定义使用p4指令集,因为后面的很多优化工作使用了P4指令集和相关特性;接着的segment .text use32定义此代码在32位代码段;然后global定义标签_fast_memcpy1为全局符号,使得C++代码中可以LINK其.obj后访问此代码;最后%define定义多个宏,用于访问函数参数。

     在C++中只需要定义fast_memcpy1函数格式并链接nasm编译生成的.obj文件即可。NASM编译时 -f 参数指定生成中间文件格式为 MS 的 32 位 COFF 格式,-o 参数指定输出文件名。

     上面这段代码非常简单,适合小内存块的快速拷贝。实际上VC编译器在处理小内存拷贝时,会自动根据情况使用 rep movsb 直接替换 memcpy 函数,通过忽略函数调用和堆栈操作,优化代码长度和性能。

     不过在 32 位的 x86 架构下,完全没有必要逐字节进行操作,使用 movsd 替换 movsb 是必然的选择。
 

以下为引用:

 global _fast_memcpy2

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy2:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]
   shr ecx, 2                  ; convert to DWORD count

   rep movsd

   pop edi
   pop esi
   ret
 



     为了展示方便,这里假设源和目标内存块本身长度都是64字节的整数倍,并且已经4K页对齐。前者保证单条指令不会出现跨CACHE行访问的情况;后者保证测试速度时不会因为跨页操作影响测试结果。等会分析CACHE时再详细解释为什么要做这种假设。

     不过因为现代CPU大多使用了很长的指令流水线,多条指令并行工作往往比一条指令效率更高,因此 AMD 文档中给出了这样的优化:
 

以下为引用:

 global _fast_memcpy3

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy3:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]
   shr ecx, 2                  ; convert to DWORD count

 .copyloop:
   mov eax, dword [esi]
   mov dword [edi], eax

   add esi, 4
   add edi, 4

   dec ecx
   jnz .copyloop

   pop edi
   pop esi
   ret
 



     标签.copyloop中那段循环实际上完成跟rep movsd指令完全相同的工作,但是因为是多条指令,理论上CPU指令流水线可以并行处理之。故而在AMD的文档中指出能有1.5%的性能提高,不过就我实测效果不太明显。相对而言,当年从486向pentium架构迁移时,这两种方式的区别非常明显。记得Delphi 3还是4中就只是通过做这一种优化,其字符串处理性能就有较大提升。而目前主流CPU厂商,实际上都是通过微代码技术,内核中使用RISC微指令模拟CISC指令集,因此现在效果并不明显。

     然后,可以通过循环展开的优化策略,增加每次处理数据量并减少循环次数,达到性能提升目的。
 

以下为引用:

 global _fast_memcpy4

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy4:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]
   shr ecx, 4                  ; convert to 16-byte size count

 .copyloop:
   mov eax, dword [esi]
   mov dword [edi], eax

   mov ebx, dword [esi+4]
   mov dword [edi+4], ebx

   mov eax, dword [esi+8]
   mov dword [edi+8], eax

   mov ebx, dword [esi+12]
   mov dword [edi+12], ebx

   add esi, 16
   add edi, 16

   dec ecx
   jnz .copyloop

   pop edi
   pop esi
   ret
 



     但这种操作就 AMD 文档上评测反而有 %1.5 性能降低,呵呵。其自己的说法是需要将读取内存和写入内存的操作分组,以使CPU可以一次性搞定。改称以下分组操作就可以比_fast_memcpy3提高3% -_-b
 
以下为引用:

 global _fast_memcpy5

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy5:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]
   shr ecx, 4                  ; convert to 16-byte size count

 .copyloop:
   mov eax, dword [esi]
   mov ebx, dword [esi+4]
   mov dword [edi], eax
   mov dword [edi+4], ebx

   mov eax, dword [esi+8]
   mov ebx, dword [esi+12]
   mov dword [edi+8], eax
   mov dword [edi+12], ebx

   add esi, 16
   add edi, 16

   dec ecx
   jnz .copyloop

   pop edi
   pop esi
   ret
 



     可惜我在P4上实在测不出什么区别,呵呵,大概P4和AMD实现流水线的思路有细微的出入吧 :D

     既然进行循环展开,为什么不干脆多展开一些呢?虽然x86下面通用寄存器只有那么几个,但是现在有MMX啊,呵呵,大把的寄存器啊 :D 改称使用MMX寄存器后,一次载入/写入操作可以处理64字节的数据,呵呵,比_fast_memcpy5可以再有7%的性能提升。
 

以下为引用:

 global _fast_memcpy6

 %define param       esp+8+4
 %define src         param+0
 %define dst         param+4
 %define len         param+8

 _fast_memcpy6:
   push esi
   push edi

   mov esi, [src]              ; source array
   mov edi, [dst]              ; destination array
   mov ecx, [len]              ; number of QWORDS (8 bytes) assumes len / CACHEBLOCK is an integer
   shr ecx, 3

   lea esi, [esi+ecx*8]        ; end of source
   lea edi, [edi+ecx*8]        ; end of destination
   neg ecx                     ; use a negative offset as a combo pointer-and-loop-counter

 .copyloop:
   movq mm0, qword [esi+ecx*8]
   movq mm1, qword [esi+ecx*8+8]
   movq mm2, qword [esi+ecx*8+16]
   movq mm3, qword [esi+ecx*8+24]
   movq mm4, qword [esi+ecx*8+32]
   movq mm5, qword [esi+ecx*8+40]
   movq mm6, qword [esi+ecx*8+48]
   movq mm7, qword [esi+ecx*8+56]

   movq qword [edi+ecx*8], mm0
   movq qword [edi+ecx*8+8], mm1
   movq qword [edi+ecx*8+16], mm2
   movq qword [edi+ecx*8+24], mm3
   movq qword [edi+ecx*8+32], mm4
   movq qword [edi+ecx*8+40], mm5
   movq qword [edi+ecx*8+48], mm6
   movq qword [edi+ecx*8+56], mm7

   add ecx, 8
   jnz .copyloop

   emms

   pop edi
   pop esi

   ret
 



     优化到这个份上,常规的优化手段基本上已经用尽,需要动用非常手段了,呵呵。

posted @ 2004-07-08 10:54 Flier Lu 阅读(1508) 评论(1) 编辑

http://www.blogcn.com/user8/flier_lu/index.html?id=1567274&run=.08E8850

我们在开发程序的时候,往往会在B/S和C/S模式之间摇摆不定,因为这两种模式在特性上各有千秋,常常想如果能够结合两者的优点就好了,呵呵。Smart Client正是结合两种模式的一种尝试,它构建于Web Service提供的功能之上,同时具有B/S模式通过网络服务器集中发布的优点,和C/S模式可以离线工作的优点,并可以按照不同的终端设备进行裁减。M$的.NET产品架构按Don Box的说法可以分为两部分:CLR和Web Service,而MS提出的Smart Client概念正是链接这两者的桥梁。
     下面这两篇文章从整体架构上介绍了Smart Client的概念:

     Smart Clients: Combining the Power of the PC with the Reach of the Web
     Smart Client Application Model and the .NET Framework 1.1

     就发布方式来说,Smart Client可以通过网络服务器发布:以 ::URL::http://RemoteWebServer/myExe.exe  形式提供直接下载运行;或者通过程序在后台调用System.Reflection.Assembly.LoadFrom自动加载。
     例如下面代码从服务器上直接载入一个模块并使用之:
 

以下为引用:

 Assembly downloadedAssembly;
 Type formType;
 Form downloadedForm;

 try
 {
   //download and load the assembly
   downloadedAssembly = Assembly.LoadFrom("::URL::http://RemoteWebServer/CSharpForm2.dll");

   // find the type in the assembly for the object we want to create
   formType = downloadedAssembly.GetType("CSharpForm2.Form2");

   //Create an instance of the desired type and show it
   downloadedForm = (Form) System.Activator.CreateInstance(formType);

   if (downloadedForm != null)
   {
     downloadedForm.Show();
   }
 }
 catch (Exception exc)
 {
   MessageBox.Show(exc.ToString());
 }
 



     而因为.NET架构的支持,这种发布方式无需担心“Dll Hell”问题,真正做到XCOPY的分发策略。

     与传统的B/S模式程序相比:Smart Client程序可以离线运行,随时随地进行工作;并可以完全利用本地机器上的强大能力,如调用本地API,使用打印机等等。与传统的C/S模式程序相比:Smart Client程序可以使用透明的Web Service调用服务器端功能,穿透防火墙,同时无需象C/S程序那样通过CD/DVD介质分发,真正做到移动代码的概念,呵呵。
     下面这篇讨论中,MS里主管Smart Client的几位老兄回答了使用者关于 Smart Client 的一些问题:

     Smart Client Development Platform

     同时MS还在开发新的客户端软件发布技术ClickOnce,让Smart Client的发布更方便、安全。

     Introducing Client Application Deployment with "ClickOnce"

     Smart Client虽新,但其使用的概念和技术并不新,呵呵。这正是微软为人诟病的一贯风格,等概念和技术发展到一定程度,然后加以整合,出一系列技术和产品,将还没站稳的先行者一网打尽 :P
     比如离线工作的概念,Borland早在几年前的 Delphi 4 就加入了TClientDataSet支持数据的离线操作,并逐渐增加自动同步和XML支持等功能;再比如ClickOnce的概念,怎么看也象 Sun 为Java客户端程序分发推出的 Web Start的概念,只是更为人性化。

     而就通用软件整体计算架构的发展来说,我认为可以根据其计算重心的转移,分为几个时期:

     Long long ago, 在传说中的Mainframe时代,计算重心稳固在计算能力高度集中的服务器端,客户端只是完成最简单的控制操作;
     然后是PC机的黄金时代,逐渐强化的台式机使得计算重心开始转移到本地机器上,摆脱主机的桎梏,单机软件迅猛发展;
     但PC机的发展速度无法完全满足IT应用的需求,因此C/S模式开始大行其道。通过文件服务器或数据库服务器的大规模应用,计算重心又一次向服务器端偏移,但客户机上仍占用相当的比重,可以说计算重心跨越两者;
     而随着Internet的飞速发展,以及终端数量的迅猛增长,C/S模式暴露出其固有的弱点,网络穿透性差以及发布和维护复杂等。瘦客户端程序和B/S模式逐渐抬头,计算重心又一次向服务器端回归;
     而今随着跨平台跨网络跨应用的需求增加,以Web Service为基础的分布式计算思想,以及网格思想的发展,使得“网络就是计算机”的概念逐渐实际化。计算重心逐渐分布在整个网络之上,用户使用时无需知道服务实际上是由谁提供的。

     而Smart Client概念的诞生,可以说正是整体计算架构发展到一定阶段后应运而生的,将会作为B/S和C/S模式的有力补充,并将随着 Web Service 的发展,以及客户端设备广泛化的趋势,逐渐侵蚀部分B/S和C/S的势力范围。 :P

 

posted @ 2004-07-08 10:52 Flier Lu 阅读(950) 评论(0) 编辑

to NGen or not to NGen, that is the question

http://www.blogcn.com/user8/flier_lu/index.html?id=1550010&run=.04005F8

    Jeffrey Richter曾在CodeGuru上发表过一篇讨论是否应该使用NGen的文章。(ngen.exe支持将CLR格式的IL代码编译成Native的代码,避免载入时再次JIT编译)

     JIT Compilation and Performance - To NGen or Not to NGen?

     文中的观点是应该在大多数程序中避免使用ngen预编译IL代码为Native代码,因为JIT编译器可能比ngen更了解用户的系统。例如JIT编译器有以下优势:

     1.可以知道用户正在使用的什么类型的CPU,是PII、PIII还是P4,并编译生成相应的优化指令
     2.可以知道用户当前是使用单CPU还是多CPU,以便优化线程锁定机制
     3.可以避免ngen静态编译带来的DLL载入时重定位问题等等

     而且使用ngen预编译代码还会带来一些潜在的问题:

     1.没有知识产权保护:与某些人预期的不同,ngen生成的Native代码不能独立发布。因为其中没有包含Metadata信息,所以还是必须与原始的IL代码一同发布
     2.ngen生成的文件可能不同步:CLR在载入ngen生成的文件时,要检测其编译时的属性是否与当前环境匹配,不匹配则使用缺省的JIT编译
     3.难以管理:ngen生成的文件不能自动删除,因此违背了.NET架构程序的XCOPY分发策略
     4.低劣的载入时性能(重定位):如果ngen生成的DLL文件的基址已经被占用,则载入时需要重定位(rebase)
     5.低劣的运行时性能:ngen生成的文件采用较为保守的假定环境,因此代码效率低于JIT编译
     6.某些应用程序域(domain)的载入策略会忽略ngen生成的文件:assembly可以以应用程序域相关(non-domain-neutral)和无关(domain-neutral)的方式被载入,但ngen生成的文件假设只有mscorlib.dll以应用程序域无关的方式被载入。因此当某个assembly通过应用程序域无关的方式被使用,则ngen生成的代码无法使用,还是要通过JIT编译。例如ASP.NET中所有具有strongly-named的assembly都会被通过应用程序域无关的方式载入。

     因此Jeffrey Richter认为,只有对于客户端程序能够有可测量的载入性能提升的情况下,才应该使用ngen预编译代码,而对于其他程序,特别是服务器端程序,都应该避免ngen的使用。
     
     不过CLR组的Junfeng Zhang并不十分赞同上述观点,故而在其BLog上发表了一篇批驳的文章
     
     JIT Compilation and Performance - To NGen or Not to NGen? 
     
     文中对Jeffrey Richter提出的ngen的几个潜在问题逐条反驳:

     1.ngen并不是用来保护知识产权的工具,只是优化载入时性能而已,可以通过其他工具如混淆器(obfuscator)保护知识产权
     2.一般只有在维护.NET Framework和自己的程序后,才会导致不同步问题,而此时可以使用ngen重新生成预编译文件
     3.ngen支持 /delete 命令行参数,因此可以通过写一个批处理文件完成自动删除
     4.重定位是所有dll都存在的问题,并不能为了避免重定位就不使用dll而采用巨大的exe文件,因此这不能作为不使用ngen的理由。而且CLR载入dll的时候会检测这种情况的发生,并在合适的情况下拒绝使用ngen生成的文件
     5.CLR目前的JIT编译器对所有代码只编译一次,而不是象Sun的Hotspot那样,监视已JIT的代码,根据动态行为重新JIT优化性能。而ngen具有的JIT无法达到的优势是可以跨assembly做一些代码的优化。不过ngen代码还是比JIT编译平均有5%-10%的性能损失,单能大大提高载入速度。
     6.ngen编译的文件的确在某些情况下无法被使用,但这并不能作为不使用ngen的理由
     
     此外ngen还有个优点是其生成的代码可以被多个进程共享,而JIT编译生成的代码在私有地址空间中。
     
     因此ngen在客户端代码的处理上还是有价值的,呵呵
     
     后面MS的Josh Williams又补充说,在64位版本的.NET中,ngen生成代码的性能可能比JIT代码有较大改善,因为ngen包含了一些对JIT来说过于复杂的全局优化措施。
     
     最后Junfeng Zhang还给出了一个性能优化的指导性文章的链接
     
     Profile-Guided Optimization with Microsoft Visual C++ 2005
     
     有趣的讨论,通过这种BLog的论战,很容易就能够获得一些通过正规渠道文章无法了解的内幕消息,呵呵

posted @ 2004-07-08 10:46 Flier Lu 阅读(599) 评论(0) 编辑

.NET 1.1中预编译ASP.NET页面实现原理浅析 [1] 自动预编译机制浅析

http://www.blogcn.com/user8/flier_lu/index.html?id=1544105&run=.0E0327A

.NET 1.1中预编译ASP.NET页面实现原理浅析

     MS在发布ASP.NET时的一大功能特性是,与ASP和PHP等脚本语言不同,ASP.NET实际上是一种编译型的快速网页开发环境。这使得ASP.NET在具有开发和修改的简便性的同时,不会负担效率方面的损失。实现上ASP.NET与JSP的思路类似,引擎在第一次使用一个页面之前,会将之编译成一个类,自动生成Assembly并载入执行。
     而通过《在WinForm程序中嵌入ASP.NET》一文中我们可以了解到,ASP.NET引擎实际上是可以无需通过IIS等Web服务器调用而被使用的,这就使得手工预编译ASP.NET页面成为可能。实际上这个需求是普遍存在的,早在ASP时代就层有第三方产品支持将ASP页面编译成二进制程序,以提高执行效率和保障代码安全性,而将伴随Whidbey发布的ASP.NET 2.0更是直接内置了预编译ASP.NET页面的功能。

     实际上网上早就有人讨论过在ASP.NET 1.1中模拟预编译特性的实现方法,例如以下两篇文章

     Pre-Compiling ASP.NET Web Pages
     Pre-Compile ASPX pages in .NET 1.1

     其思路基本上都是遍历所有需要预编译的页面文件,然后通过模拟Web页面请求的方式,触发ASP.NET引擎的自动预编译机制。这样做的好处是完全模拟真实情况,无需了解ASP.NET引擎的实现原理;但同时也会受到诸多限制,如预编译结果不透明,无法脱离原始ASP.NET页面文件使用等等,而且无法使我们从原理上理解预编译特性的实现。

     下面我将分三到四个小节,简要讨论 ASP.NET 自动编译机制的实现、ASP.NET 页面文件编译的实现以及如何在ASP.NET 1.1中实现手动预编译页面和相应分发机制。

 [1] 自动预编译机制浅析

     本节我们将详细分析讨论.NET 1.1中,ASP.NET引擎内部实现自动页面预编译的原理。

     首先,我们所说的ASP.NET页面实际上主要分为四类:

     1.Web 应用程序文件    Global.asax
     2.Web 页面文件        *.aspx
     3.用户自定义控件文件  *.ascx
     4.Web 服务程序文件    *.asmx

     Web 应用程序文件对于每个Web 应用程序来说是可选唯一的,用来处理ASP.NET应用程序一级的事件,并将被预编译为一个System.Web.HttpApplication类的子类;
     Web 页面文件是普通的ASP.NET页面,处理特定页面的事件,将被预编译为一个System.Web.UI.Page类的子类;
     用户自定义控件文件是特殊的ASP.NET页面,处理控件自身的事件,将被预编译为一个System.Web.UI.UserControl类的子类;
     Web 服务程序文件则是与前三者不太相同的一种特殊页面文件,暂时不予讨论。

     然后,前三种ASP.NET文件的编译时机也不完全相同。Web 应用程序文件在此 Web 应用程序文件第一次被使用时自动编译;Web 页面文件在此Web页面第一次被使用时自动编译,实际上是调用 HttpRuntime.ProcessRequest 函数触发预编译;用户自定义控件文件则在其第一次被 Web 页面使用的时候自动编译,实际上是调用 Page.LoadControl 函数触发预编译。

     在了解了以上这些基本知识后,我们来详细分析一下自动预编译的实现机制。

     HttpRuntime.ProcessRequest 函数是处理Web页面请求的调用发起者,伪代码如下:
 

以下为引用:

 public static void HttpRuntime.ProcessRequest(HttpWorkerRequest wr)
 {
   // 检查当前调用者有没有作为ASP.NET宿主(Host)的权限
   InternalSecurityPermissions.AspNetHostingPermissionLevelMedium.Demand();

   if(wr == null)
   {
     throw new ArgumentNullException("custom");
   }

   RequestQueue queue = HttpRuntime._theRuntime._requestQueue;

   if(queue != null)
   {
     // 将参数中的Web页面请求放入请求队列中
     // 并从队列中使用FIFO策略获取一个页面请求
     wr = queue.GetRequestToExecute(wr);
   }

   if(wr != null)
   {
     // 更新性能计数器
     HttpRuntime.CalculateWaitTimeAndUpdatePerfCounter(wr);
     // 实际完成页面请求工作
     HttpRuntime.ProcessRequestNow(wr);
   }
 }
 



     HttpRuntime.ProcessRequestNow函数则直接调用缺省HttpRuntime实例的ProcessRequestInternal函数完成实际页面请求工作,伪代码如下:
 
以下为引用:

 internal static void HttpRuntime.ProcessRequestNow(HttpWorkerRequest wr)
 {
   HttpRuntime._theRuntime.ProcessRequestInternal(wr);
 }
 


     HttpRuntime.ProcessRequestInternal函数逻辑稍微复杂一些,大致可分为四个部分。

     首先检查当前HttpRuntime实例是否第一次被调用,如果是第一次调用则通过FirstRequestInit函数初始化;
     接着调用HttpResponse.InitResponseWriter函数初始化页面请求的返回对象HttpWorkerRequest.Response;
     然后调用HttpApplicationFactory.GetApplicationInstance函数获取当前 Web 应用程序实例;
     最后使用Web应用程序实例完成实际的页面请求工作。

     伪代码如下:
 

以下为引用:

 private void HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr)
 {
   // 构造 HTTP 调用上下文对象
   HttpContext ctxt = new HttpContext(wr, 0);

   // 设置发送结束异步回调函数
   wr.SetEndOfSendNotification(this._asyncEndOfSendCallback, ctxt);

   // 更新请求计数器
   Interlocked.Increment(&(this._activeRequestCount));

   try
   {
     // 检查当前HttpRuntime实例是否第一次被调用
     if(this._beforeFirstRequest)
     {
       lock(this)
       {
         // 使用 Double-Checked 模式 避免冗余锁定
         if(this._beforeFirstRequest)
         {
           this._firstRequestStartTime = DateTime.UtcNow;
           this.FirstRequestInit(ctxt); // 初始化当前 HttpRuntime 运行时环境
           this._beforeFirstRequest = false;
         }
       }
     }

     // 根据配置文件设置,扮演具有较高特权的角色
     ctxt.Impersonation.Start(true, false);
     try
     {
       // 初始化页面请求的返回对象
       ctxt.Response.InitResponseWriter();
     }
     finally
     {
       ctxt.Impersonation.Stop();
     }

     // 获取当前 Web 应用程序实例
     IHttpHandler handler = HttpApplicationFactory.GetApplicationInstance(ctxt);

     if (handler == null)
     {
       throw new HttpException(HttpRuntime.FormatResourceString("Unable_create_app_object"));
     }

     // 使用Web应用程序实例完成实际的页面请求工作
     if((handler as IHttpAsyncHandler) != null)
     {
       IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler) handler);
       ctxt.AsyncAppHandler = asyncHandler;
       // 使用异步处理机制
       asyncHandler.BeginProcessRequest(ctxt, this._handlerCompletionCallback, ctxt);
     }
     else
     {
       handler.ProcessRequest(ctxt);
       this.FinishRequest(ctxt.WorkerRequest, ctxt, null);
     }
   }
   catch(Exception E)
   {
     ctxt.Response.InitResponseWriter();
     this.FinishRequest(wr, ctxt, E);
   }
 }
 



     HttpRuntime.ProcessRequestInternal函数中,涉及到文件预编译的有两部分:一是获取当前 Web 应用程序实例时,会根据情况自动判断是否预编译Web 应用程序文件;二是在完成实际页面请求时,会在第一次使用某个页面时触发预编译行为。

     首先来看看对 Web 应用程序文件的处理。

     HttpRuntime.ProcessRequestInternal函数中调用了HttpApplicationFactory.GetApplicationInstance函数获取当前 Web 应用程序实例。System.Web.HttpApplicationFactory是一个内部类,用以实现对多个Web应用程序实例的管理和缓存。GetApplicationInstance函数返回的是一个IHttpHandler接口,提供IHttpHandler.ProcessRequest函数用于其后对Web页面文件的处理。伪代码如下:
 

以下为引用:

 internal static IHttpHandler HttpApplicationFactory.GetApplicationInstance(HttpContext ctxt)
 {
   // 定制应用程序
   if(HttpApplicationFactory._customApplication != null)
   {
     return HttpApplicationFactory._customApplication;
   }
   // 调试请求
   if(HttpDebugHandler.IsDebuggingRequest(ctxt))
   {
     return new HttpDebugHandler();
   }

   // 判断是否需要初始化当前 HttpApplicationFactory 实例
   if(!HttpApplicationFactory._theApplicationFactory._inited)
   {
     HttpApplicationFactory factory = HttpApplicationFactory._theApplicationFactory;

     lock(HttpApplicationFactory._theApplicationFactory);
     {
       // 使用 Double-Checked 模式 避免冗余锁定
       if(!HttpApplicationFactory._theApplicationFactory._inited)
       {
         // 初始化当前 HttpApplicationFactory 实例
         HttpApplicationFactory._theApplicationFactory.Init(ctxt);
         HttpApplicationFactory._theApplicationFactory._inited = true;
       }
     }
   }

   // 获取 Web 应用程序实例
   return HttpApplicationFactory._theApplicationFactory.GetNormalApplicationInstance(ctxt);
 }
 



     在处理特殊情况和可能的实例初始化之后,调用HttpApplicationFactory.GetNormalApplicationInstance函数完成获取Web应用程序实例的实际功能,伪代码如下:
 
以下为引用:

 private HttpApplication HttpApplicationFactory.GetNormalApplicationInstance(HttpContext context)
 {
   HttpApplication app = null;

   // 尝试从已施放的 Web 应用程序实例队列中获取
   lock(this._freeList)
   {
     if(this._numFreeAppInstances > 0)
     {
       app = (HttpApplication)this._freeList.Pop();
       this._numFreeAppInstances--;
     }
   }

   if(app == null)
   {
     // 构造新的 Web 应用程序实例
    app = (HttpApplication)System.Web.HttpRuntime.CreateNonPublicInstance(this._theApplicationType);

    // 初始化 Web 应用程序实例
   app.InitInternal(context, this._state, this._eventHandlerMethods);
   }

   return app;
 }
 



     构造新的 Web 应用程序实例的代码很简单,实际上就是对Activator.CreateInstance函数的简单包装,伪代码如下:
 
以下为引用:

 internal static object HttpRuntime.CreateNonPublicInstance(Type type, object[] args)
 {
   return Activator.CreateInstance(type, BindingFlags.CreateInstance | BindingFlags.Instance |
     BindingFlags.NonPublic | BindingFlags.Public, null, args, null);
 }

 internal static object HttpRuntime.CreateNonPublicInstance(Type type)
 {
   return HttpRuntime.CreateNonPublicInstance(type, null);
 }
 



     至此一个 Web 应用程序实例就被完整构造出来,再经过InitInternal函数的初始化,就可以开始实际页面处理工作了。而HttpApplicationFactory实例的_theApplicationType类型,则是结果预编译后的Global.asax类。实际的预编译工作在HttpApplicationFactory.Init函数中完成,伪代码如下:
 
以下为引用:

 private void HttpApplicationFactory.Init(HttpContext ctxt)
 {
  if(HttpApplicationFactory._customApplication != null)
   return;

   using(HttpContextWrapper wrapper = new HttpContextWrapper(ctxt))
  {
    ctxt.Impersonation.Start(true, true);
    try
    {
      try
      {
        this._appFilename = HttpApplicationFactory.GetApplicationFile(ctxt);
      this.CompileApplication(ctxt);
      this.SetupChangesMonitor();
      }
      finally
      {
        ctxt.Impersonation.Stop();
      }
    }
    catch(Object)
    {
    }
    this.FireApplicationOnStart(ctxt);
  }
 }
 



     GetApplicationFile函数返回Web请求物理目录下的global.asax文件路径;CompileApplication函数则根据此文件是否存在,判断是预编译之并载入编译后类型,还是直接返回缺省的HttpApplication类型,伪代码如下:
 
以下为引用:

 internal static string HttpApplicationFactory.GetApplicationFile(HttpContext ctxt)
 {
   return Path.Combine(ctxt.Request.PhysicalApplicationPath, "global.asax");
 }

 private void HttpApplicationFactory.CompileApplication(HttpContext ctxt)
 {
   if(FileUtil.FileExists(this._appFilename))
   {
     ApplicationFileParser parser;

     // 获取编译后的 Web 应用程序类型
     this._theApplicationType = ApplicationFileParser.GetCompiledApplicationType(this._appFilename, context, out parser);
     this._state = new HttpApplicationState(parser1.ApplicationObjects, parser.SessionObjects);
     this._fileDependencies = parser.SourceDependencies;
   }
   else
   {
     this._theApplicationType = typeof(HttpApplication);
     this._state = new HttpApplicationState();
   }
   this.ReflectOnApplicationType();
 }
 



     分析到这里我们可以发现,内部类型System.Web.UI.ApplicationFileParser的GetCompiledApplicationType函数是实际上进行Web应用程序编译工作的地方。但现在我们暂且打住,等下一节分析编译过程时再详细解说。 :)

     然后我们看看对 Web 页面文件的处理。

     在前面分析HttpRuntime.ProcessRequestInternal函数时我们曾了解到,在获得了Web应用程序实例后,会使用此实例的IHttpAsyncHandler接口或IHttpHandler接口,完成实际的页面请求工作。而无论有否Global.asax文件,最终返回的Web应用程序实例都是一个HttpApplication类或其子类的实例,其实现了IHttpAsyncHandler接口,支持异步的Web页面请求工作。对此接口的处理伪代码如下:
 

以下为引用:

 private void HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr)
 {
   ...

   // 使用Web应用程序实例完成实际的页面请求工作
   if((handler as IHttpAsyncHandler) != null)
   {
     IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler) handler);
     ctxt.AsyncAppHandler = asyncHandler;
     // 使用异步处理机制
     asyncHandler.BeginProcessRequest(ctxt, this._handlerCompletionCallback, ctxt);
   }
   else
   {
     handler.ProcessRequest(ctxt);
     this.FinishRequest(ctxt.WorkerRequest, ctxt, null);
   }

   ...
 }
 



     HttpRuntime.ProcessRequestInternal函数通过调用HttpApplication.IHttpAsyncHandler.BeginProcessRequest函数开始页面请求工作。而HttpApplication实际上根本不支持同步形式的IHttpHandler接口,伪代码如下:
 
以下为引用:

 void HttpApplication.ProcessRequest(System.Web.HttpContext context)
 {
   throw new HttpException(HttpRuntime.FormatResourceString("Sync_not_supported"));
 }

 bool HttpApplication.get_IsReusable()
 {
   return true;
 }
 



     而在HttpApplication.IHttpAsyncHandler.BeginProcessRequest函数中,将完成非常复杂的异步调用后台处理操作,这儿就不多罗嗦了,等有机会写篇文章专门讨论一下ASP.NET中的异步操作再说。而其最终调用还是使用System.Web.UI.PageParser对需要处理的Web页面进行解析和编译。

     最后我们看看对用户自定义控件文件的处理。

     Page类的LoadControl函数实际上是在抽象类TemplateControl中实现的,伪代码如下:
 

以下为引用:

 public Control LoadControl(string virtualPath)
 {
   virtualPath = UrlPath.Combine(base.TemplateSourceDirectory, virtualPath);
   Type type = UserControlParser.GetCompiledUserControlType(virtualPath, null, base.Context);
   return this.LoadControl(type1);
 }
 


     实际的用户自定义控件预编译操作还是在UserControlParser类中完成的。

     至此,在这一节中我们已经大致了解了ASP.NET自动预编译的实现原理,以及在什么时候对页面文件进行预编译。下一节我们将详细分析ApplicationFileParser、PageParser和UserControlParser,了解ASP.NET是如何对页面文件进行预编译的。

posted @ 2004-07-08 10:45 Flier Lu 阅读(1074) 评论(2) 编辑

另一种获取系统服务描述表入口地址的方法

http://www.blogcn.com/user8/flier_lu/index.html?id=1442740&run=.0F766FA

    在《自动获取 NT 系统服务描述表与函数名映射表》一文中我使用MS提供的DbgHelp库,从符号库文件中查找KeServiceDescriptorTable和KeServiceDescriptorTableShadow符号,以获取系统服务描述表入口地址。这种方法逻辑简单,但是对不同操作系统版本的调试符号文件有依赖性,不适用于作为工具被散发出去的程序。因此这儿给出另外一种从线程本身的特性着手获取系统服务描述表入口地址的方法。
     我们所说的线程,实际上分为核心态和用户态两部分。Win32下这两者基本上是1对1的关系,其他平台如Solaris或Linux 2.6以前的版本则使用不同的映射模型。而Win32系统中核心态的线程,实际上也分为两类:工作线程和GUI线程。前者是建立核心线程的缺省类型,后者在线程第一次使用Win32k.sys系统服务时自动转换,或者使用PsConvertToGuiThread函数(ntospspsquery.c:3247)显式转换。两者之间的区别主要在于使用的资源缺省大小不同,以及使用的系统服务描述表不同。这也是为什么系统服务描述表要分为KeServiceDescriptorTable和KeServiceDescriptorTableShadow的原因之一,后者包括前者没有的对GDI服务的入口函数地址,一般在Win32k.sys中实现。核心线程对象的ETHREAD::KTHREAD::ServiceTable字段保存了此线程适用的系统调用服务表地址,此字段也被PsConvertToGuiThread函数用于判断线程类型。功能与Windows XP/2003提供的IsGUIThread函数类型。
     使用上我们可以创建一个线程,此线程不做任何实际工作,只是根据我们要取哪个系统服务描述表来决定是否调用GDI函数,如
 
以下为引用:

 class TGuiThread : public TThread
 {
 public:
   TGuiThread(void) : TThread(false)
   {
     FreeOnTerminate = false;
   }

   void __fastcall Execute(void)
   {
     ::GetDesktopWindow();
   }
 };
 



     在需要获取地址时,我们可以创建一个此线程的实例,然后通过其句柄获取内核对象地址。
 
以下为引用:

 //---------------------------------------------------------------------------
 DWORD TServiceTableApplication::GetpKeServiceDescriptorTableAddress(void) const
 {
   std::auto_ptr<TGuiThread> GuiThread(new TGuiThread());

   GuiThread->WaitFor();
   
   ::THandleTable tblHandles;

   PVOID pObj = NULL;
   TSystemHandleList& handles = tblHandles.HandleByProcessID[::GetCurrentProcessId()];

   for(TSystemHandleCPtr itHandle = handles.begin();
       itHandle != handles.end(); itHandle++)
   {
     if((HANDLE)itHandle->Handle == (HANDLE)GuiThread->Handle)
     {
       pObj = itHandle->Object;
       break;
     }
   }

   assert(pObj);

   LPVOID lpAddr;

   TPhysicalMemoryManager::Default().ReadVirtualMemory((LPCVOID)((DWORD)pObj + 0x124), &lpAddr, sizeof(lpAddr));

   return lpAddr;
 }
 



     然后读取ETHREAD内核对象偏移0x124的ServiceTable字段,即可 :D 这里的0x124偏移是Win2003下的KTHREAD结构,在Win2k下此偏移为0xDC,其他版本还需要做相应调整。
 
以下为引用:

 0:001> dt nt!_KTHREAD
    +0x000 Header           : _DISPATCHER_HEADER
    ...
    +0x120 Affinity         : Uint4B
    +0x124 ServiceTable     : Ptr32 Void
    +0x128 ApcStatePointer  : [2] Ptr32 _KAPC_STATE
    ...
 

posted @ 2004-07-08 10:43 Flier Lu 阅读(885) 评论(0) 编辑

关于信任粒度的讨论

http://www.blogcn.com/user8/flier_lu/index.html?id=1544232&run=.09DD601

    Omer van Kloeten在其BLog上提出了一个不错的建议 A Suggestion For A New Access Modifier。建议MS在语言一级增加大粒度的信任,允许信任同一软件发布商的其它Assembly,即两个具有相同Public Key Token的Assembly互相信任,而目前的信任粒度最大也只能到Assembly一级。这个想法非常不错,跟我去年折腾Metadata结构时的想法完全一样,呵呵,心有戚戚啊 
     Paul Wilson指出目前可以通过PublisherIdentityPermissionAttribute属性完成类似的约束策略,但还是来自Jim Hogg的消息比较好,MS会在下个.NET发布版本中增加Friend Assemblies的概念,呵呵

posted @ 2004-07-08 10:42 Flier Lu 阅读(287) 评论(0) 编辑

C# 2.0 中Iterators的改进与实现原理浅析

http://www.blogcn.com/user8/flier_lu/index.html?id=1511638&run=.0AA0EFB

    C#语言从VB中吸取了一个非常实用的foreach语句。对所有支持IEnumerable接口的类的实例,foreach语句使用统一的接口遍历其子项,使得以前冗长的for循环中繁琐的薄记工作完全由编译器自动完成。支持IEnumerable接口的类通常用一个内嵌类实现IEnumerator接口,并通过IEnumerable.GetEnumerator函数,允许类的使用者如foreach语句完成遍历工作。
     这一特性使用起来非常方便,但需要付出一定的代价。Juval Lowy发表在MSDN杂志2004年第5期上的Create Elegant Code with Anonymous Methods, Iterators, and Partial Classes一文中,较为详细地介绍了C# 2.0中迭代支持和其他新特性。

     首先,因为IEnumerator.Current属性是一个object类型的值,所以值类型(value type)集合在被foreach语句遍历时,每个值都必须经历一次无用的box和unbox操作;就算是引用类型(reference type)集合,在被foreach语句使用时,也需要有一个冗余的castclass指令,保障枚举出来的值进行类型转换的正确性。
 

以下为引用:

 using System.Collections;

 public class Tokens : IEnumerable
 {
   ...
   Tokens f = new Tokens(...);

   foreach (string item in f)
   {
      Console.WriteLine(item);
   }
   ...
 }
 



     上面的简单代码被自动转换为
 
以下为引用:

 Tokens f = new Tokens(...);

 IEnumerator enum = f.GetEnumerator();
 try
 {
   do {
     string item = (string)enum.get_Current(); // 冗余转换

     Console.WriteLine(item);
   }  while(enum.MoveNext());
 }
 finally
 {
   if(enum is IDisposable) // 需要验证实现IEnumerator接口的类是否支持IDisposable接口
   {
     ((IDisposable)enum).Dispose();
   }
 }
 



     好在C# 2.0中支持了泛型(generic)的概念,提供了强类型的泛型版本IEnumerable定义,伪代码如下:
 
以下为引用:

 namespace System.Collections.Generic
 {
   public interface IEnumerable<ItemType>
   {
      IEnumerator<ItemType> GetEnumerator();
   }
   public interface IEnumerator<ItemType> : IDisposable
   {
      ItemType Current{get;}
      bool MoveNext();
   }
 }
 


     这样一来即保障了遍历集合时的类型安全,又能够对集合的实际类型直接进行操作,避免冗余转换,提高了效率。
 
以下为引用:

 using System.Collections.Generic;

 public class Tokens : IEnumerable<string>
 {
   ... // 实现 IEnumerable<string> 接口

   Tokens f = new Tokens(...);

   foreach (string item in f)
   {
      Console.WriteLine(item);
   }
 }
 



     上面的代码被自动转换为
 
以下为引用:

 Tokens f = new Tokens(...);

 IEnumerator<string> enum = f.GetEnumerator();
 try
 {
   do {
     string item = enum.get_Current(); // 无需转换

     Console.WriteLine(item);
   }  while(enum.MoveNext());
 }
 finally
 {
   if(enum) // 无需验证实现IEnumerator接口的类是否支持IDisposable接口,
            // 因为所有由编译器自动生成的IEnumerator接口实现类都支持
   {
     ((IDisposable)enum).Dispose();
   }
 }
 


     除了遍历时的冗余转换降低性能外,C#现有版本另一个不爽之处在于实现IEnumerator接口实在太麻烦了。通常都是由一个内嵌类实现IEnumerator接口,而此内嵌类除了get_Current()函数外,其他部分的功能基本上都是相同的,如
 

以下为引用:

 public class Tokens : IEnumerable
 {
    public string[] elements;

    Tokens(string source, char[] delimiters)
    {
       // Parse the string into tokens:
       elements = source.Split(delimiters);
    }

    public IEnumerator GetEnumerator()
    {
       return new TokenEnumerator(this);
    }

    // Inner class implements IEnumerator interface:
    private class TokenEnumerator : IEnumerator
    {
       private int position = -1;
       private Tokens t;

       public TokenEnumerator(Tokens t)
       {
          this.t = t;
       }

       // Declare the MoveNext method required by IEnumerator:
       public bool MoveNext()
       {
          if (position < t.elements.Length - 1)
          {
             position++;
             return true;
          }
          else
          {
             return false;
          }
       }

       // Declare the Reset method required by IEnumerator:
       public void Reset()
       {
          position = -1;
       }

       // Declare the Current property required by IEnumerator:
       public object Current
       {
          get // get_Current函数
          {
             return t.elements[position];
          }
       }
    }
    ...
 }
 



     内嵌类TokenEnumerator的position和Tokens实际上是每个实现IEnumerator接口的类共有的,只是Current属性的get函数有所区别而已。这方面C# 2.0做了很大的改进,增加了yield关键字的支持,允许代码逻辑上的重用。上面冗长的代码在C# 2.0中只需要几行,如
 
以下为引用:

 using System.Collections.Generic;

 public class Tokens : IEnumerable<string>
 {
   public IEnumerator<string> GetEnumerator()
   {
     for(int i = 0; i<elements.Length; i++)
       yield elements[i];
   }
   ...
 }
 



     GetEnumerator函数是一个C# 2.0支持的迭代块(iterator block),通过yield告诉编译器在什么时候返回什么值,再由编译器自动完成实现IEnumerator<string>接口的薄记工作。而yield break语句支持从迭代块中直接结束,如
 
以下为引用:

 public IEnumerator<int> GetEnumerator()
 {
    for(int i = 1;i< 5;i++)
    {
       yield return i;
       if(i > 2)
          yield break; // i > 2 时结束遍历
    }
 }
 


     这样一来,很容易就能实现IEnumerator接口,并可以方便地支持在一个类中提供多种枚举方式,如
 
以下为引用:

 public class CityCollection
 {
    string[] m_Cities = {"New York","Paris","London"};
    public IEnumerable<string> Reverse
    {
       get
       {
          for(int i=m_Cities.Length-1; i>= 0; i--)
             yield m_Cities[i];
       }
    }
 }
 

     接下来我们看看如此方便的语言特性背后,编译器为我们做了哪些工作。以上面那个支持IEnumerable<string>接口的Tokens类为例,GetEnumerator函数的代码被编译器用一个类包装起来,伪代码如下
 

以下为引用:

 public class Tokens : IEnumerable<string>
 {
   private sealed class GetEnumerator$00000000__IEnumeratorImpl
     : IEnumerator<string>, IEnumerator, IDisposable
   {
     private int $PC = 0;
     private string $_current;
     private Tokens <this>;
     public int i$00000001 = 0;

     // 实现 IEnumerator<string> 接口
     string IEnumerator<string>.get_Current()
     {
       return $_current;
     }

     bool IEnumerator<string>.MoveNext()
     {
       switch($PC)
       {
       case 0:
         {
           $PC = -1;
           i$00000001 = 0;
           break;
         }
       case 1:
         {
           $PC = -1;
           i$00000001++;
           break;
         }
       default:
         {
           return false;
         }
       }

       if(i$00000001 < <this>.elements.Length)
       {
         $_current = <this>.elements[i$00000001];
         $PC = 1;

        return true;
       }
       else
       {
         return false;
       }
     }

     // 实现 IEnumerator 接口
     void IEnumerator.Reset()
     {
       throw new Exception();
     }

     string IEnumerator.get_Current()
     {
       return $_current;
     }

     bool IEnumerator.MoveNext()
     {
       return IEnumerator<string>.MoveNext(); // 调用 IEnumerator<string> 接口的实现
     }

     // 实现 IDisposable 接口
     void Dispose()
     {
     }
   }

   public IEnumerator<string> GetEnumerator()
   {
     GetEnumerator$00000000__IEnumeratorImpl impl = new GetEnumerator$00000000__IEnumeratorImpl();

     impl.<this> = this;

     return impl;
   }
 }
 



     从上面的伪代码中我们可以看到,C# 2.0编译器实际上维护了一个和我们前面实现IEnumerator接口的TokenEnumerator类非常类似的内部类,用来封装IEnumerator<string>接口的实现。而这个内嵌类的实现逻辑,则根据GetEnumerator定义的yield返回地点决定。
     我们接下来看一个较为复杂的迭代块的实现,支持递归迭代(Recursive Iterations),代码如下:
 
以下为引用:

 using System;
 using System.Collections.Generic;

 class Node<T>
 {
   public Node<T> LeftNode;
   public Node<T> RightNode;
   public T Item;
 }

 public class BinaryTree<T>
 {
   Node<T> m_Root;

   public void Add(params T[] items)
   {
     foreach(T item in items)
       Add(item);
   }

   public void Add(T item)
   {
     // ...
   }

   public IEnumerable<T> InOrder
   {
     get
     {
        return ScanInOrder(m_Root);
     }
   }

   IEnumerable<T> ScanInOrder(Node<T> root)
   {
     if(root.LeftNode != null)
     {
        foreach(T item in ScanInOrder(root.LeftNode))
        {
           yield item;
        }
     }

     yield root.Item;

     if(root.RightNode != null)
     {
        foreach(T item in ScanInOrder(root.RightNode))
        {
           yield item;
        }
     }
   }
 }
 



     BinaryTree<T>提供了一个支持IEnumerable<T>接口的InOrder属性,通过ScanInOrder函数遍历整个二叉树。因为实现IEnumerable<T>接口的不是类本身,而是一个属性,所以编译器首先要生成一个内嵌类支持IEnumerable<T>接口。伪代码如下
 
以下为引用:

 public class BinaryTree<T>
 {
   private sealed class ScanInOrder$00000000__IEnumeratorImpl<T>
     : IEnumerator<T>, IEnumerator, IDisposable
   {
     BinaryTree<T> <this>;
     Node<T> root;

     // ...
   }

   private sealed class ScanInOrder$00000000__IEnumerableImpl<T>
     : IEnumerable<T>, IEnumerable
   {
     BinaryTree<T> <this>;
     Node<T> root;

     IEnumerator<T> IEnumerable<T>.GetEnumerator()
     {
       ScanInOrder$00000000__IEnumeratorImpl<T> impl = new ScanInOrder$00000000__IEnumeratorImpl<T>();

       impl.<this> = this.<this>;
       impl.root = this.root;

       return impl;
     }

     IEnumerator IEnumerable.GetEnumerator()
     {
       ScanInOrder$00000000__IEnumeratorImpl<T> impl = new ScanInOrder$00000000__IEnumeratorImpl<T>();

       impl.<this> = this.<this>;
       impl.root = this.root;

       return impl;
     }
   }

   IEnumerable<T> ScanInOrder(Node<T> root)
   {
     ScanInOrder$00000000__IEnumerableImpl<T> impl = new ScanInOrder$00000000__IEnumerableImpl<T>();

     impl.<this> = this;
     impl.root = root;

     return impl;
   }
 }
 



     因为ScanInOrder函数内容需要用到root参数,故而IEnumerable<T>和IEnumerator<T>接口的包装类都需要有一个root字段,保存传入ScanInOrder函数的参数,并传递给最终的实现函数。
     实现IEnumerator<T>接口的内嵌包装类ScanInOrder$00000000__IEnumeratorImpl<T>实现原理与前面例子里的大致相同,不同的是程序逻辑大大复杂化,并且需要用到IDisposable接口完成资源的回收。
 
以下为引用:

 public class BinaryTree<T>
 {
   private sealed class GetEnumerator$00000000__IEnumeratorImpl
     : IEnumerator<T>, IEnumerator, IDisposable
   {
     private int $PC = 0;
     private string $_current;
     private Tokens <this>;
     public int i$00000001 = 0;

     public IEnumerator<T> __wrap$00000003;
     public IEnumerator<T> __wrap$00000004;
     public T item$00000001;
     public T item$00000002;
     public Node<T> root;

     // 实现 IEnumerator<T> 接口
     string IEnumerator<T>.get_Current()
     {
       return $_current;
     }

     bool IEnumerator<T>.MoveNext()
     {
       switch($PC)
       {
       case 0:
         {
           $PC = -1;
           if(root.LeftNode != null)
           {
             __wrap$00000003 = <this>.ScanInOrder(root.LeftNode).GetEnumerator();

             goto ScanLeft;
           }
           else
           {
             goto GetItem;
           }
         }
       case 1:
         {
           return false;
         }
       case 2:
         {
           goto ScanLeft;
         }
       case 3:
         {
           $PC = -1;
           if(root.RightNode != null)
           {
             __wrap$00000004 = <this>.ScanInOrder(root.RightNode).GetEnumerator();

             goto ScanRight;
           }
           else
           {
             return false;
           }
           break;
         }
       case 4:
         {
           return false;
         }
       case 5:
         {
           goto ScanRight;
         }
       default:
         {
           return false;
         }
     ScanLeft:
       $PC = 1;

       if(__wrap$00000003.MoveNext())
       {
         $_current = item$00000001 = __wrap$00000003.get_Current();
         $PC = 2;
         return true;
       }

     GetItem:
       $PC = -1;
       if(__wrap$00000003 != null)
       {
         ((IDisposable)__wrap$00000003).Dispose();
       }
       $_current = root.Item;
       $PC = 3;
       return true;

     ScanRight:
       $PC = 4;

       if(__wrap$00000004.MoveNext())
       {
         $_current = $item$00000002 = __wrap$00000004.get_Current();
         $PC = 5;
         return true;
       }
       else
       {
         $PC = -1;
         if(__wrap$00000004 != null)
         {
           ((IDisposable)__wrap$00000004).Dispose();
         }
         return false;
       }
     }
     // 实现 IDisposable 接口
     void Dispose()
     {
       switch($PC)
       {
       case 1:
       case 2:
         {
           $PC = -1;
           if(__wrap$00000003 != null)
           {
             ((IDisposable)__wrap$00000003).Dispose();
           }
           break;
         }
       case 4:
       case 5:
         {
           $PC = -1;
           if(__wrap$00000004 != null)
           {
             ((IDisposable)__wrap$00000004).Dispose();
           }
           break;
         }
       }
     }
   }
 }
 



     通过上面的伪代码,我们可以看到,C# 2.0实际上是通过一个以$PC为自变量的有限状态机完成的递归迭代块,这可能是因为有限状态机可以很方便地通过程序自动生成吧。而Dispose()函数则负责处理状态机的中间变量。

     有兴趣进一步了解迭代特性的朋友,可以到Grant Ri的BLog上阅读Iterators相关文章
     在了解了Iterators的实现原理后,再看那些讨论就不会被其表象所迷惑了 :D


 

posted @ 2004-07-08 10:42 Flier Lu 阅读(1274) 评论(1) 编辑

修订版Managed C++中析构函数的语义更改

http://www.blogcn.com/user8/flier_lu/index.html?id=1485755&run=.08F07F8

    上周Lippman在其Blog上发表了一篇介绍新版本Managed C++中析构函数语义变化的文章:Changes in Destructor Semantics in Support of Deterministic Finalization
     我们知道在C#和Managed C++中,析构函数实际上被编译器翻译成对Object.Finalize函数的重载。例如在Managed C++中定义如下的类
 
以下为引用:

 __gc class A
 {
 public:
   ~A() { Console::WriteLine(S"in ~A"); }
 };

 __gc class B : public A
 {
 public:
   ~B() { Console::WriteLine(S"in ~B");  }
 };
 



     在Managed C++的实现上被编译器自动转换成
 
以下为引用:

 // internal transformation of destructor under V1
 __gc class A
 {
 public:
    void Finalize() { Console::WriteLine(S"in ~A"); }
 };

 __gc class B : public A
 {
 public:
   void Finalize()
   {
     Console::WriteLine(S"in ~B");

     A::Finalize();
   }
 };
 



     但为了语义上兼容C++的程序,Managed C++同时也提供了一个虚方法实现析构函数。
 
以下为引用:

 __gc class A
 {
 public:
   virtual ~A()
   {
     System::GC::SuppressFinalize(this);

     A::Finalize();
   }
 };

 __gc class B : public A
 {
 public:
   virtual ~B()
   {
     System::GC:SuppressFinalize(this);

     B::Finalize();
   }
 };
 



     这样一来就允许Managed C++用户显式地调用类的Finalize()函数。
     虽然这样的思路在目前CLR版本中可以正常运转,但实际上和Dispose模式存在重合和冲突的地方,而且频繁使用Finalizer也会导致效率的降低。因此在新版本的Managed C++中,析构函数改为使用Dispose模式来实现。上面的代码被编译器自动转换为
 
以下为引用:

 // internal transformation of destructor under V2
 __gc class A : IDisposable
 {
 public:
   void Dispose()
   {
     System::GC::SuppressFinalize(this);

     Console::WriteLine( "in ~A"); }
   }
 };

 __gc class B : public A
 {
 public:
   void Dispose()
   {
     System::GC::SuppressFinalize(this);

     Console::WriteLine( "in ~B");

     A::Dispose();
   }
 };
 



     而对此类进行的析构操作,无论是直接调用类的析构函数,还是通过delete关键字隐式调用,都会被编译器翻译成对Dispose方法的调用。
     但这样一来GC就无法访问类的析构代码了,因为析构函数没有直接被转换成Finalize函数。为此Managed C++新版本提供了一个修订语法,通过''!''前缀支持显式定义Finalize函数,例如:
 
以下为引用:

 public ref class R
 {
 public:
   !R() {  Console::WriteLine( "I am the R::finalizer()!" ); }
 };
 


     被新版本Managed C++编译器自动转换为
 
以下为引用:

 // internal transformation under V2
 public ref class R
 {
 public:
   void Finalize() {  Console::WriteLine( "I am the R::finalizer()!" ); }
 };
 


     为了支持从老版本的Managed C++中将代码移植出来,MS还准备开发一个自动转换工具,将原本显式定义的Dispose()方法转换为析构函数;将原本定义的析构函数转换为!前缀支持的显式Finalize函数定义。

     有趣的是,这一实现思路与Delphi.NET完成类似析构函数语义的思路很相似,有兴趣进一步了解的朋友可以参考这篇文章

     Object Destructors and Finalizers in .NET Using C# and Delphi for .NET

     几位Borland系统的老兄也针对相关问题有一些不错的评论:

     Allen Bauer 的 The rules have not changed

     Nick Hodges 的 On Finalizers at Filafel

posted @ 2004-07-08 10:41 Flier Lu 阅读(397) 评论(0) 编辑

Whidbey 中对 GC 的功能两点增强

http://www.blogcn.com/user8/flier_lu/index.html?id=1452116&run=.0CDCD50

    Brad Abrams在其blog中发表了一篇介绍Whidbey 中对 GC 的功能增强的文章Teaching an old dog new tricks: GC fun in Whidbey。其中提到两种对Unmanaged Resource的管理的GC增强。Justin Rogers则在其blog中回应前文,提出了一些很有趣的观点The new face of the GC in Whidbey... I'm not sure this is a pretty face...

     新增的GC.AddMemoryPressure函数和GC.RemoveMemoryPressure函数可以提示GC当前Unmanaged资源的使用情况,以便GC判断在合适的时候进行回收工作;以前供WinForm内部使用的HandleCollector类也公开允许用户使用,用于管理Unmanaged资源的自动回收。

     对于CLR/JVM这种使用垃圾回收机制的环境,如何处理其无法管理的外部资源类型是一件让人头痛的事情。例如在使用Win32的位图(BITMAP)资源时,在Managed对象中只需保存一个句柄,但实际上内存占用跟位图文件大小相关,同时句柄本身的数量也是受到系统本身限制的。而这些Unmanaged资源对GC来说都是不可见的,GC只能看到这座冰山露出水面的一小部分,其余的部分只能靠程序员自觉管理,如使用Dispose模式。但这样的使用就违背了GC的基本原则,必然会出现两种内存管理模型的冲突。Whidbey中新增的这两个GC的增强,实际上就是分别对这两种内存管理模型,提供了与GC兼容的接口。

     GC.AddMemoryPressure函数和GC.RemoveMemoryPressure函数是全局性统计的函数,它们必须被使用到Unmanaged资源的CLR对象成对地调用,以保障对资源使用情况的精确跟踪。如果一个对象忘记调用RemoveMemoryPressure函数,则此对象和其Unmanaged资源在被施放后,GC认为以前用AddMemoryPressure函数注册的Unmanaged资源仍在使用,会降低GC的准确率。因此最好使用mihailik给出的封装类UnmanagedResource,通过IDispose和Finalizer确保两个函数的匹配调用。
 

以下为引用:

 public abstract class UnmanagedResource : IDisposable
 {
   readonly int m_PressureAmount;

   public UnmanagedResource(int pressureAmount)
     : this( pressureAmount, true )
   { }


   public UnmanagedResource(int pressureAmount, bool addPressureNow)
   {
     this.m_PressureAmount=pressureAmount;

     if( addPressureNow )
       ResourceAllocated();
   }

   protected void ResourceAllocated()
   {
     GC.AddMemoryPressure(PressureAmount);
   }

   protected void ResourceReleased()
   {
     GC.RemoveMemoryPressure(PressureAmount);
   }


   protected int PressureAmount
   {
     get { return m_PressureAmount; }
   }


   public void Dispose()
   {
     Dispose(true);
   }

   protected virtual void Dispose(bool disposing)
   {
     ResourceReleased();

     if( disposing )
     {
       GC.SuppressFinalize();
     }
   }

   ~UnmanagedResource()
   {
     Dispose(false);
   }
 }
 



     不过个人认为这种提示的管理粒度过大了,而且过于依赖人的自觉性。不如使用IoC模式的思想,定义一个接口IUnmanagedResource,所有使用Unmanaged资源的类都实现此接口,然后GC提供GC.RegisterForUnmanagedResource函数将对象注册到GC。此接口提供GetUnmanagedResourceSize()函数,让GC了解其Unmanaged资源的使用情况,如
 
以下为引用:

 public interface IUnmanagedResource
 {
   uint GetUnmanagedResourceSize();
 };

 class Bitmap : IUnmanagedResource
 {
    private long _size;

    uint GetUnmanagedResourceSize()
    {
      return _size;
    }

    Bitmap (string path )
    {
       _size = new FileInfo(path).Length;

       GC.RegisterForUnmanagedResource(this);

       // other work
    }
 }
 



     这样的好处是可以将对Unmanaged资源的管理粒度降低到对象一级,并让CLR对象和Unmanaged资源绑定,确保施放CLR对象时能够同步更新整体资源使用情况的统计数据,代价是GC需要维护一个和Finalizer列表类似的UnmanagedResource列表。
     在实现上GC.AddMemoryPressure函数和GC.RemoveMemoryPressure函数更新由GC的一个子类MemoryWatcher维护的几个统计值,并根据一定的策略触发GC的回收条件。

     System.Runtime.InteropServices.HandleCollector实现则比较简单,构造函数中指定阈值,自身维护一套计数器,在超出指定范围后回收句柄。使用方法很简单,如下:
 

以下为引用:

 // HandleCollector(string name, int initialThreshold,  int maximumThreshold);

 class XXX
 {
   static readonly HandleCollector GdiHandleType =
      new HandleCollector( “GdiHandles”, 10, 50);

   static IntPtr CreateSolidBrush()
   {
      IntPtr temp = CreateSolidBrushImpl(…);

      GdiHandleType.Add();

      return temp;
   }

 

   internal static void DeleteObject(IntPtr handle)
   {
      DeleteObjectImpl(handle);

      GdiHandleType.Remove();
   }
 }
 



    有兴趣的朋友可以进一步看看这个例子:
     ::URL::http://www.gotdotnet.com/userfiles/chrisan/HandleCollector.zip

posted @ 2004-07-08 10:40 Flier Lu 阅读(389) 评论(0) 编辑

在用户态进行虚拟空间地址向物理空间地址的转换

http://www.blogcn.com/user8/flier_lu/index.html?id=1428057&run=.00B7C29

    在《自动获取 NT 系统服务描述表与函数名映射表》一文中,我给出了一个从虚地址向物理地址转换的经验函数。
 
以下为引用:

 PHYSICAL_ADDRESS TPhysicalMemoryMapping::LinearAddressToPhysicalAddress(LPCVOID lpVirtualAddress)
 {
   PHYSICAL_ADDRESS addr = { 0, 0 };

   if((DWORD)lpVirtualAddress < 0x80000000L || (DWORD)lpVirtualAddress >= 0xA0000000L)
     addr.QuadPart = (DWORD)lpVirtualAddress & 0x0FFFF000;
   else
     addr.QuadPart = (DWORD)lpVirtualAddress & 0x1FFFF000;

   return addr;
 }
 



     这个函数实际上只处理了0x8000000 - 0xA0000000这段内存地址的转换,而对0xA0000000以上内存,则只是用 PA = VA & 0x0FFFF000 保障访问不会出错,这实际上并不能获取这段内存的实际内容。了解 WinNT/2K 内存布局的朋友应该知道,为了系统实现简便并保障地址转换效率,WinNT将0x80000000 - 0xA0000000内存端直接映射到物理内存,转换算法就是一个简单的 PA = VA & 0x1FFFF000;而对 0xA0000000 以上的虚拟地址,则是使用两级页表索引来实现虚地址页向物理地址页的映射(如启用 AWE 则为三级,这里暂不讨论,实现原理类似)。因此要真正访问 0xA0000000 以上虚地址内存,必须读取进程的 PDE/PTE 表。
     但问题是PDE页表一般存放在虚地址 0xC0300000,PTE 页表则从虚地址 0xC0000000 开始。也就是说这些页表本身,就存放在以页表才能访问的虚地址上。要查PDE/PTE表进行虚地址向物理地址转换,首先要知道 0xC0300000 和 0xC0000000 映射在哪个物理页上。这样一来就变成了先有鸡还是先有蛋的问题了,呵呵。 NT 内核本身,则可以通过进程 CR3 寄存器中保存的 PDE 页表起始物理地址直接访问,但不巧的是,访问 CR3 需要有 Ring 0 的状态。因此大多数介绍虚地址向物理地址转换的文章,在描述完两级映射之后,都是提供 Ring 0 层的代码演示实现,例如 webcrazy 的大作《小议Windows NT/2000分页机制》
     因为此类的文章较多了,这里我就不在罗嗦内存结构和转换算法了,有兴趣研究的朋友可以参考 Inside Win2K 3nd 和 webcrazy 的相关文章。

     既然无法知道 PDE/PTE 的物理地址,也不能直接访问 CR3,我就开始琢磨各种迂回的途径。昨天折腾了大半天,翻越了 ntoske 和 ntosmm 目录下的一堆源码,在大大体验了一把 Open Source Windows NT 的优势之后,终于找到了一个还算完美的解决方法 :P

     我们知道 PDE/PTE 是进程相关的。因为每个进程都有自己的虚拟空间,因此必须有一整套独立的页表备查。核心在进行线程切换时,如果两个线程在一个进程内,无需做页表的切换;如果两个线程跨越了进程,就必须将目标线程所在进程的Context载入,这其中就包括我们所需要的 CR3 的内容。而在翻越代码后,我发现 CR3 的内容被保存到 EPROCESS::KPROCESS::DirectoryTableBase[0] 中。这个变量保存了 PDE 页表物理地址和 hyber space PTE 页表物理地址(以后再详细介绍)。
     于是实现思路就清晰了:
     1.取当前进程的 EPROCESS
     2.读取 PDE 页表物理地址
     3.通过分解虚地址获取 PDE/PTE 表的索引
     4.查表获得目标物理页地址,读取物理页内容。

 1.取当前进程的 EPROCESS

     当前进程的 EPROCESS 地址在 ntoskrnl.exe 的空间中,因此可以通过直接映射内存访问。使用OpenProcess打开当前进程,获得句柄;使用NTDLL::ZwQuerySystemInformation函数获取所有核心句柄表,线性搜索到进程句柄,其指向的内核对象就是 EPROCESS。

 2.读取 PDE 页表物理地址

     PDE 页表物理地址在 EPROCESS 结构的偏移 0x18 处

 3.通过分解虚地址获取 PDE/PTE 表的索引

     将目标虚地址如 0xA01A8148 分解为三部分:

     DDDDDDDDDDTTTTTTTTTTBBBBBBBBBBBB
     01234567890123456789012345678901

     高10位是PDE索引;中间10位是PTE索引;末尾12位是页内字节索引。

 4.查表获得目标物理页地址,读取物理页内容。

     PDE/PTE表项都是一个DWORD,其高20位定义下一级的索引。
 

以下为引用:

 typedef struct _MMPTE_HARDWARE {
     ULONG Valid : 1;
     ULONG Write : 1;  // UP version
     ULONG Owner : 1;
     ULONG WriteThrough : 1;
     ULONG CacheDisable : 1;
     ULONG Accessed : 1;
     ULONG Dirty : 1;
     ULONG LargePage : 1;
     ULONG Global : 1;
     ULONG CopyOnWrite : 1; // software field
     ULONG Prototype : 1;   // software field
     ULONG reserved : 1;  // software field
     ULONG PageFrameNumber : 20;
 } MMPTE_HARDWARE, *PMMPTE_HARDWARE;
 


     具体查表转换算法如下
 
以下为引用:

 #define GetPdeAddress(base, va) (LPCVOID)((base) + (((ULONG)(va) >> 22) << 2))
 #define GetPteAddress(base, va) (LPCVOID)((base) + ((((ULONG)(va) >> 12) & 0x3FF) << 2))

 const PHYSICAL_ADDRESS TPhysicalMemoryMapping::LinearAddressToPhysicalAddress(LPCVOID lpVirtualAddress)
 {
   PHYSICAL_ADDRESS addr = { 0, 0 };

   if((DWORD)lpVirtualAddress >= 0x80000000L && (DWORD)lpVirtualAddress < 0xA0000000L)
   {
     addr.QuadPart = (DWORD)lpVirtualAddress & 0x1FFFF000;
   }
   else
   {
     MMPTE_HARDWARE PDE, PTE;

     m_pManager->ReadPhysicalMemoryPage(GetPdeAddress(m_pManager->PTE.LowPart, lpVirtualAddress), &PDE, sizeof(PDE));
     m_pManager->ReadPhysicalMemoryPage(GetPteAddress(PDE.PageFrameNumber << PAGE_SHIFT, lpVirtualAddress), &PTE, sizeof(PTE));

     addr.LowPart = (PTE.PageFrameNumber << PAGE_SHIFT) + BYTE_OFFSET(lpVirtualAddress);
   }
   return addr;
 }
 



     知道了目标页面的物理地址,就可以通过读取 DevicePhysicalMemory 直接获取了。

posted @ 2004-07-08 10:39 Flier Lu 阅读(1360) 评论(0) 编辑

Win32 核心 DPC 设计思想和实现思路浅析

http://www.blogcn.com/user8/flier_lu/index.html?id=1397656&run=.09D4C2F

    x86架构设计在上是基于中断思想的,因而从DOS到Win32,操作系统中大量使用中断的概念来表达异步操作的行为。但与DOS下独占的情况不同,Win32下需要由系统对多任务进行调度,因此中断响应代码必须尽可能地简单,并且尽快的将控制权交还给系统。虽然这样一来系统调度的响应速度和实现过程方便了,但还是有很多功能需要在中断响应中完成。为此,Win32核心提供了DPC(Deferred Procedure Call)和APC(Asynchronous Procedure Call)两个IRQL特殊的软件中断级别,用于实现延迟和异步的过程调用。
     从IRQL分层来说,DPC和APC是介于较高级别的设备中断和最低级别的Passive中断之间,由操作系统用于完成特殊方法调用的中断级别。与处理硬件操作的设备中断和更高级别的时钟、处理器中断不同,这两级中断纯粹是为了实现功能调用异步性而设计实现的,因此操作系统本身也对它们具有很强的依赖型。APC这里暂且不讨论,以后有机会再写篇文章专门讨论 :)

     DPC在功能上可以理解为ISR(Interrupt Service Routine)的一部分。只是因为ISR为了尽量简单和返回控制权给操作系统,而将一部分功能剥离出来放入相应DPC中,延迟调用。因为DPC的IRQL仅在APC和Passive中断之上,所以系统可以从容地处理完高级别的中断后,再在DPC一级慢慢处理积累起来的相对并不那么紧急功能。
     DPC在使用上可以理解为一个回调函数的封装对象。系统本身或者设备驱动程序,在合适的地方如设备驱动程序的AddDevice函数或DispatchPnP函数处理IRP_MN_START_DEVICE请求时,初始化一个DPC对象;在ISR中判断是否需要进一步处理中断,是则请求将DPC对象插入到系统DPC队列中;系统处理完高IRQL后,会在IRQL DISPATCH_LEVEL级别慢慢处理DPC队列中的DPC对象;每个DPC对象封装的回调函数,会使用同时封装的调用参数,被系统调用,完成在ISR中来不及完成的工作;如果需要进一步的工作,还可以继续请求插入DPC对象到DPC队列中。

     DPC对象从最终用户角度有两种:DpcForIsr和CustomDPC。前者是与设备驱动对象(Device Object)绑定的;后者则由驱动自行维护。但从实现上来说,只有一种DPC对象存在,DpcForIsr所涉及的维护函数,实际上都是对CustomDPC的一个封装而已。

     我们首先来看看初始化DPC对象的实现。KeInitializeDpc函数(ntoskedpcobj.c:39)完成具体的DPC对象的初始化,实际上就是填充一个内存结构KDPC(ntosinc tosdef.h:331)。
 

以下为引用:

 //
 // Deferred Procedure Call (DPC) object
 //

 typedef struct _KDPC {
     CSHORT Type;
     UCHAR Number;
     UCHAR Importance;
     LIST_ENTRY DpcListEntry;
     PKDEFERRED_ROUTINE DeferredRoutine;
     PVOID DeferredContext;
     PVOID SystemArgument1;
     PVOID SystemArgument2;
     PULONG_PTR Lock;
 } KDPC, *PKDPC, *RESTRICTED_POINTER PRKDPC;
 



     Type 表示此内核对象的类型,在KOBJECTS枚举类型(ntosincke.h:122)中定义,缺省为 DpcObject = 0x13。此外WinXP/2003新增了一种ThreadedDpcObject = 0x18
     Number 在多处理器环境下用于指定此DPC对象加入到哪个处理器的DPC队列中,我们等会讨论多处理器时详细描述。缺省为 0
     Importance 表示此DPC对象的重要性,在KDPC_IMPORTANCE枚举类型(ntosinc tosdef.h:321)中定义,缺省为 MediumImportance = 1
     DpcListEntry 是用于维护DPC队列的链表指针
     DeferredRoutine 是此DPC对象绑定的回调函数,后面DeferredContext、SystemArgument1和SystemArgument2分别是此回调函数被调用时的参数。如ISR中调用IoRequestDpc时,后面两个参数就用于传递Irp和Context参数给DPC的回调函数。
     Lock 保存此DPC对象所在DPC队列的自旋锁,用于锁定DPC队列,同时也用于判断此DPC对象是否被加入到一个DPC队列中。

     了解了KDPC对象的结构,实际上维护代码就非常简单了。KeInitializeDpc函数将KDPC对象结构初始化为初值;IoInitializeDpcRequest函数则只是对KeInitializeDpc函数的一个简单包装,如下
 

以下为引用:

 #define IoInitializeDpcRequest( DeviceObject, DpcRoutine ) (
     KeInitializeDpc( &(DeviceObject)->Dpc,                  
                      (PKDEFERRED_ROUTINE) (DpcRoutine),     
                      (DeviceObject) ) )
 


     注意WinXP/2003下实际上KeInitializeDpc函数和KeInitializeThreadedDpc函数都是由一个KiInitializeDpc函数完成具体工作的,只是传递的最后一个参数定义的对象类型不同。

     KeInsertQueueDpc函数(ntoskedpcobj.c:89)实际上是系统对DPC队列维护的核心函数,其伪代码如下:
 

以下为引用:

 BOOLEAN KeInsertQueueDpc (IN PRKDPC Dpc, IN PVOID SystemArgument1,IN PVOID SystemArgument2)
 {
   PKSPIN_LOCK Lock;
   KIRQL OldIrql;

   KeRaiseIrql(HIGH_LEVEL, &OldIrql);  // 提升当前IRQL到最高,屏蔽其它中断

   PKPRCB = KeGetCurrentPrcb();        // 获取当前处理器控制块

   // 通过比较Dpc->Lock是否为空,来判断此DPC对象是否已经被加入到DPC队列;
   // 如果DPC对象可以被加入到队列,则将当前处理器控制块的DPC自旋锁复制到Dpc->Lock中
   if ((Lock = InterlockedCompareExchangePointer(&Dpc->Lock, &Prcb->DpcLock, NULL)) == NULL)
   {
     // 更新当前处理器控制块的统计信息
     Prcb->DpcCount += 1;
     Prcb->DpcQueueDepth += 1;

     // 更新DPC对象的参数信息
     Dpc->SystemArgument1 = SystemArgument1;
     Dpc->SystemArgument2 = SystemArgument2;

     // 根据DPC对象优先级,决定将之加入到DPC队列的头部或尾部
     if (Dpc->Importance == HighImportance)
         InsertHeadList(&Prcb->DpcListHead, &Dpc->DpcListEntry);
     else
         InsertTailList(&Prcb->DpcListHead, &Dpc->DpcListEntry);

     // 如果当前处理器没有DPC对象活动或DPC中断请求,则进一步判断是否发出DPC中断请求
     if (Prcb->DpcRoutineActive == FALSE && Prcb->DpcInterruptRequested == FALSE)
     {
       // 如果DPC对象优先级为中高;
       // 或者DPC队列长度超过阈值MaximumDpcQueueDepth;
       // 或者DPC请求速率小于阈值MinimumDpcRate
       if ((Dpc->Importance != LowImportance) ||
           (Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||
           (Prcb->DpcRequestRate < Prcb->MinimumDpcRate))
       {
         // 满足触发条件,则发出DPC中断请求
         Prcb->DpcInterruptRequested = TRUE;
         KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
       }
     }
   }
   KeLowerIrql(OldIrql);
   return (Lock == NULL);
 }
 



     这里的几个阈值,在KiInitializeKernel函数(ntoskei386kernlini.c:246)中,根据全局变量KiMaximumDpcQueueDepth、KiMinimumDpcRate和KiAdjustDpcThreshold确定。而这几个全局变量可以通过注册表项(HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession Managerkernel)下的DpcQueueDepth、MinimumDpcRate和AdjustDpcThreshold三个键值来设置。具体的设置方法,请参考MSDN以及性能计数器的Processor% DPC Time等动态指数。

     而处理与驱动绑定的DPC对象的IoRequestDpc函数只是KeInsertQueueDpc函数的一个简单包装。
 

以下为引用:

 #define IoRequestDpc( DeviceObject, Irp, Context ) ( 
     KeInsertQueueDpc( &(DeviceObject)->Dpc, (Irp), (Context) ) )
 


     与KeInsertQueueDpc函数对应的KeRemoveQueueDpc函数(ntoskedpcobj.c:272)实际上只是完成简单的将DPC对象从DPC队列中删除的功能。

     最后对DPC对象属性进行修改的KeSetImportanceDpc函数(ntoskedpcobj.c:367)和KeSetTargetProcessorDpc函数(ntoskedpcobj.c:401)实际上都是直接修改DPC对象结构的相应域。KDPC::Number大于MAXIMUM_PROCESSORS = 32时,用于指定DPC对象的目标CPU。如调用KeSetTargetProcessorDpc(pKDpc, 2)后,pKDpc = MAXIMUM_PROCESSORS + 2。

     在了解了DPC对象和DPC队列的大致维护函数功能后,我们来看看稍微复杂一些的在多处理器下DPC队列的维护流程。

     前面提到KDPC::Number指定了DPC对象所用的处理器号,因此在KeInsertQueueDpc函数开始获取处理器控制块时,需要判断Number是否指向一个处理器,并从全局处理器控制块列表中获取相应的处理器控制块,为代码如下:
 

以下为引用:

 if (Dpc->Number >= MAXIMUM_PROCESSORS)  // Number大于MAXIMUM_PROCESSORS时用于指定处理器
 {
   Processor = Dpc->Number - MAXIMUM_PROCESSORS;
   Prcb = KiProcessorBlock[Processor];   // 全局唯一的处理器控制块列表

 }
 else
 {
   Prcb = KeGetCurrentPrcb();
 }

 KiAcquireSpinLock(&Prcb->DpcLock);      // 使用自旋锁保护处理器控制块中的DPC队列
 



     而在KeInsertQueueDpc函数中判断是否发出DPC中断请求时,也需要做更复杂的逻辑判断。
     对DPC对象目标处理器就是当前处理器的情况,可以和前面单处理器时一样处理,直接发送DPC中断请求;但对于DPC对象目标处理器是其他处理器的情况,就必须使用KiIpiSend函数发送IPI(InterProcessor Interrupt)中断,通知目标处理器执行动作。此IPI中断是介于系统掉电中断(POWER_LEVEL)和时钟中断之间的特殊IRQL,专门用于在多处理器情况下协调多个处理器的工作。
     此外就是在多处理器情况下,各种对DPC队列的操作都需要用此处理器控制块的DPC队列自旋锁保护起来,避免同步问题。

     由此我们可以看到,实际上DPC队列是每个处理器一个的,我们完全可以将某个DPC对象绑定到某个处理器上,实现类似线程亲缘性(Thread Affinity)的效果,优化在多处理器环境下的性能。但这同时也带来一个问题,就是ISR程序可以和DPC回调函数同时被调用,某种程度上也造成了开发复杂度的增加,具体处理方法请参考DDK中相关文档。

     Kernel-Mode Driver ArchitectureDesign GuideServicing InterruptsDPC Objects and DPCs

posted @ 2004-07-08 10:33 Flier Lu 阅读(1294) 评论(1) 编辑

http://202.102.53.35/user8/flier_lu/main.asp?id=1324316

Win32 调试接口设计与实现浅析 [2] 调试事件

Flier Lu @ http://flier_lu.blogone.net/

[2] 调试事件

    前面说到 Win32 下的用户态调试器实际上就是一个while循环,循环体内先等待一个调
试事件,然后处理之,最后将控制权交还给调试服务器,就好像一个窗口消息循环一样。调
试事件的核心实际上就是一个DEBUG_EVENT结构,在WinBase.h文件中定义如下:

以下为引用:

typedef struct _DEBUG_EVENT {
    DWORD dwDebugEventCode;
    DWORD dwProcessId;
    DWORD dwThreadId;
    union {
        EXCEPTION_DEBUG_INFO Exception;
        CREATE_THREAD_DEBUG_INFO CreateThread;
        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO ExitThread;
        EXIT_PROCESS_DEBUG_INFO ExitProcess;
        LOAD_DLL_DEBUG_INFO LoadDll;
        UNLOAD_DLL_DEBUG_INFO UnloadDll;
        OUTPUT_DEBUG_STRING_INFO DebugString;
        RIP_INFO RipInfo;
    } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

 


    dwDebugEventCode字段给出此调试事件的类型,dwProcessId和dwThreadId字段分别给
出调试事件发生的进程和线程ID号。调试事件一般有以下几类:

以下为引用:

#define EXCEPTION_DEBUG_EVENT       1
#define CREATE_THREAD_DEBUG_EVENT   2
#define CREATE_PROCESS_DEBUG_EVENT  3
#define EXIT_THREAD_DEBUG_EVENT     4
#define EXIT_PROCESS_DEBUG_EVENT    5
#define LOAD_DLL_DEBUG_EVENT        6
#define UNLOAD_DLL_DEBUG_EVENT      7
#define OUTPUT_DEBUG_STRING_EVENT   8
#define RIP_EVENT                   9

 


    CREATE_PROCESS_DEBUG_EVENT事件在调试的进程中创建一个新的进程或调试器开始调试
一个附加到的进程时被引发;相对的EXIT_PROCESS_DEBUG_EVENT事件在调试的进程结束最后
一个线程时被引发;每次新建/推出一个线程时会有CREATE_THREAD_DEBUG_EVENT/EXIT_THRE
AD_DEBUG_EVENT事件被引发;每次载入/卸载一个DLL时会有LOAD_DLL_DEBUG_EVENT/UNLOAD_
DLL_DEBUG_EVENT事件被引发;使用OutputDebugString函数输出一个字符串时调试器会接受
到一个OUTPUT_DEBUG_STRING_EVENT事件;异常被引发时调试器会接受到一个第一时间的EXC
EPTION_DEBUG_EVENT事件,如果调试器不处理此异常,则进入被调试进程的正常SEH调用链
,如果被调试进程也不处理,则会再次引发此事件;RIP_EVENT则一般用于报告错误事件。
    一般来说程序的调试事件按照如下顺序被引发:

以下为引用:

    CREATE_PROCESS_DEBUG_EVENT

    LOAD_DLL_DEBUG_EVENT x n // 静态载入的DLL

    CREATE_THREAD_DEBUG_EVENT & EXIT_THREAD_DEBUG_EVENT // 多线程程序中成对出现

    LOAD_DLL_DEBUG_EVENT & UNLOAD_DLL_DEBUG_EVENT // 动态载入 DLL 时成对出现

    EXCEPTION_DEBUG_EVENT x n // 随机出现

    OUTPUT_DEBUG_STRING_EVENT x n // 程序写调试信息时出现

    EXIT_PROCESS_DEBUG_EVENT

 


    接下来我们详细分析每种调试事件的内容及其被引发的时机。

    首先是建立进程的CREATE_PROCESS_DEBUG_EVENT事件和建立线程的CREATE_THREAD_DEBU
G_EVENT事件。这两个事件都是由DbgkCreateThread函数(ntos\dbgk\dbgkproc.h:211)引发
的。此函数首先检查当前线程是否具有调试端口的活动线程;然后检查当前线程是否是进程
的创建的第一个线程;如果不是第一个线程,或者调试器是挂接(attach)到一个活动进程上
(判断依据是此进程是否占用过用户态的CPU时间),则向调试子系统的调试服务器引发CRE
ATE_THREAD_DEBUG_EVENT事件;否则转而报告CREATE_PROCESS_DEBUG_EVENT事件。具体的调
试事件信息这里就不罗嗦了,有兴趣写调试器的朋友可以参考MSDN和<Debugging Applicati
ons>中相关内容。
    DbgkCreateThread函数伪代码如下:

以下为引用:

VOID DbgkCreateThread(PVOID StartAddress)
{
  if(!PsGetCurrentProcess()->DebugPort || PsGetCurrentThread()->DeadThread)
  {
    return;
  }

  PsLockProcess(Process,KernelMode,PsLockWaitForever); // 锁定进程中所有线程

  if(PsGetCurrentProcess()->Pcb.UserTime &&
     PsGetCurrentProcess()->CreateProcessReported == FALSE)
  {
    // 引发 CREATE_PROCESS_DEBUG_EVENT 事件
  }
  else
  {
    // 引发 CREATE_THREAD_DEBUG_EVENT 事件
  }

  PsUnlockProcess(PsGetCurrentProcess());
}

 


    Win32在创建用户态线程的时候,大致流程如下:

以下为引用:

  CreateThread (kernel32.dll)
  CreateRemoteThread (kernel32.dll)
  NtCreateThread (ntoskrnl.exe)
  PspCreateThread (ntos\ps\create.c:237)

 


    PspCreateThread函数在创建用户态线程时,使用PspUserThreadStartup函数(ntos\ps\
create.c:1639)作为入口函数参数,线程被创建后直接进入此函数。PspUserThreadStartu
p函数对非僵死线程和没有结束的线程初始化其APC;然后调用DbgkCreateThread函数通知调
试器采取相应动作;最后将进程的用户态CPU事件设置为1,以标示此进程已启动。对一种特
殊线程,非僵死线程但线程启动时已经停止,则直接DbgkCreateThread后立刻PspExitThrea
d,以通知调试器采取相应动作。PspUserThreadStartup函数伪代码如下:

以下为引用:

VOID PspUserThreadStartup(IN PKSTART_ROUTINE StartRoutine, IN PVOID StartContext
)
{
  if(!PsGetCurrentThread()->DeadThread && !PsGetCurrentThread()->HasTerminated)
  {
    // 初始化 APC
  }
  else
  {
    if(!PsGetCurrentThread()->DeadThread)
    {
      DbgkCreateThread(StartContext);
    }
    PspExitThread(STATUS_THREAD_IS_TERMINATING);
  }

  DbgkCreateThread(StartContext);

  if(PsGetCurrentProcess()->Pcb.UserTime == 0)
  {
    PsGetCurrentProcess()->Pcb.UserTime = 1;
  }
}

 


    与DbgkCreateThread函数对应的是DbgkExitThread函数(ntos\dbgk\dbgkproc.c:384)和
DbgkExitProcess函数(ntos\dbgk\dbgkproc.c:439),分别向调试服务器引发EXIT_THREAD_D
EBUG_EVENT和EXIT_PROCESS_DEBUG_EVENT事件。
    这两个函数被系统内核推出线程的PspExitThread函数(ntos\ps\psdelete.c:622)在合
适的时候调用。PspExitThread函数检测当前进程PCB的线程列表是否只有当前线程一个线程
,如果是则调用DbgkExitProcess函数,否则调用DbgkExitThread函数。

    对于载入和卸载DLL,实际的调用流程如下:


以下为引用:

  LoadLibrary (kernel32.dll)
  LoadLibraryEx (kernel32.dll)
  BasepLoadLibraryAsDataFile (kernel32.dll)
  NtMapViewOfSection (ntos\mm\mapview.c:204)

 


    NtMapViewOfSection函数在调用MmMapViewOfSection函数(ntos\mm\mapview.c:699)完
成实际的内存文件映射之后,会根据映射节的标记和目标进程是否是当前进程,判断是否要
调用DbgkMapViewOfSection函数(ntos\dbgk\dbgkproc.c:495)通知调试服务器有新的映象文
件被加载。与之对于MmUnmapViewOfSection函数(ntos\mm\umapview.c:88)也在判断标志和
目标进程是否是当前进程后在函数末尾调用DbgkUnMapViewOfSection函数(ntos\dbgk\dbgkp
roc.c:567)通知调试服务器有映象文件被卸载。
    与前面的几种事件不同,OutputDebugString函数(kernel32.dll)实际上是通过异常实
现的。而且有趣的是,这个函数是为数不多的W后缀Unicode转而调用A后缀Ansi版本完成实
际功能的例子。OutputDebugStringA函数(kernel32.dll)实际上使用RaiseException函数引
发了一个异常号为0x40010006的软异常,并将字符串的指针和长度作为异常参数传递。

    DbgkForwardException函数(ntos\dbgk\dbgkport.c:96)作为实际引发EXCEPTION_DEBUG
_EVENT调试事件的函数,在系统的异常分发KiDispatchException函数(ntos\ke\i386\excep
tn.c:797)中被调用。KiDispatchException函数根据异常被引发时的状态,分别完成核心和
用户态的异常处理。
    对核心态异常,首先给核心调试程序一个处理机会,然后试图分发到基于帧的SEH异常
去,没有被处理的话则再给核心调试程序一个机会,如果还是没被处理,就只能调用KeBugC
heckEx函数(ntos\ke\bugcheck.c:157)蓝屏了,呵呵。
    对用户态异常,还是首先试图让核心调试器处理,如果不行才调用DbgkForwardExcepti
on函数试图分发,没有被处理的话则多次尝试,如果还是没被处理,就停止线程并报告异常
给用户。
    伪代码如下:

以下为引用:

VOID KiDispatchException (IN PEXCEPTION_RECORD ExceptionRecord, IN PKEXCEPTION_F
RAME ExceptionFrame,
                          IN PKTRAP_FRAME TrapFrame, IN KPROCESSOR_MODE Previous
Mode, IN BOOLEAN FirstChance)
{
  CONTEXT ContextFrame;

  KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame); // 从核心异常
帧(Frame)构造异常上下文(Context)

  if (ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT)
  {
    ContextFrame.Eip--;
  }

  if (PreviousMode == KernelMode)
  {
    if (FirstChance == TRUE)
    {
      if (KiDebugRoutine && KiDebugRoutine(..., FALSE) != FALSE) goto Handle1

      if(RtlDispatchException(ExceptionRecord, &ContextFrame) == TRUE) goto Hand
led1;
    }

    if (KiDebugRoutine && KiDebugRoutine(..., TRUE) != FALSE) goto Handle1

    KeBugCheckEx(...); // 核心错误,以可控方式崩溃 -_-b 说白了就是Dead Blue Scre
en,呵呵
  }
  else // PreviousMode = UserMode
  {
    if (FirstChance == TRUE)
    {
      if (KiDebugRoutine && KiDebugRoutine(..., FALSE) != FALSE) goto Handle1

      if (DbgkForwardException(ExceptionRecord, TRUE, FALSE)) goto Handled2;

      // 将异常信息转换到用户模式,并尝试分发
    }

    if (DbgkForwardException(ExceptionRecord, TRUE, TRUE))
    {
        goto Handled2;
    }
    else if (DbgkForwardException(ExceptionRecord, FALSE, TRUE))
    {
        goto Handled2;
    }
    else
    {
        ZwTerminateThread(NtCurrentThread(), ExceptionRecord->ExceptionCode);
        KeBugCheckEx(...);
    }
  }

Handled1:
  KeContextToKframes(TrapFrame, ExceptionFrame, &ContextFrame,
                     ContextFrame.ContextFlags, PreviousMode);

Handled2:
}

 


    DbgkForwardException函数分别针对DebugException和SecondChance参数的三种组合被
调用。DebugException为True时向调试端口发送信息,否则向异常端口发送

    至此,几种常见的调试事件的引发机制就大概有了一个了解,下一节将介绍将这些调试
事件和最终用户态调试器关联起来的Win32中调试子系统的实现思路。

to be continue...

posted @ 2004-07-08 10:32 Flier Lu 阅读(1124) 评论(0) 编辑

CLR 中匿名函数的实现原理浅析

http://www.blogcn.com/user8/flier_lu/index.html?id=1397624&run=.04B5CE2

CLR 中匿名函数的实现原理浅析

     C# 2.0中提供了通过delegate实现匿名函数功能,能有效地减少用户的薄记代码工作,例如
 

以下为引用:

 ...
 button1.Click += new EventHandler(button1_Click);
 ...
 void button1_Click(Object sender, EventArgs e) {
    // Do something, the button was clicked...
 }
 ...
 


     可以被简化为直接使用匿名函数构造,如
 
以下为引用:

 ...
 button1.Click += delegate(Object sender, EventArgs e) {
   // Do something, the button was clicked...
 }
 ...
 


     关于匿名函数的使用方法可以参考Jeffrey Richter的Working with Delegates Made Easier with C# 2.0一文。简要说来就是C#编译器自动将匿名函数代码转移到一个自动命名函数中,将原来需要用户手工完成的工作自动完成。例如构造一个私有静态函数,如
 
以下为引用:

 class AClass {
   static void CallbackWithoutNewingADelegateObject() {
     ThreadPool.QueueUserWorkItem(delegate(Object obj) { Console.WriteLine(obj); }, 5);
   }
 }
 


     被编译器自动转换为
 
以下为引用:

 class AClass {
   static void CallbackWithoutNewingADelegateObject() {
     ThreadPool.QueueUserWorkItem(new WaitCallback(__AnonymousMethod$00000002), 5);
   }

   private static void __AnonymousMethod$00000002(Object obj) {
     Console.WriteLine(obj);
   }
 }
 



     而这里自动生成的函数是否为static,编译器根据使用此函数的地方是否static决定。这也是为什么C# 2.0规范里面禁止使用goto, break和continue语句从一个匿名方法里跳出,或从外面跳入其中的原因,因为他们代码虽然写在一个作用域里面,但实际上实现上并不在一起。
     更方便的是编译器可以根据匿名函数使用的情况,自动判断函数参数,无需用户在定义时指定,如
 
以下为引用:

 button1.Click += delegate(Object sender, EventArgs e) { MessageBox.Show("The Button was clicked!"); };
 


     在不使用参数时,完全等价于
 
以下为引用:

 button1.Click += delegate { MessageBox.Show("The Button was clicked!"); };
 

     相对于匿名函数的实现来说,比较复杂的是匿名函数对于其父作用域中变量的使用及其实现。MS的Grant Ri在其blog上有一系列的讨论文章。
     Anonymous Methods, Part 1 of ?
     Anonymous Methods, Part 2 of ?
     Anonymous Method Part 2 answers 

     需要解决的问题有两个:一是不在一个变量作用域中的匿名函数如何访问父函数和类的变量;二是匿名函数使用到的变量的生命周期必须与其绑定,而不能与父函数的调用生命周期绑定。这两个问题使得C#编译器选择较为复杂的独立类封装方式实现匿名函数和相关变量生命周期的管理。

     首先,匿名函数使用到的父函数中局部变量,无聊是引用类型还是值类型,都必须从栈变量转换为堆变量,以便在其作用域外的匿名函数实现代码可以访问并控制生命周期。因为栈变量的生命周期与其所有者函数是一致的,所有者函数退出后,其堆栈自动恢复到调用函数前,也就无法完成变量生命周期与函数调用生命周期的解耦。
     例如下面这个简单的匿名函数中,使用了父函数的局部变量,虽然此匿名函数只在父函数里面使用,但C#编译器还是使用独立类对其使用到的变量进行了包装。
 

以下为引用:

 delegate void Delegate1();

 public void Method1()
 {
   int i=0;

   Delegate1 d1 = delegate() { i++; };

   d1();
 }
 



     自动生成的包装代码类似如下
 
以下为引用:

 delegate void Delegate1();

 private sealed class __LocalsDisplayClass$00000002
 {
   public int i;

   public void __AnonymousMethod$00000001()
   {
     this.i++;
   }
 };

 public void Method1()
 {
   __LocalsDisplayClass$00000002 local1 = new __LocalsDisplayClass$00000002();
   local1.i = 0;

   Delegate1 d1 = new Delegate1(local1.__AnonymousMethod$00000001);

   d1();
 }
 



     但对于有多个局部变量作用域的情况就比较复杂了,例如Grant Ri在其例子中给出的代码
 
以下为引用:

 delegate void NoArgs();

 void SomeMethod()
 {
     NoArgs [] methods = new NoArgs[10];
     int outer = 0;
     for (int i = 0; i < 10; i++)
     {
         int inner = i;
         methods[i] = delegate {
             Console.WriteLine("outer = {0}", outer++);
             Console.WriteLine("i = {0}", i);
             Console.WriteLine("inner = {0}", ++inner);
         };
         methods[i]();
     }
     for (int j = 0; j < methods.Length; j++)
         methods[j]();
 }
 



     就需要一个类封装变量outer;一个类封装变量i;另外一个类封装inner和匿名函数,并引用前面两个封装类的实例。因为变量outer、i和inner有着不同的作用域,呵呵。伪代码如下:
 
以下为引用:

 private sealed class __LocalsDisplayClass$00000008
 {
   public int outer;

 };
 private sealed class __LocalsDisplayClass$0000000a
 {
   public int i;

 };
 private sealed class __LocalsDisplayClass$0000000c
 {
   public int inner;

   public __LocalsDisplayClass$00000008 $locals$00000009;
   public __LocalsDisplayClass$0000000a $locals$0000000b;

   public void __AnonymousMethod$00000007()
   {
     Console.WriteLine("outer = {0}", this.$locals$00000009.outer++);
     Console.WriteLine("i = {0}", this.$locals$0000000b.i);
     Console.WriteLine("inner = {0}", ++this.inner);
   }
 };

 public void SomeMethod()
 {
   NoArgs [] methods = new NoArgs[10];

   __LocalsDisplayClass$00000008 local1 = new __LocalsDisplayClass$00000008();
   local1.outer = 0;

   __LocalsDisplayClass$0000000a local2 = new __LocalsDisplayClass$0000000a();
   local2.i = 0;

   while(local2.i < 10)
   {
     __LocalsDisplayClass$0000000c local3 = new __LocalsDisplayClass$0000000c();
     local3.$locals$00000009 = local1;
     local3.$locals$0000000b = local2;
     local3.inner = local1.i;

     methods[local2.i] = new NoArgs(local3.__AnonymousMethod$00000007);
     methods[local2.i]();
   }

   for (int j = 0; j < methods.Length; j++)
     methods[j]();
 }
 


     总结其规律就是每个不同的局部变量作用域会有一个单独的类进行封装,子作用域中如果使用到父作用域的局部变量,则子作用域的封装类引用父作用域的封装类。相同作用域的变量和匿名方法由封装类绑定到一起,维护其一致的生命周期。

     相对于MS较为复杂的实现,Delphi.NET对嵌套函数则使用较为简单的参数传递方式,因为嵌套函数没有那么复杂的变量生命期管理要求,如
 

以下为引用:

 procedure SayHello;
 var
   Name: string;

   procedure Say;
   begin
     WriteLn(Name);
   end;
 begin
   Name := 'Flier Lu';

   Say;
 end;
 



     系统生成函数Say代码时,将使用到的上级变量如Name放入到一个自动生成的类型($Unnamed1)中,然后作为函数参数传递给Say函数,伪代码类似
 
以下为引用:

 type
   $Unnamed1 = record
     Name: string;
   end;

 procedure @1$SayHello$Say(var UnnamedParam: $Unnamed1);
 begin
   WriteLn(UnnamedParam.Name);
 end;

 procedure SayHello;
 var
   Name: string;
   Unnamed1: $Unnamed1;
 begin
   Name := 'Flier Lu';

   Unnamed1.Name := Name;

   Say(Unnamed1);
 end;
 


posted @ 2004-07-08 10:31 Flier Lu 阅读(478) 评论(0) 编辑

用WinDbg探索CLR世界 [2] 线程

http://www.blogcn.com/user8/flier_lu/index.html?id=1370342&run=.0A2F3E7

[2] 线程

    在配置好WinDbg之后,我们载入一个CLR程序并执行至CLR被载入,然后开始我们的CLR探索之旅。

    首先,使用!threads命令看看当前CLR中有哪些线程正在执行

以下为引用:

0:004> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
                             PreEmptive   GC Alloc               Lock
       ID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
  0   6ec 0014e708      6020 Enabled  00000000:00000000 00148a90     0 STA
  2   a68 00157618      b220 Enabled  00000000:00000000 00148a90     0 MTA (Finalizer)


    前面5个计数器分别表示托管(managed)线程、未启动线程、后台线程、阻塞线程和僵死线程的数量。
    下面的列表是当前托管线程的详细信息:第一个域是WinDbg的线程编号;ID是Win32线程ID;ThreadObj是线程的对象;State是一个标志位,以后再详细介绍;PreEmptive GC表示GC是否与此线程协作;GC Alloc Context是GC的相关信息;Domain是线程所在AppDomain;Lock Count是线程拥有锁的计数器;APT是线程类型,沿用COM中STA/MTA/NTA(netural)的概念;最后的Exception表示线程类型,除了普通的用户线程外还有finalizer、GC、Theadpool Worker和Threadpool Completion Port,其功能与名字相符。

    我们可以在.NET Framework SDK的Tool Developers Guide\Samples\sos子目录下找到所有sos.dll支持命令的详细说明;在rotor的clr\src\tools\sos子目录下找到针对rotor系统的sos.dll的实现代码。这份源代码在功能上实现了与CLR正规发行版本基本上相同的功能,也是我们下面研究的主要目标之一。

    其中Strike.cpp是sos功能命令的实现所在。每个sos的命令在strike.cpp中以一个函数实现,通过DECLARE_API宏定义函数参数。
以下为引用:

#define DECLARE_API(s)                             \
    CPPMOD VOID                                    \
    s(                                             \
        HANDLE                 hCurrentProcess,    \
        HANDLE                 hCurrentThread,     \
        ULONG                  dwCurrentPc,        \
        ULONG                  dwProcessor,        \
        PCSTR                  args                \
     )


    函数参数分别传入WinDbg正在调试的进程句柄、当前线程句柄、当前指令地址、处理器和命令行参数信息。函数内再对此信息进行处理,输出调试信息到WinDbg界面中。

    让我们先看看Threads命令(strike.cpp:1237)的实现原理。

    Threads函数首先从一个全局线程存储池中获取当前线程统计信息,并将之存储在一个结构并内打印统计值;然后调用GetThreadList函数(sos\util.cpp:2259)获取线程列表;对每个线程获取线程信息,并将之存储在一个结构内并打印线程详细信息;在打印线程信息时,会判断此线程的类型,并打印相关信息。

    首先来看看全局线程存储池ThreadStore类(vm\threads.h:1998)的设计和使用思路。

    CLR在启动时,会通过 CoInitializeEE 函数(vm\ceemain.cpp:1100)初始化一个执行引擎(Execute Engine),这儿的EE类似JVM的概念,实际上就是CLR的运行时环境。关于CLR的详细启动过程请参见笔者另外一篇文章《.Net平台下CLR程序载入原理分析》
    CoInitializeEE函数使用全局变量保障每个进程最多只有一个CLR环境;对没有构造CLR的进程,调用TryEEStartup函数(vm\ceemain.cpp:500)尝试初始化CLR。伪代码如下:
以下为引用:

HRESULT STDMETHODCALLTYPE CoInitializeEE(DWORD fFlags)
{
  if(++g_RefCount <= 1 && !g_fEEStarted && !g_fEEInit)
  {
    g_EEStartupStatus = TryEEStartup(fFlags);
  }
  return SUCCEEDED(g_EEStartupStatus) ?
    (SetupThread() ? S_OK : E_OUTOFMEMORY) : g_EEStartupStatus;
}


    TryEEStartup函数则以异常安全策略包装EEStartup函数(vm\ceemain.cpp:206)完成实际的CLR启动工作。在EEStartup函数中会真正调用InitThreadManager函数(vm\Threads.cpp:2068)完成线程管理器的初始化工作。而InitThreadManager函数出了初始化TLS外,绝大部分工作是由实现ThreadStore类的Singleton模式的ThreadStore::InitThreadStore函数(vm\Threads.cpp:4345)实现的。其中保存全局唯一ThreadStore类实例的就是前面获取线程统计信息的全局线程存储池。
以下为引用:

ThreadStore *g_pThreadStore;

BOOL ThreadStore::InitThreadStore()
{
    g_pThreadStore = new ThreadStore;

    return (g_pThreadStore != NULL);
}



    因此,ThreadStore类实际上是一个全局唯一的线程管理器,新增和终止一个CLR线程都需要在此存储中更新相关信息。此线程管理器除了维护一个当前线程列表的链表外,还维护了一套线程相关信息的统计值。前面Threads命令获取的几个统计值就是从此而来。而获取当前线程列表的GetThreadList函数(sos\util.cpp:2259),实际上也是直接从线程管理器的线程列表中获取每个线程对象的入口。

    最后来看看线程信息的获取步骤。

    每个线程Thread类(vm\Threads.h:544)的对象表示一个managed线程。此线程是一个逻辑上的线程,如果被启动则可能直接对应于一个系统的物理线程。而一个物理线程则无需绑定到一个被管理的逻辑线程上,物理线程却可以在多个AppDomain中共享以运行被调度到的被管理线程。此外每个被管理的线程必须有一个运行时环境(Contex),但不一定在一个确定的应用程序域(AppDomain)中。呵呵,搞糊涂了吧 :D 这里绕的几个弯子我以后再写篇详细的文章讨论好了 :P
    被管理的线程除了可以获取当前线程ID和绑定到的物理线程ID外,还有一个ThreadState状态(vm\Threads.h:576)定义其当前运行情况。
    对线程类型的判断逻辑,首先将线程与FinalizerThread(Finalizer)和GcThread(GC)两个全局变量指向的系统功能线程比较,判断是否是这两种特殊线程;然后根据线程状态的Thread::TS_ThreadPoolThread位是否被设置来判断是否在线程池中;如果在线程池中还要通过状态的Thread::TS_TPWorkerThread标志位进一步判断是否为工作者线程(Threadpool Worker),不是工作者线程则为完成端口线程(Threadpool Completion Port)。这几种线程缓冲池中线程的概念,我们以后章节讨论线程池时再详细讨论。

btw: 连续加了几天班,忙得头昏脑涨,今天总算有空继续写我的blog,呵呵

posted @ 2004-07-08 10:30 Flier Lu 阅读(1492) 评论(3) 编辑

关于 AOP 的精彩讨论 [1]

http://www.blogcn.com/user8/flier_lu/index.html?id=1311788&run=.0CEEA16

    <Shared Source CLI Essentials>一书的作者Ted Neward最近在其blog上发表了一篇关于AOP实现思路的文章<Setting the Story Straight: AOP != Interception>,引起了广泛的争议。

     他认为虽然Interception模式和AOP看起来很像,但并不等同。将AOP等同于Interception,类似于将OOP等同于数据结构加上函数指针。
 

以下为引用:

     ..., to understand AOP as Interception is like thinking OOP is data structs plus a bunch of function pointers. Just as we didn''t understand objects until we got past the idea that objects are "just code and data", ...
 


     为证明这个观点,他首先定义了AOP和Interception的概念。这一点很值得我们学习,国内很多论坛上常常莫名其妙大家大吵一架,其实说的根本不是一个东西,呵呵
 
以下为引用:

 Aspect-Oriented Programming:
 "Aspect-oriented software development is a new technology for separation of concerns (SOC) in software development. The techniques of AOSD make it possible to modularize crosscutting aspects of a system." --AOSD homepage
 "The central idea of AOP is that while the hierarchical modularity mechanisms of object-oriented languages are extremely useful, they are inherently unable to modularize all concerns of interest in complex systems. Instead, we believe that in the implementation of any complex system, there will be concerns that inherently crosscut the natural modularity of the rest of the implementation.
 "AOP does for crosscutting concerns what OOP has done for object encapsulation and inheritance--it provides language mechanisms that explicitly capture crosscutting structure. This makes it possible to program crosscutting concerns in a modular way, and achieve the usual benefits of improved modularity: simpler code that is easier to develop and maintain, and that has greater potential for reuse. We call a well modularized crosscutting concern an aspect." --An Overview of AspectJ

 Interception: "The Interceptor architectural pattern allows services to be added transparently to a framework and triggered automatically when certain events occur." --Patterns of Software Architecture, Vol 2 by Schmidt et. al, p. 109
 



     此外他还定义了概念的分离以及横切(Cross-cutting)概念。

     在讨论Interception和AOP时,他指出Interception在实现AOP时所受的限制。
 

以下为引用:

     1.Explicit support from the system for interception. POSA2 calls it the framework, COM+, .NET, servlets and EJB do it at container boundaries, but the underlying idea is the same--somewhere, some kind of construct (what I''ll colloquially refer to as "The Plumbing") is what''s providing the interception behavior, explicitly routing the call flow through the Interceptor as part of normal processing. This means that only those artifacts understood by the plumbing as first-class citizens can be so Intercepted--anything that''s not within the purview of the plumbing cannot be intercepted. For example, in the servlet framework (starting from the 2.3 specification), Interception is provided via filters, allowing servlet authors the opportunity to "hook" the request and response to a servlet or JSP (or any other URL, for that matter). However, Interception can only apply to URL request/response semantics, so filters cannot, for example, intercept calls to ordinary Java classes or calls via other channels, like RMI, to other systems.

     2.The notion of a request/response channel. Interception implicitly relies heavily on the idea of request/response semantics, since interceptors almost always fire around method entry and exit. This means that a large number of object interaction semantics are left untouched and unavailable. (If this is a bit abstract, hang on--this will make more sense later.)

     3.Interception is almost universally a runtime construct. POSA2 makes explicit reference to this fact, citing the idea that these services won''t always be desired or necessary for all cases, so the behavior from the Interceptor cannot be layered in statically--it needs to be registered at runtime. Unfortunately, this also implies a certain amount of runtime overhead, particularly if the interceptor''s event model is fairly coarse- grained. For example, in the .NET context object architecture, method calls both into and out of an object inside of a context can be intercepted via the IMessageSink-and-friends architecture. However, once registered, a context interceptor must be invoked for every method call in and/or out of the context; there is no way to specify that the interceptor is only valid for "methods of this type".
 



     首先是必须由被Interception的系统框架提供显式的支持;其次是基于请求/应答策略;最后普遍需要动态构造。当然不同环境中还是提供了其他的实现途径,例如Java/CLR都提供了动态透明代理(Dynamic Proxy)的机制,动态对目标进行封装。

     而AOP则是基于两个构造定义的:join points和advice。前者指明在哪儿插入;后者是插入的内容。故而AOP具有如下的属性
 

以下为引用:

     1.AOP systems need no underlying "plumbing". This statement is somewhat controversial, since obviously something needs to do the aspect weaving at some point in the execution lifecycle. However, this frequently isn''t a runtime construct, but a compile-time or load-time operation; for example, AspectJ is a compiler, performing all the weaving at compile-time.

     2.AOP systems can weave against anything. An AOP system is limited only by its join point model--for example, AspectJ can easily define an object-relational persistence aspect that weaves around JavaBean classes, such that any field-get access against a non-transient field would immediately trigger a database access to obtain the latest version of that field, and any field-set access against a non-transient field would immediately trigger a database access to set that value into the database. While the performance of such an aspect system would be horrendous (think of all the round-trips!), the fact that this could be done against any JavaBean class, which could be used in a servlet system, a Swing app, or a J2ME device is powerful.
 



     作者紧接着举了一个检测SQL注入攻击(injection attacks)的例子,指出针对JDBC中PreparedStatement函数无法通过简单方法完成Interception,这与AOP的原意是不相符的。

     最后,作者得出结论。

     1.在Interception和AOP之间是可以有中间状态的。例如John Lam的CLAW实现就完成了对.NET CIL代码载入时的aspect code-weaving。这种方法既能够以编译形式表达自己,又能只针对小粒度的目标方法显式完成功能,而不需要Interception那样由系统提供支持、只能针对大粒度的类、并且损失运行时的效率。
     2.AOP可以很自然地使用Interception作为其实现机制。例如JBoss的实现就是使用Interception框架解决问题的最好实例。
     3.Interception可以被看作是AOP的一种简单形式,类似于VB是OOP的一种简单形式。Interception是了解AOP的一个重要步骤,但不是终点。Interception最适用于组件边界(component boundaries)上,而AOP完全适用于组件边界内。

     可以说这篇文章还是言之有理的,但容易引起争论的地方过多了,呵呵

     Rickard Oberg在其回应文章AOP != AspectJ中批驳了Ted的一些观点。主要目标是Ted虽然没有明说,但是影射AspectJ的很多特性为AOP的必要特性。

     首先,Ted认为AOP不应有底层的薄记工作(underlying "plumbing"),而Rickard则指出AspectJ实际上也有这种工作,只不过由AspectJ的编译器自动对每个对象完成了。个人认为这儿Ted的意思可能是指AOP不应有需要使用者完成的薄记工作,而使用编译器或其他程序自动完成的则可以忽略。
     其次,Ted认为AOP系统可以针对任意粒度的任意对象,而Rickard则指出如果只使用interception时并不意味着就不是AOP了,而只是使用了一个限制较为严格的AOP。同时如果没有性能、C/S模型、序列化等需求时,也没有必要非得用AspectJ。这点上我还是比较赞同Rickard的观点的,Interception和AspectJ只是实现AOP的两种思路而已,没有冲突的。
     然后,Rickard指出Ted给出的那个检测SQL注入攻击的例子实际上是一个反模式(anti-pattern),呵呵。
     最好,Ted认为AspectJ能够适用于组件边界内,但Rickard指出,实际上很多情况下不应该介入到组件边界内。这种嵌入实际上是一种反模式,应该使用其他的模式予以回避。而在性能方面,Rickard认为Interception模式耗费的时间也是可以接受的。

 to be continue...

posted @ 2004-07-08 10:29 Flier Lu 阅读(526) 评论(0) 编辑

Win32 调试接口设计与实现浅析 [1] 用户态调试器结构初探

http://www.blogcn.com/user8/flier_lu/index.html?id=1307208&run=.002A442

Win32 调试接口设计与实现浅析

     所谓调试器实际上是一个很宽泛的概念,凡是能够以某种形式监控其他程序执行过程的程序,都可以泛称为调试器。在Windows平台上,根据调试器的实现原理大概可以将之分为三类:内核态调试器、用户态调试器和伪代码调试器。
     内核态调试器直接工作在操作系统内核一级,在硬件与操作系统之间针对系统核心或驱动进行调试,常见的有SoftICE、WinDbg、WDEB386和i386KD等等;用户态调试器则通过操作系统提供的调试接口,在操作系统和用户态程序之间针对用户态程序进行调试,常见的有各种开发环境如VC/Delphi自带的调试器,OllyDbg等等;伪代码调试器则使用目标系统自定义的调试接口,调试由用户态程序支持的脚本语言或虚拟机代码,常见的如JVM/CLR的调试工具、VB的pcode调试器、Active Script调试器等等。
     因为伪代码调试器跟具体系统实现相关性太强,不具备原理层面的通用性,本系列文章尽量不涉及其内容,以后如果有机会可以再讨论一下JVM/CLR/Active Script提供的调试接口;用户态调试器应用最广泛,参考资料也较为完整,我会花较大精力和大家探讨;核心态调试器则跟操作系统结合较为紧密,加上我也不是太熟悉,只能尽力而为了,呵呵。欢迎大家提出批评指正意见和建议 :)
     此外强烈推荐John Robbins在MSDN的Bugslayer专栏,以及其所著的<Debugging Applications>一书(中文版《应用程序调试技术》),此书中对调试器从原理到应用都有很全面的讲解。

 [1] 用户态调试器结构初探

     用户态调试器直接使用Win32 API提供的调试接口,遵循Win32的事件驱动的设计思想,其实现思路非常简单,基本框架伪代码如下:
 

以下为引用:

 //启动要调试的进程或挂接调试器到已运行的进程上
 CreateProcess(..., DEBUG_PROCESS, ...) or DebugActiveProcess(dwProcessId)

 DEBUG_EVENT de;
 BOOL bContinue = TRUE;
 DWORD dwContinueStatus;

 while(bContinue)
 {
   bContinue = WaitForDebugEvent(&de, INFINITE);

   switch(de.dwDebugEventCode)
   {
   ...
   default:
     {
       dwContinueStatus = DBG_CONTINUE;
       break;
     }
   }

   ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
 }
 



     在调试器开始调试的时候,会启动被调试程序的新进程或者挂接(attach)到一个已运行进程上,此时Win32系统会启动调试接口的服务器端;然后调试器调用WaitForDebugEvent函数等待调试服务器端的调试事件被引发;调试器根据调试事件进行相应的处理;最后调用ContinueDebugEvent函数请求调试服务器继续执行被调试进程,以等待并处理下一个调试事件。

     首先我们大致看看调试接口的服务器端的实现思路:调试服务的服务器端接口实际上是存在于被调试进程的调试端口(Debug Port),此核心对象实现上跟Win32的完成端口类似,都是通过一个核心队列实现的LPC端口。启动调试服务器实际上就是挂接Win32的调试子系统到被调试进程,并在被调试进程内构造调试端口。调试器通过调试端口与Win32的调试子系统通讯;调试子系统响应系统操作所引发的调试事件,并通过调试端口将调试事件分发给核心态/用户态调试器。

     建立被调试程序的新进程时,需要在CreateProcess函数的dwCreationFlags参数设置DEBUG_ONLY_THIS_PROCESS或DEBUG_PROCESS标志位,以表示新建的进程需要被调试。CreateProcess函数的调用路径如下
 

以下为引用:

 CreateProcessA/CreateProcessW (kernel32.dll)
 CreateProcessInternalW (kernel32.dll)
 NtCreateProcessEx (ntoskrnl.dll)
 PspCreateProcess (ntospscreate.c:969)
 


     CreateProcessInternalW函数根据传入的dwCreationFlags参数,决定是否要构造端口核心对象用于调试端口,并设置PEB的相应调试标志;PspCreateProcess会根据传入参数的调试选项和端口对象句柄,选择是否创建目标进程的调试端口;如果要创建则将传入的端口句柄转换成内核对象引用,保存在被调试程序进程的EPROCESS->DebugPort字段里。
     Win32 API提供的IsDebuggerPresent函数就是通过判断CreateProcessInternalW函数在PEB中设置的标志位来判断当前进程是否被调试的。IsDebuggerPresent函数伪代码如下:
 
以下为引用:

 BOOL IsDebuggerPresent(void)
 {
   return NtCurrentTeb()->ProcessEnvironmentBlock->BeingDebugged;
 }
 


     TEBPEB的结构可在::URL::http://www.ntinternals.net 上找到。

     不过此种方法很容易被调试器直接修改PEB内存结构所欺骗,故而有另外一种直接通过检查EPROCESS->DebugPort字段是否被使用,来判断此进程是否正在被调试的方法。以前水木上也有过几次讨论,如blowfish的《检测debugger的方法补遗》一文给出的代码。Windows XP/2003开始由Win32 API提供的 CheckRemoteDebuggerPresent 函数也是使用相同的思路,通过调用 NtQueryInformationProcess 函数查询调试端口实现的,伪代码如下:
 

以下为引用:

 BOOL CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent)
 {
   enum PROCESS_INFO_CLASS { ProcessDebugPort = 7 };

   if(hProcess && pbDebuggerPresent)
   {
     HANDLE hPort;

     *pbDebuggerPresent = NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessDebugPort, &hPort, sizeof(hPort), NULL)) ? TRUE : FALSE;

     return *pbDebuggerPresent;
   }
   return FALSE;
 }
 



     与直接创建被调试程序的新进程不同,调试已启动进程的 DebugActiveProcess 函数首先连接到Win32 系统调试服务器的端口上,然后激活当前正在运行的被调试进程的调试端口。DebugActiveProcess的伪代码如下:
 
以下为引用:

 BOOL DebugActiveProcess(DWORD dwProcessId)
 {
   if(DbgUiConnectToDbg())
   {
     HANDLE hProcess = ProcessIdToHandle(dwProcessId);

     if(hProcess)
     {
       DbgUiDebugActiveProcess(hProcess);
       NtClose(hProcess);
     }
   }
   return FALSE;
 }
 



     DbgUiConnectToDbg函数(ntosdlldlluistb.c:27)尝试连接核心提供的调试子系统端口(名为"DbgUiApiPort"),如果成功连接会获得一个端口对象(保存在DbgUiApiPort NtCurrentTeb()->DbgSsReserved[1]),和一个调试状态转换的信号灯句柄(保存在DbgStateChangeSemaphore NtCurrentTeb()->DbgSsReserved[0])用于等待调试事件。伪代码如下:
 
以下为引用:

 #define DbgStateChangeSemaphore (NtCurrentTeb()->DbgSsReserved[0])
 #define DbgUiApiPort (NtCurrentTeb()->DbgSsReserved[1])

 NTSTATUS DbgUiConnectToDbg( VOID )
 {
   NTSTATUS st = NtConnectPort(&DbgUiApiPort, L"\DbgUiApiPort", ..., &DbgStateChangeSemaphore);

   if(NT_SUCCESS(st))
   {
     NtRegisterThreadTerminatePort(DbgUiApiPort);
   }
   else
   {
     DbgUiApiPort = NULL;
   }
   return st;
 }
 



     如果连接调试子系统成功,则调用NtRegisterThreadTerminatePort函数(ntospspsdelete.c:1202)将调试端口加入到当前线程控制块的终止端口列表(ETHREAD->TerminationPortList)中。在线程结束的之前,会激活此列表中的端口,给调试器一个清理的机会。

     DbgUiDebugActiveProcess函数完成具体的激活被调试进程的调试服务器的功能。伪代码如下:
 

以下为引用:

 #define DbgUiApiPort (NtCurrentTeb()->DbgSsReserved[1])

 void DbgUiDebugActiveProcess(HANDLE hProcess)
 {
   return NtDebugActiveProcess(DbgUiApiPort) &&
          DbgUiIssueRemoteBreakin(hProcess) &&
          DbgUiStopDebugging(hProcess);
 }
 



     至于这几个函数的具体实现,等后面章节详细分析Win32调试子系统时再详细讲解,呵呵

     在被调试进程启动了调试支持后,调试器调用WaitForDebugEvent函数等待调试事件的发生。此函数实际上是对DbgUiWaitStateChange函数(ntosdlldlluistb.c:93)的一个简单包装,通过等待DbgUiConnectToDbg函数获得的调试事件信号灯来完成实际功能。如果成功获得调试事件,还会通过NtRequestWaitReplyPort函数(ntoslpclpcsend.c:717)向调试服务器通报DbgUiWaitStateChangeApi消息。

     在处理完调试事件后,调试器调用的ContinueDebugEvent函数是DbgUiContinue函数的一个简单包装,也是使用NtRequestWaitReplyPort函数向调试服务器通报DbgUiContinueApi消息。

     在完成调试功能后,WinXP/2003还提供了DebugActiveProcessStop函数停止调试。伪代码如下:
 

以下为引用:

 BOOL DebugActiveProcessStop(DWORD dwProcessId)
 {
   HANDLE hProcess = ProcessIdToHandle(dwProcessId);

   if(hProcess)
   {
     CloseAllProcessHandles(dwProcessId);
     DbgUiStopDebugging(hProcess);
     if(NtClose(hProcess))
       return TRUE;
   }
   return FALSE;
 }
 



     DbgUiStopDebugging函数(ntdll.dll)调用ZwRemoveProcessDebug函数(ntoskrnl.exe)关闭指定进程的调试端口,实现上是传入端口句柄和进程句柄,调用0xC7号系统服务完成最终功能。这个暂时就不深入讨论了,就此打住 :P

     在了解这些后,对用户态调试器的实现应该就有了一个框架性的了解:其结构就是一个基于事件的模型,然后通过向调试子系统请求调试事件并完成具体操作。

posted @ 2004-07-08 10:28 Flier Lu 阅读(1084) 评论(0) 编辑

用WinDbg探索CLR世界[1] - 安装与环境配置 [原]

http://www.blogcn.com/user8/flier_lu/index.html?id=1270368&run=.0D9CAA6

    一直以来,我对CLR的分析都是基于MSDN、.NET Framework SDK自带文档和Rotor项目提供的源代码进行静态分析,辅以自己写的一些小例子或对Rotor的修修补补,来进行有限度的动态分析。虽然也用SoftIce跟踪过某些核心函数的机制,但感觉实在是太痛苦了,呵呵。
    最近偶然之间发现我的偶像John Robbins在MSDN的BugSlayer上发表的一篇文章<SOS: It's Not Just an ABBA Song Anymore>,才发现原来用WinDbg可以如此方便的动态分析CLR的运行机制。

    首先,需要下载并安装 Microsoft Debugging Tools [/url]。最好还能下载并安装当前操作系统相应的Windows Symbol Packages
    然后,配置系统环境变量,让搜索路径指向系统.NET Framework的安装目录,既sos.dll所在目录

    set PATH=%PATH%;E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322

    启动WinDbg之后,在File/Symbol Search Path选项中加入符号文件的安装目录,如

    E:\WINDOWS\Symbols;E:\VS2003\SDK\v1.1\symbols

    或者设置系统环境变量_NT_SYMBOL_PATH(需要重起WinDbg)

    set _NT_SYMBOL_PATH=E:\WINDOWS\Symbols;E:\VS2003\SDK\v1.1\symbols

    最后,在File菜单中,用Open Executable打开一个CLR程序或者用Attach to a process附加到一个正在运行的CLR程序上。

    配置好WinDbg之后,如果打开一个新可执行程序,WinDbg会自动断点到入口,则继续运行再Break;如附加到进程则直接Break。
    然后在最下方命令行上输入系统命令 .load sos 命令载入外部扩展sos.dll。如果配置系统路径正确则这里不会有任何反应,可以继续用系统命令 .chain 查看当前载入的扩展。如下显示则表示sos.dll成功载入。
以下为引用:

0:005> .chain
Extension DLL search Path:
    E:\MS\PlatformSDK\Debugging Tools\winext;...;E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322
Extension DLL chain:
    sos: API 1.0.0, built Fri Feb 21 10:47:40 2003
        [path: E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\sos.dll]
    dbghelp: image 6.3.0005.1, API 6.0.6, built Fri Oct 24 02:11:02 2003
        [path: E:\MS\PlatformSDK\Debugging Tools\dbghelp.dll]
    ext: image 6.3.0005.1, API 1.0.0, built Fri Oct 24 09:06:45 2003
        [path: E:\MS\PlatformSDK\Debugging Tools\winext\ext.dll]
    exts: image 6.3.0005.1, API 1.0.0, built Fri Oct 24 02:10:39 2003
        [path: E:\MS\PlatformSDK\Debugging Tools\WINXP\exts.dll]
    uext: image 6.3.0005.1, API 1.0.0, built Fri Oct 24 02:10:54 2003
        [path: E:\MS\PlatformSDK\Debugging Tools\winext\uext.dll]
    ntsdexts: image 6.0.4044.0, API 1.0.0, built Wed Oct 22 02:13:21 2003
        [path: E:\MS\PlatformSDK\Debugging Tools\WINXP\ntsdexts.dll]


    在载入sos.dll之后,可以用 lm 命令看看当前有哪些模块被载入内存,如
以下为引用:

0:005> lm
start    end        module name
...
77f30000 77ffa000   ntdll        (export symbols)       E:\WINDOWS\system32\ntdll.dll
79000000 79010000   ConfigWizards     (deferred)
79040000 79085000   fusion       (deferred)
79170000 79196000   mscoree      (deferred)
791b0000 79412000   mscorwks     (deferred)
...


    对希望进行分析的模块,可以用ld命令载入相应的调试符号文件(如果有的话,呵呵)。
    如果符号文件搜索路径配置正确的话,可以看到提示
以下为引用:

0:005> ld mscorjit
Symbols loaded for MSCORJIT


    此时再用lm可以看到
以下为引用:

...
79430000 7947c000   MSCORJIT     (pdb symbols)          E:\VS2003\SDK\v1.1\symbols\mscorjit.pdb
...


    如果符号文件搜索路径配置错误,或者此模块没有调试符号文件,则会载入.dll的export表
以下为引用:

79170000 79196000   mscoree      (export symbols)       E:\WINDOWS\system32\mscoree.dll


    或者干脆没有符号
以下为引用:

79780000 79980000   mscorlib     (no symbols)


    完成以上的配置之后,就可以正式开始用WinDbg探索CLR的内部世界了,你可以敲个!SyncBlk,呵呵。
以下为引用:

0:005> !SyncBlk
Index SyncBlock MonitorHeld Recursion   Thread  ThreadID     Object Waiting
-----------------------------
Total           3
ComCallWrapper  0
ComPlusWrapper  0
ComClassFactory 0
Free            0


    这不就是昨天分析的lock锁定中的那几个计数器吗 :P

to be continue...

posted @ 2004-07-08 10:27 Flier Lu 阅读(5004) 评论(5) 编辑

自动获取 NT 系统服务描述表与函数名映射表

http://www.blogcn.com/user8/flier_lu/index.html?id=1404224&run=.0A0B923

    经历过DOS时代人朋友一定还记得内存开始处那个神奇的ISR映射表,其实NT内部的系统服务描述表(System Service Descriptor Table)也起着类似的作用。用户态程序调用系统服务时,通过某种机制(以前是INT 2EH,XP/2003改为SYSENTER/SYSCALL指令)进入核心态,然后系统根据系统服务号查表调用相应函数。
     以前碰到要查一个服务号对应函数名时,都是Ctrl+D启动SoftICE,然后加载相应符号库手工查询。前段时间重装2003系统后,使用SoftICE一直有各种各样莫名其妙的问题,搞得很是不爽。为了查一个服务号的函数名,还得跑到别人机器上,麻烦得要死。于是响应毛主席号召,自己动手丰衣足食,呵呵,写了一个小程序自动获取 NT 系统服务描述表与函数名映射表。
     实现思路起始很简单,每一步的技术也都没什么难度,就是...麻烦...sigh

     1.定位内核的ntoskrnl.exe模块
     2.载入调试符号并查找KeServiceDescriptorTable符号地址
     3.读取KeServiceDescriptorTable内容并定位描述表地址
     4.打印描述表,对每个入口地址通过符号库查询响应函数名

     下面具体说说每一步的实现思路

 1.定位ntoskrnl.exe模块

     最简单的方法莫过于使用NTDLL提供的ZwQuerySystemInformation函数查询SystemModuleInformation信息,一次性将内核态所有模块的信息读入,呵呵,后面查询函数入口地址时也会用到这些信息。
 

以下为引用:

 NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation(
     IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
     OUT PVOID SystemInformation,
     IN ULONG SystemInformationLength,
     OUT PULONG ReturnLength OPTIONAL);

 typedef struct _SYSTEM_MODULE_INFORMATION { // Information Class 11
     ULONG Reserved[2];
     PVOID Base;
     ULONG Size;
     ULONG Flags;
     USHORT Index;
     USHORT Unknown;
     USHORT LoadCount;
     USHORT ModuleNameOffset;
     CHAR ImageName[256];
 } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
 



     相关使用方法网上讨论很多,或请参考NT Native API一书,这儿就不再罗嗦。

 2.载入调试符号并查找KeServiceDescriptorTable符号地址

     MS提供的DbgHelp库还是很好用的,呵呵,直接载入你安装的符号库,查询符号信息。

     首先用SymInitialize函数初始化一下符号库引擎,对应最后需要用SymCleanup函数清除;然后用SymSetSearchPath函数设置符号库搜索路径,也可以使用_NT_SYMBOL_PATH环境变量指定;对需要载入符号库的模块,调用SymLoadModule函数载入对应符号库,并使用SymGetModuleInfo函数获取符号库信息;再使用SymEnumSymbols函数枚举模块中的符号;对我们需要的符号,可以通过SYMBOL_INFO.Address得到符号在内存中的地址。

 3.读取KeServiceDescriptorTable内容并定位描述表地址

     KeServiceDescriptorTable符号指向一个KSERVICE_TABLE_DESCRIPTOR结构,定义系统服务描述表的函数入口表和参数表的地址:
 

以下为引用:

 typedef struct _KSERVICE_TABLE_DESCRIPTOR {
     PULONG_PTR Base;
     PULONG Count;
     ULONG Limit;
     PUCHAR Number;
 } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
 


     Limit存储此服务描述表中有多少项;
     Base指向函数入口表地址,每个表项是一个DWORD,表示一个函数入口地址;
     Number指向函数参数表地址,每个表项是一个UCHAR,表示一个函数参数长度。

     但因为此结构存储在2G以上地址空间中,为核心态内存,无法在用户态访问,所以我们必须想办法直接读取核心态内存。

     思路起始也很简单,呵呵,将KeServiceDescriptorTable符号所指向的虚拟空间地址转换为物理地址,然后通过读取DevicePhysicalMemory设备直接访问物理内存。网上以前也有过很多讨论文章。

     虚拟地址向物理地址转换的方法,webcrazy以前有过精彩论述《小议Windows NT/2000分页机制》。但因为我的程序不准备在核心态跑,所以使用一个简化的经验转换算法。具体原理请参考webcrazy的文章和MmGetPhysicalAddress函数(ntosmmiosup.c:5490)的实现代码。
 

以下为引用:

 PHYSICAL_ADDRESS TPhysicalMemoryMapping::LinearAddressToPhysicalAddress(LPCVOID lpVirtualAddress)
 {
   PHYSICAL_ADDRESS addr = { 0, 0 };

   if((DWORD)lpVirtualAddress < 0x80000000L || (DWORD)lpVirtualAddress >= 0xA0000000L)
     addr.QuadPart = (DWORD)lpVirtualAddress & 0x0FFFF000;
   else
     addr.QuadPart = (DWORD)lpVirtualAddress & 0x1FFFF000;

   return addr;
 }
 


     读取物理内存实际上就是使用NtOpenSection函数打开NT内建\Device\PhysicalMemory,然后将此Section中需要访问的内存页用NtMapViewOfSection函数映射到用户态空间中,就可以直接读取。最后再相应调用NtUnmapViewOfSection函数和NtClose函数关闭映射。

 4.打印描述表,对每个入口地址通过符号库查询响应函数名

     将描述表读取后,就可以根据每个函数入口地址,先定位到某个模块,再使用SymFromAddr函数定位到某个符号。

 几个相关工具短期内可以在这里下载::URL::http://flier.5i4k.net/KernelExplorer.rar

posted @ 2004-07-08 10:26 Flier Lu 阅读(2136) 评论(1) 编辑

CLR中字符串不变性的优化

http://www.blogcn.com/user8/flier_lu/index.html?id=1269085&run=.06FE977

    自从有编程语言以来,如何处理字符串就一直是一个争论不休的问题。从C/C++用字符数组表示字符串,让用户完全控制其生命周期;到Delphi/VB通过编译器内建支持,使用引用计数自动维护字符串生命周期;再到Java/C#通过不可变字符串以及垃圾回收管理生命周期。不同的策略有着不同的倾向性,也有各自的缺点和优点。这儿我不想评论多种策略之间的优劣,只是想针对C#的实现做一点点较为深入的探讨。
     
     CLR中选择了和Java类似的不可变字符串策略,以简化生命期维护以及多线程同步问题的处理,但同时也付出了一定的效率和空间上的代价,故而不得不通过编译器一级定制来优化。
     Chris BrummeYun Jin在其BLog上讨论了需要保障字符串不变性(immutability)的原因,并指出通过PInvoke以及unsafe代码直接修改字符串内容可能带来的危害。
     
     Interning Strings & immutability
     Dangerous PInvokes - string modification
     
     为了提高效率和节约空间,CLR内部实际上维护了一个不可变字符串表。在堆中分配的字符串可以通过String.Intern函数确保其被加入此表;通过String.IsInterned函数判断自己是否在表中。如果在表中,则可以通过引用来直接对字符串进行比较,大大提高字符串比较效率。MSDN上的例子如下    
 
以下为引用:
 // Sample for String.Intern(String)
 using System;
 
using System.Text; 

 
class Sample {
     
public static void Main() {
     String s1 
= "MyTest";
     String s2 
= new StringBuilder().Append("My").Append("Test").ToString(); 
     String s3 
= String.Intern(s2); 
     Console.WriteLine(
"s1 == '{0}'", s1);
     Console.WriteLine(
"s2 == '{0}'", s2);
     Console.WriteLine(
"s3 == '{0}'", s3);
     Console.WriteLine(
"Is s2 the same reference as s1?: {0}", (Object)s2==(Object)s1); 
     Console.WriteLine(
"Is s3 the same reference as s1?: {0}", (Object)s3==(Object)s1);
     }

 }

 
/*
 This example produces the following results:
 s1 == 'MyTest'
 s2 == 'MyTest'
 s3 == 'MyTest'
 Is s2 the same reference as s1?: False
 Is s3 the same reference as s1?: True
 
*/


 


    
     如果熟悉CLR的Metadata文件结构的朋友可能立刻会想到,在Metadata表中实际上本来就有#String流和#US流,分别保存程序中固化的字符串和用户字符串。例如上面的"MyTest"字符串就会被放入流中直接载入,而CLR动态维护的字符串表就是在此基础上扩展的。
     动态创建的字符串,如前面例子中通过StringBuilder构造的字符串,则缺省放在堆中,只有用户显式调用了String.Intern函数,才会被加入到静态字符串表中。查看Rotor的代码,会发现String.Intern实际上是调用当前线程所在AppDomain的GetOrInternString函数;而进一步调用此AppDomain的字符串映射表的GetInternedString函数。
 
以下为引用:
    
 
String.Intern(String str) (bclsystemstring.cs:1194
   Thread.GetDomain().GetOrInternString(str)
   
 AppDomain.GetOrInternString(String str) (bclsystemappdomain.cs:
1558)
   InternalCall    
   
 BaseDomain::GetOrInternString(STRINGREF 
*pString) (vmappdomain.cpp:856
   m_pStringLiteralMap
->GetInternedString(pString, )   

 AppDomainStringLiteralMap::GetInternedString() (vmstringliteralmap.cpp:
196)


    
     在GetOrInternString函数中:首先会根据字符串的内容计算出其HashCode;然后使用此HashCode在当前AppDomain的字符串映射表(m_StringToEntryHashTable)中搜索;如果没有找到则进一步在CLR的全局字符串映射表(SystemDomain::GetGlobalStringLiteralMap())中搜索;如果还是没有找到,则根据参数决定是否将此字符串以HashCode为索引加入全局字符串映射表(GetInternedString函数中根据参数bAddIfNotFound判断是否添加);如果当前AppDomain可能被卸载,则还会将此字符串以HashCode为索引加入到当前AppDomain的局部字符串映射表中。伪代码如下:
 
以下为引用:

 STRINGREF *AppDomainStringLiteralMap::GetInternedString(STRINGREF *pString, BOOL bAddIfNotFound, BOOL bAppDomainWontUnload)
 
{
   StringLiteralEntry 
*Data;
   DWORD dwHash 
= m_StringToEntryHashTable->GetHash(字符串数据);
   
   
if (m_StringToEntryHashTable->GetValue(&StringData, &Data, dwHash))
   
{
     
return Data->GetStringObject();
   }

   
else
   
{
     StringLiteralEntry 
*pEntry = SystemDomain::GetGlobalStringLiteralMap()->GetInternedString(pString, dwHash, bAddIfNotFound);
     
     
if(pEntry)
     
{
       
if (!bAppDomainWontUnload)
       
{
         m_StringToEntryHashTable
->InsertValue(&StringData, (LPVOID)pEntry, FALSE);
       }

     }

     
else
     
{
       
return pEntry->GetStringObject();
     }

   }

 }

 

    
     另外一个函数String.IsInterned实际上调用路径完全一样,只是在GetInternedString没有在字符串映射表搜索到字符串时不自动加入(bAddIfNotFound = false)。
     
     由此我们可以得出一些结论:
     1.Intern String的作用域是整个CLR,虽然每个AppDomain有独立的优先缓存机制。这样既可以保障查询效率,又可以保障在不同级别(如CLR/AppDomain)载入的共享的Assembly中字符串的一致性。
     2.Intern String中的内容直接决定其HashCode,进而决定其在字符串表中的存储和索引,直接内容修改可能导致未知问题。直接修改内容后再使用String.IsInterned,就会返回一个和以前完全不同的索引项。
     3.Intern String可以通过其引用直接比较。因为在隐式(固化在Metadata的#String或#US流中)或显示(调用String.Intern)将字符串Intern的时候,内容相同的字符串都会被定位到字符串索引表的同一入口,返回相同的对象引用。

posted @ 2004-07-08 10:20 Flier Lu 阅读(898) 评论(0) 编辑

C# 中 lock 关键字的实现

http://www.blogcn.com/user8/flier_lu/index.html?id=1256525&run=.022BCC2

    刚刚在这篇文章 《How is lock keyword of C# implemented? 》中看到MS内部关于C#的lock关键字实现的一个讨论。
 
以下为引用:

 Subject: RE: How is lock keyword of C# implemented?

 At the core, it’s typically one ?lock cmpxchg“ instruction (for x86) for entry, and one for exit, plus a couple dozen other instructions, all in user mode. The lock prefix is replaced with a nop on uniprocessor machines.

 The “lock cmpxchg” instruction basically stores the locking thread’s id in the object header, so another thread that tries to lock the same object can see that it’s already locked.

 The actual implementation is a lot more complicated, of course – we use the object header for other purposes, for example, so this must be detected and dealt with, plus when a thread leaves the lock, we must detect whether other threads are waiting and so on…

 Thanks
 



     回想起前两天分析过的临界区实现,就顺便看了看rotor这方面的实现代码,发现和Windows中临界区的实现思路基本上相同。
     在rotor中,每个引用对象内部实现是一个Object对象(sscliclrsrcvmobject.h:126)的实例。而对象同步机制的实现,则是通过和Object对象绑定的ObjHeader对象(sscliclrsrcvmsyncblk.h:539)中的SyncBlock结构完成的。这种实现思路跟Delphi中的VMT的实现很相似,rotor中Object对象指针的-4偏移处存储绑定的ObjHeader对象,Delphi则在负偏移处保存VMT表。
 
以下为引用:

 class Object
 {
   //...
   
   // Access the ObjHeader which is at a negative offset on the object (because of
   // cache lines)
   ObjHeader   *GetHeader()
   {
       return ((ObjHeader *) this) - 1;
   }

   // retrieve or allocate a sync block for this object
   SyncBlock *GetSyncBlock()
   {
       return GetHeader()->GetSyncBlock();
   }
  
   //...
 };
 


    
     ObjHeader::GetSyncBlock(syncblk.cpp:1206)方法从缓冲区获取或者创建新的SyncBlock对象。SyncBlock对象则是一个使用lazily created策略的可缓存结构,调用其Monitor完成对象的实际锁定工作。
 
以下为引用:

 // this is a lazily created additional block for an object which contains
 // synchronzation information and other "kitchen sink" data

 class SyncBlock
 {
   //...
   
   AwareLock  m_Monitor;                    // the actual monitor

   void EnterMonitor()
   {
       m_Monitor.Enter();
   }
     
   //...
 };
 


    
     AwareLock类型是一个很类似临界区的轻量级同步对象,其Enter(syncblk.cpp:1413)方法使用FastInterlockCompareExchange函数尝试锁定此Monitor。如果无法锁定则判断此Monitor的所有者线程是否是当前线程:是则调用线程嵌套锁定函数;否则等待此对象锁定状态的改变。
 
以下为引用:

     Thread  *pCurThread = GetThread();

     for (;;) {

         // Read existing lock state.
         LONG state = m_MonitorHeld;

         if (state == 0) {

             // Common case: lock not held, no waiters. Attempt to acquire lock by
             // switching lock bit.
             if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, 1, 0) == 0)
                 break;

         } else {

             // It's possible to get here with waiters but no lock held, but in this
             // case a signal is about to be fired which will wake up a waiter. So
             // for fairness sake we should wait too.
             // Check first for recursive lock attempts on the same thread.
             if (m_HoldingThread == pCurThread)
                 goto Recursion;

             // Attempt to increment this count of waiters then goto contention
             // handling code.
             if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, state + 2, state) == state)
                 goto MustWait;
         }

     } 
 


    
     可以看到这儿的实现思路和临界区的实现基本上相同。
     FastInterlockCompareExchange函数(util.hpp:66)则是MS那个讨论里面提到的lock cmpxchg指令的调用之处。此函数根据编译时选项,被替换成CompareExchangeUP/CompareExchangeMP两个函数分别处理单/多处理器情况。可以参考vmi386cgenx86.cpp中的InitFastInterlockOps函数(cgenx86.cpp:2106)实现。在386平台上,这两个函数完全由汇编语言实现(i386asmhelpers.asm:366, 440)。
 
以下为引用:

 CmpXchgOps      FastInterlockCompareExchange = (CmpXchgOps)CompareExchangeUP;

 // Adjust the generic interlocked operations for any platform specific ones we
 // might have.
 void InitFastInterlockOps()
 {
   _ASSERTE(g_SystemInfo.dwNumberOfProcessors != 0);

   if (g_SystemInfo.dwNumberOfProcessors != 1)
   {
     //...

     FastInterlockCompareExchange = (CmpXchgOps)CompareExchangeMP;

     //...
   }
 }
 



 
以下为引用:

 FASTCALL_FUNC CompareExchangeUP,12
  _ASSERT_ALIGNED_4_X86 ecx
  mov eax, [esp+4] ; Comparand
  cmpxchg [ecx], edx
  retn 4  ; result in EAX
 FASTCALL_ENDFUNC CompareExchangeUP

 FASTCALL_FUNC CompareExchangeMP,12
         _ASSERT_ALIGNED_4_X86 ecx
  mov eax, [esp+4] ; Comparand
   lock  cmpxchg [ecx], edx
  retn 4  ; result in EAX
 FASTCALL_ENDFUNC CompareExchangeMP
 



     值得注意的是那个讨论里面提到“The lock prefix is replaced with a nop on uniprocessor machines”,据rain的分析,NT核心部分的DLL也做了类似的优化。

posted @ 2004-07-08 10:17 Flier Lu 阅读(2257) 评论(2) 编辑

检测当前网卡是否处于混杂模式

http://www.blogcn.com/user8/flier_lu/index.html?id=1245590&run=.0C9B086

    今天比较巧,刚刚把重起网卡的文章贴上来,就有同事要我写个检测网卡混杂模式的小工具。本以为WinPCap会提供此功能,但翻了一遍Packet32.c和其驱动代码后,发现居然没有提供接口。只好下班、跑步、吃饭,然后老老实实google找解决方法,呵呵
     实际上方法很简单,打开一个网卡设备,查询其全局统计信息(IOCTL_NDIS_QUERY_GLOBAL_STATS),然后判断相应的标志位(NDIS_PACKET_TYPE_PROMISCUOUS)是否设置,即可判断此网卡是否进入混杂模式。
     首先还是枚举网卡ID,我这儿偷懒直接用WinPCap提供的PacketGetAdapterNames函数,获取网卡列表。WinPCap 3.x返回的适配器设备名是类似DeviceNPF_{4BA2EE23-C4FE-488E-98CD-FE129206458A}这种格式的,因此要字符串操作去掉DeviceNPF_前缀,组装成\.{4BA2EE23-C4FE-488E-98CD-FE129206458A}这种形式的设备名,用CreateFile打开。
 
以下为引用:

 string::size_type idx = name.find_last_of('{');

 if(idx == string::npos)
   return;

 string strDev = "\\.\" + name.substr(idx);

 HANDLE hNic = CreateFile(strDev.c_str(), 0, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, INVALID_HANDLE_VALUE);
 



     如果打开设备成功,则可以使用DeviceIoControl函数向其发送获取全局统计信息的请求,其实这儿叫网卡设备配置信息更合适 :)
 
以下为引用:

 DWORD dwInBuf = OID_GEN_CURRENT_PACKET_FILTER, dwOutBuf = 0, dwByteReturn = 0;

 if(DeviceIoControl(hNic, IOCTL_NDIS_QUERY_GLOBAL_STATS, 
   &dwInBuf, sizeof(dwInBuf), &dwOutBuf, sizeof(dwOutBuf), &dwByteReturn, NULL))
 {
   cout << endl << " mode=";
   
   static const char *NdisFilterTypes[] = 
   {
     "Directed", "Multicast", "All Multicast", "Broadcast", 
       "Source Routing", "Promiscuous", "SMT", "All Local",
       "Group", "All Functional", "Functional", "MAC Frame"
   };
   
   for(int i=0; i<(sizeof(NdisFilterTypes) / sizeof(NdisFilterTypes[0])); i++)
   {
     if((dwOutBuf & (1 << i)) != 0)
     {
       cout << NdisFilterTypes[i] << " ";
     }
   }        
 }
 



     网卡当前模式有多个标志位,由DDK的ntddndis.h文件指定标志位意义
 
以下为引用:

 //
 // Ndis Packet Filter Bits (OID_GEN_CURRENT_PACKET_FILTER).
 //
 #define NDIS_PACKET_TYPE_DIRECTED      0x0001
 #define NDIS_PACKET_TYPE_MULTICAST    0x0002
 #define NDIS_PACKET_TYPE_ALL_MULTICAST  0x0004
 #define NDIS_PACKET_TYPE_BROADCAST    0x0008
 #define NDIS_PACKET_TYPE_SOURCE_ROUTING  0x0010
 #define NDIS_PACKET_TYPE_PROMISCUOUS   0x0020
 #define NDIS_PACKET_TYPE_SMT         0x0040
 #define NDIS_PACKET_TYPE_ALL_LOCAL    0x0080
 #define NDIS_PACKET_TYPE_MAC_FRAME    0x8000
 #define NDIS_PACKET_TYPE_FUNCTIONAL    0x4000
 #define NDIS_PACKET_TYPE_ALL_FUNCTIONAL  0x2000
 #define NDIS_PACKET_TYPE_GROUP       0x1000
 


     
     有兴趣仔细看看的朋友可以参考这个讨论Detecting if Adapter is in Promiscuous mode。有一个小工具也完成了类似的功能PromiscDetect,可惜不开源,不然我也不用折腾了,呵呵
     此外PCAUSA提供的两个小工具和例子也很不错,对了解NDIS很有帮助

     PCAUSA NDIS Developer Tools
     MACADDR II IOCTL_NDIS_QUERY_GLOBAL_STATS Sample Application

posted @ 2004-07-08 10:14 Flier Lu 阅读(5512) 评论(3) 编辑

http://www.blogcn.com/user8/flier_lu/main.asp?id=1243576

编程实现重起网卡等设备

    今天水木上有位朋友问我如何卸载WinPCap的驱动。因为此类驱动跟网卡绑定很紧密,
卸载的时候最好是要把网卡重起一下(SnifferPro就是如此)。而重起网卡的程序实现又很
少有资料介绍,前段时间好容易看到一篇文章,居然是用字符串查找到控制面板下面调用ap
plet,呵呵,够狠 -_-b。刚好前几个月有同事有类似需求,我写过一个命令行下重起网卡
的小工具,就把它翻出来大概介绍一下实现思路。

    首先是要找到需要操作的网卡的ID,这个功能实现的方法很多:最常见的是Winpcap的p
acket32.c里面提供的直接从注册表中枚举的方法;另外一种方法则是使用DDK中提供的Devi
ce Installation系列函数完成。
    枚举注册表的方法需要打开HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\
Class\{4D36E972-E325-11CE-BFC1-08002bE10318}注册表键,{4D36E972-E325-11CE-BFC1-0
8002bE10318}是Net类型设备的ID,下面每个子键定义一个网络设备,但只有一部分设备是
网卡。具体处理方法请参见Winpcap的PacketGetAdapterNames函数。
    使用Device Installation API则首先用SetupDiGetClassDevs函数获取所有类型的设备
,或者在此指定只获取特定类型设备。因为我那个程序原意是控制所有类型设备,就没有指
定类型。

以下为引用:

HDEVINFO m_hDevInfo = ::SetupDiGetClassDevs(NULL, NULL, NULL, DIGCF_ALLCLASSES |
 DIGCF_PRESENT);

 


    然后使用SetupDiEnumDeviceInfo枚举类型中所有的设备

以下为引用:

SP_DEVINFO_DATA did = { sizeof(SP_DEVINFO_DATA) };

for(int i=0; ::SetupDiEnumDeviceInfo(m_hDevInfo, i, &did); i++)
{
   //...
}

 

 

    在找到要处理的设备后,应该用CM_Get_DevNode_Status函数和注册表获取其状态,忽
略被隐藏的设备。

以下为引用:

bool CDeviceManager::IsClassHidden(const GUID *ClsGuid) const
{
  HKEY hKeyClass = ::SetupDiOpenClassRegKey(ClsGuid, KEY_READ);

  bool hidden = false;

  if(INVALID_HANDLE_VALUE != hKeyClass)
  {
    hidden = ERROR_SUCCESS == ::RegQueryValueEx(hKeyClass, REGSTR_VAL_NODISPLAYC
LASS, NULL, NULL, NULL, NULL);

    ::RegCloseKey(hKeyClass);
  }

  return hidden;
}

DWORD dwStatus = 0, dwProblem = 0;

if(CR_SUCCESS != ::CM_Get_DevNode_Status(&dwStatus, &dwProblem, did.DevInst,0))
{
  DisplayError("CM_Get_DevNode_Status");
  continue;
}

if(dwStatus & DN_NO_SHOW_IN_DM || IsClassHidden(&did.ClassGuid))
{
  continue;
}

 


    对剩下的设备则根据其Class进行过滤,只处理Net类型设备,如果前面指定只获取Net
设备则此步骤可以忽略。

以下为引用:


const std::string CDeviceManager::GetProperty(SP_DEVINFO_DATA& did, DWORD Proper
ty) const
{
  std::string buf;
  DWORD dwLength = 0;

  while(!::SetupDiGetDeviceRegistryProperty(m_hDevInfo, &did, Property, NULL,
    (PBYTE)buf.c_str(), buf.size(), &dwLength))
  {
    if(::GetLastError() == ERROR_INSUFFICIENT_BUFFER)
    {
      buf.resize(dwLength * sizeof(wchar_t));
      std::fill(buf.begin(), buf.end(), '\0');
    }
    else
    {
      break;
    }
  }
  buf.resize(strlen(buf.c_str()));

  return buf;
}

if(stricmp(GetProperty(did, SPDRP_CLASSGUID).c_str(), "{4d36e972-e325-11ce-bfc1-
08002be10318}") == 0)
{
  // ...
}

 


    满足上述限制的设备,就是我们要处理的网卡。可以直接修改其状态:SetupDiSetClas
sInstallParams函数设置参数;SetupDiCallClassInstaller完成参数修改。

以下为引用:

bool CDeviceManager::ChangeDeviceState(SP_DEVINFO_DATA& did, DWORD State) const
{
  SP_PROPCHANGE_PARAMS pcp = {sizeof(SP_CLASSINSTALL_HEADER)};

  pcp.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
  pcp.Scope = (State == DICS_START || State == DICS_STOP )? DICS_FLAG_CONFIGSPEC
IFIC : DICS_FLAG_GLOBAL;
  pcp.StateChange = State;

  if(!::SetupDiSetClassInstallParams(m_hDevInfo, &did,
    (SP_CLASSINSTALL_HEADER *)&pcp, sizeof(pcp)))
  {
    DisplayError("SetupDiSetClassInstallParams");
    return false;
  }


  //
  // Call the ClassInstaller and perform the change.
  //
  if(!::SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, m_hDevInfo, &did))
  {
    DisplayError("SetupDiCallClassInstaller");
  }
  return true;
}

ChangeDeviceState(did, DICS_STOP);    // 停止
ChangeDeviceState(did, DICS_START);   // 启动
ChangeDeviceState(did, DICS_ENABLE);  // 启用
ChangeDeviceState(did, DICS_DISABLE); // 禁用

posted @ 2004-07-08 10:13 Flier Lu 阅读(3041) 评论(7) 编辑

《4.4 BSD 操作系统设计与实现》

http://www.blogcn.com/user8/flier_lu/index.html?id=1215094&run=.0999083

    如果说有一本原文著作能够让我以读侦探小说的心情去阅读的话,那一定是这本《The Design and Implementation of the 4.4 BSD Operating System》了。由"Gods of BSD"执笔,与《Design of the UNIX Operating System》等经典著作齐名的此书,是当之无愧的BSD bible。
     以前也看过几本分析操作系统原理和实现的著作,但要么过于学术气(如机工出的几本操作系统原理书籍),要么过于拘泥于细节实现代码之上(现在如过江之鲫的XXX源码分析)。而此书则能够做到在设计理论上高屋建瓴却不脱离实际;在实现方法上娓娓道來但不拘泥于实现;时而给出一幅结构关系图,让人对设计思路一目了然。能做到如此举重若轻者,大师也。:D

     非常感谢人民邮电出版社引进了此书的英文影印版,价格公道量又足。虽是原文,但文字并不深奥难读,一段时间来每天早上读几节已成为我的一点爱好,呵呵。中国电力出版社则找浙大的学生翻译出版了中文版。不过十几个学生译者齐头并进,估计翻译质量够呛,如果不是实在没时间的话,强烈建议还是购买影印版阅读原文。阅读时可以配合 FreeBSD 源代码相互应证,作为BSD的重要发展的FreeBSD虽然改进不少,但底层实现思路还是沿用4.4BSD的,基本上没有什么障碍。
         
     
     
 4.4BSD操作系统设计与实现(英文影印版)


 
 4.4BSD操作系统设计与实现(中文版)

     希望以后还能有机会仔细重新研读此书。:P

 BSD精神的延继
     ——评《4.4BSD操作系统的设计与实现》
 

以下为引用:

 经典著作的新版  BSD精神的延继

            ——评《4.4BSD操作系统的设计与实现》

  

 清华大学网络中心   张  辉

  

 《The Design and Implementation of 4.4BSD Operating System》是介绍BSD最为知名和权威的经典著作的最新一版。该书的几位作者在BSD开发圈中被推崇为“Gods of BSD”,他们以曾在4.3/4.4BSD的开发过程中起到的重要推动作用而闻名于世,而他们在1989年撰写的该书上一版——《The Design and Implementation of 4.3BSD Operating System》几乎是全美各所大学计算机科学系操作系统课程的指定参考书,同时该书也是所有BSD爱好者案头必备的Bible。

  

 由于BSD长期以来一直在美国的大学和研究所中流行,没有像UNIX的另一风格System V那样为商业公司所把持。在国内除学术界之外的其他领域,长期以来对于BSD的了解都远不及System V广泛。因此,我们有必要回顾一下BSD的过去。

  

 自从1969年UNIX由Ken Thompson和同在贝尔实验室计算机研究小组(AT&T Bell Laboratories, Computer Research Group)的同事们一起公开发表以来,UNIX系统发展出诸多各具特色、后来又再度会聚的流派,其中占主导地位的有两大流派。一是AT&T所开发的UNIX System V,另一是加州大学伯克利分校(University of California at Berkeley,UC Berkeley)的计算机系统研究组(Computer System Research Group,CSRG)开发的伯克利软件发行版本(Berkeley Software Distribution,BSD)。

  

 同富有商业背景和气息的AT&T UNIX相比,源于美国国防部(DoD)高级研究规划署(Advanced Research Plan Agency,ARPA。ARPA也支持Internet前身ARPAnet的开发和建立)设立的科研项目,诞生于世界顶级学府(UC Berkeley)的BSD UNIX在操作系统概念和实现技术上更体现出了锐意进取和敢于创新的精神。Berkeley 是所有BSD UNIX的发源地。从1977年起,它免费发布了数千份1BSD和2BSD(PDP-11使用),以及4BSD(VAX使用)。BSD吸引并激发了Berkeley的研究机构内众多师生的极大创造热情,即使是在Berkeley以外的地方,BSD仍然牢牢地占据了UNIX在研究领域的应用。在UNIX蓬勃发展和不断进步的整个20世纪七八十年代,UNIX界内的人士无一不对BSD UNIX报以崇敬的目光,因为BSD就是创新、突破的代名词。与此同时,众多的商业公司也纷纷采用或者吸收BSD的思想和实现,以增强各自的产品。为国内早期UNIX用户熟悉的SunOS(Solaris的前一版本)就是其中的代表,而在美国,Sun的用户则主要是理论、工程学术界。今天Linux的情况与之是如此相似。

  

 然而,时光进入20世纪90年代,一方面AT&T起诉BSD造成了法律纠纷,商业公司Sun、HP等纷纷转向AT&T的Unix System V。直到1994年,不含任何AT&T Unix代码的4.4BSD-Lite发布,法律问题才完全结束,然而长达数年的法律纠纷已经给BSD带来了巨大的影响;另一方面BSD的研究背景使得BSD的发布一直是由学术机构来完成的,这赋予了BSD在技术上太多的变化,但它们却往往不兼容。虽然这是程序员们所热衷和推崇的情况,可是商业公司却并不喜欢,BSD的产业化受到了限制。结果,BSD的开发人员相继离开,而DoD也在1993年最终结束了这一研究项目。

  

 伴随最近几年GNU和Linux的兴起,很多Linux/Unix的使用者,都知道了一个名为FreeBSD的操作系统,但大部分人对BSD的了解却愈来愈少,甚至把它同FreeBSD等混为一谈。其实,FreeBSD只是CSRG在1992年终止开发之前发行的最后一个BSD版本——4.4BSD的一个著名分支。BSD的著名后继有OpenBSD、NetBSD、FreeBSD和BSDI等,这些BSD版本支持包括StrongARM、M68k、MIPS、x86、Alpha、SPARC、VAX、PA-RISC、PowerPC,直至嵌入式系统在内的几乎所有目前使用的体系结构。而众多商业版本的UNIX变体以及免费的Linux发布版本,都无一例外地吸收了BSD中丰富的新思想和新技术。

  

 学院气息浓厚的BSD为UNIX乃至Internet起到了巨大的支持和推动作用。从1977年的1BSD开始,到1992年的4.4BSD,历史上的每一个BSD版本都引入了重大的创新。让我们看看BSD的辉煌历史吧。

 1978年:2.xBSD首次引入了csh;

 1978年:3BSD,引入虚拟存储(virtual memory)的概念;

 1980年:4BSD,引入termcap、curses、vi;

 1981年:4.1BSD,引入作业控制(job control)、vfork()、自动内核配置;

 1983年:4.2BSD,率先实现TCP/IP协议栈、提供网络编程接口socket,引入UFS文件系统,支持长文件名和符号链接,改进了System V差强人意的信号处理机制(signal handling),引入进程间通信机制(Inter-process communication,IPC);

 1986-1988年:4.3BSD,引入文件系统Fat FFS,重新编写了TCP算法;

 1989年:4.3BSD,实现大部分P1003.1标准,实现了网络文件系统(NFS)、内存文件系统(memory file system ,MFS)、Kerberos。

 1992年6月:4.4BSD,引入虚拟存储系统(virtual memory system,VMS)、虚拟文件接口(virtual filesystem interface)、在UDP或者TCP上的NFS系统以及其他多种改进。

  

 BSD最杰出的贡献就是其发布了世界上第一个TCP/IP协议栈的实现,如今在各种操作系统上从事网络编程的人员所熟悉的socket接口,都来自于BSD。目前FreeBSD的用户大多是网络服务提供商ISP和网络内容提供商ICP。著名的yahoo就是由多台运行FreeBSD的PC组成的机群。而Internet上最繁忙的ftp服务器ftp.cdrom.com(目前为ftp.freesoftware.com),单台服务器支持的每天传输量都在700GB以上,也是由FreeBSD构成的。具有讽刺意味的是,属于微软的hotmail,其大部分的服务器原本也是运行FreeBSD的,微软收购hotmail后曾多次想将它们迁移到NT平台上,但均告失败。这正好折射出BSD在网络应用上功能的强大。

  

 BSD的各个后继版本稳定性好、安全性高、网络功能强的突出特色使之更成为计算机网络、安全方面应用的首选平台。而包括Linux在内的所有UNIX系统变体,也都或多或少地吸收和融入了BSD思想和技术。因此,掌握BSD内核的精髓、了解其作为操作系统具备的独到之处,以及被其他OS广泛借鉴、移植的各种设计原理和思路,都极具研究和实用价值。

  

 《The Design and Implementation of 4.4BSD Operating System》由于作者是4.3/4.4BSD开发过程中的重要组织者和开发人员,所以对BSD的理解和介绍极为深入全面,本书也继承了前一版本的特色,继续成为介绍BSD操作系统的经典。其内容丰富,覆盖了BSD内核的核心设计思想和技术亮点。这包括:系统调用、存储管理、进程管理、文件系统、I/O、进程通信、网络通信等操作系统涉及到的所有方面。书中还介绍了4.4BSD的内部结构和实现4.4BSD系统功能中所采用到的概念、数据结构和算法。同时也指出了BSD与AT&T UNIX的不同之处,并对其设计思想和背景作了精准的阐述。该书对于采用UNIX,特别是BSD中的新技术、新特点来进行的研究、开发工作极具参考价值。

  

 此外,本书的每一章都提供了若干参考文献,给读者指出了各章相关内容的更多资料,而且还附带了习题,这种教科书式的体例更适合读者学习巩固。

  

 我们完全可以相信,《The Design and Implementation of 4.4BSD Operating System》会让UNIX内核研发人员、UNIX应用研发人员、UNIX系统管理员以及UNIX的众多爱好者们受益匪浅。

  

 需要指出的是,在阅读本书以前应该具备操作系统的基础知识,比如学过操作系统课程,否则难度较大。另外,如果能够和《The Design and Implementation of UNIX Operating System》(介绍AT&T UNIX的经典)、《Advanced Programming UNIX Environment》(引用了BSD例子的UNIX编程经典)、《UNIX Network Programming》(UNIX BSD socket编程经典)配合比照起来一起阅读本书,并在阅读同时动手进行一些编程和验证工作,则效果更好。
 


posted @ 2004-07-08 10:11 Flier Lu 阅读(2205) 评论(0) 编辑

Win32 临界区实现原理浅析

http://www.blogcn.com/user8/flier_lu/index.html?id=1205525&run=.0748049

    去年11月的MSDN杂志曾刊登过一篇文章 Break Free of Code Deadlocks in Critical Sections Under Windows ,Matt Pietrek 和 Russ Osterlund 两位对临界区(Critical Section)的内部实现做了一次简短的介绍,但点到为止,没有继续深入下去,当时给我的感觉就是痒痒的,呵呵,于是用IDA和SoftIce大致分析了一下临界区的实现,大致弄明白了原理后也就没有深究。现在乘着Win2k源码的东风,重新分析一下这块的内容,做个小小的总结吧 :P
     临界区(Critical Section)是Win32中提供的一种轻量级的同步机制,与互斥(Mutex)和事件(Event)等内核同步对象相比,临界区是完全在用户态维护的,所以仅能在同一进程内供线程同步使用,但也因此无需在使用时进行用户态和核心态之间的切换,工作效率大大高于其它同步机制。
     临界区的使用方法非常简单,使用 InitializeCriticalSection  InitializeCriticalSectionAndSpinCount 函数初始化一个 CRITICAL_SECTION 结构;使用 SetCriticalSectionSpinCount 函数设置临界区的Spin计数器;然后使用 EnterCriticalSection  TryEnterCriticalSection 获取临界区的所有权;完成需要同步的操作后,使用 LeaveCriticalSection 函数释放临界区;最后使用 DeleteCriticalSection 函数析构临界区结构。
     以下是MSDN中提供的一个简单的例子

以下为引用:

 // Global variable
 CRITICAL_SECTION CriticalSection;

 void main()
 {
     ...

     // Initialize the critical section one time only.
     if (!InitializeCriticalSectionAndSpinCount(&CriticalSection, 0x80000400) )
         return;
     ...

     // Release resources used by the critical section object.
     DeleteCriticalSection(&CriticalSection)
 }

 DWORD WINAPI ThreadProc( LPVOID lpParameter )
 {
     ...

     // Request ownership of the critical section.
     EnterCriticalSection(&CriticalSection);

     // Access the shared resource.

     // Release ownership of the critical section.
     LeaveCriticalSection(&CriticalSection);

     ...
 }
 


     首先看看构造和析构临界区结构的函数。
     InitializeCriticalSection 函数(ntosdll esource.c:1210)实际上是调用 InitializeCriticalSectionAndSpinCount 函数(resource.c:1266)完成功能的,只不过传入一个值为0的初始Spin计数器;InitializeCriticalSectionAndSpinCount 函数主要完成两部分工作:初始化 RTL_CRITICAL_SECTION 结构和 RTL_CRITICAL_SECTION_DEBUG 结构。前者是临界区的核心结构,下面将着重讨论;后者是调试用结构,Matt 那篇文章里面分析的很清楚了,我这儿就不罗嗦了 :P
     RTL_CRITICAL_SECTION结构在winnt.h中定义如下:

以下为引用:

 typedef struct _RTL_CRITICAL_SECTION {
     PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

     //
     //  The following three fields control entering and exiting the critical
     //  section for the resource
     //

     LONG LockCount;
     LONG RecursionCount;
     HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
     HANDLE LockSemaphore;
     ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
 } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
 


     InitializeCriticalSectionAndSpinCount 函数中首先对临界区结构进行了初始化

     DebugInfo 字段指向初始化临界区时分配的RTL_CRITICAL_SECTION_DEBUG结构;
     LockCount 字段是临界区中最重要的字段,初始值为-1,当临界区被获取(Hold)时此字段大于等于0;
     RecursionCount 字段保存当前临界区所有者线程的获取缓冲区嵌套层数,初始值为0;
     OwningThread 字段保存当前临界区所有者线程的句柄,初始值为0;
     LockSemaphore 字段实际上是一个auto-reset的事件句柄,用于唤醒等待获取临界区的阻塞线程,初始值为0;
     SpinCount 字段用于在多处理器环境下完成轻量级的CPU见同步,单处理器时没有使用(初始值为0),多处理器时设置为SpinCount参数值(最大为MAX_SPIN_COUNT=0x00ffffff)。此外 RtlSetCriticalSectionSpinCount 函数(resource.c:1374)的代码与这儿设置SpinCount的代码完全一样。

     初始化临界区结构后,函数会根据SpinCount参数的一个标志位判断是否需要预先初始化 LockSemaphore 字段,如果需要则使用NtCreateEvent创建一个具有访问权限的同步用事件核心对象,初始状态为没有激发。这一初始化本来是在 EnterCriticalSection 函数中完成的,将之显式提前可以进一步优化 EnterCriticalSection 函数的性能。
     值得注意的是,这一特性仅对Win2k有效。MSDN里面说明如下:

以下为引用:

 Windows 2000:  If the high-order bit is set, the function preallocates the event used by the EnterCriticalSection function. Do not set this bit if you are creating a large number of critical section objects, because it will consume a significant amount of nonpaged pool. This flag is not necessary on Windows XP and later, and it is ignored.
 

     与之对应的 DeleteCriticalSection 函数完成关闭事件句柄和是否调试结构的功能。

     临界区真正的核心代码在win2kprivate tosdlli386critsect.asm里面,包括_RtlEnterCriticalSection、_RtlTryEnterCriticalSection和_RtlLeaveCriticalSection三个函数。

     _RtlEnterCriticalSection 函数 (critsect.asm:85) 首先检查SpinCount是否为0,如果不为0则处理多处理器架构下的问题[分支1];如为0则使用原子操作给LockCount加一,并判断是否其值为0。如果加一后LockCount大于0,则此临界区已经被获取[分支2];如为0则获取当前线程TEB中的线程句柄,保存在临界区的OwningThread字段中,并将RecursionCount设置为1。调试版本则调用RtlpCriticalSectionIsOwned函数在调试模式下验证此缓冲区是被当前线程获取的,否则在调试模式下激活调试器。最后还会更新TEB的CountOfOwnedCriticalSections计数器,以及临界区调试结构中的EntryCount字段。
     如果此临界区已经被获取[分支2],则判断获取临界区的线程句柄是否与当前线程相符。如果是同一线程则直接将RecursionCount和调试结构的EntryCount字段加一;如果不是当前线程,则调用RtlpWaitForCriticalSection函数等待此临界区,并从头开始执行获取临界区的程序。
     多CPU情况的分支处理方式类似,只是多了对SpinCount的双重检查处理。下面是我手工归纳的一张 _RtlEnterCriticalSection 函数流程图,可能有错,欢迎指正 :P

   

 Visio 2003 版本的流程图

     接着的_RtlTryEnterCriticalSection(critsect.asm:343)函数是一个轻量级的尝试获取临界区的函数。伪代码如下:
 

以下为引用:

 if(CriticalSection->LockCount == -1)
 {
   // 临界区可用
   CriticalSection->LockCount = 0;
   CriticalSection->OwningThread = TEB->ClientID;
   CriticalSection->RecursionCount = 1;

   return TRUE;
 }
 else
 {
   if(CriticalSection->OwningThread == TEB->ClientID)
   {
     // 临界区是当前线程获取
     CriticalSection->LockCount++;
     CriticalSection->RecursionCount++;

     return TRUE;
   }
   else
   {
     // 临界区已被其它线程获取
     return FALSE;
   }
 }
 


     最后的_RtlLeaveCriticalSection(critsect.asm:271)函数释放已获取的临界区,实现就比较简单了,实际上就是对嵌套计数器和锁定计数器进行操作。伪代码如下:

以下为引用:

 if(--CriticalSection->RecursionCount == 0)
 {
   // 临界区已不再被使用
   CriticalSection->OwningThread = 0;

   if(--CriticalSection->LockCount)
   {
     // 仍有线程锁定在临界区上
     _RtlpUnWaitCriticalSection(CriticalSection)
   }
 }
 else
 {
   --CriticalSection->LockCount
 }
 


 btw: 看源代码、分析流程再加上写文章时间加起来还没有画那个流程图时间长,真是ft死了...-_-b

posted @ 2004-07-08 10:09 Flier Lu 阅读(4130) 评论(0) 编辑

编译 CHM 项目的 API 接口

http://www.blogcn.com/user8/flier_lu/index.html?id=1196784&run=.04005F8

    Microsoft 提供的HTML Help Workshop只支持在GUI界面或者使用其自带的HCC.exe程序编译CHM项目文件(.hpp),并不直接提供API供第三方软件使用。而实际上其CHM项目编译器的HHA.DLL中提供了名为HHA_CompileHPP的导出函数,实现了对.hpp的CHM项目文件直接进行编译,并通过两个回调函数通知用户当前编译进度。
 
以下为引用:

   BOOL WINAPI HHA_CompileHHP(PCSTR pszHhpFile, FARPROC pLogString, FARPROC pProgress);
 


     参数pszHhpFile传入要编译的CHM项目工程文件文件(.hhp);pLogString和pProgress分别执行两个回调函数,向用户报告当前编译进度。
 
以下为引用:

   typedef void (CALLBACK *LOGSTRINGPROCA)(PCSTR pszMsg);
   typedef BOOL (CALLBACK *PROGRESSPROCA)(PCSTR pszFile);
 


     使用时只需要载入动态链接库HHA.DLL,并通过函数名字(HHA_CompileHHP)或序号(0x13F)获得函数入口即可。

 btw: CHM的文件格式好些也被搞定了,Code Project上有一个支持Pocket PC 2003的开源CHM阅读器

     下面HHA_CompileHHP的一个Delphi移值版本:

以下为引用:

 unit ChmCompiler;

 interface

 uses
   Windows, SysUtils;

 type
   TChmCompileFunc = function(pszHhpFile: PChar; pLogString: Pointer; pProgress: Pointer): Bool; stdcall;

   TLogMessageEvent = procedure(Msg: string) of object;
   TProgressEvent = function(FileName: string): Boolean of object;

   TChmCompiler = class
   private
     FDll: HMODULE;
     FProc: TChmCompileFunc;

     FOnLogMessage: TLogMessageEvent;
     FOnProgress: TProgressEvent;
   protected
     constructor Create;
   public
     destructor Destroy; override;

     class function Default: TChmCompiler;

     function Compile(FileName: TFileName): Boolean;

     property OnLogMessage: TLogMessageEvent read FOnLogMessage write FOnLogMessage;
     property OnProgress: TProgressEvent read FOnProgress write FOnProgress;
   end;

 implementation

 uses
   ActiveX;

 var
   GChmCompiler: TChmCompiler;

 procedure LogMessageFunc(pszMsg: PChar); stdcall;
 begin
   Assert(Assigned(GChmCompiler));

   if Assigned(GChmCompiler.OnLogMessage) then
     GChmCompiler.OnLogMessage(pszMsg);
 end;

 function ProgressFunc(pszFile: PChar): Bool; stdcall;
 begin
   Assert(Assigned(GChmCompiler));

   if Assigned(GChmCompiler.OnProgress) then
     Result := GChmCompiler.OnProgress(pszFile)
   else
     Result := True;
 end;

 { TChmCompiler }

 constructor TChmCompiler.Create;
 begin
   FDll := SafeLoadLibrary('HHA.DLL');

   if FDll = 0 then RaiseLastOSError;

   FProc := TChmCompileFunc(GetProcAddress(FDll, 'HHA_CompileHHP'));
   //FProc := TChmCompileFunc(GetProcAddress(FDll, PChar($13F)));

   if @FProc = nil then RaiseLastOSError;

   CoInitialize(nil);
 end;

 destructor TChmCompiler.Destroy;
 begin
   CoUninitialize();
   
   FProc := nil;

   if FDll <> 0 then
     Win32Check(FreeLibrary(FDll));

   inherited;
 end;

 class function TChmCompiler.Default: TChmCompiler;
 begin
   if not Assigned(GChmCompiler) then
     GChmCompiler := TChmCompiler.Create;

   Result := GChmCompiler;
 end;

 function TChmCompiler.Compile(FileName: TFileName): Boolean;
 var
   szFileName: PChar;
   LogMessageProc, ProgressProc, CompileProc: Pointer;
   Ret: BOOL;
 begin
   szFileName := PChar(FileName);
   LogMessageProc := @LogMessageFunc;
   ProgressProc := @ProgressFunc;
   CompileProc := @FProc;
   asm
     XOR EAX, EAX
     PUSH EAX
     MOV EAX, ProgressProc
     MOV ECX, LogMessageProc
     MOV EDX, szFileName;
     PUSH EAX
     PUSH ECX
     PUSH EDX
     CALL CompileProc
     MOV Ret, EAX
   end;
   Result := Ret;
 end;

 initialization
   GChmCompiler := nil;

 finalization
   if Assigned(GChmCompiler) then FreeAndNil(GChmCompiler);

 end.
 



posted @ 2004-07-08 10:07 Flier Lu 阅读(2244) 评论(7) 编辑

RunDll32 的使用方法与实现原理

http://www.blogcn.com/user8/flier_lu/index.html?id=1190096&run=.03463D0

    RunDll32.exe是Windows系统自带的一个直接执行DLL中导出函数的小工具,使用方式上与16位系统的RunDll.exe兼容,但提供32位的支持。

   RUNDLL32.EXE <dllname>,<entrypoint> <optional arguments>

     首先指定函数所在DLL名称,然后是函数入口名称,最后是可选的参数。RunDll32.exe会使用标准的DLL搜索策略定位DLL,但最好是指定完整路径名,避免名称冲突。此外需要注意的是,DLL名称和函数入口名称之间的逗号必须要有,因为程序在解析命令行时是依靠此标识分隔字符串的。
     具体的使用方法可以参考微软知识库提供的文档

     INFO: Windows Rundll and Rundll32 Interface

     使用这个工具可以完成很多令人意想不到的管理功能,例如在脚本中退出Windows以及调用控制面板等更多控制功能等。
   
     RUNDLL and RUNDLL32
     Using Rundll

     因为RunDll32实际上是提供了一个调用任意DLL函数的代理,所以只要此函数以名字导出,并且遵循一定的函数签名(Function Signature),就可以很方便的通过RunDll32调用。

     void CALLBACK EntryPoint(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);

     而实际上RunDll32也可以应用在不符合此函数签名的函数上,例如前面提到的关闭Windows功能,调用的

     BOOL ExitWindowsEx(UINT uFlags, DWORD dwReason);

     虽然ExitWindowsEx的函数签名与RunDll32要求的不符,但实际上也可以使用,因为他在堆栈使用上可以利用RunDll32传入的参数。传入的hwnd被当成uFlags使用;hInst被当成dwReason使用。因为这两个值都是系统相关伪随机的,因此这样调用的效果伪随机(依赖窗口和模块句柄的分配算法)。这也是为什么使用此函数时往往要敲多次以尝试成功。
     详细的分析文章可以参考这个讨论What can go wrong when you mismatch the calling convention? 

     在了解了RunDll32.exe的使用方法后,我们来看看其实现原理。其源代码可以在Win2K源码包的win2kprivatewindowsshell undll32目录下找到。

     整个实现代码很简单,大概分为三个部分:ModuleEntry和WinMain前半部分完成初始化和其它薄记工作;ParseCommand完成命令解析、DLL载入以及函数入口获得,是核心部分;InitStubWindow和WinMain后半部分完成函数调用所需窗口句柄的初始化以及实际函数调用。

     ParseCommand函数首先对命令行字符串进行分解,验证传入Dll名称是否指向有效的PE映像文件,并尝试载入DLL。
     在定义了WX86符号的Alpha系统上,需要使用wx86.dll导出的Wx86LoadX86Dll函数完成实际的DLL载入,并使用Wx86ThunkProc函数构造一个Thunk块,包装实际函数的调用。
     如果定义了WINNT符号的系统,还需要从DLL中获得DOS头的和载入配置信息中的版本信息,更新PEB的相关字段模拟相应Windows版本。
     在获取函数入口地址时,RunDll32会尝试获得相应后缀为W和A的函数名入口地址。因为Win32API中凡是使用了字符串的函数一般都提供了Unicode/Ansi两个版本。这样就可以避免要求用户在命令行强行指定,RunDll32也会字段转换命令行为对应形式的字符串,传递给目标函数。
     最后的调用很简单,依次传入Stub窗口句柄、模块句柄、命令行和显示属性,并用一个try...except保护起来,处理所有的(EXCEPTION_EXECUTE_HANDLER)异常情况,弹出异常信息对话框。

posted @ 2004-07-08 10:03 Flier Lu 阅读(1791) 评论(0) 编辑

重用的粒度

http://www.blogcn.com/user8/flier_lu/index.html?id=1183014&run=.08F07F8

    最近正在看的两本书《产生式编程》(简称为GP)和《C++网络编程 卷2》(简称为CNPv2)中都讨论到了软件开发中重用的粒度问题。特别是[GP]中一针见血地指出

以下为引用:

     在OO发展的早期,人们相信对象是可以重用的,并且可重用的软件会作为应用程序开发的副产品,简单地“释放”出来。现在,OO社团普遍认识到这种想法离实际情况简直是十万八千里。可重用地软件要求必须自己地进行工程,为重用而进行工程(engineering for reuse)要求进行充分地研究。

 《产生式编程》第3章3.2节
 


     [GP]中进一步指出应该从两个方面:问题空间视角(分析方法)和解空间视角(实现技术)来讨论重用地问题。
     问题空间视角:我们一般关注的是单独的系统,这种重用是有限的,针对特定客户需求和背景的。而[GP]则指出应该更多地关注产品线、系统族以及相关领域。产品线是一系列相关的旨在“满足一个给定市场”的系统集合,其组成元素不一定要有很强相关性。系统族则是“一组分享足够多的通用属性的系统,可以使用一组通用的资源来构建它”。而系统族的构建需要针对特定领域的领域工程来划分范围。
     解空间视角:从传统的函数库、类库和模式,到高层的组件和框架,一层层演化而来。函数库关注算法的可重用性;类库则进一步将具有强内聚性的数据与算法绑定;模式则是抽象的针对处理变化的知识的归纳;组件则偏向于基于外部接口的封装。而框架则是[CNPv2]主要关注的层面,它不同于类库的被动性和独立性,其成员往往主动并且协作地通过回调(Callback)和IoC(Inversion of Control)来与使用者交互;框架同时也是模式的实践;框架和组件则是高度协作互不从属。

 btw: [GP]不愧是善于做逻辑思辨的德国人的著作,前三章写的高屋建瓴,让我第一遍看的时候感觉云里雾里,呵呵,思考几天后才好歹摸到点门道。希望把第一部分读完后回头再读,能够有所体会。:P

posted @ 2004-07-08 10:02 Flier Lu 阅读(412) 评论(0) 编辑

Writing Secure Code, Second Edition

http://www.blogcn.com/user8/flier_lu/index.html?id=1168765&run=.0F495B6

    2002年读过的这本《编写安全的代码》 无论是从选题还是内容上都是难得的佳作。不久前此书的第二版由原作者Michael Howard和David LeBlanc推出,原书《Writing Secure Code》 从512页扩充到了《Writing Secure Code, Second Edition》  的800页,新增了8个章节,大大得到充实。China-Pub上已经可以购买第二版的影印版《编写安全的代码(第2版)》(英文影印版) ,据说第二版的中文版也在翻译中,呵呵,期待ing...

    

     根据两个版本的目录大致对比了一下

   第一版中第二章“设计安全的系统”,被扩展为三章:
     2.The Proactive Security Development Process
     3.Security Principles to Live By    
     4.Threat Modeling
   第一版中第12章“基于Web服务的安全”被合并到新增的第13章
   第二版新增
     10.All Input Is Evil
     12.Database Input Issues 
     13.Web-Specific Input Issues 
     14.Internationalization Issues
     20.Performing a Security Code Review 
     22.Building Privacy into Your Application
     24.Writing Security Documentation and Error Messages 
   

posted @ 2004-07-08 10:00 Flier Lu 阅读(1120) 评论(0) 编辑

全0的MAC地址 (00:00:00:00:00:00)

http://www.blogcn.com/user8/flier_lu/index.html?id=1139019&run=.0CEEA16

    前两天同事抓到了包括MAC地址全0的包,在讨论和请教牛人后,大概得出的结论是这种MAC地址在共享网络下面是有效的。据说long long ago时这种MAC地址和主机地址部分全0的IP地址一样,是用于广播的(rain提供)。不过现在这种MAC好像已经不再作为特殊地址保留(scz测试),而部分系统如BSD系列还保留主机地址全0的IP地址的广播效果。
     scz的详细测试结果如下:

以下为引用:

 1) Linux

 Linux下ifconfig修改MAC地址前必须先down掉相应接口,改了MAC之后再
 up。但是Linux下将MAC设置成全零后(此时无错误提示),相应接口up失败:

 ifconfig eth0 hw ether 00:00:00:00:00:00

 Linux虽然自身无法设置全零MAC,但可与全零MAC的系统正常通信。

 2) x86/Solaris

 x86/Solaris 9不必down/unplumb接口,可直接修改MAC地址:

 ifconfig dnet0 ether 00:00:00:00:00:00

 全零MAC地址可与同一HUB上的Windows系统通信。

 3) Windows 98/NT/2000/XP/2003

 Windows XP通过GUI界面设置全零MAC时无错误提示,但真实通信时仍然使用原MAC,
 "ipconfig -all"查看得到的MAC地址也是原MAC。

 Windows 98/2000/XP都可与全零MAC的系统正常通信,NT/2003未测试,应该也是可以
 的。

 XP下MAC地址在注册表中的相应位置:

 HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlClass{4D36E972-E325-11CE-BFC1-08002bE10318}<nnnn>

 NetCfgInstanceId    REG_SZ  {可变串}
 NetworkAddress      REG_SZ  000000111111

 4) x86/FreeBSD

 FreeBSD下ifconfig修改MAC地址前不必down掉相应接口,可直接修改,并且全零MAC
 地址与同一HUB上的Windows系统通信无误:

 ifconfig lnc0 ether lladdr 00:00:00:00:00:00

 各种系统是否接受全零MAC地址是实现相关的,并不统一。注意,上述系统不但在同
 一子网,而且在同一HUB上。一般交换机不接受全零MAC地址,导致ARP解析失败,无
 法获取目标MAC,IP通信自然也就失败。
 

posted @ 2004-07-08 09:56 Flier Lu 阅读(4025) 评论(1) 编辑

posted @ 2004-07-08 09:53 Flier Lu 阅读(3785) 评论(4) 编辑

C#中实现WebBrowser控件的HTML源代码读写

http://www.blogcn.com/user8/flier_lu/index.html?id=1125200&run=.0D9CAA6

趁周末想折腾一下嵌入ASP.NET的WinForm程序
 需要用到WebBrowser控件的HTML源码读写
 就把以前的一些代码片断移值到C#下
 顺便发个帖子备忘,呵呵
  
 思路其实很简单,直接通过document.documentElement.outerHTML
 或者使用IPersistStreamInit接口直接对流进行处理
 前者我就不废话了,后者实现方法如下
  
 首先是写入HTML到已初始化的WebBrowser控件
 初始化可以通过Navigate("about:blank")完成
 必须确保WebBrowser.Document != null
 否则应该推迟到DocumentComplete事件再读写
  
 

UCOMIStream stream = null;
  
 CreateStreamOnHGlobal(Marshal.StringToHGlobalUni(value), 
trueout stream);
   



 
if(stream != null)
  
 
{
   IPersistStreamInit persistentStreamInit 
=
     (IPersistStreamInit)WebBrowser.Document;
  
   persistentStreamInit.InitNew();
   persistentStreamInit.Load(stream);
   persistentStreamInit 
= null;
 }


  
 UCOMIStream是COM中IStream的CLR版本
 CreateStreamOnHGlobal函数从一个字符串的地址
 创建一个IStream供使用
  

 [DllImport("ole32.dll", PreserveSig=false)]
 
static extern void CreateStreamOnHGlobal(IntPtr hGlobal,
   Boolean fDeleteOnRelease, [Out] 
out UCOMIStream pStream);


  
 然后就是通过IPersistStreamInit接口初始化并载入HTML源码,
 IPersistStreamInit接口CLR缺省没有导入,定义如下
  

 [ComVisible(true), ComImport(), Guid("7FD52380-4E07-101B-AE2D-08002B2EC713"),
  InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
 
public interface IPersistStreamInit
 
{
   
void GetClassID([In, Out] ref Guid pClassID);
  
   [
return: MarshalAs(UnmanagedType.I4)] [PreserveSig]
   
int IsDirty();
  
   
void Load([In, MarshalAs(UnmanagedType.Interface)] UCOMIStream pstm);
   
void Save([In, MarshalAs(UnmanagedType.Interface)] UCOMIStream pstm,
             [In, MarshalAs(UnmanagedType.I4)] 
int fClearDirty);
   
void GetSizeMax([Out, MarshalAs(UnmanagedType.LPArray)] long pcbSize);
   
void InitNew();
 }


  
 读取HTML也是类似思路,将HTML源码写到一个IStream中
 然后转换成字符串供C#代码使用,不过实现方式比较麻烦
  
 比较简单的方法还是使用ole32.dll提供的函数
 重建流,但这需要预先设定流的长度,如
  

UCOMIStream stream = null;
  
 CreateStreamOnHGlobal(Marshal.AllocHGlobal(
4096), trueout stream);
  
 IPersistStreamInit persistentStreamInit 
=
   (IPersistStreamInit)WebBrowser.Document;
  
 persistentStreamInit.Save(stream, 
0);
 persistentStreamInit 
= null;
  
 IntPtr pStr;
  
 GetHGlobalFromStream(stream, 
out pStr);
  
 
return Marshal.PtrToStringAnsi(pStr);


  
 然后使用GetHGlobalFromStream函数和
 Marshal.PtrToStringAnsi将流转换为字符串
 另外一种方法是自行实现一个支持IStream接口的类
 通过流的方式灵活完成读取操作,我比较喜欢这种 

 using(MemoryStream stream = new MemoryStream())


 
{
   ComStreamAdapter adapter 
= new ComStreamAdapter(stream);
  
   IPersistStreamInit persistentStreamInit 
=
     (IPersistStreamInit)WebBrowser.Document;
  
   persistentStreamInit.Save(adapter, 
0);
  
   stream.Seek(
0, SeekOrigin.Begin);
  
   
using(StreamReader reader = new StreamReader(stream))
   
{
     
return reader.ReadToEnd();
   }

 }


  
 这里的ComStreamAdapter是一个使用了adapter模式的类
 将普通的System.IO.Stream转换为IStream支持的类
  
   

  public class ComStreamAdapter : UCOMIStream
     
{
       
private Stream _stream;


  
       
public ComStreamAdapter(Stream stream)
       
{
         _stream 
= stream;
       }

  
       
UCOMIStream Members

  
       
public void Stat(out STATSTG pstatstg, int grfStatFlag)
       
{
         pstatstg 
= new STATSTG ();
       }

  
       
#endregion
     }



posted @ 2004-07-08 09:31 Flier Lu 阅读(8056) 评论(3) 编辑

MS.Net CLR扩展PE结构分析

http://www.blogcn.com/user8/flier_lu/index.html?id=1682030&run=.06FE977

    2001-2002年时曾针对 .NET Framework 1.0 版本的文件格式做过一些分析,整理成文章后发表在绿盟月刊和程序员杂志增刊上,转贴过来以备引用。相关代码可以从 Delphi JEDI - JCL项目的最新 snapshot 包中获取。过段时间有空再继续分析一下 1.1 和 2.0 版本的格式吧,呵呵

 MS.Net CLR扩展PE结构分析 (1)
 MS.Net CLR扩展PE结构分析 (2)
 MS.Net CLR扩展PE结构分析 (3)
 MS.Net CLR扩展PE结构分析 (4)

posted @ 2004-07-08 09:27 Flier Lu 阅读(710) 评论(2) 编辑

.Net平台下CLR程序载入原理分析 [草稿]

http://www.blogcn.com/user8/flier_lu/index.html?id=1369910&run=.0C124A1

发信人: flier (小海->找啊找工作 :)), 信区: DotNET
 标  题: .Net平台下CLR程序载入原理分析(草稿)
 发信站: BBS 水木清华站 (Wed Mar 13 02:08:04 2002)
  
 注意:本系列文章在水木清华BBS(smth.org)之.Net版首发,
      转载请保留以上信息,发表请与作者联系

   与传统的Win32可执行程序中的本机代码(Native Code)不同,
 微软推出的.Net架构中,可执行程序的代码是以类似Java Byte Code的
 IL (Intermediate Language)伪代码形式存在的。在.Net可执行程序载入后,
 IL代码由CLR (Common Language Runtime)从可执行文件中取出,
 交由JIT (Just-In-Time)编译器,根据相应的元数据(Metadata),
 实时编译成本机代码后执行。

   因此,一个CLR可执行程序的启动过程可以分为三个步骤。
   首先,Windows的可执行程序载入器(OS Loader)载入
 PE (Portable Executable)结构的可执行文件映像(PE Image),
 将执行权传递给CLR的支持库中的Unmanaged Code。
   其次,启动或使用现有的CLR引擎,建立新的应用域(Application Domain),
 将配件(Assembly)载入到此应用域中。
   最后,将执行权从Unmanaged Code传递给Managed Code,执行配件的代码。

   下面我将详细说明以上步骤。
   自从Win95发布以来,可执行程序的PE结构就没有发生大的改动。
 此次.Net平台发布,也只是利用了PE结构中现有的预留空间,
 以保持PE结构的稳定,最大程度保持向后兼容。
 (详情请参看笔者《MS.Net平台下CLR 扩展PE结构分析》一文)
   CLR程序在编译后,将可执行程序入口直接以一个间接跳转指令
 指向mscoree.lib中的_CorExeMain函数(DLL将入口指向_CorDllMain函数)。
 因此CLR可执行程序在被OS Loader载入后,将由_CorExeMain函数处理CLR引擎
 启动事宜。此函数将启动或使用一个现有的CLR Host来加载IL代码。

   常见的CLR Host有ASP.Net、IE、Shell、数据库引擎等等,
 他们的作用是启动一个CLR实例,管理在此CLR实例中运行的CLR程序。
   我们接着来看一看一个CLR Host是如何实际运作的。
   CLR作为一个引擎,在同一台计算机上是可以存在多个版本的,
 不同版本之间可以通过配置良好共存。在
 %windir%Microsoft.NETFramework
 (%windir%表示Windows系统目录所在位置)目录下,
 我们可以看到以版本号为目录名的多个CLR版本,
 如%windir%Microsoft.NETFrameworkv1.0.3705等等,
 也可以在注册表的
 HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFrameworkpolicyv1.0
 键下查看详细的版本兼容性.Name是Build号,Value是兼容的Build号.
 而每一个CLR版本又分为Server和Workstation两类运行库,
 我们等会讲创建CLR时会详细谈到.
   CLR Host在启动CLR之前,必须通过一个startup shim的库进行操作,
 实际上就是mscoree.dll,他提供了版本无关的操作函数,以及启动CLR所需
 的支持,如CorBindToRuntimeEx函数.
   CLR Host通过shim的支持库,将CLR引擎载入到进程中.具体函数如下
 STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion,
   LPCWSTR pwszBuildFlavor, DWORD startupFlags,
   REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);
   参数pwszVersion指定要载入的CLR版本号,注意必须在前面带一个小写的"v",
 如"v1.0.3705",可以通过查阅前面提到的注册表键,获取当前系统安装的不同CLR
 版本情况,或指定固定的CLR版本.也可以传递NULL给这个参数,系统将自动选择最新
 版本的CLR载入.
   参数pwszBuildFlavor则指定载入的CLR类型,"srv"和"wks".
 前者适用于多处理器的计算机,能够利用多CPU提高并行性能.对单CPU
 系统而言,无论指定哪种类型都会载入"wks",传递NULL也是如此.
   参数startupFlags是一个组合参数.由多个标志位组成.
   STARTUP_CONCURRENT_GC标志指定是否使用并发的GC(Garbage Collection)
 机制,使用并发GC能够提高系统的用户界面相应效率,适合窗口界面使用较多的程序.
 但并发GC会因为无谓的线程上下文(Thread Context)切换损失效率.
   以下三个参数用于指定配件载入优化策略.我们等会详细讨论.
   STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN     = 0x1 << 1,
   STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN      = 0x2 << 1,
   STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3 << 1,
   接着的三个参数用于获取ICorRuntimeHost接口.
   实际调用实例如下.
 CComPtr<ICorRuntimeHost> spHost;
 CHECK(CorBindToRuntimeEx(NULL, L"wks",
   STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
   CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void **)&spHost));
   这行代码载入最高版本CLR的wks类型运行库,为单应用域进行优化并使用并发GC机制.
   前面提到了配件载入优化策略,要理解这个概念,我们必须先了解应用域的概念.
 传统Win程序中,资源的分配管理单位是进程,操作系统以进程边界将应用程序实例隔离开,

 单个进程的崩溃不会对其他进程产生直接影响,进程也不能直接使用其他进程的资源.
 进程很好,但使用进程的代价太大,为此Win32引入了线程的概念.同一进程中的线程能够
 共享资源,线程管理和切换的代价也远远小于进程.但因为在同一进程中,线程的崩溃会直接

 影响到其他线程的运行,也无法约束线程间数据的直接访问等等.
   为此,CLR中引入了Application Domain应用域的概念.应用域是介于进程和线程
 之间的一种逻辑上的概念.他既有线程轻巧,管理切换快捷的优点,也有进程在稳定性方面
 的优点,单个应用域的崩溃不会直接影响到同一进程中的其他应用域,应用域也无法直接
 访问同一进程中的其他应用域的资源,这方面和进程完全相同.
   而CLR的管理就是完全面向应用域一级.CLR不能卸载(Unload)某个类型或配件,
 必须以应用域为单位启动/停止代码,获取/释放资源.
   CLR在执行一个配件时,会新建一个应用域,将此配件放入新的应用域.如果多个应用域
 同时使用到一个配件,就要涉及到前面提到的配件载入优化策略了.最简单的方法是使用
 STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN标志,每个应用域拥有一份独立的
 配件的镜像,这样速度最快,管理最方便,但占用内存较多.相对的是所有应用域共享一份
 配件的镜像,(使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN标志)
 这样节约内存,但在此配件中存在静态变量等数据时,因为要保证每个应用域有独立的数据
 ,
 所以会一定程度上影响效率.折中的方案是使用
 (使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST标志)
 此时,只有那些有Strong Name的配件才会被多个应用域共享.
   这里又涉及到一个概念Strong Name.他是一个配件的身份证明,他由配件的
 名字/版本/culture以及数字签名等组成.在配件发布时用以区别不同版本.
 也在安全/版本控制等方面起到重要作用,以后有机会会专门讲解.暂且跳过.
   获取了ICorRuntimeHost接口的指针后,我们可以以此指针取得当前/缺省应用域,
 并可枚举CLR引擎实例中所有的应用域.
   当前应用域是指当前线程运行时所在应用域.注意线程属于进程,但不属于某个应用域,
 一个线程可以跨应用域操作.可以通过线程类的Thread.GetDomain获取线程当前所在
 应用域.
   缺省应用域是CLR引擎载入后自动建立的应用域,其生命期贯串CLR引擎的使用期,
 一般在此应用域中执行CLR Host的Managed Code端管理代码,而不执行用户代码.
   接下来,是载入用户代码所在配件的时候了.方法有两种,一是接着使用完全的
 Native Code或者说Unmanaged Code通过BCL的COM包装接口操作;二是将操作
 移交给Managed Code部分的CLR Host代码执行.后者实现简单,速度较快.
 笔者以后将单独以一篇文章介绍CLR Host的Managed Code部分代码的设计编写.
 这里将简要介绍第一种实现.
   以Unmanaged Code完整实现CLR Host虽然麻烦,但功能更加强大.但因为操作中
 要不断在Unmanaged/Managed Code之间切换,效率受到一定影响.(切换的调用
 是通过IDispatch接口实现,本身效率就很低,加上CCW(COM Callable Wrapper)
 的包装,低于直接使用Managed Code的效率.
   以Unmanaged Code调用配件,必须知道配件的部分信息,如配件的名字,
 要调用的类的名字,要调用的函数等等.可以指定参数的方式来使用,也可以通过
 PE映像中CLR头的IL入口EntryPointToken以及Metadata的信息来获取
 (详情请参看笔者《MS.Net平台下CLR 扩展PE结构分析》一文Metadata篇)
 这里为了示例简单,采用参数传递方式.
   if(argc < 4)
   {
     cerr << "Usage: " << argv[0] << " <Assembly Name> <Class Name> <Main Funct
 ion Name> <Parameters>" << endl;
   }
   else
   {
     _bstr_t bstrAssemblyName(argv[1]),
             bstrClassName(argv[2]),
             bstrMainFuncName(argv[3]);
     ...
   }
   例子中以命令行方式传递配件/类/函数名信息.
   spUnk = NULL;
   CHECK(spHost->GetDefaultDomain(&spUnk));
   spAppDomain = spUnk; spUnk = NULL;
   首先获取缺省应用域,在此应用域中创建指定配件中指定类.这里为例子简洁
 直接在缺省应用域中载入配件,实际开发中应避免这种方式,而采用建立新应用域
 的方式来载入配件.关于新建应用域以及建立时的配置,设计问题较多,以后再专门
 写文章详述,这里略去.
   _ObjectHandlePtr spObj = spAppDomain->CreateInstance(bstrAssemblyName, bstrC
 lassName);
   CComPtr<IDispatch> spDisp = spObj->Unwrap().pdispVal;
   建立配件中类实例后,取得一个_ObjectHandlePtr类型值,
 通过Unwrap()调用获取IDispatch接口,然后就可以通过此接口,以传统的COM
 方式控制CLR中的类.
     int ArgCount = argc-4;
     DISPID dispid;
     LPOLESTR rgszName = bstrMainFuncName;
     VARIANTARG *pArgs = new VARIANTARG[ArgCount];
     for(int i=0; i<ArgCount; i++)
     {
       VariantInit(&pArgs[i]);
       pArgs[i].vt = VT_BSTR;
       pArgs[i].bstrVal = _bstr_t(argv[4+i]);
     }
     DISPPARAMS dispparamsNoArgs = {pArgs, NULL, ArgCount, 0};
     CHECK(spDisp->GetIDsOfNames(IID_NULL, &rgszName, 1, LOCALE_SYSTEM_DEFAULT,
  &dispid));
     CHECK(spDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_MET
 HOD,
       &dispparamsNoArgs, NULL, NULL, NULL));
     delete[] pArgs;
   以上例子代码,将命令行传入参数放入参数数组,以IDispatch->Invoke调用指定名字
 的方法.其后台操作均由CCW进行传递.如果要直接运行一个Assembly,可以使用
 IAppDomain.ExecuteAssembly更加便捷.如
   CHECK(spAppDomain->ExecuteAssembly(bstrAssemblyName, NULL));
   至此,一个简单但完整的CLR Host程序就完成了,他可以以完全的Unmanaged Code
 启动CLR引擎,载入指定Assembly,以指定参数运行指定的类的方法.
   下面是完整的示例程序,VC7编译通过,VC6修改一下应该也没有问题.
 hello.cs
 using System;
 namespace Hello
 {
         /// <summary>
         /// Summary description for Class1.
         /// </summary>
         public class Hello
         {
                 public void SayHello(string Name)
                 {
                         Console.WriteLine("Hello "+Name);
                 }
         }
 }
 ClrHost.cpp
 // CLRHost.cpp : Defines the entry point for the console application.
 //
 #include "stdafx.h"
 #include <mscoree.h>
 #import <mscorlib.tlb> rename("ReportEvent", "ReportEvent_")
 using namespace mscorlib;
 #include <assert.h>
 #include <string>
 #include <memory>
 #include <iostream>
 using namespace std;
 typedef HRESULT (__stdcall * GetInfoFunc)(LPWSTR pbuffer, DWORD cchBuffer, DWO
 RD* dwlength);
 #define CHECK(v) 
   if(FAILED(v)) 
     cerr << "COM function call failed - " << GetLastError() << " at " << __FIL
 E__ << ", " << __LINE__ << endl;
 wstring GetInfo(GetInfoFunc Func)
 {
   wchar_t szBuf[MAX_PATH];
   DWORD dwLength;
   if(SUCCEEDED((Func)(szBuf, MAX_PATH, &dwLength)))
     return wstring(szBuf, dwLength);
   else
     return NULL;
 }
 int _tmain(int argc, _TCHAR* argv[])
 {
   CComPtr<ICorRuntimeHost> spHost;
   CHECK(CorBindToRuntimeEx(NULL, L"wks",
     STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
     CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void **)&spHost));
   wcout << L"Load CLR " << GetInfo(GetCORVersion)
         << L" from " << GetInfo(GetCORSystemDirectory)
         << endl;
   CHECK(spHost->Start());
   CComPtr<IUnknown> spUnk;
   CComPtr<_AppDomain> spAppDomain;
 #ifdef _DEBUG
   CHECK(spHost->GetDefaultDomain(&spUnk));
   spAppDomain = spUnk; spUnk = NULL;
   wcout << L"Default AppDomain is " << (wchar_t *)spAppDomain->GetFriendlyName
 () << endl;
   CHECK(spHost->CurrentDomain(&spUnk));
   spAppDomain = spUnk; spUnk = NULL;
   wcout << L"Current AppDomain is " << (wchar_t *)spAppDomain->GetFriendlyName
 () << endl;
   HDOMAINENUM hEnum;
   CHECK(spHost->EnumDomains(&hEnum));
   spUnk = NULL;
   while(spHost->NextDomain(hEnum, &spUnk) != S_FALSE)
   {
     spAppDomain = spUnk; spUnk = NULL;
     wcout << (wchar_t *)spAppDomain->GetFriendlyName() << endl;
   }
   CHECK(spHost->CloseEnum(hEnum));
 #endif // _DEBUG
   if((argc < 2) || (argc == 3))
   {
     cerr << "Usage: " << argv[0] << " <Assembly Name> <Class Name> <Main Funct
 ion Name> <Parameters>" << endl;
   }
   else
   {
     spUnk = NULL;
     CHECK(spHost->GetDefaultDomain(&spUnk));
     spAppDomain = spUnk; spUnk = NULL;
     _bstr_t bstrAssemblyName(argv[1]);
     if(argc == 2)
     {
       CHECK(spAppDomain->ExecuteAssembly(bstrAssemblyName, NULL));
     }
     else
     {
       _bstr_t bstrClassName(argv[2]),
               bstrMainFuncName(argv[3]);
       _ObjectHandlePtr spObj = spAppDomain->CreateInstance(bstrAssemblyName, b
 strClassName);
       CComPtr<IDispatch> spDisp = spObj->Unwrap().pdispVal;
       DISPID dispid;
       LPOLESTR rgszName = bstrMainFuncName;
       DISPPARAMS dispparamsArgs = {NULL, NULL, 0, 0};
       int ArgCount = argc-4;
       if(ArgCount > 0)
       {
         dispparamsArgs.cArgs = ArgCount;
         dispparamsArgs.rgvarg = new VARIANTARG[ArgCount];
         VARIANTARG *pArgs = dispparamsArgs.rgvarg;
         for(int i=0; i<ArgCount; i++)
         {
           VariantInit(&pArgs[i]);
           pArgs[i].vt = VT_BSTR;
           pArgs[i].bstrVal = _bstr_t(argv[4+i]);
         }
       }
       CHECK(spDisp->GetIDsOfNames(IID_NULL, &rgszName, 1, LOCALE_SYSTEM_DEFAUL
 T, &dispid));
       CHECK(spDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_M
 ETHOD,
         &dispparamsArgs, NULL, NULL, NULL));
       delete[] dispparamsArgs.rgvarg;
     }
   }
   CHECK(spHost->Stop());
         return 0;
 }

posted @ 2004-07-08 09:26 Flier Lu 阅读(1494) 评论(3) 编辑