Da Vinci's CyberSpace

手把青秧插满田, 低头便见水中天; 心地清净方为道, 退步原来是向前.
posts - 17, comments - 8, trackbacks - 0, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

2008年7月8日

    任何时候系统内存资源相对磁盘空间来说都是相形见拙的。因为虚拟内存机制,使我们可以有相对丰富的地址资源(通常32bit的虚拟地址,可以有4G的寻址 空间),而这些资源对物理内存来说一般情况是总是绰绰有余的。所以在现代操作系统中,总是在相对紧张时使用一些策略,如FIFO、LRU等将物理内存的一 些页面置入相对便宜的磁盘空间资源中。一般的UNIX系统,独立使用一个分区,即swap partition。而这方面Windows只是使用普通的文 件,通常命名为pagefile.sys,位于各分区的根目录中。由于受到用于pagefile的PTE的限制(PTE中使用4bit来识别操作的 pagefile),所以Windows最多可以支持16个pagefile.sys。

    从上描述,pagefile.sys本身 就是一个比较特殊的文件,根据系统的情况它的大小是可扩展的,通常我们可以使用“控制面板”的“系统”小Applet来设置。由于其特殊性, Windows在启动阶段会对每个pagefile.sys建立相应的FILE_OBJECT,并且设置SharedRead字段为False,而且在 System进程,每个FILE_OBJECT都分别有一个句柄指向,这样即只允许系统自身对其操作,避免用户对其进行删除等误操作。

     为了对pagefile.sys进行管理,Windows中有一个长度为16的数组,用于对pagefile.sys的组织。每个成员分别对应一个 pagefile。这个数组由系统变量MmPagingFile指向,每个成员都是一个指向MMPAGING_FILE的结构,这个结构有如下的格式:

    +0x000 Size             : Uint4B
    +0x004 MaximumSize      : Uint4B
    +0x008 MinimumSize      : Uint4B
    +0x00c FreeSpace        : Uint4B
    +0x010 CurrentUsage     : Uint4B
    +0x014 PeakUsage        : Uint4B
    +0x018 Hint             : Uint4B
    +0x01c HighestPage      : Uint4B
    +0x020 Entry            : [2] Ptr32 _MMMOD_WRITER_MDL_ENTRY
    +0x028 Bitmap           : Ptr32 _RTL_BITMAP
    +0x02c File             : Ptr32 _FILE_OBJECT
    +0x030 PageFileName     : _UNICODE_STRING
    +0x038 PageFileNumber   : Uint4B
    +0x03c Extended         : UChar
    +0x03d HintSetToZero    : UChar
    +0x03e BootPartition    : UChar
    +0x040 FileHandle       : Ptr32 Void

通 过这个结构,我们可以很容易的得到相应pagefile的使用情况(MaximumSize、MinimumSize、FreeSpace、 CurrentUsage、PeakUsage,请参阅windbg的!vm命令),其对应的FILE_OBJECT等。另外通过FILE_OBJECT 的DeviceObject与Vpb字段,我们就可知道这个pagefile所处的分区及分区使用的文件系统等等信息。我们来详细介绍一下Bitmap成 员。

    Bitmap是一个RTL_BITMAP的结构,其定义在ntddk.h中:

    typedef struct _RTL_BITMAP {
        ULONG SizeOfBitMap;                     // Number of bits in bit map
        PULONG Buffer;                          // Pointer to the bit map itself
    } RTL_BITMAP;

     与页框数据库(Pfn Database)与虚拟内存(x86平台PAGE_SIZE 4k)一样,Windows也将pagefile分割成4K一块块 的大小,称为一页,每页由Bitmap对应的1 bit指定状态。1为占用,0为空闲。通过使用RtlFindClearBits或是 RtlFindClearBitsAndSet等函数对Bitmap进行操作,寻找这些文件的未用页面。虽然Bitmap指明占用状态时,Windows 常以4k为单位,但为了提高性能,Windows在一次写pagefile的单位通常为64k(MmModifiedWriteClusterSize个 页)。还有MMMOD_WRITER_MDL_ENTRY,等我下面提及相关内容时再加以说明。

    先用windbg来消化一下上面的讨论:

    kd> dd MmPagingFile l 10  //从输出结果可以看出我的机子上设了两个pagefile。
    80547020  80d2af80 feec1548 00000000 00000000
    80547030  00000000 00000000 00000000 00000000
    80547040  00000000 00000000 00000000 00000000
    80547050  00000000 00000000 00000000 00000000
    kd> dd @$p l 40   //第一个pagefile的情况。
    80d2af80  00006400 0000c800 00006400 00000c38
    80d2af90  000057c7 000057c7 00000000 00000000
    80d2afa0  feea1cb8 feea1c18 fecbb000 feddc428
            .
            .
            .

    kd> dd feddc428 l 4  //从上面给出的MMPAGING_FILE,很容易得到file object(Offset 0x2c)。
    feddc428  00700005 80ecf2f0 80ecf268 fee66c10

    kd> !devobj 80ecf2f0   //aFILE_OBJECT的结构在ntddk.h中给出,其第三个dword就是DEVICE_OBJECT。
    Device object (80ecf2f0) is for:
     HarddiskVolume2 \Driver\Ftdisk DriverObject 80d97030
    Current Irp 00000000 RefCount 1316 Type 00000007 Flags 00001150
    Vpb 80ecf268 Dacl e13d1484 DevExt 80ecf3a8 DevObjExt 80ecf490 Dope 80ecf210 DevNode 80d95bd0 
    ExtensionFlags (0000000000)  
    AttachedDevice (Upper) 80d954b8 \Driver\VolSnap
    Device queue is not busy.

    另外FILE_OBJECT的第四个dword(fee66c10)就是VPB结构,你使用!vpb分析分析,限于篇幅,我就不在这儿列出了。

    通过上面windbg的分析,我们已经基本上对pagefile有了一定的了解了,下面转入内存子系列与IO子系统(调用FSD)对pagefile的组织管理。

     通常情况下,对于进程可见的永远是虚拟地址,存取某个虚拟地址,对于不存在的地址(对于X86,即其PTE的P位为0),通过触发硬件中断(X86为 int e),由软件来对这些PTE进行解析,譬如原型PTE(我在《探究Windows 2000/XP原型PTE》中详细介绍),或是过渡PTE (Transition PTE,某些页面由于进程工作集修整等原因,成为可被使用的页面,但这些页面的内容当前对这些进程仍有效,可随时重新使用,所以 Windows使用Transition这个术语区别于纯粹的Free或Zeroed列表,我在《解析Winndows 2000/XP物理内存管理》中 提及PFN列表)等,而对于Page File,实际上也有一个对应的pte指向相应pagefile.sys,完成解析工作 (MiResolvePageFileFault),处理页面错误(通过IoPageRead,下面会介绍)。

    所以在继续讨论之前我们来说明一下Pagefile PTE,它的格式如下:

    Valid            : Pos 0, 1 Bit
    PageFileLow      : Pos 1, 4 Bits
    Protection       : Pos 5, 5 Bits
    Prototype        : Pos 10, 1 Bit
    Transition       : Pos 11, 1 Bit
    PageFileHigh     : Pos 12, 20 Bits

     对于Prototype PTE与Transition PTE,总有1bit用于识别相应的PTE,如上的Prototype字段,但对于 PageFile PTE,却没有对应的识别bit,实际上MiDispatchFault(由KiTrap0E调用),是在解析完 Prototype PTE(MiResolveProtoPteFault)、Transition PTE (MiResolveTransitionFault)、还有MiResolveDemandZeroFault后,才调用 MiResolvePageFileFault的,当然在MiResolveProtoPteFault处理过程中也是最后调用 MiResolvePageFileFault的。

    假设我们存取一个当前驻留在pagefile中的页面,通过 MiDispatchFault,控制权转到MiResolvePageFileFault后,他会根据PTE的PageFileLow来索引 MMPAGING_FILE数组,即判断这一页面位于哪个pagefile.sys中,因为PageFileLow为4个bit,所以Windows最多 可以支持16个pagefile.sys。这样内存子系统根据这个索引从MmPagingFile中描述的页文件结构取出这个pagefile的 FILE_OBJECT(上面介绍过)。加上PageFileHigh所指定的pagefile.sys的偏移值, MiResolvePageFileFault通过返回一个值为0xC0033333的特殊NTSTATUS通知MiDispatchFault调用 IoPageRead得到此页面。IoPageRead的原型如下(定义于ntifs.h中):

    NTKERNELAPI
    NTSTATUS
    IoPageRead(
        IN PFILE_OBJECT FileObject,
        IN PMDL MemoryDescriptorList,
        IN PLARGE_INTEGER StartingOffset,
        IN PKEVENT Event,
        OUT PIO_STATUS_BLOCK IoStatusBlock
    ); 

     当然在调用IoPageRead之前,内存管理器必须分配一个物理页面,必要的时候还要调用MiRemoveAnyPage腾出空间,然后调用 MiInitializeReadInProgressPfn,将这一页框置成ReadInProgress状态,然后将IoPageRead所需要的 MDL参数MemoryDescriptorList指向这一页面。MDL的虚拟地址字段也就是IoPageRead读入的页面映射的虚拟地址,也即满足 我们先前假设的页面错误。

    IoPageRead实际上通过Allocate一个IRP,用DIRECT_IO的方式(即我们提供 的MDL),然后设置一个Complete Routine,用于取消页面读取之前的ReadInProgress状态,再通过IoCallDriver 调用IO子系统调用对应的File System Driver(通常由FILE_OBJECT的VPB参数确定),至于FSD是如何读取 pagefile.sys的,这儿不加以讨论,ntifs提供的fastfat的源代码是学习的方向。

    需要指出的是 IoPageRead是一个同步操作,即只有等待页面读完毕以后才可以往下处理。这也是MiDispatchFault只能运行于 DISPATCH_LEVEL IRQL之下的主要原因。IoPageRead通过设备分配的IRP的 IRP_SYNCHRONOUS_PAGING_IO的标志来实现同步的。另外他也设置了IRP_PAGING_IO、IRP_NOCACHE标志,用于 与FSD之间的特殊通信要求。

    由于工作集修整等的需要,通过MiModifiedPageWriter(MPW)线程实行将某些 页面置入pagefile中。MPW使用MMPAGING_FILE结构的_MMMOD_WRITER_MDL_ENTRY类型的Entry进行操作, _MMMOD_WRITER_MDL_ENTRY不仅仅由MiModifiedPageWriter使用,他还要让MiMappedPageWriter 使用(用于Mapped file),所以_MMMOD_WRITER_MDL_ENTRY结构不仅函有MDL成员,还包含Control Area等 等。限于篇幅,我不将其结构列出。MPW通过IoAsynchronousPageRead将页面按前面说的一次 MmModifiedWriteClusterSize个页面写入pagefile中。对于IoAsynchronousPageRead其使用的 IRP flag是IRP_PAGING_IO与IRP_NOCACHE,说明他是异步操作的。这也可从他的名字看出,区别于Windows提供的另一个 相关过程IoSynchronousPageWrite,他是同步的。

    讲到这儿,对于page file的组织管理的一些基本的 印象应该是有的。最后需要指出的一点是,对于IoPageRead不仅仅是对于pagefile的,其也可以针对mapped file,还有 MiModifiedPageWriter,要不是避免死锁也不会区分出MiMappedPageWriter,实际上Windows内部内存管理器对于 pagefile与mappedfile的管理使用是基本相同的,而FSD的处理也只是一点点的区别而已。所以结合我以前介绍的如 Control Area等概念,对mapped file等的理解也是可以参照本文的。还有同样的一句话,错误地方希望得到你的指点。

