Da Vinci's CyberSpace

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

2009年5月29日

Dump 文件分析很大程度上就是分析蓝屏产生的原因。这种系统级的错误算是Windows提示错误中比较严重的一种(更严重的还有启动黑屏等硬件或软件兼容性错误等等)。说它是比较严重,是因为毕竟Windows还提供了dump文件给用户分析,至少能比较容易的找到错误的原因。一般蓝屏要么是内核程序中的异常或违规,要么是数据结构的损坏,也有boot或shutdown的时候内核出错。有时候蓝屏是一闪而过,紧接着是系统重启;有时候是蓝屏等待。总之蓝屏的时候都提示了一些停止代码和错误信息,不过这些提示是不全面的,最多知道哪个模块出错(比如驱动)。想了解进一步的信息,或者通过搜索引擎,最好的方式当然是dump文件分析。当然,如果有更进一步研究的欲望,内核调试是更好的方法,不过这需要某些软件支持和调试技巧。

类型
Dump文件有三种:完整内存转储,内核内存转储,小内存转储。System Properties中的高级选项中可以看到这些设置。
完整内存转储太大,一般是物理内存大小或多一些,包括了用户进程页面,这种方式不实用,2GB的物理内存转储出来至少要2GB的磁盘空间(还有文件头信息)。内核转储一般是200MB大小(物理内存小于4GB),它只是包含了所有属于内核模式的物理内存。小内存转储一般是64KB(64位上是 128KB),这两种方式是更常用的。
小内存转储在\Windows\Minidump下生成了一个叫Mini日期+序列号.dmp的文件,这个珍贵的资源就是系统Crash时刻的状态,只不过小内存转储只记录的有限的信息,而且在你分析时,如果windbg没有设置符号服务器的路径(关于符号服务器,请参考Windbg内核调试之二: 常用命令),那么你的当前系统必须和发生蓝屏的系统的Ntoskrnl.exe版本相同,否则就有找不到符号的问题产生。
启动windbg,用Open Crash Dump打开dump文件,或者直接拖动文件到windbg中,windbg显示如下信息:


Loading Dump File [C:\dbg\Mini052809-01.dmp]
Mini Kernel Dump File: Only registers and stack trace are available
Symbol search path is: SRV*d:/temp/*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows Vista Kernel Version 6000 (Service Pack 1) UP Free x86 compatible
Kernel base = 0x80400000 PsLoadedModuleList = 0x8046e8f0
Debug session time: Thu May 28 16:12:29.031 2009 (GMT+8)
System Uptime: not available
Loading Kernel Symbols.............................................................................................................
Loading unloaded module list...................

Loading UserSymbols

********************************************************************************                                                                                                                                           **                        Bugcheck Analysis                                                                                         **                                                                                                                                           ********************************************************************************

Use !analyze -v to get detailed debugging information.
BugCheck 7F, {0, 0, 0, 0}

大致上提示了Crash系统的版本,加载符号的过程,如果找不到符号文件,还会提示Unable to load image。如下错误就是找不到ntoskrnl.exe的符号文件:

Unable to load image \SystemRoot\system32\ntoskrnl.exe, Win32 error 0n2
*** WARNING: Unable to verify timestamp for ntoskrnl.exe
*** ERROR: Module load completed but symbols could not be loaded for ntoskrnl.exe

命令
通过lm命令查看模块列表。另外,如果出现Unable to load image,说明没有找到这个文件,这个时候需要查看是否加载了正确的符号文件。设置符号服务器路径(.symfix命令)是很有必要的,因为调试机器和Crash机器的环境很可能不一致。
运行命令kb,显示调用栈的信息。如果有正确的符号设置,可以看到调用的函数名。如果你在调试自己驱动程序的蓝屏问题,请确保设置正确该驱动程序的符号路径,不然就会出现Stack unwind information not available的问题。加入正确的符号文件(pdb)后,可以用命令!reload重新加载符号文件。
通过!thread和!process,可以显示当前进程和线程。或者通过dt nt!_KTHREAD 地址和dt nt!_EPROCESS地址来查看线程和进程结构。

Windbg提供了自动分析dump文件的机制。通过命令!analyze –v,windbg可以自动做分析,显示如下信息:


*******************************************************************************
*                                                                                                                                          *
*                         Bugcheck Analysis                                                                                       *
*                                                                                                                                          *
*******************************************************************************

DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high.   This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: e1147008, memory referenced
Arg2: 0000001c, IRQL
Arg3: 00000000, value 0 = read operation, 1 = write operation
Arg4: fbe93403, address which referenced memory

Debugging Details:
------------------
READ_ADDRESS:   e1147008 Paged pool
CURRENT_IRQL:   1c
FAULTING_IP:
myfault+403
fbe93403 8b06             mov      eax,dword ptr [esi]

DEFAULT_BUCKET_ID:   DRIVER_FAULT
BUGCHECK_STR:   0xD1
PROCESS_NAME:   NotMyfault.exe

TRAP_FRAME:   f9357b80 --(trap fffffffff9357b80)
ErrCode = 00000000
eax=00000000 ebx=8111f330 ecx=000000d1 edx=0000001c esi=e1147008 edi=00000000
eip=fbe93403 esp=f9357bf4 ebp=f9357c58 iopl=0          nv up ei pl zr na pe nc
cs=0008   ss=0010   ds=0023   es=0023   fs=0030   gs=0000              efl=00010246
myfault+0x403:
fbe93403 8b06             mov      eax,dword ptr [esi]   ds:0023:e1147008=????????
Resetting default scope

LAST_CONTROL_TRANSFER:   from 804f880d to 80527da8

STACK_TEXT:
f9357734 804f880d 00000003 f9357a90 00000000 nt!RtlpBreakWithStatusInstruction
f9357780 804f93fa 00000003 e1147008 fbe93403 nt!KiBugCheckDebugBreak+0x19
f9357b60 80540853 0000000a e1147008 0000001c nt!KeBugCheck2+0x574
f9357b60 fbe93403 0000000a e1147008 0000001c nt!KiTrap0E+0x233
WARNING: Stack unwind information not available. Following frames may be wrong.
f9357c58 805759d1 ffb5c3b0 8111f318 811d9130 myfault+0x403
f9357d00 8056e33c 00000090 00000000 00000000 nt!IopXxxControlFile+0x5e7
f9357d34 8053d808 00000090 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
f9357d34 7c92eb94 00000090 00000000 00000000 nt!KiFastCallEntry+0xf8
0012f9f0 7c92d8ef 7c801671 00000090 00000000 ntdll!KiFastSystemCallRet
0012f9f4 7c801671 00000090 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc
0012fa54 004018c2 00000090 83360018 00000000 0x7c801671

STACK_COMMAND:   kb

FOLLOWUP_IP:
myfault+403
fbe93403 8b06             mov      eax,dword ptr [esi]

SYMBOL_STACK_INDEX:   4
FOLLOWUP_NAME:   MachineOwner
MODULE_NAME: myfault
IMAGE_NAME:   myfault.sys
DEBUG_FLR_IMAGE_TIMESTAMP:   43774e1d
SYMBOL_NAME:   myfault+403
FAILURE_BUCKET_ID:   0xD1_myfault+403
BUCKET_ID:   0xD1_myfault+403
Followup: MachineOwner


一般是按照如下:停止码解释,陷阱帧寄存器信息,蓝屏属性(有些除零错误就在这里显示),栈调用,错误指令位置(FOLLOWUP_IP),出错源代码和汇编代码行,错误代码行,出错模块信息(包括负责人等信息),来组织自动分析信息。

通过r命令,可以显示Crash时刻寄存器的状态和最后的命令状态。

通过d命令,可以显示当前内存的地址。在定位了错误代码行了之后,就可以进一步进行内核调试和系统调试了。

posted @ 2009-05-29 21:35 Da Vinci 阅读(166) | 评论 (0)编辑

2009年5月7日

需求

  1. 你能给出一些非功能性(或者质量)需求的例子么?

在某个时间范围内,产品运行稳定的程度;在某些压力极端情况(断电、饱和度),产品运行稳定的程度;不正常宕机后,产品的恢复度量。

  1. 如果客户需要高性能、使用极其方便而又高度安全,你会给他什么建议?

方便与高性能、高度安全是不完全统一的。方便带来的问题是安全性的降低,或性能不能达到最合适的程度;高性能会导致复杂性;安全性会让性能不能达到最佳。最好的方式是选择一种优先级,比如以安全性为主要的考核标准,适当安排性能和难易程度;或者以性能为主。尽量达到三者的平衡点,在某些极端的情况可比保证性能降低,但安全有保证。等等建议。

  1. 你能给出一些用来描述需求的不同技术么?它们各自适用于什么场景?
  2. 需求跟踪是什么意思?什么是向前追溯,什么是向后追溯?
  3. 你喜欢用什么工具跟踪需求?
  4. 你怎么看待需求变化?它是好是坏?给出你的理由。
  5. 你怎样研究需求,发现需求?有哪些资源可以用到?
  6. 你怎么给需求制定优先级?有哪些技术?
  7. 在需求过程中,用户、客户、开发人员各自的职责是什么?
  8. 你怎么对待不完整或是令人费解的需求?

以客户为导向,和客户召开需求review meeting,向客户表述自己对当前需求的理解。如果有不完整或者理解有误的地方,客户会及时纠正。一定要在需求明确的前提下进一步设计,不正确的理解需求会导致项目失败。

功能设计

  1. 在功能设计中有哪些隐喻?给出几个成功的例子。
  2. 如果有些功能的执行时间很长,怎么能让用户感觉不到太长的等待?
  3. 如果用户必须要在一个很小的区域内,从一个常常的列表中选择多个条目,你会用什么控件?
  4. 有哪些方法可以保证数据项的完整?
  5. 建立系统原型有哪些技术?
  6. 应用程序怎样建立对用户行为的预期?给出一些例子。
  7. 如何入手设计一组数量庞大而又复杂的特性,你能举出一些设计思路吗?
  8. 有一个列表,其中有10个元素,每个元素都有20个字段可以编辑,你怎样设计这种情况?如果是1000个元素,每个元素有3个字段呢?
  9. 用不同的颜色对一段文本中的文字标记高亮,这种做法有什么问题?
  10. Web环境和Windows环境各有些什么限制?

技术设计

  1. 什么是低耦合和高聚合?封装原则又是什么意 思?                          

低耦合是针对类与类间的依赖关系而言,类具有单一职责,低耦合度的类不依赖其它类,从而保持其独立性;高聚合针对类中各个职责的关联程度而言。封装原则是对类的可变性而言,应遵守开放-封闭原则,对修改封闭,对添加开放。

  1. Web应用中,你怎样避免几个人编辑同一段数据所造成的冲突?
  2. 知道设计模式吗?你用过哪些设计模式?在什么场合下用 的?        

可复用面向对象的设计中,设计模式是被反复使用的,经过分类的代码设计经验总结。其目的是为了可重用代码,让代码容易理解。设计模式的四个要素是模式名称、问题、解决方案、效果。
用过很多设计模式,Singlton,Factory,Template,Adapter,Composite等等。

  1. 是否了解什么是无状态的业务层?长事务如何与之相适应?
  2. 在搭建一个架构,或是技术设计时,你用过几种图?

UML图,类图,接口图。

  1. N层架构中都有哪些层?它们各自的职责是什么?
  2. 有哪些方法可以确保架构中数据的正确和健壮?
  3. 面向对象设计和面向组件设计有哪些不同之处?
  4. 怎样在数据库中对用户授权、用户配置、权限管理这几项功能建模?
  5. 怎样按照等级制度给动物王国(包括各种物种和各自的行为)建模?

程序设计

  1. 你怎样保证你的代码可以处理各种错误事件?
  2. 解释一下什么是测试驱动开发,举出极限编程中的一 些原 则。       

TDD是极限编程的一种,特点是用测试来驱动开发,需求和详细设计后,根据测试用例,先编写测试代码,最后根据需求,逐步完成功能代码的添加。典型的模型有"V"模型和"X"模型。极限编程原则:持续整合,测试,快速反馈,接受变化,简单设计,小版本,计划制定....

  1. 别人代码的时候,你最关心什么地 方?                                  

 目的,设计方法。

  1. 什么时候使用抽象类,什么时候使用接口?

抽象类(is-a)中的方法可以实现,接口中的方法不能在定义时实现(like-a)。两者都不能实例化。抽象类对其子类部分可见。接口可以实现类似多重继承(针对java)。因为接口一旦创建就不能改变,所以当要创建多个版本的组件时,应使用抽象类,因为通过更新基类,子类也会更新;或者希望通过基类控制子类的行为时(设计模式中的),也应该使用抽象类,因为可以通过在抽象基类中添加实现,控制子类的状态。接口的原则是接口不变性(对象的定义与实现),当创建的功能被大量不相关对象使用时,适合使用接口。

  1. 除了IDE以外,你还喜欢哪些必不可少的工具?

文本编辑和比较工具,sysinternalsdebug工具。

  1. 你怎么保证代码执行速度快,而又不出问题?

正确的算法设计;降低依赖耦合度;运用代码检测工具查找耗时模块。

  1. 什么时候用多态,什么时候用委派?
  2. 什么时候使用带有静态成员的类,什么时候使用单例?

带有静态成员的类能保证其不会被实例化。单例算是一个应用,其中构造函数设置成私有的,有一个静态方法获取该类的实例。单例是实现延迟实例化的方法,这种对象创建型的模式保证了一个类只有一个实例,可以控制其客户何时访问它。这对注册表的管理设计或多线程等一些地方有很大的左右。

  1. 你在代码里面怎么提前处理需求的变化?给一些例子。

增加读取配置文件的代码,若需求变化,修改相关的配置文件就可以了。

  1. 描述一下实现一段代码的过程,从需求到最终交付。

定义需求,概要设计,详细设计(组件划分,结构图),编码,单元测试,集成测试,完全测试,发布。

算法

  1. 怎样知道一个数字是不是2的乘方?怎样判断一个数是不是奇数?

2取余。(0现在也被认为是偶数了吧)

  1. 怎样找出链表中间的元素?

已得到长度:单个循环;未知长度:可用两个指针(一头一尾)实现。

  1. 怎样改变10,000个静态HTML页面中所有电话号码的格式?
  2. 举出一个你所用过的递归的例子。

删除非空文件夹所有内容(C++)

  1. 在散列表和排序后的列表中找一个元素,哪个查找速度最快?

散列表。

  1. 不管是书、杂志还是网络,你从中所学到的最后一点算法知识是什么?

KMP算法。

  1. 怎样把字符串反转?你能不用临时的字符串么?

递归、逐个交换、用栈结构实现等等。

  1. 你愿意用什么类型的语言来编写复杂的算法?

C/C++

  1. 有一个数组,里面是从11,000,000的整数,其中有一个数字出现了两次,你怎么找出那个重复的数字?
  2. 你知道旅行商问题(Traveling Salesman Problem么?

数据结构

  1. 怎样在内存中实现伦敦地铁的结构?
  2. 怎样以最有效的方式在数据库中存储颜色值?
  3. 队列和堆栈区别是什么?

队列是先进先出结构,堆栈是先进后出结构

  1. 用堆或者栈存储数据的区别是什么?
  2. 怎样在数据库中存储N维向量?
  3. 你倾向于用哪种类型的语言编写复杂的数据结构?

C/C++

  1. 21的二进制值是什么?十六制值呢?

二进制:00010101;十六进制:0x15

  1. 不管是书、杂志还是网络,你从中所学到的最后一点数据结构的知识是什么?

Dijkstra最短路径。

  1. 怎样在XML文档中存储足球比赛结果(包括队伍和比分)?
  2. 有哪些文本格式可以保存Unicode字符?

测试

  1. 什么是回归测试?怎样知道新引入的变化没有给现有的功能造成破坏?
  2. 如果业务层和数据层之间有依赖关系,你该怎么写单元测试?
  3. 你用哪些工具测试代码质量?

Rational PurifyRational QuantifyCode ReviewerIntel vTunesSource Monitor

  1. 在产品部署之后,你最常碰到的是什么类型的问题?
  2. 什么是代码覆盖率?有多少种代码覆盖率?

代码覆盖率是单元测试中衡量测试好坏的方式,是一种代码覆盖程度的衡量。种类很多,语句覆盖、判定覆盖、条件覆盖、路径覆盖等等。

  1. 功能测试和探索性测试的区别是什么?你怎么对网站进行测试?
  2. 测试套件、测试用例、测试计划,这三者之间的区别是什么?你怎么组织测试?
  3. 要对电子商务网站做冒烟测试,你会做哪些类型的测试?
  4. 客户在验收测试中会发现不满意的东西,怎样减少这种情况的发生?
  5. 你去年在测试和质量保证方面学到了哪些东西?

维护

  1. 你用哪些工具在维护阶段对产品进行监控?

Windbgsysinternals,脚本。

  1. 要想对一个正在产品环境中被使用的产品进行升级,该注意哪些重要事项?

升级不影响现有环境;升级的组件不影响其它组件的正常使用;不引入bug;若出现重大问题可顺利取消升级并具有rollback机制。

  1. 如果在一个庞大的文件中有错误,而代码又无法逐步跟踪,你怎么找出错误?

代码中加入Log机制;或者利用某些静态代码分析工具(限于语法或简单逻辑错误)

  1. 你怎样保证代码中的变化不会影响产品的其他部分?

运用合适的开发和设计方法,比如某些设计模式,组件模型等,减小组件间的耦合度。

  1. 你怎样为产品编写技术文档?

根据产品满足的需求,结合实际产品功能的划分,采用由顶向下逐步细化的方式(top-down)

  1. 你用过哪些方式保证软件产品容易维护?

代码中加入灵活的Log机制,产生bug后能及时发现错误点;尽量使用一些脚本、可配置ini文件或二进制文件,控制代码的执行方式,增加灵活度。

  1. 怎样在产品运行的环境中进行系统调试?
  2. 什么是负载均衡?负载均衡的方式有哪些种?
  3. 为什么在应用程序的生命周期中,软件维护费用所占的份额最高?
  4. 再造工程(re-engineering)和逆向工程(reverse engineering)的区别是什么?

配置管理

  1. 你知道配置管理中基线的含义么?怎样把项目中某个重要的时刻冻结?

基线标志着开发过程中的各个millstones。形成文档并复审通过后,基线就形成了,标志着开发过程中的一个阶段进入正式的可控状态。基线不是不可以修改,但必须经过合适的标准来评估,即Change Management中要求的过程进行。基线是进一步开发的的基准和出发点。

冻结项目中的重要时刻:形成规范文档,评估之后获得认证。

  1. 你一般会把哪些东西纳入版本控制?

Source Code,二进制文件

  1. 怎样可以保证团队中每个人都知道谁改变了哪些东西?

版本控制工具,脚本追踪。

  1. TagBranch的区别是什么?在什么情况下该使用tag,什么时候用branch

Tag即标签,不可提交。Branch是分支可以提交。Branch可以包含多个TagBranch是同一个系统上功能差异大或实现了不同需求的不同产品线,一般在新的产品线生成的时候产生;Tag是记录版本历史的Millstones,不能修改,一般在重要功能完成或去掉了某个功能,新建Branch之后,或者测试之前,生成Tag

  1. 怎样管理技术文档——如产品架构文档——的变化?
  2. 你用什么工具管理项目中所有数字信息的状态?你最喜欢哪种工具?
  3. 如果客户想要对一款已经发布的产品做出变动,你怎么处理?

对此产品的Source Code利用版本控制工具做Branch,不影响其他产品的维护,并在此Branch基础上做改动。

  1. 版本管理和发布管理有什么差异?
  2. 对文本文件的变化和二进制文件的变化进行管理,这二者有什么不同?
  3. 同时处理多个变更请求,或是同时进行增量开发和维护,这种事情你怎么看待?

项目管理

  1. 范围、时间、成本,这三项中哪些是可以由客户控制 的?                     

应该说客户对三者都具有直接或间接可控性。其中scope是客户可控性最大 的,项目初始化期,scope根据客户需求进行定义,planning阶段,客户可根据实际情况选择改变或添加需求,从而改变或增加scope。一个好的 PM或项目负责人应该在项目初期预见到scope可能的变化,从而进行相关的risk评估和change管理,以便降低scope对项目的影响。
对于timebudget,客户也可以进行间接控制。如果scope的变动对项目产生影响,PM应联系timebudget的实际责任人并进行相关的调整。当然这种情况对项目并不利。

  1. 谁该对项目中所要付出的一切做出估算?谁有权设置最后期限?

应该是PM需要做出估算。最后期限应该是协商的结果(个人理解)

  1. 少交付的次数,或是减少每个每个交付中的工作量,你喜欢哪种做 ?

第二种,能让客户切实感受到项目的进度,同时又对项目的完成产生信心。能看到实际的产生物才是客户想要的。

  1. 你喜欢用哪种图来跟踪项目进度? 

Gannt chart

  1. 代和增量的区别在哪 里?  

迭代和增量在开发过程中联系紧密。迭代是针对工作流而言,而增量是产品的完 善而言。迭代是一个完整的过程,开发过程中完成的目标就是一个迭代,比如第一次迭代完成初步的产品模型,第二次迭代实现产品功能的细节;增量是对某个功能 的添加或者完善,每次的增量都增加部分功能,最终形成完整产品。
现实中这两种开发方法常混合在一起使用。

  1. 试着解释一下风险管理中用到的实践。风险该如何管理?
  2. 你喜欢任务分解还是滚动式计划?
  3. 你需要哪些东西帮助你判断项目是否符合时间要求,在预算范围内运作?
  4. DSDMPrince2Scrum,这三者之间有哪些区别?

只了解Scrum,一种敏捷软件开发模型,能快速响应变化,有backlogsprintsprint backlogscrumMastertime-box等要点。

  1. 如果客户想要的东西太多,你在范围和时间上怎样跟他达成一致呢?

Scope 上,对于客户的需求进行分解,评估实际可能需要的时间,告知客户如果需求过多,TimeBudget会相应的增加。并和客户协调,尽可能在某个 deliverable交付部分主要的需求功能,把Scope的内容先缩小到一个可控时间的范围并让客户实际得到粗略的但是覆盖了重要功能的产品,而在下 一个deliverable完成另外某个Scope。按客户的要求分解Scope,从而实现时间上的细化。分清必须做的和不必要做的,立刻需要完成的和可 以延后完成的。

 

posted @ 2009-05-07 22:04 Da Vinci 阅读(33) | 评论 (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 阅读(204) | 评论 (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 阅读(69) | 评论 (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 阅读(103) | 评论 (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: sys
    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   sys32v   (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
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0x0):
    81f28000 4d              dec     ebp
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0x1):
    81f28001 5a              pop     edx
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0x2):
    81f28002 90              nop
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0x3):
    81f28003 0003            add     byte ptr [ebx],al
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0x5):
    81f28005 0000            add     byte ptr [eax],al
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0x7):
    81f28007 000400          add     byte ptr [eax+eax],al
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0xa):
    81f2800a 0000            add     byte ptr [eax],al
    sys32v!SysAllocatePostCopyWorkItem <PERF> (sys32v+0xc):
    81f2800c ff              ???

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

    kd> u 81f42780
    sys32v!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 [sys32v!SysDbgFlags (81f49c74)]
    81f42792 83e004          and     eax,4
    81f42795 7424            je      sys32v!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\Sys"
       +0x024 HardwareDatabase : 0x81af6ed8 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
       +0x028 FastIoDispatch   : (null)
       +0x02c DriverInit       : 0x81f4a005     sys32v!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 阅读(1788) | 评论 (1)编辑

    2008年3月25日


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

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



                         图1.CLR启动程序创建的域
               (转载自Inside .NET Framework:CLR runtime object)

    注:下文中的AppDomain特指应用程序域,对于系统域和共享域保持原来的指定

    要注意的是,这里的域不同于C#类中域的概念.C#类中域指类和对象相关的变量,这里的域其实是一种轻量的进程,你完全可以把它当成进程来理解.有人也许不明白既然有了进程的概念,干吗又搞出一个域来.其实这是为了实现在服务器中加载多个应用程序才提出的.AppDomain与进程相比优势在于:
         1.所需系统资源更少
         2.进程之间的AppDomain可以共享资源
    但是千万别把AppDomain和线程搞混,这二者之间没有从属的关系.地球人都知道线程是进程的孩子,但是线程和AppDomain的关系类似于正交关系.同一时间内多个线程可以运行在一个AppDomain中,多个AppDomain也可以跨越一个线程.

    下面, 我们先大致看一下系统域和共享域的一些基本概念,再着重探讨AppDomain的知识.

    系统域(System Domain):系统域初始化共享域和AppDomain。它在共享域中加载了mscorlib.dll()。同时,系统域产生了进程的接口ID(不知道这样描述对不对),以此来创建AppDomain的接口虚拟表映射的接口。在进程中,系统域监控所有的域,加载(load)和卸载(unload)AppDomain也有它完成。可以认为系统域是CLR中的基础域。

    共享域(Shared Domain):在.NET中,不是所有的代码都属于某个域,但这些代码却是用户代码所必需的。共享域就负责加载这部分代码。比如大家熟悉的Object,Array,String,delegate等。这些代码在CLR启动程序时先被加载到共享域中.共享域中有一个程序集映射表,通过这个表来查找共享的程序集的依赖关系. 可以认为共享域是CLR中的带有索引的共享文件夹.

    注:用户代码和控制台程序也可以加载到共享域.(具体方法?..不急..以后告诉你)

    AppDomain

    终于说到学习.NET要接触最多的AppDomain了.其实CLR启动应用程序时,就会自动创建一个AppDomain的实例,叫默认的AppDomain(Default AppDomain).大部分的应用程序在运行期间只创建一个域.有些应用程序有多个AppDomain,它们之间由.NET Remoting通信。AppDomain之间是彼此独立的。一个AppDomain无法访问其他的AppDomain的程序集和对象(并不意味着不能相互通信),并且可以定义自己的程序集访问策略。
    通过System.AppDomain类的实例,可以获得进程中一个AppDomain的引用:

        using System;
        using System.Threading;
        class MyAppDomain
       {
            static void Main()
           {
               Thread.CurrentThread.Name = "MyDomain";
               AppDomainSetup info = new AppDomainSetup();
               AppDomain myAppDomain = AppDomain.CurrentDomain;
               AppDomain newDomain = AppDomain.CreateDomain("MyDomain", null, info);
       
               myAppDomain.ExecuteAssembly("AssemblyName.exe"); // load your assembly name
               AppDomain.unload(myDomain);
            }
        }

    上面代码创建了一个AppDomain的实例,并调用ExecuteAssembly方法加载一个程序集。其中AppDomainSetup为CLR定位信息。unload为AppDomain类的卸载域方法。注意:这段代码中ExecuteAssembly()方法的线程调用了AssemblyName.exe程序集的线程。这样一个线程就跨越了两个AppDomain。这也说明了AppDomain和线程之间没有包含关系。

    到这里,我们已经对CLR中域的概念,各种域的结构有了一个大概的认识。AppDomain是CLR中最基本的概念,在CLR的整个框架中都与它有着千丝万缕的联系,掌握它是学习CLR的基础。有关AppDomain间通信的方法,我们在.NET Remoting篇中再详细阐述。


    posted @ 2008-03-25 22:25 Da Vinci 阅读(179) | 评论 (0)编辑

    2008年3月22日


    运用Windbg进行内核调试, 熟练的运用命令行是必不可少的技能. 但是面对众多繁琐的命令, 实在是不可能全部的了解和掌握. 而了解Kernel正是需要这些命令的指引, 不断深入理解其基本的内容. 下面, 将介绍最常用的一些指令, 使初学Kernel调试的朋友们能有一个大致的了解. 至于如何熟练的运用它们, 还需要实际的操作过程中进行反复的琢磨.

    Windbg能够方便的进行远程调试和本地进程调试(只限于User模式), 远程调试又分User mode和Kernel mode两种. 个人认为用Windbg进行远程的User mode调试还不如用Visual Studio来的方便, 毕竟要一些相应的配置才行, 而Visual Studio只需要远程机器的IP地址即可.(当然, 如果你在LiveCD或WinPE下进行User mode的调试, 这时候没有VS, Windbg就是不二之选了). Windbg的优势就在于远程的Kernel模式的调试, 这是VS所做不到的.

    1. 首先是设置符号路径(无论在User mode下还是Kernel mode下都需要), 在Windbg的符号路径对话框中, 输入以下符号路径:

          srv*c:\symbols*http://msdl.microsoft.com/download/symbols

    这个符号路径是自动在微软给定的symbols路径下进行自动搜索, 一些常用的符号都可以利用这个链接自动找到. 当然, 如果你的本地已经有相应的符号路径, 包含本地的也可以.

    2. 等Windbg的联机状态设置好后, 按下Ctrl+break, 中断当前的Kernel状态, 在Windbg的命令行窗口输入"?", 则会输出帮助菜单, 在这个Menu中会显示一些常用的命令:

      (1)断点指令
         B[C|D|E] [<bps>]
         clear|disable|enable breakpoints
     
         BL
         list breakpoints
     
         BP <address>
         set soft breakpoints
     
         BA <access> <size> <addr>
         break on access
     
      (2)数据查看指令
         D[type][<range>]
         dump memory
     
         DT [-n|y] [[mod!]name] [[-n|y]fields][address] [-l list] [-a[]|c|i|o|r[#]|v]
         dump using type information
     
         DV [<name>] 
         dump local variables
     
      (3)数据修改指令
         E[type] <address> [<values>]
         enter memory values
     
      (4)运行
         G[H|N] [=<address> [<address>...]] 
         go
     
         P [=<addr>] [<value>]
         step over
     
      (5)堆栈操作
         K[b|p|P|v]
     
      (6)显示加载的模块列表
         LM
         list modules
     
      (7)寄存器操作
         R [[<reg> [= <expr>]]]
         view or set registers
     
      (8)Search指令
         S[<opts>] <range> <values>
         search memory
     
      (9)跟踪指令T,TA,TB,TC,WT,P,PA,PC
     
      (10)退出
          Q

      (11)反汇编
          U[<range>]

    其中最常用的就是反汇编操作和显示模块操作. LM命令显示当前加载的模块. 当你连接过程中, Windbg提示相应的module找不到时, 就可以运用这个命令进行查看. LM的一个扩展命令是"lm t n", 这个命令显示当前所有加载的驱动信息(过去的命令是!driver),在调试内核驱动的过程中非常有用,可以找到相应驱动的起始地址。反汇编命令u, 可以在相应的地址中逐步的解析代码,这在内核调试中是最常用的一种查看代码的方式。

    除上面的一些基本命令之外,还有一些非常有用的指令:
     
      (1)K[KB|KP]
         显示当前的堆栈,当然也可以用alt+6直接调出窗口显示
     
      (2)!process
         显示当前的进程EXPROCESS状态,!process 0 0 显示所有的进程状态

      (3)!thread
         显示当前的线程状态,dt nt!_ethread显示ETHREAD结构

      (4)!drvobj [path]
         列出当前的驱动程序在驱动对象中的例程,其中path是驱动的设备路径,例如: !drvobj \filesystem\fat 2 列出FAT文件系统驱动的例程

      (5)dt nt!_*
         查看内核的数据结构
     
      (6)!stack 0
         显示线程当前地址
     
      (7)!ioapic
         查看I/O的中断控制器

      (8)!irql
         查看CPU的IRQL,这在CPU中断调试中非常有用

      (9)!exqueue
         可以看系统辅助的线程列表

      (10)!reg viewlist
         注册表的存储显示,!reg hivelist显示注册表一个存储的内存使用量

      (11)!vm
         显示系统的内存池信息

      (12)dt _TOKEN
         显示内部访问令牌

      (13)!object \device
         显示设备对象信息,用winobj工具也可以看到

    以上命令是一些常用的内核调试命令, 还有非常多的命令技巧, 不可能全部一一解释. 在今后的文章, 还会结合具体的调试过程分析进行逐步介绍. 内核的调试无非是处理器, 系统设备, 内存, 进程线程, 注册表, 驱动这几大类的信息, 每一类都有很多的命令, 需要我们在实际的调试过程中不断认识和体会它的用法. 当然, 理解这些命令用法的前提是需要我们对操作系统内部构造有一个清晰的认识. 这需要不断的学习, 对OS有一个全局的把握, 这样才能更深入的理解它. 而通过Windbg的内核调试, 我们有一种更好的方式直接与Kernel打交道, 这对我们深入理解和认识操作系统有很大的帮助. 下一节我们将通过一个实际的调试driver的例子, 来进一步认识Windbg在内核调试中的作用.


    posted @ 2008-03-22 12:30 Da Vinci 阅读(1208) | 评论 (0)编辑

    2008年3月20日


    Windbg进行内核调试,需要一些基本的技巧和设置,在这个系列文章中,我将使用Windbg过程中所遇到的一些问题和经验记录下来,算是对Kernel调试的一个总结,同时也是学习Windows系统内核的另一种过程。

    很多人说Windbg不如SoftIce好用, 但是我使用过程中还是觉得Windbg能更好的反映系统状态, 而且相比SoftIce, Windbg更稳定(虽然它的部分操作略显复杂), 下面介绍Windbg的Kernel模式调试第一部分: 双机连接设置.

    Vista和XP不同, 没有boot.ini文件, 需要用bcdedit进行启动设置。(关于启动数据配置编辑器BCD的具体设置, 参见另一篇文章: (From MS)Vista: 启动配置数据编辑器(BCD))

    在administrator权限下, 进入command line模式,  键入bcdedit命令, 会出现以下界面:

     

    然后, 设置端口COM1, baudrate为115200 (除COM1外, 也可以用1394或USB. 1394用起来比COM口快多了, 当然前提是你需要有1394卡及其驱动. 很恶心的是Vista不再支持1394的文件传输协议, 但是用windbg双机调试还是可以的)
    命令为:
    bcdedit /dbgsettings {serial [baudrate:value][debugport:value] | 1394 [channel:value] | usb }



    接着, 我们需要复制一个开机选项, 以进入OS的debug模式
    命令为:
    bcdedit /copy {current} /d DebugPoint
    DebugPoint为选项名称, 名字可以自己定义. 然后复制得到的ID号.



    接着增加一个新的选项到引导菜单
    bcdedit /displayorder {current} {ID}
    这里的{ID}的ID值是刚生成的ID值.



    激活DEBUG : bcdedit /debug {ID} ON
    这里的{ID} 的ID值还是刚才的ID值.



    命令执行成功后, 重新启动机器.

    选择DebugPoint登录,开启Windbg

    连接成功, 则显示如下:
    Microsoft (R) Windows Debugger  Version 6.6.0007.5
    Copyright (c) Microsoft Corporation. All rights reserved.

    Opened \\.\pipe\com_1
    Waiting to reconnect...
    Connected to Windows Vista 6000 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.
    Symbol search path is: symsrv*symsrv.dll*F:\symbols*http://msdl.microsoft.com/download/symbols
    Executable search path is: 
    Windows Vista Kernel Version 6000 MP (1 procs) Free x86 compatible
    Built by: 6000.16386.x86fre.vista_rtm.061101-2205
    Kernel base = 0x81800000 PsLoadedModuleList = 0x81911db0
    System Uptime: not available
    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:
    81881760 cc              int     3

    总结: 虽然利用VMware虚拟机能更方便的设置双机的调试环境, 而且这种模拟环境也是大多数人使用的(方便), 但是如果有双机条件的话,  还是希望大家能够使用两台机器, 因为用虚拟机进行Kernel调试, 真不是一般的慢! 基本就等于死机. 即时你的主机内存2G, 分给VMware1G, 还是会相当卡(Kerenl模式与User模式不同).

    posted @ 2008-03-20 14:38 Da Vinci 阅读(1571) | 评论 (1)编辑


    操作文件基本上是每个应用程序都必须做的事情。除了必要的配置信息外,用户的工作最终都要以文件的形式保存到磁盘上。保存和获取这些信息可以使用独立的磁盘文件,也可以使用系统自带的数据库——注册表。

    本章首先介绍底层操作文件的API函数和MFC中对 应的CFile类;然后介绍一些与操作文件相关的逻辑驱动器和目录方面的知识,包括驱动器的格式化和卷标设置、目录的创建和删除等;接着,本章介绍使用 API函数和ATL库中的CRegKey类操作注册表的方法;本章还重点讨论了内存映射文件在读写磁盘文件和建立共享内存方面的应用;本章最后介绍一个多 线程的文件分割系统的开发过程。

    8.1  文件操作

    文件的输入输出(I/O)服务是操作系统的重要部 分。Windows提供了一类API函数来读、写和管理磁盘文件。MFC将这些函数转化为一个面向对象的类——CFile,它允许将文件视为可以由 CFile成员函数操作的对象,如Read和Write等。CFile类实现了程序开发者执行底层文件I/O需要的大部分功能。

    并不是在任何时候使用CFile类都是方便的,特别是要与底层设备(如COM口、设备驱动)进行交互的时候,所以本节主要讨论管理文件的API函数。事实上,了解这些函数之后,自然就会使用CFile类了。

    8.1.1  创建和读写文件

    使用API函数读写文件时,首先要使用 CreateFile函数创建文件对象(即打开文件),调用成功会返回文件句柄;然后以此句柄为参数调用ReadFile和WriteFile函数,进行 实际的读写操作;最后调用CloseHandle函数关闭不再使用的文件对象句柄。

    1.打开和关闭文件

    CreateFile是一个功能相当强大的函数,Windows下的底层设备差不多都是由它打开的。它可以创建或打开文件、目录、物理磁盘、控制台缓冲区、邮槽和管道等。调用成功后,函数返回能够用来访问此对象的句柄,其原型如下:

    HANDLE CreateFile (

      LPCTSTR lpFileName,                                                // 要创建或打开的对象的名称

      DWORD dwDesiredAccess,                                 // 文件的存取方式

      DWORD dwShareMode,                                                // 共享属性

      LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性

      DWORD dwCreationDisposition,                                 // 文件存在或不存在时系统采取的行动

      DWORD dwFlagsAndAttributes,                                   // 新文件的属性

      HANDLE hTemplateFile                                                // 一个文件模板的句柄

    );

    各参数含义如下。

    (1)lpFileName参数是要创建或打开的对 象的名称。如果打开文件,直接在这里指定文件名称即可;如果操作对象是第一个串口,则要指定“COM1”为文件名,然后就可以像操作文件一样操作串口了; 如果要打开本地电脑上的一个服务,要以“"""."服务名称"”为文件名,其中的“.”代表本地机器;也可以使用CreateFile打开网络中其他主机 上的文件,此时的文件名应该是“""主机名"共享目录名"文件名”。

    (2)dwDesiredAcces参数是访问方式,它指定了要对打开的对象进行何种操作。指定GENERIC_READ标志表示以只读方式打开;指定GENERIC_WRITE标志表示以只写方式打开;指定这两个值的组合,表示要同时对打开的对象进行读写操作。

    (3)dwShareMode参数指定了文件对象的共享模式,表示文件打开后是否允许其他代码以某种方式再次打开这个文件,它可以是下列值的一个组合:

    l          0                                             不允许文件再被打开。C语言中的fopen函数就是这样打开文件的

    l          FILE_SHARE_DELETE      允许以后的程序代码对文件删除文件(Win98系列的系统不支持这                                          个标志)

    l          FILE_SHARE_READ          允许以后的程序代码以读方式打开文件

    l          FILE_SHARE_WRITE         允许以后的程序代码以写方式打开文件

    (4)dwCreationDisposition参数指定了当文件已存在或者不存在时系统采取的动作。在这里设置不同的标志就可以决定究竟是要打开文件,还是要创建文件。参数的可能取值如下:

    l          CREATE_ALWAYS    创建新文件。如果文件存在,函数会覆盖这个文件,清除存在的属性

    l          CREATE_NEW                    创建新文件。如果文件存在,函数执行失败

    l          OPEN_ALWAYS                  如果文件已经存在,就打开它,不存在则创建新文件

    l          OPEN_EXISTING               打开存在的文件。如果文件不存在,函数执行失败

    l          TRUNCATE_EXISTING    打开文件并将文件截断为零,当文件不存在时函数执行失败

    (5)dwFlagsAndAttributes参数用来指定新建文件的属性和标志。文件属性可以是下面这些值的组合:

    l          FILE_ATTRIBUTE_ARCHIVE            标记归档属性

    l          FILE_ATTRIBUTE_HIDDEN              标记隐藏属性

    l          FILE_ATTRIBUTE_READONLY        标记只读属性

    l          FILE_ATTRIBUTE_READONLY        标记系统属性

    l          FILE_ATTRIBUTE_TEMPORARY    临时文件。操作系统会尽量把所有文件的内容保持在内                                                             存中以加快存取速度。使用完后要尽快将它删除

    此参数还可同时指定对文件的操作方式,下面是一些比较常用的方式:

    l          FILE_FLAG_DELETE_ON_CLOSE    文件关闭后系统立即自动将它删除

    l          FILE_FLAG_OVERLAPPED               使用异步读写文件的方式

    l          FILE_FLAG_WRITE_THROUGH      系统不会对文件使用缓存,文件的任何改变都会被系统                                                             立即写入硬盘

    (6)hTemplateFile参数指定了一个文件模板句柄。系统会复制该文件模板的所有属性到当前创建的文件中。Windows 98系列的操作系统不支持它,必须设为NULL。

    打开或创建文件成功时,函数返回文件句柄,失败时返回INVALID_HANDLE_VALUE(-1)。如果想再详细了解失败的原因,可以继续调用GetLastError函数。

    用不同的参数组合调用CreateFile函数可以完成不同的功能,例如,下面的代码为读取数据打开了一个存在的文件。

             HANDLE hFile;

             hFile = ::CreateFile("myfile.txt",             // 要打开的文件

                       GENERIC_READ,                         // 要读这个文件

                       FILE_SHARE_READ,                            // 允许其他程序已只读形式再次打开它

                       NULL,                                            // 默认安全属性

                       OPEN_EXISTING,                        // 仅仅打开存在的文件(如果不存不创建)

                       FILE_ATTRIBUTE_NORMAL,  // 普通文件

                       NULL);                                           // 没有模板

             if(hFile == INVALID_HANDLE_VALUE)

             {                ……// 不能够打开文件       }

    仅当当前目录中存在名称为myfile.txt的文 件时,上面的CreateFile才能执行成功。由于为dwCreationDisposition参数指定了OPEN_EXISTING,所以当要打开 的文件不存在时,CreateFile返回INVALID_HANDLE_VALUE,而不会创建这个文件。如果想创建一个文件以便向里面写入数据,可以 使用下面的代码:

             HANDLE hFile;

             hFile = CreateFile("myfile.txt",                         // 要创建的文件

                       GENERIC_WRITE,                       // 要写这个文件

                       0,                                                     // 不共享

                       NULL,                                            // 默认安全属性

                       CREATE_ALWAYS,                      // 如果存在就覆盖

                       FILE_ATTRIBUTE_NORMAL,  // 普通文件

                       NULL);                                           // 没有模板

             if(hFile == INVALID_HANDLE_VALUE)

             {                ……// 不能够打开文件       }

    要关闭打开的文件,直接以CreateFile返回的文件句柄调用CloseHandle函数即可。

    2.移动文件指针

    系统为每个打开的文件维护一个文件指针,指定对文件 的下一个读写操作从什么位置开始。随着数据的读出或写入,文件指针也随之移动。当文件刚被打开时,文件指针处于文件的头部。有时候需要随机读取文件内容, 这就需要先调整文件指针,SetFilePointer函数提供了这个功能,原型如下:

    DWORD SetFilePointer (

                     HANDLE hFile,                           // 文件句柄

                     LONG lDistanceToMove,            // 要移动的距离

                     PLONG lpDistanceToMoveHigh,         // 移动距离的高32位,一般设置为NULL

                     DWORD dwMoveMethod          // 移动的模式

                       );

    dwMoveMethod参数指明了从什么地方开始移动,可以是下面的一个值:

    l          FILE_BEGIN               开始移动位置为0,即从文件头部开始移动

    l          FILE_CURRENT        开始移动位置是文件指针的当前值

    l          FILE_END                            开始移动位置是文件的结尾,即从文件尾开始移动

    函数执行失败返回-1,否则返回新的文件指针的位置。

    文件指针也可以移动到所有数据的后面,比如现在文件的长度是100 KB,但还是可以成功的将文件指针移动到1000 KB的位置。这样做可以达到扩展文件长度的目的。

    SetEndOfFile函数可以截断或者扩展文件。该函数移动指定文件的结束标志(end-of-file,EOF)到文件指针指向的位置。如果文件扩展,旧的EOF位置和新的EOF位置间的内容是未定义的。SetEndOfFile函数的用法如下:

    BOOL SetEndOfFile(HANDLE hFile );

    截断或者扩展文件时,要首先调用SetFilePointer移动文件指针,然后再调用SetFilePointer函数设置新的文件指针位置为EOF。

    3.读写文件

    读写文件的函数是ReadFile和WriteFile,这两个函数既可以同步读写文件,又可以异步读写文件。而函数ReadFileEx和WriteFileEx只能异步读写文件。

    从文件读取数据的函数是ReadFile,向文件写入数据的函数是WriteFile,操作的开始位置由文件指针指定。这两个函数的原型如下:

    BOOL ReadFile(

           HANDLE hFile,                                       // 文件句柄

           LPVOID lpBuffer,                                          // 指向一个缓冲区,函数会将读出的数据返回到这里

           DWORD nNumberOfBytesToRead,      // 要求读入的字节数

           LPDWORD lpNumberOfBytesRead,    // 指向一个DWORD类型的变量,

                                                                               // 用于返回实际读入的字节数

           LPOVERLAPPED lpOverlapped                    // 以便设为NULL

    );

    BOOL WriteFile (hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);

    当用WriteFile写文件时,写入的数据通常被Windows暂时保存在内部的高速缓存中,等合适的时候再一并写入磁盘。如果一定要保证所有的数据都已经被传送,可以强制使用FlushFileBuffers函数来清空数据缓冲区,函数的惟一参数是要操作的文件句柄。

    BOOL FlushFileBuffers (HANDLE hFile );

    4.锁定文件

    当对文件数据的一致性要求较高时,为了防止程序在写入的过程中其他进程刚好在读取写入区域的内容,可以对已打开文件的某个部分进行加锁,这就可以防止其他进程对该区域进行读写。加锁和解锁的函数是LockFile和UnlockFile,它们的原型如下:

    BOOL  LockFile(

        HANDLE hFile,                                               // 文件句柄

        DWORD dwFileOffsetLow,                            // 加锁的开始位置

        DWORD dwFileOffsetHigh,

        DWORD nNumberOfBytesToLockLow,        // 加锁的区域的大小

        DWORD nNumberOfBytesToLockHigh

        );

    UnlockFile ( hFile, dwFileOffsetLow, dwFileOffsetHigh,

                       nNumberOfBytesToUnlockLow, nNumberOfBytesToUnlockHigh);

    dwFileOffsetLow和 dwFileOffsetHigh参数组合起来指定了加锁区域的开始位置,nNumberOfBytesToLockLow和 nNumberOfBytesToLockHigh参数组合起来指定了加锁区域的大小。这两个参数都指定了一个64位的值,在Win32中,只使用32位 就够了。

    如果加锁文件的进程终止,或者文件关闭时还未解锁,操作系统会自动解除对文件的锁定。但是,操作系统解锁文件花费的时间取决于当前可用的系统资源。因此,进程终止时最好显式地解锁所有已锁定的文件,以免造成这些文件无法访问。

    8.1.2  获取文件信息

    1.获取文件类型

    Windows下的许多对象都称之为文件,如果想知道一个文件句柄究竟对应什么对象,可以使用GetFileType函数,原型如下:

    DWORD GetFileType(HANDLE hFile);

    函数的返回值说明了文件类型,可以是下面的一个值:

    l          FILE_TYPE_CHAR             指定文件是字符文件,通常是LPT设备或控制台

    l          FILE_TYPE_DISK               指定文件是磁盘文件

    l          FILE_TYPE_PIPE                指定文件是套节字,一个命名的或未命名的管道

    l          FILE_TYPE_UNKNOWN 不能识别指定文件,或者函数调用失败

    2.获取文件大小

    如果确定操作的对象是磁盘文件,还可以使用GetFileSize函数取得这个文件的长度。

    DWORD GetFileSize(

      HANDLE hFile,                // 文件句柄

      LPDWORD lpFileSizeHigh       // 用于返回文件长度的高字。可以指定这个参数为NULL

    );

    函数执行成功将返回文件大小的低双字,如果lpFileSizeHigh参数不是NULL,函数将文件大小的高双字放入它指向的DWORD变量中。

    如果函数执行失败,并且 lpFileSizeHigh是NULL,返回值将是INVALID_FILE_SIZE;如果函数执行失败,但lpFileSizeHigh不是 NULL,返回值是INVALID_FILE_SIZE,进一步调用GetLastError会返回不为NO_ERROR的值。

    如果返回值是INVALID_FILE_SIZE, 应用程序必须调用GetLastError来确定函数调用是否成功。原因是,当lpFileSizeHigh不为NULL或者文件大小为 0xffffffff时,函数虽然调用成功了,但依然会返回INVALID_FILE_SIZE。这种情况下,GetLastError会返回 NO_ERROR来响应成功。

    3.获取文件属性

    如果要查看文件或者目录的属性,可以使用GetFileAttributes函数,它会返回一系列FAT风格的属性信息。

    DWORD GetFileAttributes(LPCTSTR lpFileName);        // lpFileName指定了文件或者目录的名称

    函数执行成功,返回值包含了指定文件或目录的属性信息,可以是下列取值的组合:

    l          FILE_ATTRIBUTE_ARCHIVE                 文件包含归档属性

    l          FILE_ATTRIBUTE_COMPRESSED        文件和目录被压缩

    l          FILE_ATTRIBUTE_DIRECTORY            这是一个目录

    l          FILE_ATTRIBUTE_HIDDEN                            文件包含隐含属性

    l          FILE_ATTRIBUTE_NORMAL                 文件没有其他属性

    l          FILE_ATTRIBUTE_READONLY             文件包含只读属性

    l          FILE_ATTRIBUTE_SYSTEM                   文件包含系统属性

    l          FILE_ATTRIBUTE_TEMPORARY T      文件是一个临时文件

    这些属性对目录也同样适用。INVALID_FILE_ATTRIBUTES(0xFFFFFFFF)是函数执行失败后的返回值。

    下面是快速检查某个文件或目录是否存在的自定义函数,可以将它用在自己的工程中。

    BOOL FileExists(LPCTSTR lpszFileName, BOOL bIsDirCheck)

    {       // 试图取得文件的属性

                       DWORD dwAttributes = GetFileAttributes(lpszFileName);

         if(dwAttributes == 0xFFFFFFFF)

            return FALSE;

             if ((dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY)

             {       if (bIsDirCheck)

                                return TRUE;

                       else

                                return FALSE;

             }

             else

             {       if (!bIsDirCheck)

                                return TRUE;

                       else

                                return FALSE;

             }

    }

    第2个参数bIsDirCheck指定要检查的对象是目录还是文件。

    与GetFileAttributes相对应的函数是SetFileAttributes,这个函数用来设置文件属性。

    BOOL SetFileAttributes(

      LPCTSTR lpFileName,              // 目标文件名称

      DWORD dwFileAttributes                 // 要设置的属性值

    );

    8.1.3  常用文件操作

    1.拷贝文件

    拷贝文件的函数是CopyFile和CopyFileEx,其作用都是复制一个存在的文件到一个新文件中。CopyFile函数的用法如下:

    BOOL CopyFile(

      LPCTSTR lpExistingFileName, // 指定已存在的文件的名称

      LPCTSTR lpNewFileName,                // 指定新文件的名称

      BOOL bFailIfExists                    // 如果指定的新文件存在是否按出错处理

    );

    CopyFileEx函数的附加功能是允许指定一个回调函数,在拷贝过程中,函数每拷贝完一部分数据,就会调用回调函数。用户在回调函数中可以指定是否停止拷贝,还可以显示进度条来指示拷贝的进度。

    2.删除文件

    删除文件的函数是DeleteFile,仅有的参数是要删除文件的名称。

    BOOL DeleteFile(LPCTSTR lpFileName);

    如果应用程序试图删除不存在的文件,DeleteFile将执行失败。如果目标文件是只读的,函数也会执行失败,出错代码为ERROR_ACCESS_DENIED。为了删除只读文件,先要去掉其只读属性。

    DeleteFile函数可以标识一个文件为“关闭时删除”。因此,直到最后一个到此文件的句柄关闭之后,文件才会被删除。

    下面的自定义函数RecursiveDelete示例了如何删除指定目录下的所有文件和子目录。

    void RecursiveDelete(CString szPath)

    {       CFileFind ff;       // MFC将查找文件的API封装到了CFileFind类。读者可参考下面的框架使用这个类

             CString strPath = szPath;

             // 说明要查找此目录下的所有文件

             if(strPath.Right(1) != """")

                       strPath += """";

             strPath += "*.*";

             BOOL bRet;

             if(ff.FindFile(strPath))

             {       do

                       {       bRet = ff.FindNextFile();

                                if(ff.IsDots())  // 目录为“.”或者“..”?

                                         continue;

                                strPath = ff.GetFilePath();

                                if(!ff.IsDirectory())

                                {       // 删除此文件

                                         ::SetFileAttributes(strPath, FILE_ATTRIBUTE_NORMAL);

                                         ::DeleteFile(strPath);

                                }

                                else

                                {       // 递归调用

                                         RecursiveDelete(strPath);

                                         // 删除此目录(RemoveDirectory只能删除空目录)

                                         ::SetFileAttributes(strPath, FILE_ATTRIBUTE_NORMAL);

                                         ::RemoveDirectory(strPath);

                                }

                       }

                       while(bRet);

             }

    }

    用DeleteFile函数删除的文件不会被放到回收站,它们将永远丢失,所以请小心使用RecursiveDelete函数。

    3.移动文件

    移动文件的函数是MoveFile和MoveFileEx函数。它们的主要功能都是用来移动一个存在的文件或目录。MoveFile函数用法如下:

    BOOL MoveFile(

      LPCTSTR lpExistingFileName, // 存在的文件或目录

      LPCTSTR lpNewFileName                 // 新的文件或目录

    );

    当需要指定如何移动文件时,请使用MoveFileEx函数。

    BOOL MoveFileEx(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, DWORD dwFlags);

    dwFlags参数可以是下列值的组合:

    l          MOVEFILE_DELAY_UNTIL_REBOOT      函数并不马上执行,而是在操作系统下一此重新启动时才移动文件。在AUTOCHK执行之后,系统立即移动文件,这是在创建任何分页文件之前进行的。因此,这个值使函数能够删除上一次运行时使用的分页文件。只有拥有管理员权限的用户才可以使用这个值

    l          MOVEFILE_REPLACE_EXISTING             如果目标文件已存在的话,就将它替换掉

    l          MOVEFILE_WRITE_THROUGH                 直到文件实际从磁盘移除之后函数才返回

    如果指定了MOVEFILE_DELAY_UNTIL_REBOOT标记,lpNewFileName参数可以指定为NULL,这种情况下,当系统下一次启动时,操作系统会删除lpExistingFileName参数指定的文件。

    8.1.4  检查PE文件有效性的例子

    PE文件格式是任何可执行模块或者DLL的文件格 式,PE文件以64字节的DOS文件头(IMAGE_DOS_HEADER结构)开始,之后是一小段DOS程序,然后是248字节的NT文件头 (IMAGE_NT_HEADERS结构)。NT文件头的偏移地址由IMAGE_DOS_HEADER结构的e_lfanew成员给出。

    检查文件是不是有效PE文件的一个方法是检查IMAGE_DOS_HEADER和IMAGE_NT_HEADERS结构是否有效。IMAGE_DOS_HEADER结构定义如下:

    typedef struct _IMAGE_DOS_HEADER {     

        WORD   e_magic;                       // DOS可执行文件标记,为“MZ”。依此识别DOS头是否有效

             ...                                                              // 其他成员,没什么用途

        LONG   e_lfanew;                       // IMAGE_NT_HEADERS结构的地址

      } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

    IMAGE_NT_HEADERS结构定义如下:

    typedef struct _IMAGE_NT_HEADERS { 

                               DWORD Signature;             // PE文件标识,为“PE"0"0”。依此识别NT文件头是否有效

                               IMAGE_FILE_HEADER FileHeader; 

                               IMAGE_OPTIONAL_HEADER OptionalHeader;

                      } IMAGE_NT_HEADERS,

    为了编程方便,Windows为DOS文件标记和PE文件标记都定义了宏标识。

    #define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ

    #define IMAGE_NT_SIGNATURE                  0x00004550     // PE00

    检查文件是否为PE文件的步骤如下:

    (1)检验文件头部第一个字的值是否等于IMAGE_DOS_SIGNATURE,是则说明DOS MZ头有效。

    (2)一旦证明文件的DOS头有效后,就可用e_lfanew来定位PE头了。

    (3)比较PE头的第一个字是否等于IMAGE_NT_SIGNATURE。如果这个值也匹配,那么就认为该文件是一个有效的PE文件。

    下面是验证PE文件有效性的代码,在配套光盘的08ValidPE工程下。

    BOOL CMyApp::InitInstance()

    {       // 弹出选择文件对话框

             CFileDialog dlg(TRUE);

             if(dlg.DoModal() != IDOK)

                       return FALSE;

             // 打开检查的文件

             HANDLE hFile = ::CreateFile(dlg.GetFileName(), GENERIC_READ,

                       FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

             if(hFile == INVALID_HANDLE_VALUE)

                                MessageBox(NULL, "无效文件!", "ValidPE", MB_OK);

             // 定义PE文件中的DOS头和NT头

             IMAGE_DOS_HEADER dosHeader;

             IMAGE_NT_HEADERS32 ntHeader;

             // 验证过程

             BOOL bValid = FALSE;

             DWORD dwRead;

             // 读取DOS头

             ::ReadFile(hFile, &dosHeader, sizeof(dosHeader), &dwRead, NULL);

             if(dwRead == sizeof(dosHeader))

             {       if(dosHeader.e_magic == IMAGE_DOS_SIGNATURE) // 是不是有效的DOS头?

                       {       // 定位NT头

                                if(::SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) != -1)

                                {       // 读取NT头

                                         ::ReadFile(hFile, &ntHeader, sizeof(ntHeader), &dwRead, NULL);

                                         if(dwRead == sizeof(ntHeader))

                                         {       if(ntHeader.Signature == IMAGE_NT_SIGNATURE)      // 是不是有效的NT头

                                                            bValid = TRUE;

                                         }

                                }

                       }

             }

             // 显示结果

             if(bValid)

                       MessageBox(NULL, "是一个PE格式的文件!", "ValidPE", MB_OK);

             else

                       MessageBox(NULL, "不是一个PE格式的文件!", "ValidPE", MB_OK);

             ::CloseHandle(hFile);

             return FALSE;

    }

    上述代码简单明确,先利用Windows定义的宏 IMAGE_DOS_SIGNATURE判断DOS头,比较DOS头的e_magic字段;再通过DOS头的e_lfanew字段定位到NT头;最后检查 NT头的Signature字段是不是IMAGE_NT_SIGNATURE(即“PE"0"0”)。

    8.1.5  MFC的支持(CFile类)

    CFile是一个相当简单的封装了一部分文件I/O 处理函数的类。它的成员函数用于打开和关闭文件、读写文件数据、删除和重命名文件、取得文件信息。它的公开成员变量m_hFile保存了与CFile对象 关联的文件的文件句柄。一个受保护的CString类型的成员变量m_strFileName保存了文件的名称。成员函数GetFilePath、 GetFileName和GetFileTitle能够用来提取整个或者部分文件名。比如,如果完整的文件名是“C:"MyWork" File.txt”,GetFilePath返回整个字符串,GetFileName返回“File.txt”,GetFileTitle返回 “File”。

    但是详述这些函数就会忽略CFile类的特色,这就是用来写数据到磁盘和从磁盘读数据的函数。下面简单介绍CFile类用法。

    1.打开和创建文件

    使用CFile类打开文件有两种方法。

    (1)构造一个未初始化的CFile对象,调用CFile::Open函数。下面的部分代码使用这个技术以读写权限打开一个名称为File.txt的文件。

    CFile file;

    if(file.Open(_T ("File.txt"), CFile::modeReadWrite))

    {       // 打开文件成功}

    CFile::Open函数的返回值是BOOL类型的变量。如果打开文件出错,还想进一步了解出错的原因,可以创建一个CFileException对象,传递它的地址到Open函数的第3个参数。

    CFile file;

    CFileException e;

    if (file.Open(_T ("File.txt"), CFile::modeReadWrite, &e))

    {       // 打开文件成功}

    else

    {       // 打开文件失败,告诉用户原因

             e.ReportError();

    }

    如果打开失败,CFile::Open函数会使用描 述失败原因的信息初始化一个CFileException对象。ReportError成员函数基于这个信息显示一个出错对话框。可以通过检查 CFileException类的公有成员m_cause找到导致这个错误的原因。

    (2)使用CFile类的构造函数。可以将创建文件对象和打开文件合并成一步,如下面代码所示。

    CFile file(_T ("File.txt"), CFile::modeReadWrite);

    如果文件不能被打开,CFile的构造函数会抛出一个CFileException异常。因此,使用CFile::CFile函数打开文件的代码通常使用try和catch块来捕获错误。

    try

    {       CFile file(_T ("File.txt"), CFile::modeReadWrite);

    }

    catch(CFileException* e)

    {       // 出错了!

             e->ReportError();

             e->Delete();

    }

    删除MFC抛出的异常是程序写作者的责任,所以在程序中处理完异常之后要调用异常对象的Delete函数。

    为了创建一个文件而不是打开一个存在的文件,要在CFile::Open或者CFile构造函数的第二个参数中包含上CFile::modeCreate标记,如下代码所示。

    CFile file(_T("File.txt"), CFile::modeReadWrite | CFile::modeCreate);

    如果以这种方式创建的文件存在,它的长度会被截为0。为了在文件不存在时创建它,存在的时候仅打开而不截去,应再包含上CFile::modeNoTruncate标记,如下面代码所示。

    CFile file(_T("File.txt"), CFile::modeReadWrite | CFile::modeCreate | CFile::modeNoTruncate);

    默认情况下,由CFile::Open或 CFile::CFile打开的文件使用的是独占模式,即CreateFile API中的第3个参数dwShareMode被设为了0。如果需要,在打开文件时也可以指定一个共享模式,以明确同意其他访问此文件的操作。这里是4个可 以选择的共享模式:

    l          CFile::shareDenyNone         不独占这个文件

    l          CFile::shareDenyRead          拒绝其他代码对这个文件进行读操作

    l          CFile::shareDenyWrite         拒绝其他代码对这个文件进行写操作

    l          CFile::shareExclusive            拒绝其他代码对这个文件进行读和写操作(默认)

    另外,还可以指定下面3个对象访问类型中的一个:

    l          CFile::modeReadWrite         请求读写访问

    l          CFile::modeRead                  仅请求读访问

    l          CFile::modeWrite                 仅请求写访问

    常用的做法是允许其他程序以只读方式打开文件,但是拒绝它们写入数据。

    CFile file(_T("File.txt"), CFile::modeReadWrite | CFile::modeCreate | CFile::modeNoTruncate);

    如果在上面的代码执行之前,文件已经以可写的方式打开了,这个调用将会失败,CFile类会抛出CFileException异常,异常对象的m_cause成员等于CFileException::sharingViolation。

    CFile类的成员函数Close会调用 CloseHandle API关闭应用程序打开的文件对象句柄。如果句柄没有关闭,类的析构函数也会调用Close函数关闭它。显式调用Close函数一般都是为了关闭当前打开 的文件,以便使用同样的CFile对象打开另一个文件。

    2.读写文件

    CFile类中从文件中读取数据的成员函数是Read。例如,下面的代码申请了一块4KB大小的文件I/O缓冲区,每次从文件读取4KB大小的数据。

    BYTE buffer[4096];

    CFile file (_T("File.txt"), CFile::modeRead);

    DWORD dwBytesRemaining = file.GetLength();

    while(dwBytesRemaining)

    {       UINT nBytesRead = file.Read(buffer, sizeof(buffer));

             dwBytesRemaining -= nBytesRead;

    }

    文件中未读取的字节数保存在 dwBytesRemaining变量里,此变量由CFile::GetLength返回的文件长度初始化。每次调用Read之后,从文件中读取的字节数 (nBytesRead)会从dwBytesRemaining变量里减去。直到dwBytesRemaining为0整个while循环才结束。

    CFile类还提供了Write成员函数向文件写入数据,Seek成员函数移动文件指针,它们都和相关API一一对应。可以通过跟踪程序的执行来查看这些函数的实现代码。

    posted @ 2008-03-20 08:44 Da Vinci 阅读(544) | 评论 (0)编辑

    Firefox 3