posted @ 2008-07-08 14:27 Da Vinci 阅读(33) | 评论 (0)编辑

2008年6月23日

C++字符串完全指引之二 —— 字符串封装类


原著:Michael Dunn


原文出处:CodeProject:The Complete Guide to C++ Strings, Part II


引言

因为C语言风格的字符串容易出错且不易管理,黑客们甚至利用可能存在的缓冲区溢出bug把C语言风格的字符串作为攻击目标,所以出现了很多字符串封装 类。不幸的是,在某些场合下我们不知道该使用哪个字符串类,也不知道怎样把一个C风格的字符串转换成一个字符串封装类。
这篇文章将介绍所有在Win32 API, MFC, STL, WTL 和 Visual C++ 运行库中出现的字符串类型。我将描述每一个类的用法,告诉大家怎样创建每一个类的对象以及怎样把一个类转换成其他类。受控字符串和Visual C++ 7中的类两部分是Nish完成的。
为了更好的从这篇文章中受益,你必须要明白不同的字符类型和编码,这些内容我在第一部分中介绍过。

Rule #1 of string classes

  使用cast来实现类型转换是不好的做法,除非有文档明确指出这种转换可以使用。
促使我写这两篇文章的原因是字符串类型转换中经常遇到的一些问题。当我们使用cast把字符串从类型X转换到类型Z的时候,我们不知道为什么代码不能正常 工作。各种各样的字符串类型,尤其是BSTR,几乎没有在任何一个地方的文档中被明确的指出可以用cast来实现类型转换。所以我想一些人可能会使用 cast来实现类型转换并希望这种转换能够正常工作。
除非源字符串是一个被明确指明支持转换操作符的字符串包装类,否则cast不对字符串做任何转换。对常量字符串使用cast不会起到任何作用,所以下面的代码:

void SomeFunc ( LPCWSTR widestr );
main()
{
SomeFunc ( (LPCWSTR) "C:\\foo.txt" ); // WRONG!
}
肯定会失败。它可以被编译,因为cast操作会撤消编译器的类型检查。但是,编译可以通过并不能说明代码是正确的。
在下面的例子中,我将会指明cast在什么时候使用是合法的。
C-style strings and typedefs

正如我在第一部分中提到的,windows APIs 是用TCHARs来定义的,在编译时,它可以根据你是否定义_MBCS或者_UNICODE被编译成MBCS或者Unicode字符。你可以参看第一部分 中对TCHAR的完整描述,这里为了方便,我列出了字符的typedefs

Type Meaning
WCHAR Unicode character (wchar_t)
TCHAR MBCS or Unicode character, depending on preprocessor settings
LPSTR  string of char (char*)
LPCSTR constant string of char (const char*)
LPWSTR  string of WCHAR (WCHAR*)
LPCWSTR  constant string of WCHAR (const WCHAR*)
LPTSTR  string of TCHAR (TCHAR*)
LPCTSTR  constant string of TCHAR (const TCHAR*)

一个增加的字符类型是OLETYPE。它表示自动化接口(如word提供的可以使你操作文档的接口)中使用的字符类型。这种类型一般被定义成 wchar_t,然而如果你定义了OLE2ANSI预处理标记,OLECHAR将会被定义成char类型。我知道现在已经没有理由定义OLE2ANSI (从MFC3以后,微软已经不使用它了),所以从现在起我将把OLECHAR当作Unicode字符。
这里给出你将会看到的一些OLECHAR相关的typedefs:

Type Meaning
OLECHAR Unicode character (wchar_t)
LPOLESTR  string of OLECHAR (OLECHAR*)
LPCOLESTR constant string of OLECHAR (const OLECHAR*)

  还有两个用于包围字符串和字符常量的宏定义,它们可以使同样的代码被用于MBCS和Unicode builds :

Type Meaning
_T(x) Prepends L to the literal in Unicode builds.
OLESTR(x) Prepends L to the literal to make it an LPCOLESTR.

  在文档或例程中,你还会看到好多_T的变体。有四个等价的宏定义,它们是TEXT, _TEXT, __TEXT和__T,它们都起同样的做用。

COM 中的字符串 —— BSTR 和 VARIANT

很多自动化和COM接口使用BSTR来定义字符串。BSTRs中有几个"陷阱",所以这里我用单独的部分来说明它。
BSTR 是 Pascal-style 字符串(字符串长度被明确指出)和C-style字符串(字符串的长度要通过寻找结束符来计算)的混合产物。一个BSTR是一个Unicode字符串,它的长度是预先考虑的,并且它还有一个0字符作为结束标记。下面是一个BSTR的示例:

06 00 00 00 42 00 6F 00 62 00 00 00
--length-- B o b EOS

注意字符串的长度是如何被加到字符串数据中的。长度是DWORD类型的,保存了字符串中包含的字节数,但不包括结束标记。在这个例子中,"Bob"包含 3个Unicode字符(不包括结束符),总共6个字节。字符串的长度被预先存储好,以便当一个BSTR在进程或者计算机之间被传递时,COM库知道多少 数据需要传送。(另一方面,一个BSTR能够存储任意数据块,而不仅仅是字符,它还可以包含嵌入在数据中的0字符。然而,由于这篇文章的目的,我将不考虑 那些情况)。
在 C++ 中,一个 BSTR 实际上就是一个指向字符串中第一个字符的指针。它的定义如下:

BSTR bstr = NULL;
bstr = SysAllocString ( L"Hi Bob!" );
if ( NULL == bstr )
// out of memory error
// Use bstr here...
SysFreeString ( bstr );
自然的,各种各样的BSTR封装类为你实现内存管理。
另外一个用在自动化接口中的变量类型是VARIANT。它被用来在无类型(typeless)语言,如Jscript和VBScript,来传递数 据。一个VARIANT可能含有很多不同类型的数据,例如long和IDispatch*。当一个VARIANT包含一个字符串,字符串被存成一个 BSTR。当我后面讲到VARIANT封装类时,我会对VARIANT多些介绍。

字符串封装类

到目前为止,我已经介绍了各种各样的字符串。下面,我将说明封装类。对于每个封装类,我将展示怎样创建一个对象及怎样把它转换成一个C语言风格的字符 串指针。C语言风格的字符串指针对于API的调用,或者创建一个不同的字符串类对象经常是必需的。我不会介绍字符串类提供的其他操作,比如排序和比较。
重复一遍,除非你确切的明白结果代码将会做什么,否则不要盲目地使用cast来实现类型转换。

CRT提供的类

_bstr_t
_bstr_t是一个对BSTR的完整封装类,实际上它隐藏了底层的BSTR。它提供各种构造函数和操作符来访问底层的C语言风格的字符串。然而, _bstr_t却没有访问BSTR本身的操作符,所以一个_bstr_t类型的字符串不能被作为输出参数传给一个COM方法。如果你需要一个BSTR*参 数,使用ATL类CComBSTR是比较容易的方式。
一个_bstr_t字符串能够传给一个接收参数类型为BSTR的函数,只是因为下列3个条件同时满足。首先,_bstr_t有一个向wchar_t* 转换的转换函数;其次,对编译器而言,因为BSTR的定义,wchar_t*和BSTR有同样的含义;第三,_bstr_t内部含有的wchar_t*指 向一片按BSTR的形式存储数据的内存。所以,即使没有文档说明,_bstr_t可以转换成BSTR,这种转换仍然可以正常进行。
// Constructing
_bstr_t bs1 = "char string"; // construct from a LPCSTR
_bstr_t bs2 = L"wide char string"; // construct from a LPCWSTR
_bstr_t bs3 = bs1; // copy from another _bstr_t
_variant_t v = "Bob";
_bstr_t bs4 = v; // construct from a _variant_t that has a string

// Extracting data
LPCSTR psz1 = bs1; // automatically converts to MBCS string
LPCSTR psz2 = (LPCSTR) bs1; // cast OK, same as previous line
LPCWSTR pwsz1 = bs1; // returns the internal Unicode string
LPCWSTR pwsz2 = (LPCWSTR) bs1; // cast OK, same as previous line
BSTR bstr = bs1.copy(); // copies bs1, returns it as a BSTR

// ...
SysFreeString ( bstr );
注意_bstr_t也提供char*和wchar_t*之间的转换操作符。这是一个值得怀疑的设计,因为即使它们是非常量字符串指针,你也一定不能使用这些指针去修改它们指向的缓冲区的内容,因为那将破坏内部的BSTR结构。

_variant_t
_variant_t是一个对VARIANT的完整封装,它提供很多构造函数和转换函数来操作一个VARIANT可能包含的大量的数据类型。这里,我将只介绍与字符串有关的操作。
// Constructing
_variant_t v1 = "char string"; // construct from a LPCSTR
_variant_t v2 = L"wide char string"; // construct from a LPCWSTR
_bstr_t bs1 = "Bob";
_variant_t v3 = bs1; // copy from a _bstr_t object

// Extracting data
_bstr_t bs2 = v1; // extract BSTR from the VARIANT
_bstr_t bs3 = (_bstr_t) v1; // cast OK, same as previous line
注意
如果类型转换不能被执行,_variant_t方法能够抛出异常,所以应该准备捕获_com_error异常。

还需要注意的是
没有从一个_variant_t变量到一个MBCS字符串的直接转换。你需要创建一个临时的_bstr_t变量,使用提供Unicode到MBCS转换的另一个字符串类或者使用一个ATL转换宏。
不像_bstr_t,一个_variant_t变量可以被直接作为参数传递给一个COM方法。_variant_t
继承自VARIANT类型,所以传递一个_variant_t来代替VARIANT变量是C++语言所允许的。

STL 类
STL只有一个字符串类,basic_string。一个basic_string管理一个以0做结束符的字符串数组。字符的类型是 basic_string模般的参数。总的来说,一个basic_string类型的变量应该被当作不透明的对象。你可以得到一个指向内部缓冲区的只读指 针,但是任何写操作必须使用basic_string的操作符和方法。
basic_string有两个预定义的类型:包含char的string类型和包含wchar_t的wstring类型。这里没有内置的包含TCHAR的类型,但是你可以使用下面列出的代码来实现。
// Specializations
typedef basic_string tstring; // string of TCHARs

// Constructing
string str = "char string"; // construct from a LPCSTR
wstring wstr = L"wide char string"; // construct from a LPCWSTR
tstring tstr = _T("TCHAR string"); // construct from a LPCTSTR

// Extracting data
LPCSTR psz = str.c_str(); // read-only pointer to str''s buffer
LPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr''s buffer
LPCTSTR ptsz = tstr.c_str(); // read-only pointer to tstr''s buffer
不像_bstr_t,一个basic_string变量不能在字符集之间直接转换。然而,你可以传递由c_str()返回的指针给另外一个类的构造函数(如果这个类的构造函数接受这种字符类型)。例如:
// Example, construct _bstr_t from basic_string
_bstr_t bs1 = str.c_str(); // construct a _bstr_t from a LPCSTR
_bstr_t bs2 = wstr.c_str(); // construct a _bstr_t from a LPCWSTR
ATL 类

CComBSTR
CComBSTR 是 ATL 中的 BSTR 封装类,它在某些情况下比_bstr_t有用的多。最引人注意的是CComBSTR允许访问底层的BSTR,这意味着你可以传递一个CComBSTR对象 给COM的方法。CComBSTR对象能够替你自动的管理BSTR的内存。例如,假设你想调用下面这个接口的方法:
// Sample interface:
struct IStuff : public IUnknown
{
// Boilerplate COM stuff omitted...
STDMETHOD(SetText)(BSTR bsText);
STDMETHOD(GetText)(BSTR* pbsText);
};
CComBSTR有一个操作符--BSTR方法,所以它能直接被传给SetText()函数。还有另外一个操作--&,这个操作符返回一个 BSTR*。所以,你可以对一个CComBSTR对象使用&操作符,然后把它传给需要BSTR*参数的函数。
CComBSTR bs1;
CComBSTR bs2 = "new text";

pStuff->GetText ( &bs1 ); // ok, takes address of internal BSTR
pStuff->SetText ( bs2 ); // ok, calls BSTR converter
pStuff->SetText ( (BSTR) bs2 ); // cast ok, same as previous line
CComBSTR有和_bstr_t相似的构造函数,然而却没有内置的向MBCS字符串转换的函数。因此,你需要使用一个ATL转换宏。
// Constructing
CComBSTR bs1 = "char string"; // construct from a LPCSTR
CComBSTR bs2 = L"wide char string"; // construct from a LPCWSTR
CComBSTR bs3 = bs1; // copy from another CComBSTR
CComBSTR bs4;

bs4.LoadString ( IDS_SOME_STR ); // load string from string table
// Extracting data
BSTR bstr1 = bs1; // returns internal BSTR, but don''t modify it!
BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous line
BSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTR
BSTR bstr4;
bstr4 = bs1.Detach(); // bs1 no longer manages its BSTR
// ...
SysFreeString ( bstr3 );
SysFreeString ( bstr4 );
注意在上个例子中使用了Detach()方法。调用这个方法后,CComBSTR对象不再管理它的BSTR字符串或者说它对应的内存。这就是bstr4需要调用SysFreeString()的原因。
做一个补充说明:重载的&操作符意味着在一些STL容器中你不能直接使用CComBSTR变量,比如list。容器要求&操作符返回 一个指向容器包含的类的指针,但是对CComBSTR变量使用&操作符返回的是BSTR*,而不是CComBSTR*。然而,有一个ATL类可以 解决这个问题,这个类是CAdapt。例如,你可以这样声明一个CComBSTR的list:
std::list< CAdapt<CComBSTR> > bstr_list;

  CAdapt提供容器所需要的操作符,但这些操作符对你的代码是透明的。你可以把一个bstr_list当作一个CComBSTR的list来使用。

CComVariant
CComVariant是VARIANT的封装类。然而,不像_variant_t,在CComVariant中VARIANT没有被隐藏。事实上你 需要直接访问VARIANT的成员。CComVariant提供了很多构造函数来对VARIANT能够包含的多种类型进行处理。这里,我将只介绍和字符串 相关的操作。

// Constructing
CComVariant v1 = "char string"; // construct from a LPCSTR
CComVariant v2 = L"wide char string"; // construct from a LPCWSTR
CComBSTR bs1 = "BSTR bob";
CComVariant v3 = (BSTR) bs1; // copy from a BSTR

// Extracting data
CComBSTR bs2 = v1.bstrVal; // extract BSTR from the VARIANT
不像_variant_t,这里没有提供针对VARIANT包含的各种类型的转换操作符。正如上面介绍的,你必须直接访问VARIANT的成员并且确 保这个VARIANT变量保存着你期望的类型。如果你需要把一个CComVariant类型的数据转换成一个BSTR类型的数据,你可以调用 ChangeType()方法。
CComVariant v4 = ... // Init v4 from somewhere
CComBSTR bs3;

if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) ))
bs3 = v4.bstrVal;
像_variant_t一样,CComVariant也没有提供向MBCS字符串转换的转换操作。你需要创建一个_bstr_t类型的中间变量,使用提供从Unicode到MBCS转换的另一个字符串类,或者使用一个ATL的转换宏。

ATL转换宏

ATL:转换宏是各种字符编码之间进行转换的一种很方便的方式,在函数调用时,它们显得非常有用。ATL转换宏的名称是根据下面的模式来命名的[源类 型]2[新类型]或者[源类型]2C[新类型]。据有第二种形式的名字的宏的转换结果是常量指针(对应名字中的"C")。各种类型的简称如下:
A: MBCS string, char* (A for ANSI)
W: Unicode string, wchar_t* (W for wide)
T: TCHAR string, TCHAR*
OLE: OLECHAR string, OLECHAR* (in practice, equivalent to W)
BSTR: BSTR (used as the destination type only)

  所以,W2A()宏把一个Unicode字符串转换成一个MBCS字符串。T2CW()宏把一个TCHAR字符串转转成一个Unicode字符串常量。
为了使用这些宏,需要先包含atlconv.h头文件。你甚至可以在非ATL工程中包含这个头文件来使用其中定义的宏,因为这个头文件独立于ATL中 的其他部分,不需要一个_Module全局变量。当你在一个函数中使用转换宏时,需要把USES_CONVERSION宏放在函数的开头。它定义了转换宏 所需的一些局部变量。
当转换的目的类型是除了BSTR以外的其他类型时,被转换的字符串是存在栈中的。所以,如果你想让字符串的生命周期比当前的函数长,你需要把这个字符 串拷贝到其他的字符串类中。当目的类型是BSTR时,内存不会自动被释放,你必须把返回值赋给一个BSTR变量或者一个BSTR封装类以避免内存泄漏。
下面是一些各种转换宏的使用例子:

// Functions taking various strings:
void Foo ( LPCWSTR wstr );
void Bar ( BSTR bstr );
// Functions returning strings:
void Baz ( BSTR* pbstr );
#include <atlconv.h>
main()
{
using std::string;
USES_CONVERSION; // declare locals used by the ATL macros
// Example 1: Send an MBCS string to Foo()
LPCSTR psz1 = "Bob";
string str1 = "Bob";

Foo ( A2CW(psz1) );
Foo ( A2CW(str1.c_str()) );

// Example 2: Send a MBCS and Unicode string to Bar()
LPCSTR psz2 = "Bob";
LPCWSTR wsz = L"Bob";
BSTR bs1;
CComBSTR bs2;

bs1 = A2BSTR(psz2); // create a BSTR
bs2.Attach ( W2BSTR(wsz) ); // ditto, assign to a CComBSTR
Bar ( bs1 );
Bar ( bs2 );

SysFreeString ( bs1 ); // free bs1 memory
// No need to free bs2 since CComBSTR will do it for us.

// Example 3: Convert the BSTR returned by Baz()
BSTR bs3 = NULL;
string str2;
Baz ( &bs3 ); // Baz() fills in bs3
str2 = W2CA(bs3); // convert to an MBCS string
SysFreeString ( bs3 ); // free bs3 memory
}
正如你所看见的,当你有一个和函数所需的参数类型不同的字符串时,使用这些转换宏是非常方便的。

MFC类

CString
因为一个MFC CString类的对象包含TCHAR类型的字符,所以确切的字符类型取决于你所定义的预处理符号。大体来说,CString 很像STL string,这意味着你必须把它当成不透明的对象,只能使用CString提供的方法来修改CString对象。CString有一个string所不 具备的优点:CString具有接收MBCS和Unicode两种字符串的构造函数,它还有一个LPCTSTR转换符,所以你可以把CString对象直 接传给一个接收LPCTSTR的函数而不需要调用c_str()函数。
// Constructing
CString s1 = "char string"; // construct from a LPCSTR
CString s2 = L"wide char string"; // construct from a LPCWSTR
CString s3 ( '' '', 100 ); // pre-allocate a 100-byte buffer, fill with spaces
CString s4 = "New window text";

// You can pass a CString in place of an LPCTSTR:
SetWindowText ( hwndSomeWindow, s4 );

// Or, equivalently, explicitly cast the CString:
SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );
你可以从你的字符串表中装载一个字符串,CString的一个构造函数和LoadString()函数可以完成它。Format()方法能够从字符串表中随意的读取一个具有一定格式的字符串。
// Constructing/loading from string table
CString s5 ( (LPCTSTR) IDS_SOME_STR ); // load from string table
CString s6, s7;
// Load from string table.
s6.LoadString ( IDS_SOME_STR );

// Load printf-style format string from the string table:
s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );
第一个构造函数看起来有点奇怪,但是这实际上是文档说明的装入一个字符串的方法。 注意,对一个CString变量,你可以使用的唯一合法转换符是LPCTSTR。转换成LPTSTR(非常量指针)是错误的。养成把一个CString变 量转换成LPTSTR的习惯将会给你带来伤害,因为当你的程序后来崩溃时,你可能不知道为什么,因为你到处都使用同样的代码而那时它们都恰巧正常工作。正 确的得到一个指向缓冲区的非常量指针的方法是调用GetBuffer()方法。 下面是正确的用法的一个例子,这段代码是给一个列表控件中的项设定文字:
CString str = _T("new text");
LVITEM item = {0};
item.mask = LVIF_TEXT;
item.iItem = 1;
item.pszText = (LPTSTR)(LPCTSTR) str; // WRONG!
item.pszText = str.GetBuffer(0); // correct

ListView_SetItem ( &item );
str.ReleaseBuffer(); // return control of the buffer to str
pszText成员是一个LPTSTR变量,一个非常量指针,因此你需要对str调用GetBuffer()。GetBuffer()的参数是你需要 CString为缓冲区分配的最小长度。如果因为某些原因,你需要一个可修改的缓冲区来存放1K TCHARs,你需要调用GetBuffer(1024)。把0作为参数时,GetBuffer()返回的是指向字符串当前内容的指针。
上面划线的语句可以被编译,在这种情况下,甚至可以正常起作用。但这并不意味着这行代码是正确的。通过使用非常量转换,你已经破坏了面向对象的封装,并对 CString的内部实现作了某些假定。如果你有这样的转换习惯,你终将会陷入代码崩溃的境地。你会想代码为什么不能正常工作了,因为你到处都使用同样的 代码而那些代码看起来是正确的。
你知道人们总是抱怨现在的软件的bug是多么的多吗?软件中的bug是因为程序员写了不正确的代码。难道你真的想写一些你知道是错误的代码来为所有的 软件都满是bug这种认识做贡献吗?花些时间来学习使用CString的正确方法让你的代码在任何时间都正常工作把。
CString 有两个函数来从一个 CString 创建一个 BSTR。它们是 AllocSysString() 和SetSysString()。
// Converting to BSTR
CString s5 = "Bob!";
BSTR bs1 = NULL, bs2 = NULL;
bs1 = s5.AllocSysString();
s5.SetSysString ( &bs2 );
SysFreeString ( bs1 );
SysFreeString ( bs2 );
COleVariant
COleVariant和CComVariant.很相似。COleVariant继承自VARIANT,所以它可以传给接收VARIANT的函数。 然而,不像CComVariant,COleVariant只有一个LPCTSTR构造函数。没有对LPCSTR 和LPCWSTR的构造函数。在大多数情况下这不是一个问题,因为不管怎样你的字符串很可能是LPCTSTRs,但这是一个需要意识到的问题。 COleVariant还有一个接收CString参数的构造函数。
// Constructing
CString s1 = _T("tchar string");
COleVariant v1 = _T("Bob"); // construct from an LPCTSTR
COleVariant v2 = s1; // copy from a CString
像CComVariant一样,你必须直接访问VARIANT的成员。如果需要把VARIANT转换成一个字符串,你应该使用ChangeType()方 法。然而,COleVariant::ChangeType()如果失败会抛出异常,而不是返回一个表示失败的HRESULT代码。
// Extracting data
COleVariant v3 = ...; // fill in v3 from somewhere
BSTR bs = NULL;
try
{
v3.ChangeType ( VT_BSTR );
bs = v3.bstrVal;
}
catch ( COleException* e )
{
// error, couldn''t convert
}
SysFreeString ( bs );

WTL 类

CString
WTL的CString的行为和MFC的 CString完全一样,所以你可以参考上面关于MFC的 CString的介绍。

CLR 和 VC 7 类

System::String是用来处理字符串的.NET类。在内部,一个String对象包含一个不可改变的字符串序列。任何对String对象的 操作实际上都是返回了一个新的String对象,因为原始的对象是不可改变的。String的一个特性是如果你有不止一个String对象包含相同的字符 序列,它们实际上是指向相同的对象的。相对于C++的使用扩展是增加了一个新的字符串常量前缀S,S用来代表一个受控的字符串常量(a managed string literal)。
// Constructing
String* ms = S"This is a nice managed string";
你可以传递一个非受控的字符串来创建一个String对象,但是样会比使用受控字符串来创建String对象造成效率的微小损失。这是因为所有以S作为前缀的相同的字符串实例都代表同样的对象,但这对非受控对象是不适用的。下面的代码清楚地阐明了这一点:
String* ms1 = S"this is nice";
String* ms2 = S"this is nice";
String* ms3 = L"this is nice";
Console::WriteLine ( ms1 == ms2 ); // prints true
Console::WriteLine ( ms1 == ms3); // prints false
正确的比较可能没有使用S前缀的字符串的方法是使用String::CompareTo()
  Console::WriteLine ( ms1->CompareTo(ms2) );
Console::WriteLine ( ms1->CompareTo(ms3) );
上面的两行代码都会打印0,0表示两个字符串相等。 String和MFC 7 CString之间的转换是很容易的。CString有一个向LPCTSTR的转换操作,而String有两个接收char* 和 wchar_t*的构造函数,因此你可以把一个CString变量直接传给一个String的构造函数。
CString s1 ( "hello world" );
String* s2 ( s1 ); // copy from a CString
反方向的转换也很类似
String* s1 = S"Three cats";
CString s2 ( s1 );
这也许会使你感到一点迷惑,但是它确实是起作用的。因为从VS.NET 开始,CString 有了一个接收String 对象的构造函数。
  CStringT ( System::String* pString );      
对于一些快速操作,你可能想访问底层的字符串:
String* s1 = S"Three cats";
Console::WriteLine ( s1 );
const __wchar_t __pin* pstr = PtrToStringChars(s1);
for ( int i = 0; i < wcslen(pstr); i++ )
(*const_cast<__wchar_t*>(pstr+i))++;
Console::WriteLine ( s1 );
PtrToStringChars()返回一个指向底层字符串的const __wchar_t* ,我们需要固定它,否则垃圾收集器或许会在我们正在管理它的内容的时候移动了它。

在 printf-style 格式函数中使用字符串类

当你在printf()或者类似的函数中使用字符串封装类时你必须十分小心。这些函数包括sprintf()和它的变体,还有TRACE和 ATLTRACE宏。因为这些函数没有对添加的参数的类型检查,你必须小心,只能传给它们C语言风格的字符串指针,而不是一个完整的字符串类。
例如,要把一个_bstr_t 字符串传给ATLTRACE(),你必须使用显式转换(LPCSTR) 或者(LPCWSTR):
_bstr_t bs = L"Bob!";
ATLTRACE("The string is: %s in line %d\n", (LPCSTR) bs, nLine);

  如果你忘了使用转换符而把整个_bstr_t对象传给了函数,将会显示一些毫无意义的输出,因为_bstr_t保存的内部数据会全部被输出。

所有类的总结

两个字符串类之间进行转换的常用方式是:先把源字符串转换成一个C语言风格的字符串指针,然后把这个指针传递给目的类型的构造函数。下面这张表显示了怎样把一个字符串转换成一个C语言风格的字符串指针以及哪些类具有接收C语言风格的字符串指针的构造函数。

Class  string type convert to char*? convert to const char*? convert to wchar_t*? convert to const wchar_t*? convert to BSTR? construct from char*? construct from wchar_t*?
_bstr_t BSTR yes cast1 yes cast yes cast1 yes cast yes2 yes yes
_variant_t BSTR no no no cast to
_bstr_t3
cast to
_bstr_t3
yes yes
string MBCS no yes c_str() method no no no yes no
wstring Unicode no no no yes c_str() method no no yes
CComBSTR BSTR no no no yes cast to BSTR yes cast yes yes
CComVariant BSTR no no no yes4 yes4 yes yes
CString TCHAR no6 in MBCS
builds, cast
no6 in Unicode
builds, cast
no5 yes yes
COleVariant BSTR no no no yes4 yes4 in MBCS
builds
in Unicode
builds
  • 1、即使 _bstr_t 提供了向非常量指针的转换操作符,修改底层的缓冲区也会已引起GPF如果你溢出了缓冲区或者造成内存泄漏。
  • 2、_bstr_t 在内部用一个 wchar_t* 来保存 BSTR,所以你可以使用 const wchar_t* 来访问BSTR。这是一个实现细节,你可以小心的使用它,将来这个细节也许会改变。
  • 3、如果数据不能转换成BSTR会抛出一个异常。
  • 4、使用 ChangeType(),然后访问 VARIANT 的 bstrVal 成员。在MFC中,如果数据转换不成功将会抛出异常。
  • 5、这里没有转换 BSTR 函数,然而 AllocSysString() 返回一个新的BSTR。
  • 6、使用 GetBuffer() 方法,你可以暂时地得到一个非常量的TCHAR指针。

  • posted @ 2008-06-23 16:39 Da Vinci 阅读(27) | 评论 (0)编辑

    2008年5月24日


    网上讲委托的文章有N多,技术书籍与文档中涉及.NET的也必讲委托,原因很简单:过去没有。这样一个新的point在初学者看起来似乎有些高深莫测,但实际上它是我们过去都使用过的一种技术的近义词,这种技术就是函数指针,.NET用委托来实现类型安全的回调函数机制。委托就是一种类(引用类型),C#的关键词delegate创建了这个类。

       注:委托类的父类是System.MulticastDelegate,我们没办法直接继承它。

    委托概览

    OK,现在创建一个委托看看:
     1using System;
     2public class dotnet
     3{
     4    public delegate void Test();  //没有参数的委托
     5    public delegate void Test1( Int32 item );
     6
     7    public void foo()
     8    {
     9        Console.Writeline("no parameters");
    10    }

    11    public int foo1( int item )
    12    {
    13        return item;
    14    }

    15}

    16public static void Main()
    17{
    18    Test t = new Test( foo );
    19    t();   //调用foo方法,t为委托对象
    20    Test1 t1 = new Test1( foo1 );
    21    Int32 i = t1( 100 );  //调用 foo1(100)方法,t1为委托对象
    22}

    似乎不是太复杂,但是编译器和CLR还是隐瞒了我们很多东西(Lippman在《Inside C++ Object Model》中对编译器在背后默默所做的事情表达了他的看法)。那么事实是什么呢?看下面的代码:

    public delegate void Dele( Object a, Int32 item, Int32 num );

    编译器把这句代码翻译成一个更复杂的类:

     1public class Test : System.MulticastDelegate
     2{
     3    public Test( Object target, Int32 methodPtr); //一个构造器
     4    public void virtual Invoke( Object a, Int32 item, Int32 num );  //一个Invoke方法,其中参数和原委托参数相同
     5       
     6    //委托的异步调用
     7    public virtual IAsyncResult BeginInvoke( Object a, Int32 item, Int32 num, AsynsCallback callback, Object object );
     8    public virtual void EndInvoke( IAsyncResult result);
     9}

    10

    上面可以看到Test类继承了MulticastDelegate类。在MulticastDelegate类中有一些字段涉及到委托的内部机制:_target字段指示了被回调的对象,_methodPtr是一个整数,用来标志回调方法。在上述Test类中的构造器参数就代表这两个私有字段。另外MulticastDelegate类还有_prev字段指示了另外的一个委托对象,这主要用在委托链表中,下面会再讨论。
    在我们构造委托时,构造器把对象的引用传递给target参数,另外在元数据中的MethodDef或MethodRef的标记传递给methodRef。当我们调用一个委托对象时,会调用该对象的Invoke方法。例如在刚才创建的委托对象中,编译器会把t()翻译成t.Invoke()。

    匿名委托

    委托本身的价值主要体现在事件处理方面,关于事件本篇不想多说,后续主题会讨论。下面来看一下C#2.0里面增加的匿名委托。
    匿名委托(确切的说是匿名方法声明委托)就是允许一个与委托关联的代码被内联地写入使用委托的地方,这使得代码对于委托的实例很直接。MSDN:"如果使用匿名方法,则不必创建单独的方法,因此减少了实例化委托所需的编码系统开销"。OK,看看C#1.x中的委托版本:

    public delegate void Test();

    public void test()
    {
        Test test 
    = new Test(foo);
        test();
    }

    public static void foo()
    {
        Console.WriteLine("
    Anonymous Method");
    }

    Now, C#2.0的匿名委托版本:

    public void test()
    {
       Test test 
    = delegate() { Console.WriteLine("Anonymous Method"); };
       test();
    }

    Oh,你说很简单了!至少方法不用写了。不急,先看看它内部是什么原理:


                                            图1. C#1.x 命名委托反编译代码


                                            图2. C#2.0匿名委托反编译代码

    上面可以看到,委托类没有任何改变,倒是在Program类里添加了一个新的方法:<Main>b__0:void()。当使用了匿名方法的类里,CLR会自动生成一个跟调用命名方法时有同样签名的EventHandler和一个method来处理,所以,匿名方法只是减少了我们的编码量。在查看IL代码时可以看到,Program类里有一个名为<>9__CachedAnonymousMethodDelegate1的EventHandler和一个名为<Main>b__0的跟EventHandler的签名符合的方法。来看看这个新生成的方法:

    .method private hidebysig static void  '<Main>b__0'() cil managed
    {
      .custom instance 
    void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
      
    // Code size       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      
    " Anonymous Method "
      IL_0006:  call       
    void [mscorlib]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    // end of method Program::'<Main>b__0'

    这里实际上就是执行了Console.WriteLine("Anonumous Method")。

        注:关于匿名方法更多深入的主题, 请参考:http://www.microsoft.com/china/msdn/library/langtool/vcsharp/CreElegCodAnymMeth.mspx?mfr=true
                                                              
    委托链

    委托链技术使委托更有价值,在MulticaseDelegate对象中的_prev字段就是指向了另一个MulticaseDelegate对象的引用。这样委托对象就可以组成一个链表。我们可以利用Delegate类中的Combine方法创建链表,用Remove方法移除链表中的一个委托。

       注:Combine方法有两种形式:委托数组形式和head/tail形式,每种都可以操作委托链表。

    例如,操作一个委托链表:
    Test test1 = new Test(Fun1); // Fun1, Fun2为调用相应的方法
    Test test2 = new Test(Fun2);

    //创建一个委托链表
    Test test = (Test) Delegate.Combine(test1,test2);

    //从委托链表上移除一个委托对象
    test = (Test) Delegate.Remove(test, new Test1(Fun1));

    在创建委托链表操作中,编译器会判断委托链表的_prev字段是否为空,若不为空,则递归调用链表上的委托对象;在移除委托链表操作中,调用Remove方法时需要创建一个新的委托对象。该委托对象的_target和_methodPtr字段会被初始化。Remove在委托链表中寻找与该委托链表相等的委托对象,如果找到则移除(修正委托链表的_prev字段),最后返回委托链表头。
    需要注意的是Invoke方法调用每个委托对象之前的对象,这样虽然每个对象都会调用到,但是实际上最终得到的返回值只是最后一个方法的返回值,在这期间得到的方法的返回值会被丢弃。另外若调用期间发生了异常,则会影响到后续方法的调用。改善这种情况的方法是利用GetInvocationList方法:

    public virtual Delegate[] GetInvocationList();

    该方法返回一个委托数组,它遍历委托链表上的委托对象,并为拷贝每一个对象到数组中,同时这些数组中的委托对象的_prev字段都会被设为空,这样就达到了相对独立的目的。例如对上述test委托链表:

    Delegate[] deleArray = test.GetInvocationList();

    foreach( Test t in deleArray )
       
    //do something.


    委托与设计模式


    在设计模式中,也有一种委托机制Delegation(注意Delegation并不是一种模式)。它是一种组合方法,两个对象处理同一个请求,接受请求的对象把操作委托给其代理者(即委托)。类似于子类将其请求交给父类处理。State、Strategy和Visitor模式都使用了委托的机制。可以看出.NET中的委托和Delegation的意思很接近,但在实现中有些不同。.NET委托能更加灵活的实现对象间的解耦,发挥设计模式中委托机制的作用。

    关于委托还有更多有意义的话题,比如事件与委托技术、反射技术与委托、委托在实际组件设计中的作用等,会在今后的探索中不断深入讨论。


    参考资料: Jeffrey Richter《Applied Microsoft .NET Framework Programming》

    posted @ 2008-05-24 18:20 Da Vinci 阅读(63) | 评论 (0)编辑

    2008年3月27日


    这次我们通过一个实际调试驱动的例子,来逐步体会Windbg在内核调试中的作用.
    由于条件所限,大多数情况下,很多人都是用VMware+Windbg调试内核(VMware的确是个好东西).但这样的调试需要占用大量的系统资源,对于和我一样急性子的朋友来说这是不可接受的:).利用双机调试就可以让你一边喝咖啡一边轻松的看结果,而不至于郁闷的等待每次长达数分钟的系统响应.有关双机调试的基本设置,请参考:http://www.cnblogs.com/Sonic2007/archive/2008/03/20/1114807.html

    本次调试驱动所构建的环境如下:

    host computer: WinXP+Windbg
    Target computer:  Vista SP1
    driver object: syscow
    connect setting: 1394数据线

    说明: 1.1394卡在很多机器上都已经没有了,Vista也取消了1394的数据连接协议(调试还是可以的),但不可否认的是利用1394数据线连接调试要比COM口和USB速率快很多(为什么好用的东西却得不到支持!).2.本次调试的driver是公司开发的某个软件的驱动程序,拿来尝试在Vista SP1下track.由于涉及到商业机密,本驱动源代码不便公开.3.Vista SP1就没什么好说的了,前些天才发布,MS又一个失败的典型.

    OK,Let's go!
    该驱动是一个类型sr.sys(MS的System Restore驱动)的Filter Driver,属于文件系统过滤驱动,加载在文件系统驱动上层,由Filter Manager负责与用户层和底层通信.连接到目标机后,按下Ctrl+break中断当前状态.(注:你也可以进入到explorer之后再中断,为了了解驱动加载时的进入点,以及系统启动时内核的装态,我们中断到这里)

    Microsoft (R) Windows Debugger  Version 6.6.0007.5
    Copyright (c) Microsoft Corporation. All rights reserved.

    Using 1394 for debugging
    Opened \\.\DBG1394_INSTANCE01
    Waiting to reconnect...
    Connected to Windows Vista 6000 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.
    Symbol search path is: D:\symbolslocal; D:\IR\SystemOK\Restore\Driver\objchk_wlh_x86\i386
    Executable search path is:
    *** ERROR: Symbol file could not be found.  Defaulted to export symbols for ntkrnlmp.exe -
    Windows Vista Kernel Version 6000 MP (1 procs) Free x86 compatible
    Built by: 6000.16584.x86fre.vista_gdr.071023-1545
    Kernel base = 0x81800000 PsLoadedModuleList = 0x81908ad0
    System Uptime: not available
    WARNING: Whitespace at start of path element
    WARNING: Whitespace at start of path element
    Break instruction exception - code 80000003 (first chance)
    *******************************************************************************
    *                                                                                                                                          
    *   You are seeing this message because you pressed either                                                        
    *       CTRL+C (if you run kd.exe) or,                                       
    *       CTRL+BREAK (if you run WinDBG),                                     
    *   on your debugger machine's keyboard.                                     
    *                                                                            
    *                   THIS IS NOT A BUG OR A SYSTEM CRASH                      
    *                                                                            
    * If you did not intend to break into the debugger, press the "g" key, then  
    * press the "Enter" key now.  This message might immediately reappear.  If it
    * does, press "g" and "Enter" again.                                         
    *                                                                            
    *******************************************************************************
    nt!RtlpBreakWithStatusInstruction:
    818355e8 cc              int     3

    然后,在Command line里键入lm,查看当前系统加载的模块和驱动(会发现我们的driver列在其中):

    kd> lm
    start    end        module name
    80404000 80412000   PCIIDEX    (deferred)            
    80412000 80419000   intelide   (deferred)            
    80419000 80429000   mountmgr   (deferred)            
    80429000 80438000   volmgr     (deferred)            
    80438000 8045d000   pci        (deferred)            
    8045d000 80465000   msisadrv   (deferred)            
    80465000 8046e000   WMILIB     (deferred)            
    8046e000 804b1000   acpi       (deferred)            
    804b1000 804be000   WDFLDR     (deferred)            
    804be000 80539000   Wdf01000   (deferred)            
    80539000 8061a000   CI         (deferred)            
    8061a000 80655000   CLFS       (deferred)            
    80655000 8065d000   BOOTVID    (pdb symbols)         
    8065d000 80666000   PSHED      (deferred)            
    80666000 806c6000   mcupdate_GenuineIntel   (deferred)            
    806c6000 806ce000   kdcom      (deferred)            
    81800000 81b95000   nt         (pdb symbols)         
    81b95000 81bc9000   hal        (pdb symbols)        
    81c06000 81c0e000   spldr      (deferred)            
    81c0e000 81c44000   volsnap    (deferred)            
    81c44000 81cae000   ksecdd     (deferred)            
    81cae000 81db6000   Ntfs       (deferred)            
    81db6000 81def000   NETIO      (deferred)            
    81def000 81e1a000   msrpc      (deferred)            
    81e1a000 81f1e000   ndis       (deferred)            
    81f1e000 81f270c0   PxHelp20   (deferred)            
    81f28000 81f4f000   syscow32v   (private pdb symbols) 
    81f4f000 81f5f000   fileinfo   (deferred)            
    81f5f000 81f90000   fltmgr     (deferred)            
    81f90000 81fae000   ataport    (deferred)            
    81fae000 81fb6000   atapi      (deferred)            
    81fb6000 82000000   volmgrx    (deferred)            
    8234f000 82358000   crcdisk    (deferred)            
    82358000 82368000   agp440     (deferred)            
    82368000 82389000   CLASSPNP   (deferred)            
    82389000 8239a000   disk       (deferred)            
    8239a000 823bd000   fvevol     (deferred)            
    823bd000 823e2000   ecache     (deferred)            
    823e2000 823f1000   mup        (deferred)            
    823f1000 82400000   partmgr    (deferred)  

    注: 若符号文件没有加载成功,Windbg会提示响应的符号找不到,不过一般Windbg会自己寻找符号文件路径.实在找不到时,就包含
    srv*c:\symbols*http://msdl.microsoft.com/download/symbols, 然后reload一下(!reload).

    另外,键入lm t n, 我们可以查看更为详细的模块及驱动信息.
    然后,键入!thread和Kp,查看当前的线程详细信息和堆栈(或者Alt+6也可以看stack).注意当前thread的ID:

    kd> !thread
    THREAD 84254ae8  Cid 0004.0008  Teb: 00000000 Win32Thread: 00000000 RUNNING on processor 0
    Not impersonating
    Owning Process            84254d90       Image:         System
    Wait Start TickCount      0              Ticks: 1 (0:00:00:00.015)
    Context Switch Count      1            
    UserTime                  00:00:00.0000
    KernelTime                00:00:00.0015
    Win32 Start Address nt!Phase1Initialization (0x819433ae)
    Stack Init 81c06000 Current 81c05db8 Base 81c06000 Limit 81c03000 Call 0
    Priority 31 BasePriority 8 PriorityDecrement 0
    ChildEBP RetAddr  Args to Child             
    81c05af0 818aa92c 00000001 81867999 0002625a nt!RtlpBreakWithStatusInstruction (FPO: [1,0,0])
    81c05af8 81867999 0002625a 00000000 00000001 nt!KdCheckForDebugBreak+0x22 (FPO: [0,0,0])
    81c05b18 81836cfd 81928100 000000d1 81c05b9c nt!KeUpdateRunTime+0x270
    81c05b18 81ba4130 81928100 000000d1 81c05b9c nt!KeUpdateSystemTime+0xed (FPO: [0,2] TrapFrame @ 81c05b28)
    81c05b9c 81ba3fd0 81bb28a0 8181dced 81c05bc8 hal!XmGetCodeByte+0x30 (FPO: [Non-Fpo])
    81c05bac 81ba40c5 81bb28a0 0000c000 00001da4 hal!XmEmulateStream+0x88 (FPO: [Non-Fpo])
    81c05bc8 81ba374d 00000010 81c05c0c 8181dced hal!XmEmulateInterrupt+0x80 (FPO: [Non-Fpo])
    81c05bdc 81ba0a1c 00000010 81c05c0c 00000000 hal!x86BiosExecuteInterruptShadowed+0x43 (FPO: [Non-Fpo])
    81c05bf8 81ba0a5b 00000010 81c05c0c 00000000 hal!x86BiosCall+0x22 (FPO: [Non-Fpo])
    81c05c2c 80656697 80806ae0 8080f438 00000000 hal!HalpBiosDisplayReset+0x25 (FPO: [Non-Fpo])
    81c05c58 81b2cd6d 00000001 81b0ab01 80806ae0 BOOTVID!VidInitialize+0x135 (FPO: [Non-Fpo])
    81c05c7c 81b3f098 00000001 80806ae0 00000007 nt!InbvDriverInitialize+0x81
    81c05d74 819433bb 81c05dc0 819afbad 80806ae0 nt!Phase1InitializationDiscard+0xd0
    81c05d7c 819afbad 80806ae0 81c0e680 00000000 nt!Phase1Initialization+0xd
    81c05dc0 8189a346 819433ae 80806ae0 00000000 nt!PspSystemThreadStartup+0x9d
    00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16

    kd> kp
    ChildEBP RetAddr 
    81c05af0 818aa92c nt!RtlpBreakWithStatusInstruction
    81c05af8 81867999 nt!KdCheckForDebugBreak+0x22
    81c05b18 81836cfd nt!KeUpdateRunTime+0x270
    81c05b18 81ba4130 nt!KeUpdateSystemTime+0xed
    81c05b9c 81ba3fd0 hal!XmGetCodeByte+0x30
    81c05bac 81ba40c5 hal!XmEmulateStream+0x88
    81c05bc8 81ba374d hal!XmEmulateInterrupt+0x80
    81c05bdc 81ba0a1c hal!x86BiosExecuteInterruptShadowed+0x43
    81c05bf8 81ba0a5b hal!x86BiosCall+0x22
    81c05c2c 80656697 hal!HalpBiosDisplayReset+0x25
    81c05c58 81b2cd6d BOOTVID!VidInitialize+0x135
    81c05c7c 81b3f098 nt!InbvDriverInitialize+0x81
    81c05d74 819433bb nt!Phase1InitializationDiscard+0xd0
    81c05d7c 819afbad nt!Phase1Initialization+0xd
    81c05dc0 8189a346 nt!PspSystemThreadStartup+0x9d
    00000000 00000000 nt!KiThreadStartup+0x16

    键入!process [PID] 0, 查到当前进程:

    kd> !process 0004.0008 0
    PROCESS 84254d90  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
        DirBase: 00122000  ObjectTable: 830001d0  HandleCount:   1.
        Image: System
        VadRoot 00000000 Vads 0 Clone 0 Private 0. Modified 0. Locked 0.
        DeviceMap 00000000
        Token                             83003830
        ElapsedTime                       00:00:00.015
        UserTime                          00:00:00.000
        KernelTime                        00:00:00.000
        QuotaPoolUsage[PagedPool]         0
        QuotaPoolUsage[NonPagedPool]      0
        Working Set Sizes (now,min,max)  (4, 0, 0) (16KB, 0KB, 0KB)
        PeakWorkingSetSize                0
        VirtualSize                       0 Mb
        PeakVirtualSize                   0 Mb
        PageFaultCount                    0
        MemoryPriority                    BACKGROUND
        BasePriority                      8
        CommitCharge                      0

            THREAD 84254ae8  Cid 0004.0008  Teb: 00000000 Win32Thread: 00000000 RUNNING on processor 0

    在lm命令列出的信息中,start是模块的起始地址,通过键入"u 驱动起始地址",我们可以反汇编出它的代码:

    kd> u 81f28000
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0x0):
    81f28000 4d              dec     ebp
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0x1):
    81f28001 5a              pop     edx
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0x2):
    81f28002 90              nop
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0x3):
    81f28003 0003            add     byte ptr [ebx],al
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0x5):
    81f28005 0000            add     byte ptr [eax],al
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0x7):
    81f28007 000400          add     byte ptr [eax+eax],al
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0xa):
    81f2800a 0000            add     byte ptr [eax],al
    syscow32v!SysCowAllocatePostCopyWorkItem <PERF> (syscow32v+0xc):
    81f2800c ff              ???

    逐步查找(enter),最终我们可以发现driver的入口点.这个过程其实是非常慢的,因为系统内核在加载驱动实际代码的过程中进行了N多次调用.如果我们本身有驱动的代码,也可以直接包含源代码路径,通过在实际代码中设置断点,让Windbg自己中断到相应的代码位置(在实际调试内核的过程中,这几乎是不可能的,因为你不会得到Windows内核或某个驱动程序的源代码.Linux系列的某些driver们又另当别论).这里为了方便,我包含了syscow的源代码,增加断点直接走到DriverEntry例程:

    kd> u 81f42780
    syscow32v!DriverEntry [隐藏了address]:
    81f42780 8bff            mov     edi,edi
    81f42782 55              push    ebp
    81f42783 8bec            mov     ebp,esp
    81f42785 51              push    ecx
    81f42786 c745fc010000c0  mov     dword ptr [ebp-4],0C0000001h
    81f4278d a1749cf481      mov     eax,dword ptr [syscow32v!SysCowDbgFlags (81f49c74)]
    81f42792 83e004          and     eax,4
    81f42795 7424            je      syscow32v!DriverEntry+0x3b (81f427bb)

    其中显示的汇编代码,是内核调用驱动是进行的操作,其实也和实际代码相对应.

    在driver代码中,如果要查看当前参数值,用dv命令:

    kd> dv
       DriverObject = 0x84663730
       RegistryPath = 0x8084b560
             status = 8
           dontload = 0

    另外,用"dt 参数名"可以看某个参数的当前值.

    kd> dt DriverObject
    Local var @ 0x81c05af8 Type _DRIVER_OBJECT*
    0x84663730
       +0x000 Type             : 4
       +0x002 Size             : 168
       +0x004 DeviceObject     : (null)
       +0x008 Flags            : 2
       +0x00c DriverStart      : 0x81f28000
       +0x010 DriverSize       : 0x27000
       +0x014 DriverSection    : 0x84230a68
       +0x018 DriverExtension  : 0x846637d8 _DRIVER_EXTENSION
       +0x01c DriverName       : _UNICODE_STRING "\FileSystem\SysCow"
       +0x024 HardwareDatabase : 0x81af6ed8 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
       +0x028 FastIoDispatch   : (null)
       +0x02c DriverInit       : 0x81f4a005     syscow32v!GsDriverEntry+0
       +0x030 DriverStartIo    : (null)
       +0x034 DriverUnload     : (null)
       +0x038 MajorFunction    : [28] 0x8189a5c1     nt!IopInvalidDeviceRequest+0

    这样,我们就可以通过上述的这些命令,逐步分析一个驱动程序在加载和执行过程中的情况。另外,Windbg还有众多内核调试命令,如!irp可以查看一个对象的数据结构,!devobj可以查看设备对象等,在今后的操作系统内核学习过程中会不断用到这些命令。

    从上面的操作过程我们可以看出,利用Windbg调试驱动程序,或者说进行内核调试,是非常方便的。如果我们对Windows内核有一定的了解,同时拥有一定汇编语言的功底,就可以有Windbg进行简单的系统排障(比如系统加载是出现蓝屏,或是某个系统模块出现问题)、驱动学习等。同时,这个过程也可以让我们更深入的理解操作系统原理。另外,Windbg也可以进行系统服务(service)的调试,这就是User mode的调试过程了。


    posted @ 2008-03-27 21:16 Da Vinci 阅读(632) | 评论 (1)编辑

    2008年3月25日


    CLR(公共语言运行库)可以说是整个.NET平台的核心元素.基本上托管应用程序所有的操作都是需要CLR的监管和处理.这些操作包括进程内应用程序的加载, IL语言转换为机器语言, 异常管理, 垃圾回收,加载程序集等等.

    CLR执行托管代码前,实际上会创建三个应用程序域, 它们是系统域(System Domain),共享域(Shared Domain)和缺省应用程序域(Default AppDomain).其中系统域和共享域对于托管代码和CLR的宿主程序(如控制台程序,ASP.NET等)不可见.域可以通过AppDomain.CreateDomain方法创建(下文会详细叙述这个过程),在非托管的代码中,可以使用一个ICORRutimeHost的接口创建(这个接口我也没用过,一般都是托管的代码).对于复杂的宿主程序,由网站根据应用程序的数目来建立域。



                         图1.CLR启动程序创建的域
       &nb