九天雁翎的博客
如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。 -- Paul Graham

SelfExtractor自解压模块理解文档

SelfExtractor理解文档

 九天雁翎:blog.jtianling.cn

ReadToc(CString Filename)

首先从文件尾反向Seek一个strlen(文件标志)的距离,然后读取文件标志,判断此文件标志.错误即返回INVALID_SIG.

然后再从文件尾反向Seek一个strlen(文件标志)+sizeof(int)的距离,读取一个整数,此整数代表文件数量.当其为0,即返回NOTHING_TO_DO,

 

然后根据文件数量,循环依次反向的Seek,读取文件名长度,文件名,文件长度,文件在文档中的偏移值.将各文件的信息保存在m_InfoArray数组中,将最后得到的偏移值,保存在m_nTOCSize,此偏移值即整个文档头的大小.

 

 

int CSelfExtractor::ExtractOne(CFile* file, int index, CString Dir);

通过m_InfoArray中的数据,读入,并写入创建的新文件;写入时一次写1000字节,没有1000字节即写入剩下大小.

 

int CSelfExtractor::ExtractAll(CString Dir, funcPtr pFn /*= NULL*/, void * userData /*= NULL*/)

首先读取文档头,得到保存有文件信息的m_InfoArray数组,然后依据此数组,分别调用ExtractOne解压到磁盘上,解压后还可以调用funcPtr类型的用户定义函数.

 

int CSelfExtractor::CreateArchive(CFile* pFile, funcPtr pFn, void* userData)

先分别从m_InfoArray数组中读入文件的信息,并从外部读入,写入到文档中,

然后写文档头

 

 

int CSelfExtractor::Create(UINT resource, CString Filename, funcPtr pFn /* = NULL */, void* userData /*=NULL*/)

分别用FindResource,LoadResource,LockResource函数读入执行文件资源.

将整个执行文件全部写入新创建的文件.调用CreateArchive完成接下来的文件的写入.

 

int CSelfExtractor::Create(CString ExtractorPath, CString Filename, funcPtr pFn /* = NULL */, void* userData /*=NULL*/)

不是以资源形式给出可执行文件时:

先利用   

CShellFileOp shOp;

       shOp.SetFlags(FOF_FILESONLY | FOF_NOCONFIRMATION | FOF_SILENT);

       shOp.AddFile(SH_SRC_FILE, ExtractorPath);

       shOp.AddFile(SH_DEST_FILE, Filename);

       if(shOp.CopyFiles() != 0)

              return COPY_FAILED;

来完成从一个用户指定可执行文件到新建立文档的复制,接着还是利用调用CreateArchive完成接下来的文件的写入.

阅读全文....

MPQ文档布局分析

MPQ文档布局分析(以暗黑破坏神2的一个补丁patch_d2.MPQ和燃烧远征的一个补丁patch-2.MPQ文档为实例,以下简称D2,P2)

 

说明:因为MPQ实际上是由很多文件数据组成,包含很多文件,为了区别好MPQ文件与一般文件,这里将MPQ文件在作为一个集合的时候称为文档(ARCHIVE),表示实际存储在硬盘上的一个文件时称为MPQ文件(file),而文件都指保存于MPQ中的一般文件.

 

说明2:intel x86系统的存储格式都是低位在前(Little-endian),比如一个int32型的数据44(00 00 00 2Ch)存储在文件和内存中实际为2C 00 00 00h,下面不再说明.

 

说明3:以下以[]后缀中表示此类型一共重复几次,()前缀表示一个此类型所占字节数.比如(1)char[4]表示此处有41个字节的char类型数据.

 

说明4:本文主要信息来自The MoPaQ File Format 1.0.txt,只是附带个人结合D2,P2后的分析,因为会经常提到原文件,以后简称其为MFF.另外也经常会提到http://www.zezula.net/en/mpq/mpqformat.html一文,简称为MFH.

 

说明5:在一段文本的开头的十六进制数字都表示文件中的偏移地址,在段中,都以offset +十六进制数字说明此处表示在原文件的偏移地址.

 

文档总体布局

-          文档头

-          文件数据

-          文件数据 特别文件

-          哈希(hash)

-          (block)

-          扩展块

-          强数字标志(strong digital signature)

 

文档头:

00H: (1)char[4]: 指示这个文件是MPQ文件,肯定是ASCII “MPQ” 1Ah.

D2前四个字节为4D 40 51 1Ah 吻合.

P2前四个字节为4D 40 51 1Ah吻合.

另外,MFH中知道,当第4个字节(1)1Ah的时候表示这里开始的是一个MPQ文档头,这里还可以为1Bh,表示这里开始的是一个MPQ shunt结构.

 

04H: (4) int32: 文档头文件的大小.

D220 00 00 00h,32个字节.

P2 2C 00 00 00h ,44个字节.

 

08H: (4)int32: 整个文档的大小,包括文档头信息.不包含强数字信息(假如有的话).在燃烧远征中是从文件头到文件尾的所有信息.

D225 1B 20 00h,2104101个字节, 从显示上来看,与文件大小一致,意思是此文件可能不包括强数字信息.

P210 CC 12 00h,1231888个字节,从显示上来看,与文件大小一致,意思是此文件可能不包括强数字信息.或者因为此文件为燃烧远征文件,所以为文件大小.

 

0CH: (2)int16: 版本信息,表示的是MoPaQ的版本.当其为负的时候,MPQAPI将不会打开文档.

已知版本:

原始版本:0,文档头应为32个字节,不支持大文档.

燃烧远征:1,文档头应为44个字节,支持大文档.

D200 00h,0,表示此MPQ文件为原始版本文件,另外,此文件的文档头从上面看的确为32各字节,吻合.

P201 00h,1,表示此MPQ文件为燃烧远征文档,另外,此文件的文档头从上面看的确为44个字节,吻合.

 

0EH: (1)int8 扇区偏移大小(SectorSizeShift):指示的是每一个文档的逻辑扇区的大小.总为512 * (2^扇区偏移大小).此处MFF指出,因为Storm librarybug,在其中规定只能为3,计算后为512* (2^3) = 4096字节.MFH可以知道,这里的意思其实就是指一个未经压缩的块大小,通常为4KB.但是假如一个文件经过了压缩,一个块就是变长的了.

D2,P2中此处值都为3,也就是指示此文档逻辑扇区的大小为4096字节,Storm library的规定的一样,可能因为大部分暴雪的MPQ文档中此值都为3,所以Storm library才会因为不经意的存在此bug,而且就算有此bug也没有太在意,因为不至于太影响使用.另外,MFF中说此处为(1)int8,并且在以后也没有说明offset 0FH处的数值含义,D2,P2中都为00h,怀疑offset 0EH处值应为(2)int16,结合0F一起, 03 00h,还是表示3,

 

10H: (4)int32 哈希表偏移值,指示从文档开始,到哈希表开始地址的偏移量.

D265 0E 1C 00h 1838693个字节, 从文档大小2104101个字节来看,处于文档的中部,并偏向尾部,与文档总体布局一致,因为很明显,文件数据和特别文件的数据才应该是占用文档空间的主要部分.

P250 CB 12 00h 1231696个字节,从文档大小1231888来看,处于文档的尾部,也符合以上推论,但是此是哈希表便宜值处于这么尾部而不是和D2一样处于相对中部的位置,说明P2的哈希表,块表,扩展块表比较小,一共才192个字节.可能是因为P2中所含文件较大,而文件的数量较小,所以需要用来索引的内容较小,从后面的分析来看是正确的.

 

14H: (4)int32 块表偏移值, 指示从文档开始,到块表开始地址的偏移量.

D265 0E 20 00,2100837个字节,处于哈希表偏移值1838693和文档大小2104101之间, 与文档总体布局一致.并且从此可以得出结论,D2的哈希表大小为262144个字节.最后块 Table 扩展块表加起来还有3264个字节.

P2D0 CB 12 00,1231824个字节, 处于哈希表偏移值1231696和文档大小1231888之间, 与文档总体布局一致.并且从此可以得出结论,D2的哈希表大小为128个字节. 最后块 Table 扩展块表加起来还有64个字节.

 

 

18H:(4)int32 哈希表入口数量,必须是2的幂,而且对于原始版本MPQ文件必须小于2^16,对于燃烧远征版本必须小于2^20.

D2中为00 40 00 00,16384,2^24.

P2中为08 00 00 00,8,2^3.

以上数据符合开始对于D2文件多,P2文件少的推论,结论有待下一步验证.另外,从这里也可以算出每个哈希表入口的大小为128/8 = 16个字节,16384 * 16 = 262144也可以得到验证.

 

1CH: (4)int32 块表入口数量.

D2中为CC 00 00 00,204,

P2中为04 00 00 00,4.

MFH中提到,块表的入口数量比块的数量要大一,最后一个入口被用来得到最后块的大小.并且每个入口为16个字节.

 

 

只在燃烧远征(P2)及其以后版本才出现的文档头文件为:

20H:(8)int64 从文档头开始的扩展块表偏移值,

因为D2此处已经是文件数据,从此一下不再说明.

P2中此处为00 00 00 00 00 00 00 00,即无扩展块表.

 

28H:(2)int16 大文档哈希表偏移值的高16位值.

P2中此处为00 00,因为P2并不是大文件,所以用不上此值.

 

2AH:(2)int16 大文档块表偏移值的高16位值,

P2中此处为00 00,因为P2并不是大文件,所以用不上此值.

 

最后通过20H2AH上面的分析,可以得出结论就是原始格式的MPQ文件并没有扩展块表,因为那里没有连扩展块表的偏移值都没有说明,另外,因为D2,P2中都没有扩展块表和强数字信息,所以块表实际就是文档的结尾,实际上通过块表入口数量*块表入口大小,也可以得出相同结论(16*204 = 3264 ,16*4 = 64)

 

在文档偏移量为0的时候,文档头是文档的第一个结构,然而,在包含文档的文件中,文档的偏移量不是必须为0.在文件中文档的偏移量在这里被称为文档偏移量(Archive Offset),假如文档不是文件的开头,就必须开始与一个磁盘扇区的边界(512个字节).

MFH中又可以知道,当处理MPQ的时候,应用程序只需要一个一个查找0x200h,0x400h(512的倍数)直到找到文档头,或者到达文件的末尾.这样可以方便的将MPQ文档储存在其他文件格式中,比如EXE,这样甚至可以在安装文件setup.exe,install.exe中假如MPQ文档.

早期版本的Storm库需要文档在文件的结尾(文档偏移量 + 文档大小 = 文件大小),但是在更新的版本中已经不需要了.(因为强数字信息不再被认为是文档的一部分).

以上即为全部的MPQ文件头分析.

 

 

块表:

块表包含文档内每个区域的入口,区域可能是文件,空闲空间,或者没有使用的块表入口,其中空白空间主要来自于删除的文件数据,可以被新文件覆盖.空闲空间入口应该有非零的块偏移值和块大小.和为零的文件大小,Flags.未使用的块表入口应该有为零的块大小,文件大小,Flags.块表是使用”(block table)”key的哈希算法加密的.

而各种数据实际上就是放在一个一个的由块表指定的块中.

 

首先,从文件头已经知道,D2的块表开始于00 20 0E 65h,P2的块表开始于00 12 CB D0h.对比分析从上面两个地址开始.为了更加明了,我将从MFF中的相对于块表的偏移量,(对于每个文档都适用)计算出绝对偏移量(当然只适用于D2,P2这两个文件,不过对于分析这两个文件来说,更加清晰,以下只分析了第一个入口).此表不是像我先想的那样有表头,从块表的一开始就是块的入口,和对此块的描述,偏移4个字节以后就是第二个入口,以此类推.

相关的函数为EncryptBlockTable()加密, DecryptBlockTable()解密.

在加密解密的时候有两个地方需要注意,而我开始没有注意的是:

1.       运行加解密算法前,需要先运行PrepareStormBuffer()函数,初始化好用来加解密的Buffer.

2.       Block加解密算法必须是一次从头开始对一整块数据进行,比如,可以一次解密第一个入口的第一个值(块偏移值),但是不能单独对第二个值(块大小)解密.可以一次解密整个第一个入口的4DWORD的值,但是不能单独对第二个入口进行解密,简而言之,在对后面数据解密的时候必须要有前面的数据.并且要得出正确的结果,那么前面的任何数据都不能有任何错误.很显然,加密的时候也需要这样,经过测试,的确如此.

第一个入口的结构如下:

00H:(4)int32 相对于文档开始的块偏移值.

D2中地址为00 20 0E 65h ,数据为AB 67 48 3DH,

P2中地址为00 12 CB D0h,数据为A7 67 48 3DH,

以上都为加密数据,但是其实我们是知道块的偏移值的,文档头下面就应该是块,D2中应该从32个字节以后,即从20H开始,P2应该是从44个字节以后,即从2CH开始.知道这些以后,正好可以验证一下Strom库的哈希算法 经检验,DecryptBlockTable解密数据后,和猜测吻合.

 

04H:(4)int32 块大小.假如块为文件,某些文档表示这里就是压缩过的文件的大小.

D2中的地址为00 20 0E 69H,数据为 B9 E4 08 CAH, 解密后为14104

P2中的地址为00 12 CB D4H,数据为 D9 0E 06 CAH, 解密后为974196

 

08H:(4)int32 储存在块中的文件数据大小,只有在块是一个文件的时候有效;不然的话无意义,而且应该为0.假如文件是压缩的,这里表示的是解压后的文件数据大小.

D2中的地址为00 20 0E 6DH,数据为 7F BF 34 F8H, 解密后为85652

P2中的地址为00 12 CB D8H,数据为37 7A 5B F8H, 解密后为2089956

 

0CH:(4)int32 块的位掩码标志位.

以下为已经确定的数值:

80 00 00 00H: 接下来有表示文件数据类型的位值,那么这个块是一个文件.不然块是一个空闲空间或者未使用的空间.假如这个块不是一个文件,所有其他的标志位应该为0.

01 00 00 00H: 文件作为单一单元储存,而不是分成很多扇区.

00 02 00 00H: 文件的密钥经过块偏移和文件大小调整.文件必须是加密的.

00 01 00 00H: 文件是加密的.

00 00 02 00H: 文件是压缩的.文件不能被imploded?

00 00 01 00H: 文件是imploded,文件不能被压缩.

D2中的地址为00 20 0E 71H, 数据为75 DB 3B E8H, 解密后为80 00 01 00H,按上面的意思来看,这表示D2的此块块为一个imploded的未压缩文件.

P2中的地址为00 12 CB DCH,数据为AD 16 3E EEH, 解密后为84 00 02 00H,上面的表没有说明04 00 00 00H位表示什么意思,但是可以看看出此文件是被压缩的文件.

 

哈希表:

为了快速存取,作为文件名的替代,MPQ中使用了以2的各次幂为大小的文件的哈希表.一个文件通过它的完整文件路径名,语言和平台唯一识别.一个文件在哈希表中的home入口用一个文件的完整文件路径名的哈希计算得到.当此入口已经被其他文件占用的时候,使用了顺序溢出方式(progressive overflow),这个文件被放置在下一个可用的哈希表入口.在哈希表中搜索一个想要的文件过程是,home入口直到这个文件被找到,或者搜寻整个哈希表.或者是碰到一个空的哈希表入口(文件块索引为FF FF FF FFH).哈希表通过以”(hash table)”keyhash运算加密.

以下内容与块表类似,也只看第一个入口,但是每一个入口的结构还是相同的.

相关的函数为加密哈希表:EncryptHashTable(),解密哈希表:DecryptHashTable(),文件完整路径名哈希值计算方法1(A):DecryptName1(),文件完整路径名计算方法2(B): DecryptName2(),得到一个文件的哈希表home入口:DecryptHashIndex(),得到一个文件真正的哈希表入口:GetHashEntry().

其中GetHashEntry()算法的在BlizzardMPQ文件格式搜索算法 - GameRes游戏开发论坛.htm一文中有详细描述:

1.计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验)
2. 
察看哈希表中的这个位置

3. 
哈希表中这个位置为空吗?如果为空,则肯定该字符串不存在,返回
4. 
如果存在,则检查其他两个哈希值是否也匹配,如果匹配,则表示找到了该字符串,返

5. 
移到下一个位置,如果已经越界,则表示没有找到,返回
6. 
看看是不是又回到了原来的位置,如果是,则返回没找到
7. 
回到3”

其中用来确定位置的哈希值,指的就是哈希表home入口.DecryptHashIndex()函数得到,两个用来效验的哈希值就是用算法A和算法B得到的哈希值,分别用DecryptName1(),DecryptName2(),函数得到,另外这三个值都是仅仅用来比较,所以都是单向的,函数源码的具体理解待理解源码的时候再去看.虽然在strom库里面被命名为DecryptName,其实是个从文件完整路径名到一个DWORD整数的哈希计算过程,更像是HashEncrypt.

 

从上面文档头的分析可知:

D2的哈希表位置开始于00 1C 0E 65H. P2的哈希表开始于00 12 CB 50H.

因为需要一块解密,所以4个字节的

00H:(4)int32 文件路径哈希值A: 用算法A得到的文件路径哈希值.

04H:(4)int32 文件路径哈希值B: 用算法B得到的文件路径哈希值.

08H:(2)int16 这个文件的语言.这是一个windows LANGID数据类型而且使用同样的值.0表示是默认语言(American English),或者这个文件是语言无关的.

0AH:(1)int8 平台:这个文件被用来使用的平台.0表示默认平台,目前没有看到过其他的值.(很明显应该为int16,估计是文档中的错误)

0CH:(4)int32 文件块索引:假如哈希表入口是有效地,这个是个到这个文件的块表的索引.不然的话,可能为下面两个值:

FF FF FF FFH: 哈希表入口为空的,而且一直是空的.结束搜索一个给定的文件.

FF FF FF FEH: 哈希表入口是空的,但是某些时候是有效的(意思就是说,这个文件以前被删除了).继续向下搜索给定的文件.

上面这个特性要说明的是,MPQ,一个文件被删除以后,实际上并不能回收空间,仅仅是删除了这个文件的路口,此时最后这个文件块索引就标志为FF FF FF FEH,那么这个文件后面还可能有文件,所以继续搜索.补充一点的就是,虽然空间不能被回收了,但是当新的文件增加进来的时候,只要这个文件比原来删除的文件要小,就可以放进空闲下来的空间.但是新的文件较大的时候新的文件将扩展.这点在频繁删除增加文件后会导致空间的很大浪费,唯一的解决办法就是先读出这个MPQ文档的数据,然后重新构建一个新的MPQ文档.感谢the MPQ API Library 2.0的创作者Justin Olbrantz(Quantam),他写了一个mpqcompactarchive()函数,用来解决此问题.另外他还写了另外两个有用的函数MpqRenameFile(),用来改变MPQ文档中已有文件的文件名, MpqAddWAVToArchive(),用来在添加WAV文件的时候对其进行特殊的更有效的压缩.

D2中此入口的数据为:

00H:33 30 C3 79

04H:28 D9 32 98

08H:BC 73

0AH:6F 9F

0CH:B2 88 4E E9

解密后,D2中连续两个入口都为全FF,没有办法判断.用了很久时间也没有办法改变这一点,可能是因为文件删除了,所以才会这样..............

P2中可以得出比较合理的结果,文件语言和平台都为0,块索引在最开始的两个哈希表入口中分别为02,所以推测是 文件块索引的表示不是相对于整个文件,而是相对于块表的开始而言.(MFF中没有描述),不然, DecryptHashTable()函数得出的结果都是错的.

而这里又暂时还不知道具体的文件名,没有办法通过EncryptHashTable()函数来检验P2中得到的结果是否正确.

 

以下内容基本上翻译自MFF,自己顺便理解并记录成中文.因为还没有通过实际代码编写调用函数来检验.因为下面内容不像文档头和哈希表,块表一样,能知道存储的是什么内容,光从文件中也不能反向计算哈希值前的文件完整路径名,所以只能暂时通过这种方式了解一下文档的大概结构.

 

文件数据:

每个文件的数据都由下面部分组成:

00H:(4)int32(文件中的扇区+1)扇区偏移值表(SectorOffsetTable: 相对于文件数据开始处每个扇区的偏移值.最后的入口包含文件的大小,使得它可能很容易的计算出任何给定扇区的大小.假如信息可以被计算出来,这个表是不会出现的.

扇区偏移值表后: 文件中每个扇区的数据,首尾相连的打包

通常来说,文件数据简单的分开存在扇区里.所有的扇区,将包含文档头扇区偏移大小确定大小的文件数据.依据整个文件数据的大小不同,最后的扇区可能少于这个数值.假如文件是压缩的或者imploeded,扇区将比他包含的文件的要小.在压缩或者imploded的文件的各个扇区中,也可能存储这未压缩的数据,当且仅当这个文件的这个扇区中的数据不能被需要的算法压缩时.这时,在扇区偏移值表中的扇区值就等于在扇区中的文件数据大小. 

各扇区的格式决定于扇区的种类.没有压缩的扇区仅仅是原始的文件数据.

Imploded扇区是原始压缩过的数据,再经过implode算法计算过的值.

压缩过的扇区中数据是用一个或两个压缩算法压缩过的.而且有以下的结构:

00H:压缩位掩码:指示了这个扇区的压缩种类,可以复合使用.它们以以下列表的顺序应用,而且应该用相反的顺序解压.这个字节从总共的扇区大小计算而来,意味如果数据不能被至少两个字节压缩,扇区将不能作为未压缩的方式存储数据.也就是说,这个字节是用扇区数据加密的,假如可以的话.

    40h: IMA ADPCM mono

    80h: IMA ADPCM stereo

    01h: Huffman encoded

    02h: Deflated (see ZLib)

    08h: Imploded (see PKWare Data Compression Library)

    10h: BZip2 compressed (see BZip2)

01h: byte(SectorSize - 1) SectorData : 这个扇区被压缩过的数据.

假如这个文件是作为单一单元存储的(在文件标志中有指示),那么这里只有一个存储了整个文件数据的单一扇区.

假如文件是加密的,每个扇区(假如可以应用的话,在压缩或implosion以后)是用filekey加密的.基本的文件key(base key)是不带路径的文件名,假如这个key是修改过的(就像在文件标志中指示的那样),最后的key通过((base key + 块偏移值 文档偏移值)XOR 文件大小)计算得到.(MFF原注:Strom库中使用AND替代XOR是不对的).每个扇区都是使用这个key + 在文件中以零为基础的扇区为索引.

 

假如存在扇区偏移值表(SectorOffsetTable)的话,是用值为-1key加密的.

扇区偏移值表在大小和在文件中所有扇区的偏移值可以从文件大小中计算出来的时候是被忽略的.这可能有数种情况:

假如文件是没有压缩/imploded过的,这时,大小和所有扇区的偏移值在扇区偏移值大小(SectorSizeShift)的基础上是已知的,假如文件作为单一的压缩/imploded单元存储,这时,扇区偏移值表被忽略,因为就如前面提到的那样,单一文件的扇区与块大小和文件大小相符.然而,假如文件是压缩/imploded过的而且不是作为单一的单元存储,甚至这个文件只有一个扇区,扇区偏移值表也要有.

 

文件列表:

文件列表是MPQ一个非常简单的扩展,包含文档中文件的(大部分)文件路径.文件的语言和平台不存储在文件列表里面.文件列表包含在文件”(listfile)”(默认的语言和平台),而且仅仅是一个简单的文本文件,在里面文件的路径通过’;’,0DH,0AH或它们的组合分隔.文件”(listfile)”可能不列在文件列表中.有了这个列表,就可以先通过这个列表了解文档中一共包含了哪些文件,这样就可以通过文件名的哈希计算来验证前面得到的哈希表是否正确了.当然,假如不知道前两个哈希表入口表示的是哪两个文件,要么就如同算法一样遍历哈希表,要么就遍历计算文件:)

 

附加属性:

可选属性,这些属性在MPQ文档格式已经确定了以后再加入进来,所以不是每个文档都必须有所有(任何)这些属性.假如一个文档包含一个给定的属性,将会有一个对每个在块表中的块分别适用的属性值,尽管当这个块不是文件的时候这个属性值将没有意义.属性值的顺序和块在块表重的顺序一样.属性总是以数组的形式存储在”(attributes”)文件中(默认的语言和平台).属性值对应的文件不是必须有效的.不像所有其他结构一样,在附加属性中入口不保证是相连的.并且,看到在一些文档中,为了防止别人对文档的查看,存在恶意的附加属性位置移动.

此结构的意义如下:

00H:(4)int32 版本:定义附加属性的版本,到目前位置肯定是100.

04H:(4)int32 存在的属性: 在附加属性中存在的位掩码.

00 00 00 01H: CRC32文件

00 00 00 02H: 时间戳(timestamps)文件

00 00 00 04H: MD5文件.

08H:(4)int32 块列表入口的CRC32.文档中每一个块未压缩的文件数据的CRC32.当文档没有CRC32的时候忽略.

CRC32以后:  文档中每一个块的时间戳.格式是windowsFILETIME格式.假如不存在,忽略.

在时间戳以后: MD5文档中每一个块的未压缩文件数据的MD5.没有的话,忽略.

 

 

阅读全文....

MPQ Strom库使用及源代码理解文档

MPQ Strom库使用及源代码理解文档

九天雁翎

读取文件流程:

1.       用要打开文档的文件名调用SFileOpenArchive()函数打开文档,得到打开文档的句柄.

2.       用上步得到的文档句柄,和要打开的文件名调用SFileOpenFileEx()函数,得到打开的文件句柄.

3.       用上步得到的文件句柄,调用SFileReadFile()函数,读取数据.

4.       关闭文件,文档句柄.

 

各函数调用方式及参数意义参看MPQ().

因为牵涉到太多原始的底层操作,和各样的加密解密,所以看的比较仔细,各函数的实现如下

SFileOpenArchive():

1.       首先调用PrepareStormBuffer(),确保以后调用各解密函数时此Buffer已经初始化.

 

2.       利用CreateFile()这个windows API打开需要打开的文档.

 

3.       动态创建一个TMPQArchive类型的变量,保存在名叫ha的指针当中,以后都直接用ha,代表这个变量.

TMPQArchive的原型如下:

struct TMPQArchive

{

//  TMPQArchive * pNext;                // Next archive (used by Storm.dll only)

//  TMPQArchive * pPrev;                // Previous archive (used by Storm.dll only)

    char          szFileName[MAX_PATH]; // Opened archive file name

    HANDLE        hFile;                // File handle

    DWORD         dwPriority;           // Priority of the archive

    LARGE_INTEGER ShuntPos;             // MPQShunt offset (only valid if a shunt is present)

    LARGE_INTEGER MpqPos;               // File header offset (relative to the begin of the file)

    LARGE_INTEGER HashTablePos;         // Hash table offset (relative to the begin of the file)

    LARGE_INTEGER BlockTablePos;        // Block table offset (relative to the begin of the file)

    LARGE_INTEGER ExtBlockTablePos;     // Ext. block table offset (relative to the begin of the file)

    LARGE_INTEGER MpqSize;              // Size of MPQ archive

 

    TMPQFile    * pLastFile;            // Recently read file

    DWORD         dwBlockPos;           // Position of loaded block in the file

    DWORD         dwBlockSize;          // Size of file block

    BYTE        * pbBlockBuffer;        // Buffer (cache) for file block

    DWORD         dwBuffPos;            // Position in block buffer

    TMPQShunt   * pShunt;               // MPQ shunt (NULL if not present in the file)

    TMPQHeader2 * pHeader;              // MPQ file header

    TMPQHash    * pHashTable;           // Hash table

    TMPQBlock   * pBlockTable;          // Block table

    TMPQBlockEx * pExtBlockTable;       // Extended block table

   

    TMPQShunt     Shunt;                // MPQ shunt. Valid only when ID_MPQ_SHUNT has been found

    TMPQHeader2   Header;               // MPQ header

 

    TMPQAttr    * pAttributes;          // MPQ attributes from "(attributes)" file (NULL if none)

    TFileNode  ** pListFile;            // File name array

    DWORD         dwFlags;              // See MPQ_FLAG_XXXXX

};

 

4.    找到MPQ文档的开始,意义参看MPQ文档局分析中对文档头的描述部分.

此步稍微复杂:

首先看一个为TMPQHeader2结构.原型如下:

struct TMPQHeader2 : public TMPQHeader

{

    // Offset to the beginning of the extended block table, relative to the beginning of the archive.

    LARGE_INTEGER ExtBlockTablePos;

 

    // High 16 bits of the hash table offset for large archives.

    USHORT wHashTablePosHigh;

 

    // High 16 bits of the block table offset for large archives.

    USHORT wBlockTablePosHigh;

};

 

struct TMPQHeader

{

    // The ID_MPQ ('MPQ/x1A') signature

    DWORD dwID;                        

 

    // Size of the archive header

    DWORD dwHeaderSize;                  

 

    // Size of MPQ archive

    // This field is deprecated in the Burning Crusade MoPaQ format, and the size of the archive

    // is calculated as the size from the beginning of the archive to the end of the hash table,

    // block table, or extended block table (whichever is largest).

    DWORD dwArchiveSize;

 

    // 0 = Original format

    // 1 = Extended format (The Burning Crusade and newer)

    USHORT wFormatVersion;

 

    // Power of two exponent specifying the number of 512-byte disk sectors in each logical sector

    // in the archive. The size of each logical sector in the archive is 512 * 2^SectorSizeShift.

    // Bugs in the Storm library dictate that this should always be 3 (4096 byte sectors).

    USHORT wBlockSize;

 

    // Offset to the beginning of the hash table, relative to the beginning of the archive.

    DWORD dwHashTablePos;

   

    // Offset to the beginning of the block table, relative to the beginning of the archive.

    DWORD dwBlockTablePos;

   

    // Number of entries in the hash table. Must be a power of two, and must be less than 2^16 for

    // the original MoPaQ format, or less than 2^20 for the Burning Crusade format.

    DWORD dwHashTableSize;

   

    // Number of entries in the block table

    DWORD dwBlockTableSize;

};

 

实际意义也可以参看看MPQ文档布局分析,就是整个文档头的结构.TMPQHeader, TMPQHeader2,就是wFormatVersion0,和为1时的两个文档头版本.

这里通过循环来寻找MPQ文档的开头:

 

{

a.这里首先读取了一个TMPQHeader2的文档头.

 

b.然后经过恰当的大头小头机数据转换(因为intel是小头机并且是函数实现的默认方式,此步其实完全忽略了,以后不再提及,但实际都需要这一步).

 

c.在第一次循环的时候通过IsAviFile()函数判断了一下此MPQ文件是否是AVI文件,因为据源代码中注释说明,MPQ文件实际就是AVI文件的时候,它仅仅是改变了扩展名的AVI文件.(IsAviFile()函数利用AVI文件开始的数据标志实现)

 

d.当实际读入的数据不等于TMPQHeader2的大小的时候,说明此文件中根本没有MPQ文档,断开循环,并报错ERROR_BAD_FORMAT.

 

e.判断读入的文档头数据是否指示此文档为一个MPQ shunt的文档.是的话保存相关数据在TMPQShunt结构中.并从 TMPQShuntdwHeaderPos指示的位置开始继续搜寻MPQ文档.并且从最后Stromlib处理来看,此位置应该是紧接着就是文档头,而不用继续通过跳转512个字节循环搜寻.

 

struct TMPQShunt

{

    // The ID_MPQ_SHUNT ('MPQ/x1B') signature

    DWORD dwID;

 

    DWORD dwUnknown;

 

    // Position of the MPQ header, relative to the begin of the shunt

    DWORD dwHeaderPos;

};

 

因为Shunt部分没有相关文档的说明,从源代码中处理Shunt的方式,命名,TMPQShunt的结构来看,可能这是暴雪为了隐藏真正的MPQ文档而使用的一种机制.首先,一般情况下MPQ文档都是在一个扇区的边界,512个字节的整数倍处,所以搜寻MPQ文档开头时,都是一次搜寻512个字节.Shunt结构中用三个字节dwHeaderPos来指定MPQ文档的开始部分,可以离开上述约束.并且Shunt结构的dwIDMPQ文档头的dwID都有MPQ,无论一般用户是总是通过512字节的整数倍来搜寻MPQ文档头,或是直接碰到MPQ就直接开始MPQ文档的读取,还是不理解shunt结构的dwHeaderPos意义,都会导致读取MPQ文档的错误.dwHeaderPos的意义从注释上看还比较特殊,它指示的含义为从一个shunt结构开始的偏转值,而不是相对于整个文件的偏转值,就更加深了理解难度.当然,现在已经有人告诉我们了.....巨人的肩膀上.....

 

f.此时方才判断, 保存下来的TMPQHeader2结构的dwID成员是否MPQ文档的(MPQ1AH),找到后,将地址结构保存在haMpqPos成员变量中.

g.然后再通过判断版本号和获得的文档头大小是否一致来验证获得的文档头是否有效.无效就断开循环,结束查找.注释说明在某些情况下(比如W3M Map Protectors),暴雪可能会在文档头文件中加入一些无效的干扰数据,strom.dll文件明显忽略了这些东西,也就是我们没有办法去理解这些数据.继续也是徒劳.这时返回错误信息ERROR_NOT_SUPPORTED.暴雪的加密和干扰手段一个接一个,还不止这些...........

 

h.在此次循环中,假如ha成员变量的pShunt指针指向不为空,即表示已经经过了一次shunt的跳转,而通过上面几步的判断,此处还是不为文档头,表示文档已经损坏.

 

i.                指针跳转512个字节,开始下一轮的搜寻.

}

 

5.    经过上一步,没有错误的话,应该得到了文档头,此步先判断文档头的版本号,是原始版本就清空扩展数据部分.

 

6.    通过文档头的数据判断,确定dwBlockSize,即一个块的大小,计算方法如MPQ文档布局中指出的512 * (2^扇区偏移大小).stromlib源代码中通过位移来完成,效果一样.

 

7.    通过RelocateMpqTablePositions()函数得到正确的哈希表和块表的位置.文档头指示的哈希表,块表位置其实都是相对于MPQ文档开始位置而言, 此步的含义在于将其全部转换为相对于整个文件的位置,并保存ha的变量中.同时还得到了整个MPQ文档的大小.

 

 

RelocateMpqTablePositions()函数的具体实现步骤如下:

{

 

a)      先通过GetFileSize()这个windows API函数得到整个文件的大小.

 

b)      通过将文档头中的哈希表,块表偏移量加上第4步中保存的文档头开始的位置,得到的就是哈希表,块表相对于整个文件开始的偏移量了.

 

c)      是扩展版本的话,还通过同样的方法,得出了扩展块表相对于整个文件的偏移量.此两步最后都通过和第1步得到的文件大小做比较,确定数据有效.

 

d)      通过得到块表,哈希表,扩展块表三个中表的最后位置的最大值,来确定整个MPQ文档的结尾(都是利用上几步得到的相对于整个文件开始位置的偏移量来计算的),用此值减去文档头的位置(也是相对于整个文件开始位置的偏移量)来得到整个MPQ文档的大小.

 

 

{   

从此步中可以看出的结论是,MPQ文档结构中,各个表的实际位置不一定会像 <MPQ文档布局分析>中提到的那样,是哈希表,块表,扩展块表的顺序,因为各个表的位置都由文档头精确定位,的确也可以不按此顺序.所以stromlib源代码中通过这种比较复杂的方法来得到整个文档的大小.并且从stromlib的计算方式来看,起码表数据都是放在文档的最后.

可是个人认为太过于复杂,就算三个表的先后顺序是不确定的,但是三个表的所有入口都是一个接一个的,意思就是,不可能三个表混在一起,那么,要得到整个文档的大小,只需要先判断三个表的偏移量哪个最大(在未计算前,文档头中指示的数据都是相对于MPQ文档开始的偏移量),再将最大偏移量的那个值加上其相对应的表大小,得出的偏移量,就是整个MPQ文档的大小,因为此偏移量也是相对与MPQ文档开始的位置而言,并且是文档的结尾.

}

}

 

8.    stormlib源代码中此时为哈希表,块表,扩展块表,块分别分配了空间,并将ha的成员变量中pHashTable, pBlockTable, pExtBlockTable, pbBlockBuffer指向相应地址.假如分配失败,返回ERROR_NOT_ENOUGH_MEMORY.这里stromlib还指出,为了以后文件的添加,块表应该和哈希表一样大.通过这样的方式来避免最后缓存的溢出.

 

9.    将整个哈希表都读入hapHashTalbe指向的空间中.因为已可以计算出哈希表大小,假如读入的数据不对,表示此文档已损坏,返回ERROR_FILE_CORRUPT错误.此时还是加密数据.(hash tablekey,调用DecryptHashTable()函数,解密数据.此时stromlib注释中提到, MPQ protectors甚至可能通过重写一部分哈希表来干扰别人的读取,而整个哈希表只要一开始有一个位不对,后面的所有数据都是无效的........暂时不知道怎么检查....

 

10.将整个块表都读入hapBlockTalbe指向的空间中,因为当文件删除时,似乎会出现文档头声明的块表入口数量多于实际块表入口数量的情况,读入的数据少于文档头声明数量时,要以实际读入的数据为准.在解密的时候,为了防止出现原块表头没有加密而重复解密导致错误的情况.stromlib中首先判断了一下原块表是否被加密.( 通过块的位掩码标志位低8位总为零的情况来判断,详见<MPQ文档布局分析>,不为0即加密过的数据)加密的话,(block table)key调用DecryptBlockTable()函数解密.

 

11.将整个扩展块表都读入hapExtBlockTalbe指向的空间中,原始版本中即将此块清零,燃烧远征版本就读入此数据.stromlib中没有解密过程.就我猜测可能此扩展部分为未加密的原始数据.就暴雪的作风好像很难得,要么就是还没有人分析出扩展块表的解密算法............

 

12.此时stromlib作者为了确定块表解密正确,进行了一些判断.具体方法就是判断各块表入口指向的地址和指向块的大小是否都在MPQ文档大小以内.不是,自然解密出现了问题,返回错误代码ERROR_BAD_FORMAT.另外,不知道是否是我没有找到,stromlib源代码中定义了 DWORD dwMaxBlockIndex = 0;后就再也没有对其赋过其他值,而直接通过这种方式TMPQBlock * pBlockEnd = ha->pBlockTable + dwMaxBlockIndex + 1;得到块的结尾....似乎有点问题.个人觉得此处的dwMaxBlockIndex应该等于第10步中得到的实际块表入口数量.不然源代码中相当于只验证了第一个块表入口.

 

13.假如调用时没有在SFileOpenArchive()函数的第三参数指定不打开listfile,那么就将listfile文件读入ha,listfile的具体含义和结构,参看MPQ文档布局分析相关部分.

 

 

{

a.    此步首先利用SListFileCreateListFile()函数在ha中为listfile的相关成员变量分配空间.

b.    然后调用SFileAddListFile()函数实际的添加数据.此一步函数相当于整个一个从MPQ文档中读取文件的过程,只不过文件名为(listfile).然后通过GetLine()函数每次读取一行并添加到相关链表中而已.

}

 

14.假如调用时没有在SFileOpenArchive()函数的第三参数指定不读入属性,就调用SAttrFileLoad()函数读入属性数据.此步的实际过程与上面读入listfile很类似,也是一个完整的一个从MPQ文档中读取文件的过程,只不过文件名为 (attributes).

 

15.最后,假如中途出现错误,先利用FreeMPQArchive()函数释放ha的空间,再关闭文件句柄,设定错误代码为上述步骤中设定的错误代码值.

 

16.一切正确的时候, 且全局变量pFirstOpen为空,即将打开文件的句柄赋值给pFileOpen,再赋值给SFileOpenArchive()函数的第四参数,作为一个输出,同时返回ERROR_SUCCESS表示此函数调用成功.

 

至此, SFileOpenArchive()函数的全过程理解完毕.因为此步的意义在于将一个纯粹二进制的文件的文档头,哈希表,块表,扩展块表,尽量正确的分别读出,解释,保存在一个指向TMPQArchive结构的指针变量ha.每一步都需要非常正确,容不得一点错误,所以stromlib作者每一步都加入了足够多的错误验证.每一步也都要预防暴雪对文件的加密,所以代码相当的琐碎.理解起来也的确花了过多的时间.

 

 

SFileOpenFileEx()

1.       首先验证参数是否都有意义.

 

2.    SFileOpenFileEx()函数第三个参数为SFILE_OPEN_BY_INDEX,即通过文件索引来定位文件,此索引实际就是块表入口的索引,这里需要特别注意的是,此处是索引,就像数组的索引一样,而不是偏移量.就如在<MPQ文档布局分析>中根据哈希表数据猜测的一样,哈希表最后一个值实际也是记录对应块表的索引,而不是偏移量.所以,此步就是遍历整个哈希表,判断此哈希表入口的最后一个值(即对应块表的索引)是否等于此索引,等于即表示找到对应的哈希表入口.但是在测试程序中,我用索引调用此函数一直没有成功,原因不明.

 

3.    SFileOpenFileEx()函数第三个参数为SFILE_OPEN_LOCAL_FILE,调用OpenLocalFile()函数打开本地磁盘的文件.

 

4.    SFileOpenFileEx()函数第三个参数为0或者说是SFILE_OPEN_FROM_MPQ,才是最常用的调用方式,即以完整路径的文件名为第二参数,确定要打开的文件. 此步较为重要,最主要的步骤即调用GetHashEntryEx()函数得到相应的语言版本的此文件名对应的哈希表入口.

 

 

{

GetHashEntryEx()函数的具体实现又调用了GetHashEntry()函数来得到此任何语言的此文件名确定的哈希表入口.

首先看GetHashEntry()函数的实现:

 

{

a)         首先stromlib在此函数中也判断了一下第二参数的文件名是否是作为一个索引给出的.这里作者通过判断此值是否小于块表的数量来判断,当的确小于块表的数量时,作者认为此数值为一个索引,得到哈希表的入口的方法与SFileOpenFileEx()函数第2步一样.

 

b)        当经过判断不是以索引来搜索哈希表入口的时候,即表示第二参数的确是以完整路径文件名给出的.stromlib作者首先将此参数用DecryptHashIndex()得到home入口,此入口是一个DWORD值的索引值.DecryptName1(),DecryptName2()进行了2次哈希运算得到两个用来验证的值.大概理论方法在<MPQ文档布局分析>哈希表一节有所描述.

 

 

c)        此时循环查找两个验证值都符合且此哈希表入口的文件块索引(BlockIndex)不为HASH_ENTRY_DELETED(FF FF FF FEH表示此文件已被删除)的哈希表入口,此时此入口即为正确入口,返回.不然循环查找,直到某哈希表入口的文件块索引(BlockIndex)HASH_ENTRY_FREE(FF FF FF FFH)或已经遍历整个哈希表时,循环结束.这里要说的是,当查找到此哈希表的结尾还没有找到结果时,将从此哈希表的开始继续查找,直到碰上前述的返回或结束条件.

}

 

GetHashEntryEx()函数的实现

{

a)         首先调用GetHashEntry()函数,返回了一个此文件的任意语言版本的哈希表的入口.

 

b)        此时再开始一个类似于GetHashEntry()函数实现第C步似的遍历循环查找过程,此时的返回条件为此哈希表入口的语言为要查找的语言.并且,在碰到语言为LANG_NEUTRAL的文件时进行保存,当没有查找到确定需要语言版本的哈希表入口,就返回这个语言版本的哈希表入口偏移值.

}

5.       通过比较获得的块索引和块表大小,验证一下获得的块索引值.

 

6.       通过获得的块索引得到块的偏移地址,验证此块表入口指向的文件地址(此时为相对于MPQ文档的偏移值)是否正确(通过比较文件地址和MPQ文档的大小,还比较了此块表入口指向的块的大小值和文件的大小.验证此块表入口的文件标志位.(是否有MPQ_FILE_EXISTS(0x80 00 00 00H),是否有除了已知有效标志以外的位)

 

7..以上验证全部通过后,动态分配一个TMPQFile格式空间,由一个名叫hf指针指向这个空间..

      

8.利用已经得到的哈希表入口和块表入口等初始化hf指向的新创建的空间.

 

       9. hf->nBlocks  = (hf->pBlock->dwFSize + ha->dwBlockSize - 1) / ha->dwBlockSize;(?)块的数量 = (块中未压缩的文件数据 + 文档的块大小 1) / 文档的块大小;(?)

   

    10.假如块入口显示此块为压缩的话,分配空间用来解压,stormlib源码注释提到因为每个文件开始位置都存储了一个DWORD来保存文档中每个块相对文件开始位置的偏移值.在我写<MPQ文档布局分析>,此处好像是一个表,这一部分在写<MPQ文档布局分析>时就没有太明白.文件数据的具体结构这一块还不是太清除.最后分配了(nBlocks + 2)DWORD空间.

 

    11.假如文件由索引来打开调用SFileGetFileName()函数(以后再看此函数)暂时先看以文件名为索引的情况.

{

首先得到不带路径的文件名,如在<MPQ文档布局文件>数据这一部分分析的那样,这就是加密的文件的key.利用此key调用DecryptFileSeed()函数解密.

假如在块入口的标志中有MPQ_FILE_FIXSEED(00 02 00 00H),表示文件的密钥经过块偏移和文件大小调整.此时将得到的 (解密数据 + 此块的相对文档头的偏移量)^文件数据解压后的大小,:

hf->dwSeed1 = (hf->dwSeed1 + hf->pBlock->dwFilePos) ^ hf->pBlock->dwFSize;

<MPQ文档布局分析>中的描述为:

基本的文件key(base key)是不带路径的文件名,假如这个key是修改过的(就像在文件标志中指示的那样),最后的key通过((base key + 块偏移值 文档偏移值)XOR 文件大小)计算得到.每个扇区都是使用这个key + 在文件中以零为基础的扇区为索引.

 

目前此函数中还看不到此值的作用,但是返回的时候保存了下来,估计以后调用SFileRead的时候要作为索引值用到.

}

   

    12.通过块的索引,读入每个文件相应的CRC32,FILETIME,MD5,之所以可以通过块的索引来索引attribute,因为attribute和块是一一对应的.

 

    13.假如前面有错误,就释放hf的空间,并设置相应的错误代码.hf作为文件句柄赋给第四参数作为输出,并返回ERROR_SUCCESS表示成功.

 

}

 

 

FindFreeHashEntry()函数实现:

1.    先得到完整文件名的3个哈希值.

2.    如开始所知,第一个为home入口,寻找即从home入口开始顺序查找,直到找到一个空闲/删除的哈希表入口为止,没有找到即返回NULL,表示没有找到.

3.    假如找到了空闲哈希表入口即将其值置为相关值.

4.    然后开始寻找空闲的块表入口,方法是从索引为零开始,顺序检查每个块表的文件标志位是否为有文件,标志为没有时,表示找到空闲块表入口.

5.    当没有找到空闲块表入口时,在块表的最后添加一个块表入口,并赋给该哈希表入口的第四个DWROD(即其对应的块表索引).

6.    一切成功以后,返回此哈希表入口的指针.

 

 

AddFileToArchive()函数实现:

1.    得到需要加入的文件的大小,当其小于0x04H,不进行加密和文件key的修正,当其小于0x20H时不进行压缩.当文件大于4GB,报错,stormlib注释说明,MPQ中的当个文件大小不能大于4GB.

2.    为此文件的入口动态分配一个TMPQFile结构的空间,由指针hf指向.初始化此结构.

3.    利用GetHashEntryEx()函数,检测是否已经有完全一样的文件(包括文件名,语言和平台,此函数实现在前面已经说明).当已经存在时,检测添加文件的标志位是否有MPQ_FILE_REPLACEEXISTING,没有此标志位即报错ERROR_ALREADY_EXISTS,有的话设定标志位表示已经覆盖,正确设置块的位置,但是不做任何实际覆盖工作?要写入一个修改过的新的同名的文件怎么办? (?)

 hf->pBlockEx = ha->pExtBlockTable + hf->pHash->dwBlockIndex;

 hf->pBlock = ha->pBlockTable + hf->pHash->dwBlockIndex;

 bReplaced = TRUE;

4.    当没有找到完全一致的入口时,FindFreeHashEntry()函数找到一个空闲的哈希表入口.此函数实现在前面已经解释.并计算出此哈希表入口的索引值.

5.    此时先得到第一个文件块的偏移量(相对于文档开始处,得到方法即是跳过文档头.从第一个块表入口开始,同时不断验证此块表入口是否空闲.不空闲,就同时将文件块偏移量跳过这个入口指定的块大小,顺序查找方式与FindFreeHashEntry()函数一致,当其找到一个空闲的块表入口时,此块表入口即通过FindFreeHashEntry()函数找到的块表入口,且文件块偏移量正好跳过了前面所有块表入口指定块大小的总和.此时,认为找到了一个空闲的块空间.当找到块表入口末尾也没有找到空闲空间时,此时文件块偏移量也正好跳过了全部占用的文件块,到达文件数据块的末尾,此时认为这里即空闲空间.stormlib源代码中,个人认为.此两种情况实际可以合为一处,即少一个比较赋值操作.但是疑问在于,此处没有判断在文件块中找到的空闲块是否大于需要写入的文件,留待看以后有没有判断.(?)

6.    当此文档是原始版本时,确定添加此文件后的大小也不能大于4GB.方法是,首先将得到的文件偏移量(相对于整个文件而言)加上要加上的文件大小,再加上哈希表大小,块表入口,当此偏移量超过32位可以表示的时候,即表示添加文件后超过4GB.返回错误ERROR_DISK_FULL.这种方法实际是检测当添加文件处于文件数据块的尾部时的情况.当时我本以为,当新添加的文件处于文件块中时,这种方法不能保证文件大小不超过4GB,因为此时添加的文件和哈希表中还可能有数据.但是实际效果是,只要保证此空闲空间大于新添加的文件,那么只要原文件保证了小于4GB,那么此文件实际没有增加体积,自然也小于4GB.

7.    当块偏移值大于哈希表入口时,返回错误ERROR_HANDLE_DISK_FULL.

当文件是加密的时候得到seed(即附加的偏移值),当文件的加密key是修正过的,进一步计算修正过的seed,方法和读取的时候一致.见上面的SFileOpenFileEx()

函数11.

8.    假如MPQ文档的属性中指定了需要CRC32,FILETIME,MD5,hf的相关值指针指向ha的相关位置.即将文档中相关位置和目前文件中的相关指针关联在一起,以后改变hf的指针值就直接修改了文档中的相关值,为以后的修改提供方便.

9.    为压缩的数据分配buffer,首先计算了一个文件需要的块数量(文件的总大小 / 文档的块大小 + 1),假如文件大小正好是文档的块大小的整数倍,就将此数量再加一,目的是确保块数量充裕(需要在最后一个块尾保存额外数据).实际分配缓存大小仅仅一次分配了一个块的大小.

10.为此文件的扇区偏移表分配空间,为压缩的文件数据分配块大小*2大小的控件.

11.设定文件指针到新添加文件需要写入的地方,并将此文件的哈希表和块表赋相关值.这里很奇怪,没有写入前就将块表的标志位修改成MPQ_FILE_EXISTS,,按道理应该先写入后再修改...不然写入的时候失败了怎么办?

12.假如文件是压缩的,计算出块偏移值表的大小.并用此值来初始化先前分配的块偏移表,并将此块偏移表的第一个值设为此值.

13.接下来将块偏移表先写入文件.此时标志此MPQ文件已被改变.并将块表的第二个表示文件大小的值修正,即加上块表大小.

14.最关键的一部分来了,实际写入文件数据的部分:

{

    a)首先初始化CRC32MD5(没有判断需不需要?不需要的话,每次计算简直是极大的浪费(?))

    b)从文件开始读取数据,一次一个块的大小.

    c)计算CRC32,MD5.

    d)判断是否需要压缩,需要就压缩读入缓存的文件数据,假如压缩后文件数据没有变小,就保存原始数据.

e)将块偏移值表修改成正确的值, 理解上...因为上一个值表示目前写入数据的开始位置,那么将下一个值赋值成上个值+写入的大小.

f)假如需要加密,加密数据.加密的时候利用了前面计算的seed,目前可以确定的是,以前的理解有问题,seed是利用来加密数据的一个key,而不仅仅是附加的偏移值.

g)将数据写入MPQ文档.假如写入的大小不一致,又报ERROR_DISK_FULL错误.

h)将压缩后的大小继续加上此块的大小.

i)当所有的文件块都写入后,将计算得到的CRC,MD5写入hf.

}

15.通过以上的循环,一次一次写入了一个文件的所有块并完成了此文件的块偏移值表.

16.当文件标志位有MPQ_FILE_HAS_EXTRA,将最后一个块偏移值表的值赋值为前一个值.本来此值应该是此文件的结尾,但是此值现在变成了此文件最后一个块的开始位置.(?)因为对MPQ_FILE_HAS_EXTRA标志不理解,所以不明白此步的作用.

17.假如文件是加密的,此时将此文件的块偏移值表也加密.重新将指针调到此文件的应该在MPQ文档中写入的开始位置,写入文件.无语了.

18.假如整个写入过程都成功,就升级哈希表和块表及文档头,失败就清空哈希表,从前面来看,假如写入失败的话,块表没有被清空(?)

升级哈希表和块表过程如下:

{

   a)假如此文件是在块表最后添加的(即此块表的索引大于文档头的块表大小时),块表的大小加1.

   b)哈希表,块表的整体位置向后移动此文件实际写入的大小.并将相对于文档开始位置的偏移量写入文档头.

   c)假如在文件添加后,文档大于4G,将相关高16位的值修改成正确偏移地址.通过判断以前文档头的块表偏移值高16位是否存在来辨别是否是新版本的文档.因为前面已经判断过了,不是新版本此时文档不会超过4G,所以此时的判断是安全的.

  

}

19.释放写入文件时为文件分配的缓存.

 

阅读全文....

C++函数调用原理理解

空程序:

int main()

{

00411360  push        ebp       ;压入ebp

00411361  mov         ebp,esp     ;ebp = esp,保留esp,待函数调用完再恢复,因为函数调用中肯定会用到esp.

00411363  sub         esp,0C0h ;esp-=0C0h(192);为该函数留出临时存储区

;将其他指针或寄存器中的值入栈,以便在函数中使用这些寄存器。

00411369  push        ebx       ;压入ebx

0041136A  push        esi       ;压入esi

0041136B  push        edi       ;压入edi

0041136C  lea         edi,[ebp-0C0h] ;读入[ebp-0C0h]有效地址,即原esp-0C0h,正好是为该函数留出的临时存储区的最低位

00411372  mov         ecx,30h   ;ecx = 30h(48),30h*4 = 0C0h

00411377  mov         eax,0CCCCCCCCh ;eax = 0CCCCCCCCh;

0041137C  rep stos    dword ptr es:[edi] ;重复在es:[edi]存入30;0CCCCCCCCh? Debug模式下把Stack上的变量初始化为0xcc,检查未初始化的问题

return 0;

0041137E  xor         eax,eax   ;eax清零,作为返回值

}

;各指针出栈

00411380  pop         edi          ;弹出edi

00411381  pop         esi          ;弹出esi

00411382  pop         ebx          ;弹出ebx

00411383  mov         esp,ebp      ;esp复原

00411385  pop         ebp          ;弹出ebp,也复原

00411386  ret                      ;返回

 

 

函数调用:

         

int _tmain(int argc, _TCHAR* argv[])

{

同上理解, 保存现场

004113D0  push        ebp 

004113D1  mov         ebp,esp

004113D3  sub         esp,0F0h ;一共留了0F0h(240)空间

004113D9  push        ebx 

004113DA  push        esi 

004113DB  push        edi 

004113DC  lea         edi,[ebp-0F0h]

004113E2  mov         ecx,3Ch ; ecx = 3C(60),3C*4 = 0F0h,

004113E7  mov         eax,0CCCCCCCCh

004113EC  rep stos    dword ptr es:[edi]

同上理解.

    int a = 1, b = 2, c = 3;

定义a,b,c并存储在为函数留出的临时存储空间中.

004113EE  mov         dword ptr [a],1

004113F5  mov         dword ptr [b],2

004113FC  mov         dword ptr [c],3

    int d = Fun1(a, b, c);

参数反向入栈

00411403  mov         eax,dword ptr [c]

00411406  push        eax 

00411407  mov         ecx,dword ptr [b]

0041140A  push        ecx 

0041140B  mov         edx,dword ptr [a]

0041140E  push        edx 

调用Fun1

0041140F  call        Fun1 (4111DBh) ;Call调用时将下一行命令的EIP压入堆栈

恢复因为Fun1参数入栈改变的栈指针,因为Fun13个参数,一个整数4个字节,0Ch(12)个字节

00411414  add         esp,0Ch

00411417  mov         dword ptr [d],eax

将返回值保存在d.

    return 0;

返回值为0,eax清零

0041141A  xor         eax,eax

 

}

 

恢复现场

0041141C  pop         edi 

0041141D  pop         esi 

0041141E  pop         ebx 

以下全为运行时ESP检查:

先恢复因为为main预留空间而改变的栈指针

0041141F  add         esp,0F0h

00411425  cmp         ebp,esp

00411427  call        @ILT+320(__RTC_CheckEsp) (411145h)

正常时只需要以下两句就可以正常恢复esp,再出栈,又可以恢复ebp.

0041142C  mov         esp,ebp

0041142E  pop         ebp 

0041142F  ret     ;main返回             

 

 

int Fun1(int a, int b, int c)

{

同上理解, 保存现场

00411A70  push        ebp 

00411A71  mov         ebp,esp

00411A73  sub         esp,0E4h ;留了0E4H(228)空间,

00411A79  push        ebx 

00411A7A  push        esi 

00411A7B  push        edi 

00411A7C  lea         edi,[ebp-0E4h]

00411A82  mov         ecx,39h ; 39H(57)*4 = 0E4H(228)

00411A87  mov         eax,0CCCCCCCCh

00411A8C  rep stos    dword ptr es:[edi]

    int d = 4, e = 5;

定义变量

00411A8E  mov         dword ptr [d],4

00411A95  mov         dword ptr [e],5

 

    int f = Fun2(a, b, c, d, e);

再次参数反向入栈

00411A9C  mov         eax,dword ptr [e]

00411A9F  push        eax 

00411AA0  mov         ecx,dword ptr [d]

00411AA3  push        ecx 

00411AA4  mov         edx,dword ptr [c]

00411AA7  push        edx 

00411AA8  mov         eax,dword ptr [b]

00411AAB  push        eax 

00411AAC  mov         ecx,dword ptr [a]

00411AAF  push        ecx 

 

用Fun2

00411AB0  call        Fun2 (4111D6h) ;Call调用时将下一行命令的EIP压入堆栈

 

00411AB5  add         esp,14h ;恢复因为参数入栈改变的栈指针,因为Fun25个参数,一个整数4个字节,14h(20)个字节

Fun2函数的返回值(保存在eax),赋值给f;

00411AB8  mov         dword ptr [f],eax

 

    return f;

将保留在f中的Fun1的返回值保存在eax中返回

00411ABB  mov         eax,dword ptr [f]

}

恢复现场

00411ABE  pop         edi 

00411ABF  pop         esi 

00411AC0  pop         ebx 

 

以下全为运行时ESP检查:

先恢复因为预留函数存储控件而改变的栈指针,

00411AC1  add         esp,0E4h

再比较ebp,esp,假如程序运行正确,两个值应该相等.

00411AC7  cmp         ebp,esp

00411AC9  call        @ILT+320(__RTC_CheckEsp) (411145h)

正常时只需要以下两句就可以正常恢复esp,再出栈,又可以恢复ebp.

00411ACE  mov         esp,ebp

00411AD0  pop         ebp 

返回mainpop堆栈中的EIP开始执行

00411AD1  ret   

 

int Fun2(int a, int b, int c, int d, int e)

{

同上理解, 保存现场

00412050  push        ebp 

00412051  mov         ebp,esp

00412053  sub         esp,0E4h ;保留0E4H(228)

00412059  push        ebx 

0041205A  push        esi 

0041205B  push        edi 

0041205C  lea         edi,[ebp-0E4h]

00412062  mov         ecx,39h ; 39H(57)*4 = 0E4H(228)

00412067  mov         eax,0CCCCCCCCh

0041206C  rep stos    dword ptr es:[edi]

 

    int f  = 6, g = 7;

定义变量

0041206E  mov         dword ptr [f],6

00412075  mov         dword ptr [g],7

 

    int h = a + b + c + d + e + f + g;

相加,存入a,再保存在h

0041207C  mov         eax,dword ptr [a]

0041207F  add         eax,dword ptr [b]

00412082  add         eax,dword ptr [c]

00412085  add         eax,dword ptr [d]

00412088  add         eax,dword ptr [e]

0041208B  add         eax,dword ptr [f]

0041208E  add         eax,dword ptr [g]

00412091  mov         dword ptr [h],eax

 

    return  h;

将返回值h的值保存在eax

00412094  mov         eax,dword ptr [h]

 

}

恢复现场

00412097  pop         edi 

00412098  pop         esi 

00412099  pop         ebx 

0041209A  mov         esp,ebp

0041209C  pop         ebp 

0041209D  ret ;回fun1 ,pop堆栈中的EIP开始执行

阅读全文....

Breakpad在进程中完成dump的流程描述

Breakpad在进程中完成dump的流程描述

 

ExceptionHandler构造函数调用Initialize函数完成初始化.

pipe_name不为空的时候,利用智能指针创建一个CrashGenerationClient对象,并注册后赋值给成员变量crash_generation_client_.

这是调用IsOutOfProcess成员函数,确定是否正常创建了CrashGenerationClient,这里的注释说明, CrashGenerationClientCrashGenerationServer的创建主要是为了一个进程外的dump,也就是说,在不影响原程序运行的情况下,正常的dump,这里解释了在原测试程序中为何不调用CrashGenerationServer也可以正常dump,因为当CrashGenerationClient没有正常创建和注册的时候,程序就开始一个进程内的dump,也就是说,原程序将没有办法正常运行的dump.

因为这个简单的测试程序中调用的是五参数的构造函数构造ExceptionHandler,更没有开启CrashGenerationServer,所以这里走的是进程内dump流程.

关于ExceptionHandler构造函数参数的主要信息在<Breakpad 使用方法理解文档>.

 

此时ExceptionHandler自己创建线程来处理异常.首先还创建了信标handler_start_semaphore_,handler_finish_semaphore_,都设为无信号状态.

 

创建新线程用的是CreateThread函数,新线程调用的函数名为ExceptionHandlerThreadMain,参数为this

此线程的作用是无限的等待handler_start_semaphore_Semaphore信号,等到Semaphore信号后,直接就调用WriteMinidumpWithException函数,开始dumpDump后打开handler_finish_semaphore_Semaphore信号.

 

在开启等待线程后, Initialize函数继续进行,并载入相关库文件,初始化了两个函数指针成员变量,并设置好dump路径,

 

最后根据handler_types标志位,调用windows API确定取得各种异常情况下调用的函数.这些函数最后得到异常信息,处理后再通过判断IsOutOfProcess()调用WriteMinidumpOnHandlerThread()开启handler_start_semaphore_semaphore信号,开始dump.

 

       if (handler_types & HANDLER_EXCEPTION)

           previous_filter_ = SetUnhandledExceptionFilter(HandleException);

 

#if _MSC_VER >= 1400  // MSVC 2005/8

       if (handler_types & HANDLER_INVALID_PARAMETER)

           previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter);

#endif  // _MSC_VER >= 1400

 

       if (handler_types & HANDLER_PURECALL)

           previous_pch_ = _set_purecall_handler(HandlePureVirtualCall);

 

以上三个函数的含义分别是

SetUnhandledExceptionFilter(HandleException)确定出现没有控制的异常发生时调用的函数为HandleException.

_set_invalid_parameter_handler(HandleInvalidParameter)确定出现无效参数调用发生时调用的函数为HandleInvalidParameter.

_set_purecall_handler(HandlePureVirtualCall)确定纯虚函数调用发生时调用的函数为HandlePureVirtualCall.

 

在其定义的三个函数中,流程基本一样,都是先通过定义一个AutoExceptionHandler成员变量,通过上述三个函数恢复上一个用户的异常控制函数,以处理在此次异常处理中发生的异常.然后通过一些数据处理,再判断IsOutOfProcess(),这里我没有用CrashGeneration机制,自然还是返回false,此时这三个函数都是调用WriteMinidumpOnHandlerThread(),开启handler_start_semaphore_semaphore信号,效果就是开始调用ExceptionHandlerThreadMain线程开始dump.接下来,他们判断此次异常控制是否成功,不成功就将交给上一层的异常处理函数处理.

 

这一步具体的实现上:

HandleException()函数中,如同SHE常过滤器要求的那样,当成功时,返回EXCEPTION_EXECUTE_HANDLER,表示控制了异常,不成功时,此函数先判断是否有用户的上层异常过滤器函数,有的话,直接先利用它处理,没有就返回EXCEPTION_CONTINUE_SEARCH,告诉操作系统开始上一层异常处理的搜索.这里可以参看结构化异常处理程序理解文档第25章内容.

 

HandleInvalidParameter(),成功就直接通过exit(0)退出主线程了,不成功就调用上一层此类异常函数处理.当没有上一层此类异常函数,并且在Debug,其调用_invalid_parameter()函数,向编译器报告一个无效参数的信息.

 

HandlePureVirtualCall()函数中,之前加入了额外写入dump文件的信息,一个MDRawAssertionInfo结构,type被设为MD_ASSERTION_INFO_TYPE_PURE_VIRTUAL_CALL.其他流程类似.

 

以上基本就是不使用CrashGeneration在进程中完成dump的流程

 

 

 

阅读全文....

Breakpad 使用方法理解文档

Breakpad 使用方法理解文档.

只需要在任何异常前正常创建一个ExceptionHandler类的成员函数,就可以完成异常的捕捉及dump.

在创建ExceptionHandler,第一参数为宽字符表示的dump存储路径,第二参数为dump前客户需要运行的程序,程序原型应该为

 

bool (*FilterCallback)(void* context,

EXCEPTION_POINTERS* exinfo,

MDRawAssertionInfo* assertion);

 

程序返回false将不产生dump,返回true才正常dump.

 

第三参数为客户Dump后需要运行的程序,程序原型为:

 

bool (*MinidumpCallback)(const wchar_t* dump_path,

                                   const wchar_t* minidump_id,

                                   void* context,

                                   EXCEPTION_POINTERS* exinfo,

                                   MDRawAssertionInfo* assertion,

                                   bool succeeded);

 

程序返回true表示异常全部被控制,程序正常运行,返回false,程序仍然保留异常,以方便交给其他程序处理,比如各平台的原生异常处理系统.

 

 

HandlerType:确定需要控制的异常类型,HANDLER_ALL,控制所有类型的异常.

 

 

ExceptionHandler有两个版本的构造函数:

  ExceptionHandler(const wstring& dump_path,

                   FilterCallback filter,

                   MinidumpCallback callback,

                   void* callback_context,

                   int handler_types);

 

  ExceptionHandler(const wstring& dump_path,

                   FilterCallback filter,

                   MinidumpCallback callback,

                   void* callback_context,

                   int handler_types,

                   MINIDUMP_TYPE dump_type,

                   const wchar_t* pipe_name,

                   const CustomClientInfo* custom_info);

 

 

dump_path()用来返回dump的路径.

Set_dump_path(const wstring &dump_path)用来设置路径.

 

WriteMinidump()

WriteMinidump(const wstring &dump_path,

                            MinidumpCallback callback, void* callback_context);立即写dump文件.

 

WriteMinidumpForException(EXCEPTION_POINTERS* exinfo)用用户提供的异常信息立即写dump文件.

 

get_requesting_thread_id()得到出现异常或者调用WriteMinidump()函数的线程ID.

 

 

bool ExceptionHandler::WriteMinidumpWithException(

    DWORD requesting_thread_id,

    EXCEPTION_POINTERS* exinfo,

MDRawAssertionInfo* assertion)

为最主要的函数,具体的写下Dump文件.

 

这个结构很明显是用来保存异常信息的

typedef struct _EXCEPTION_RECORD {

    DWORD    ExceptionCode;

    DWORD ExceptionFlags;

    struct _EXCEPTION_RECORD *ExceptionRecord;

    PVOID ExceptionAddress;

    DWORD NumberParameters;

    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

    } EXCEPTION_RECORD;

 

typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;

 

 

typedef struct _CONTEXT {

 

    //

    // The flags values within this flag control the contents of

    // a CONTEXT record.

    //

    // If the context record is used as an input parameter, then

    // for each portion of the context record controlled by a flag

    // whose value is set, it is assumed that that portion of the

    // context record contains valid context. If the context record

    // is being used to modify a threads context, then only that

    // portion of the threads context will be modified.

    //

    // If the context record is used as an IN OUT parameter to capture

    // the context of a thread, then only those portions of the thread's

    // context corresponding to set flags will be returned.

    //

    // The context record is never used as an OUT only parameter.

    //

 

    DWORD ContextFlags;

 

    //

    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is

    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT

    // included in CONTEXT_FULL.

    //

 

    DWORD   Dr0;

    DWORD   Dr1;

    DWORD   Dr2;

    DWORD   Dr3;

    DWORD   Dr6;

    DWORD   Dr7;

 

    //

    // This section is specified/returned if the

    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.

    //

 

    FLOATING_SAVE_AREA FloatSave;

 

    //

    // This section is specified/returned if the

    // ContextFlags word contians the flag CONTEXT_SEGMENTS.

    //

 

    DWORD   SegGs;

    DWORD   SegFs;

    DWORD   SegEs;

    DWORD   SegDs;

 

    //

    // This section is specified/returned if the

    // ContextFlags word contians the flag CONTEXT_INTEGER.

    //

 

    DWORD   Edi;

    DWORD   Esi;

    DWORD   Ebx;

    DWORD   Edx;

    DWORD   Ecx;

    DWORD   Eax;

 

    //

    // This section is specified/returned if the

    // ContextFlags word contians the flag CONTEXT_CONTROL.

    //

 

    DWORD   Ebp;

    DWORD   Eip;

    DWORD   SegCs;              // MUST BE SANITIZED

    DWORD   EFlags;             // MUST BE SANITIZED

    DWORD   Esp;

    DWORD   SegSs;

 

    //

    // This section is specified/returned if the ContextFlags word

    // contains the flag CONTEXT_EXTENDED_REGISTERS.

    // The format and contexts are processor specific

    //

 

    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

 

} CONTEXT;

 

typedef CONTEXT *PCONTEXT;

 

 

 

typedef struct _EXCEPTION_POINTERS {

    PEXCEPTION_RECORD ExceptionRecord;

    PCONTEXT ContextRecord;

} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

 

 

typedef struct {

  /* expression, function, and file are 0-terminated UTF-16 strings.  They

   * may be truncated if necessary, but should always be 0-terminated when

   * written to a file.

   * Fixed-length strings are used because MiniDumpWriteDump doesn't offer

   * a way for user streams to point to arbitrary RVAs for strings. */

  u_int16_t expression[128];  /* Assertion that failed... */

  u_int16_t function[128];    /* ...within this function... */

  u_int16_t file[128];        /* ...in this file... */

  u_int32_t line;             /* ...at this line. */

  u_int32_t type;

} MDRawAssertionInfo;

 

 

 

实现上:FilterCallback(ExceptionHandler构造函数的第二参数)不为空的时候,首先用WriteMinidumpWithException函数的原参数直接调用FilterCallback,(参数类型一致),完成用户在调用Dump先需要做的工作.

然后通过IsOutOfProcess()成员函数判断是否此时是Exception_handler自身来进行dump,详细内容,参看 <Breakpad在进程中完成dump的流程文字描述>

IsOutOfProcess()返回真的时候,应该表示此时处理是在进程外发生的,也就是利用crash_generationclient,server的机制完成,不影响原进程的运行.

此时调用CrashGenerationClient类型的成员变量的RequestDump函数完成dump.

 

当返回假的时候,此时先判断是否从dbghelp.dll中正确的importMiniDumpWriteDump函数,然后创建了一个新的可写的文件.

当创建成功后,ExceptionHander加入异常信息到user_streams,然后调用minidump_write_dump_(实际就是dbghelp.dll中的MiniDumpWriteDump函数)完成dump.

 

主要的信息都在WriteMinidumpWithException函数的第二参数及第三参数里面.从注释上看,这些信息属于breakpad自己新增加的附加信息,但是增加这些信息对以前的dump文件正常使用没有影响,这些信息来自出现纯虚函数调用和无效参数调用时,breakpad自定义的用户函数中添加的信息.

 

    _set_invalid_parameter_handler(ExceptionHandler::HandleInvalidParameter);

#endif  // _MSC_VER >= 1400

_set_purecall_handler(ExceptionHandler::HandlePureVirtualCall);

 

, HandleInvalidParameter, HandlePureVirtualCall中添加的信息.

此信息类型为MDRawAssertionInfo,HandlePureVirtualCall函数调用时,仅仅是设定异常类型为HandlePureVirtualCall,当出现无效参数调用时,增加的内容较多:

  _snwprintf_s(reinterpret_cast<wchar_t*>(assertion.expression),

               sizeof(assertion.expression) /, sizeof(assertion.expression[0]),

               _TRUNCATE, L"%s", expression);

  _snwprintf_s(reinterpret_cast<wchar_t*>(assertion.function),

               sizeof(assertion.function) / sizeof(assertion.function[0]),

               _TRUNCATE, L"%s", function);

  _snwprintf_s(reinterpret_cast<wchar_t*>(assertion.file),

               sizeof(assertion.file) / sizeof(assertion.file[0]),

               _TRUNCATE, L"%s", file);

  assertion.line = line;

  assertion.type = MD_ASSERTION_INFO_TYPE_INVALID_PARAMETER;

 

分别为调用无效参数的表达式,函数名,文件名,行号,最后设定异常类型为

MD_ASSERTION_INFO_TYPE_INVALID_PARAMETE.

 

最后关闭文件,再调用callback_函数,也就是ExceptionHandler构造函数的第三参数.

 

其他主要内容在 <Breakpad在进程中完成dump的流程文字描述>描述.

 

 

阅读全文....

关于以前应用程序运行出现配置错误的问题的解决方案

问题很简单,就是因为缺乏支持库,这里给大家几个建议,因为我的程序是在VS2005+sp1编译的,所以需要它特别的支持库,可以在以下地址找到下载,文件名为vcredist_x86.rar,另外,也有可能是因为.NET的支持库问题,目前我没有碰到,因为我的程序好像也没有用到.NET的特性.假如有需要的,可以到微软的网站去下载,另外,当然你可以直接下我的源代码,然后重新编译.

http://groups.google.com/group/jiutianfile/files

最后,本来我可以重新用静态链接的方式编译的......但是,还是推荐你们安装以上文件比较一劳永逸.不然我每个编译出来的程序都要大一块.

当然,你不想从我给的地址下载,也可以在微软的网站去搜搜,

全名叫

Microsoft Visual C++ 2005 SP1 Redistributable Package (x86)

阅读全文....

从一个简单的三元结构 看C,C++

初到北京,适应了一下,好久学习了(大概一周多),重新开始学学。

严蔚敏的数据结构(C语言版),第一个介绍的数据结构是一个三元结构,她命名为Triple,她用类C伪码描述的。我自己用C++实现了一下。这里想说说感受。先看我写的源代码:

 

#include <iostream>

#include <cassert>

using namespace std;

 

template <typename T>

class Triplet

{

public:

    Triplet(T v1 = T(), T v2 = T(), T v3 = T());

    T Get(size_t i);

    void Put(size_t i, const T& e);

    bool IsAscending();

    bool IsDescending();

    T Max();

    T Min();

private:

    T m_value[3];

};

 

template <typename T>

Triplet<T>::Triplet(T v1 = T(), T v2 = T(), T v3 = T())

{

    m_value[0] = v1;

    m_value[1] = v2;

    m_value[2] = v3;

}

 

template <typename T>

T Triplet<T>::Get(size_t i)

{

    assert(i >= 1 && i <= 3);

    return m_value[i - 1];

}

 

template <typename T>

void Triplet<T>::Put(size_t i, const T &e)

{

    assert(i >= 1 && i <= 3);

    m_value[i - 1] = e;

}

 

template <typename T>

bool Triplet<T>::IsAscending()

{

    return (m_value[0] < m_value[1] && m_value[1] < m_value[2]);

}

 

template <typename T>

bool Triplet<T>::IsDescending()

{

    return (m_value[0] > m_value[1] && m_value[1] > m_value[2]);

}

 

template <typename T>

T Triplet<T>::Max()

{

    return m_value[0] > m_value[1] ? (m_value[0] > m_value[2] ? m_value[0] : m_value[2]) : (m_value[1] > m_value[2] ? m_value[1] : m_value[2]);

}

 

template <typename T>

T Triplet<T>::Min()

{

    return m_value[0] < m_value[1] ? (m_value[0] < m_value[2] ? m_value[0] : m_value[2]) : (m_value[1] < m_value[2] ? m_value[1] : m_value[2]);

}

首先,想起来Bjarne Stroustrup说的话,所谓支持一种编程范例,指的不是能不能用一种编程语言去写这种风格的程序,而是看这个编程语言有没有对这个风格的原生的支持,能不能很简单的写出来。

比如,很多人就很喜欢说,用C语言也可以写面向对象的程序,的确,是可以,看严蔚敏《数据结构》(C语言版)你可以用C语言去实现她描述的那些数据结构,那就像在写面向对象程序一样,但是怎么样呢?

当我用C++来写同样的程序的时候,我会发现,C++增加了很多新的特性来支持面向对象,使得比用C语言来编写要简单的多。从Triplet来看,首先InitTriplet被构造函数替代了,这样更为方便,其次,因为C++的类机制,可以同时创建多个Triplet对象,而只需要写一次,并且,减少了在自由空间动态分配内存的需求,这样很大的减少了错误的发生。还有,模板机制的引进为泛型对象的编写提供了便利,这样一类对象都只需要写一次。仅仅是这样一个简单的小类,就可以看出C++一些新的机制的作用,还没有用到继承。随便说说,我也不是想说C++就是完美的,就是说用C语言去开发面向对象的程序,好像实在是费力不讨好。

阅读全文....

在微软Windows中支持多指针设备(Supporting Multiple Pointing Devices in Microsoft Windows)

在微软Windows中支持多指针设备

 

原文作者:Michael Westergaard

译者:湖南大学 谢祁衡

 

在此声明:此文为本人毕业论文翻译的文章,请勿转载,不然出现什么问题,本人可是要找麻烦的。特别是出现学校方面因为在网上发现此文怀疑本人抄袭的时候。另外,因为本文已经收录在学校了,所以我有足够的证据能证明我上交此文的时间先于网络上有此文的时间。原文请到google上搜索,我不转载,不过也可以到我创建的讨论多鼠标的google群里面去下载,http://groups.google.com/group/Single-Display-Groupware

    摘要: 本文描述了在微软Windows中的一个包含多指针设备支持API的实现。微软Windows本来不支持多指针设备控制独立的指针,但是我们或其他人已经完成了很多解决此问题的实现。在这里我们描述了一个通常的解决方法,和一个应用程序怎么样可以通过一个框架使用它。这个设备驱动程序和支持其的API将免费提供。感兴趣的人可以联系作者以获得更多信息。

 

 

1   引言:

 

    为现代桌面应用程序开发的交互技术正在改变。更早的时候,通常是用一个指针设备(典型的是一个鼠标或者轨迹球),和一个键盘。然而,碰到提供更复杂交互技术的程序不再少见,比如使用多个指针设备,每人控制一个指针。微软Windows不是本来就完全支持超过一个指针设备。假如多个指针设备连接上,它们都控制着同一个指针。已经有了很多为解决此问题的方法,使得可以有多个独立的指针。这里我描述了一个通常的实现,它允许程序很简单的使用多个指针设备。这个解决方法被设计在微软Windows 2000/XP中使用。接下来,我提到的支持平台仅仅是Windows,并且用“鼠标”表示任何指针设备。(鼠标,轨迹球,笔等)。

    多鼠标在不同的设定下,可能有很多优点。比如,一个人可能一手用鼠标而另一手用轨迹球,用鼠标来精确定位,而用轨迹球做更大范围的移动。这可以在改变一个窗口大小的时候用来移动窗口,而这些用来做桌面电脑Windows中的版面设计是非常有用的。这被称作双手调整(two-hand resize)。这个交互技术也可能在其他情况下被使用,比如在一个动作中放大和移动组件。一个人也可以假想使用两个或者更多鼠标,而每个鼠标有着不同的用途:一个鼠标用来绘制矩形,而另一个用来画圆。这样用户有一个从一个器件到另一个的直接的物理切换。这在绘制图形模型语言Coloured Petri Nets (CPN) [11]时非常有用。为了此例中的目的,CPN是一个有不同节点的指向图形,一些被描绘成矩形,一些被描绘成圆形。很多现代的交互技术,比如tool-glasses[3]或者浮动调色板(floating palettes),需要至少两个鼠标;一个鼠标用来移动调色板,而另一个用来选择工具。我们相信使用多鼠标将引起用户在本地从一个手到另一个手里转移负担,因此,可以降低长期累积的操作伤害。也可以设想更多的人,每个人有一个鼠标协同在一个大屏幕上解决问题。这就是WorkSPACE [17]的一个目的所在。

    CPN Tools [2, 4] 是一个CPN的编辑和模拟软件,它就是用了很多先进的交互技术。从最开始此计划的一个目的就是使用先进的交互技术,包括,但不限于加入先进的多指针设备交互技术。在此计划的早期环境里,Windows对于支持多鼠标的限制变得很明显,然后就实现了一个自己的解决方案。这个自己开发的解决方案产生了一个额外的程序,它将直接探测串口,有哪个鼠标已经连接上,解码从已连接上鼠标发来的信号,并且发送事件到主程序。鉴于支持任意多数鼠标的需要,第一个解决方案被证明是非常低效的。首先,得到串口鼠标变得越来越困难,而我们至少支持USB鼠标,最好是也支持串口鼠标,PS/2鼠标和任何将来的鼠标,而与此同时,为所有的鼠标只提供一个通用的接口。在第一个解决方案里面这些都是不可能的,因为原始鼠标被Windows控制,而第二个鼠标被衍生的应用程序控制。第二,这个解决方案需要在Windows里面使第二个鼠标失效,这可能导致用户不得不在桌面上有一个基本上在CPN Tools应用程序里没有用的额外鼠标。第三,这个解决方案不是通用的,当鼠标的接口改变时,比如改到USB 时,都要重新实现。因为这些原因,我们决定实现一个新的解决方案。因为我们从早期的实现中获得了很多经验,我们决定做一个独立于CPN Tools的实现,足够通用到给其他人使用。这个实现的一个目标是让设计的API至少像Windows提供的普通鼠标接口一样容易使用。

    这个任务的困难可以通过看其他使用多鼠标的尝试有所了解。MAME:Analog+ [1](一个支持多鼠标的游戏库),和Multiple Input Devices (MID) 计划[7, 8] ( 一个支持多鼠标的通用的Java库)尝试使用多鼠标,但是在Windows 2000里面都失败了。两个程序都引用了我们也从微软接收的同一个信息。在Windows NT/2000/XP中不能分辨多鼠标的输入是一个设计决定,并且两个计划都说明了在这些操作系统中使用多鼠标是不可能的。另外,MAME:Analog+说明了你必须使用USB设备来得到多鼠标。此外,两个库都集中于每个用户使用一个鼠标,而没有认识到每一个用户使用多个鼠标的潜力。

 

 

2   设计概要与需求

 

    这个设备驱动程序和支援API被设计来提供一个硬件与用户层间的接口。总的体系结构如图1所示。

这个设备驱动程序负责滤过鼠标的事件,就像是从各个不同硬件的驱动程序中接受一样。低层的API负责与设备驱动程序交流和提供一个到更高层的回馈机制。高层API允许用户层程序高效地或通过预定的方式接收鼠标事件。

这个抽象对于支持多鼠标的框架程序有用。Octopus[5]是一个类似框架的例子。与Octopus的整合的进一步细节将在第3章中描述。CPN Tools是一个在Octopus上的用户层程序实例。

    示意图1

   

    设备驱动程序和支持其API的框架图(左边),对比显示了鼠标通常工作方式(右边)。这些可以同时相容的存在于旧应用程序中。本文主要描述了方框加粗的部分。

 

2.1 设备驱动

    即使一个鼠标意味着要在使用了这个设备驱动和本文描述的API的程序中使用,它在Windows中必须也能作为一个有效的普通鼠标,因为不这样的话,它将不能在一个没有使用此设备驱动的程序中使用。

    接下来,我们将提到普通的鼠标和作为Windows鼠标/游标的相关指针游标。

因为我们想要支持很大数量的不同鼠标,为存在的每个不同类型的鼠标提供一个设备驱动程序实现是不行的。为了让应用程序对所有有效的指针设备使用一个单一的接口,在此描述的驱动可以也应该被安装在一个系统的所有鼠标之中。

    因为这些原因,我决定以过滤式驱动的方式来实现这个设备驱动程序。[15]这些允许我们在任何鼠标的驱动栈中作钩子,安装我们自己的驱动接口作为这个鼠标接口的一个补充,而且滤过任何我们想传递给Windows鼠标的硬件事件。此外,一个滤过式驱动可以被安装在一个便利的点上,在这里硬件的不同已经被排除了,但是它仍然可能分辨不同的鼠标。

    这里接下来的三个章节利用了一些关于Windows驱动开发的知识和一部分Windows API,但是即使略过也不失文意。为了使实现尽量的简单,我以Microsoft的Windows驱动程序开发软件包(Driver Development Kit)的moufiltr驱动作为蓝本。

    当你尝试开发鼠标过滤式鼠标驱动程序的时候,这个驱动程序是一个很好的起点;

基本上只需要修改一个函数来实现过滤鼠标事件。这个 moufiltr 驱动已经作为一个在所有普通鼠标驱动程序mouclass顶端的过滤器进行安装,使得它很容易用单一的驱动支持串口,PS/2 和 USB 设备。它很容易用 IoRegisterDeviceInterface 来增加我们自己的设备界面。

    我们遇到的第一个问题是Windows 本身的寄存器中只把任何鼠标类设备当作单一的用户,安装了过滤器的也一样。通常一个程序和任意一个驱动程序交流靠的是先寻找到正确的设备(一个驱动程序的实例)。然后,程序用创建文件一样的方式得到了一个用来交流的句柄,就像创建了一个在这个驱动程序名字空间(name-space)中的“文件”一样。这个句柄可以使用DeviceIoControl 函数利用Device Input and Ouput Control(IOCTL)[13]用来和设备驱动程序交流。因为Windows本身的寄存器独享到鼠标的入口,所以不可能得到一个允许实现IOCTL的句柄。然而,得到一个允许查询设备属性的句柄还是可能的。当我们尝试这样做的时候,设备驱动程序收到通知然后传递被我们查询的文件的信息。我们使用这个在一个文件名中加进函数调用,用得到文件名的形式。

<device-name>/execute/<function>(/<parameter>)*

    这样可能看起来不像一个好的解决方案,但是它却有两个很好的属性:它简洁而且很容易有个逆向函数。因为简洁是一个目标,这本身就证明了这是好的解决方法。考虑到应用程序会崩溃和应用程序设计者会忘记关闭文件的事实,逆向函数的调用应该明显。因为当一个应用程序终止或崩溃时,Windows自动关闭属于此应用程序的所有打开文件,我们得到了一个此实现的近似效果。假如一个应用程序已经通知驱动使服务启动,这个应用程序会在停止,甚至崩溃时,自动地告诉设备驱动程序停止服务。

    第二个问题是当鼠标事件发生的时候,我们想要实现一个用户模式的回调。因为这可能频率很高的发生,所以我们想要一个相当轻量级的机制,而且可以实现一个回调。我们已经考虑过使用带名字的事件,但是发现这样对于我们的目的来说有一点过于复杂。我们也想使用提供了非常快的用户模式回调却没有文档支持的函数 KeUserModeCallback,但是因为我们不知道我们是否在一个线程的环境中,所以无法使用。

    我们最后决定使用Asynchronous Procedure Calls (APC).APCs 可以实现从系统一个部分到另一个部分的调用,但是只在这个接收部分所在的线程里。我们因此可以在用户模式做一个从设备驱动程序到用户模式的回调。APC 的功能包括在普通的Windows库里,但是却没有很好的文档支持。幸运的是文献[6, 16]包含了开始需要的足够信息。

    这个设备驱动程序接口提供了四个函数到用户模式。Get,UnGet,Suspend,UnSuspend。Get函数是用来在鼠标上做钩子和注册一个用户模式的回调。当这个函数被一个设备调用的时候,它停止传递事件,所以Windows的鼠标将不知道任何事件。我们将物理上的鼠标与逻辑上的Windows鼠标分开并加进我们自己的逻辑鼠标是有效的。UnGet是相反的函数,用来放弃回调并且使鼠标返回给Windows操作。

    Suspend函数是用来临时将鼠标从应用程序中挂起。当一个应用程序已经调用了Get函数,并且想要Windows临时从鼠标接收事件,但是同时也接受它自己的事件,还要防止其他应用程序得到鼠标的情况下,这个函数被调用。有趣的是,如果鼠标移动到应用程序窗口外面并且用户应该再有一个普通的Windows指针。

    总之,所有的驱动程序可能是示意图2中描述的三种状态之一,括号里的文字指示了哪个进程可以调用给定的函数,并且只有指示的转换才是合法的。设备驱动应该确保一个应用程序正使用鼠标时,其他应用程序不能使用鼠标。它只允许特定的进程调用不同状态的特定函数,以此达到上述目的。当一个进程已经调用了Get函数,它就变成这个设备的所有者,直到Unget的函数被调用,只有所有者进程可以对这个设备调用函数。这些是为了防止代表其它程序的外部进程阻止访问设备驱动程序。

 

示意图2

 

    设备驱动程序可能在上述的三种状态之一。在free状态时,鼠标就像普通的鼠标一样。在hooked状态时,这个鼠标就被应用程序所捕捉,而且不能控制Windows的鼠标。在suspended状态时,鼠标对于用户来说就像free状态一样,但是调用了Get函数的应用程序仍然控制着它。这些在括号里的名字指示了哪个进程被允许调用相应的函数来改变状态。

 

 

2.2 低层API

    尽管设备驱动程序提供了一个相当简单的到鼠标的接口,但是要和设备驱动交流却让人有点生厌,因为必须列举所有的鼠标并且搜寻鼠标来使用和手动调用设备驱动提供的函数。低层API通过提供一个简单的接口来设定一个回调函数来使这个任务更简单。另外,它简单的利用了自然数简化了鼠标的识别(与Windows里提供的没有标识的普通鼠标相反),而且加进了额外的错误控制机制。基本上低层的API隐藏了操作系统来支持多鼠标,所以建立在低层API上的程序就很容易移植,比如说,如果微软决定完全支持多鼠标。一个低层API已经利用标准C和BETA语言实现了。BETA实现是建立在C语言实现上面的。

    低层API提供了五个主要的函数:RegisterCallback, GetMice, UnGet- Mouse, SuspendMouse,和 UnSuspendMouse。低层API包含了一些更多的函数,但是它们对于理解这个API并不是那么重要。RegisterCallback负责注册一个回调函数给应用程序。回调函数是一个在任何已经加了钩子的鼠标发送事件的时候被调用的函数。所有鼠标只有一个回调函数可以被注册,而且在所有加钩子的鼠标中应该多路传输。假如不能接受这些,不得不实现另外一个低层的API。GetMice函数用来得到一个给定数量的鼠标,或者当给参数0的时候得到所有可用的鼠标。API的用户没有任何办法控制哪个鼠标被加钩子。这些看起来都不是主要问题,作为一个应用程序将可能总是给所有可用的鼠标加钩子,并且,最终被一个框架程序支持,以让用户决定哪个鼠标被用来干什么。GetMice函数返回实际得到鼠标的数量。我们实际得到的鼠标将被编号。当稍后利用UnGetting函数使鼠标给更高层程序负责时,任何记录都是需要的。当一个鼠标已经通过调用这个函数被加了钩子,事件就通过注册的回调函数来响应。假如没有回调函数被注册,新的事件将被忽略。UnGetMouse, SuspendMouse, 和 UnSuspendMouse 函数基于特定的鼠标调用,假如它已经通过GetMice函数被加了钩子的话。

这个API不是线程安全的,也就是说,假如它们在不同的线程里面,函数的调用必须被互斥保护。这些是可以接受的,因为它假设那些函数将被单线程程序或程序初始化时调用。这些API确保了注册的回调函数的正常连续调用,所以它也没有保证线程安全。

 

2.3 高层API

    高层API建立在低层API回调函数的基础上。作为BETA语言并不能高效的支持这些(它不用真的操作系统线程),所以我们决定实现一个更高层的API。这个API必须处理这些从回调函数中得到的信息,而且把它们作为普通的事件提供给应用程序。进一步说,就像已经提到的,低层API不是线程安全的,高层API也有这个特点。高层API也负责所有输入设备的加速问题以及为每一个鼠标绘制指针。最后,高层API提供每个鼠标相对及绝对位置的调用。目前,高层API被设计成一个程序的静态链接,而且管理这个程序的驱动访问。每个都静态地链接到高层API的多个程序,可以同时运行。但是任何给定的鼠标一次只能被一个应用程序加载钩子;比如说,假如一个用户有三个安装好的鼠标,而一个需要两个鼠标的应用程序,剩下的一个鼠标对其他应用程序是有效的。所有的鼠标都按照“先进,先利用”(“First come,first served”)原则决定是否有效的。高层的API是为了Octopus框架程序而被使用和设计的,所以可能不是太适合其他框架或应用程序的需要。在未来,可能会有一个高层API的动态链接库的实现。

    高层API产生一个线程来监听新的回调函数。收到的信息可以利用PostMessage 函数被处理和发送给主应用程序窗口。假如需要的话,指针可以被更新。

高层API提供了十个函数:Initialise,Cleanup,SuspendMouse,UnSuspendMouse, GetRelativePosition,GetAbsolutePosition,SetAbsolutePosition,SetCursor,

LockCanvas和UpdateCursors。Initialise和Cleanup被用来打开和关闭高层API,SuspendMouse 和UnSuspendMouse 仅仅是同名低层API函数的线程安全版本,所以对于它们就不加过多的描述了。GetRelativePosition,GetAbsolutePosition,和 SetAbsolutePosition是为了那些比起事件方式更喜欢轮流检测方式接收鼠标信息的应用程序而设计的。SetCursor,LockCanvas,和UpdateCursors是用来设置和绘制指针的。

    Initialise 函数开始一个线程来监听低层API的回调函数。它需要的参数依次为,需要监听鼠标的编号,发送事件的窗口,绘制鼠标指针的显示上下文(Display Context),与一些描述这个API行为的标记。这些标记控制打开或关闭API的一些特性。例如一个程序员可以选择他是否需要发送事件给一个主窗口(假如不要,就不需要给出一个窗口作为参数),他是否想要绘制一个指针(假如不要,就不需要传递一个显示上下文作为参数),这个指针是否应该被剪切后再传递给显示上下文,鼠标是否需要加速,还有当鼠标被挂起(suspended)时,事件是否应该被发送。Cleanup 函数取消获得(UnGet)所有的被加钩子的鼠标,关闭通过Initialise 函数开始的进程,并且释放所有运行API时获得的资源。

    就像已经提到的,GetRelativePosition,GetAbsolutePosition和SetAbsolutePosition 函数本来是为了更喜欢检测方式获得鼠标信息的程序设计,但是需要的时候,它们也可以被用来与事件结合。所有这些函数都以鼠标的编号为参数。SetAbsolutePosition 函数另外还取一个点(坐标)作为参数。两个Get函数返回一个点(坐标)。GetRelativePosition 函数得到特定鼠标自从上次调用此函数后的相对移动。结果被加速度影响,但是不被剪接所影响。GetAbsolutePosition 和 SetAbsolutePosition 函数得到或设置绝对的位置。加速度和剪切都会影响位置。

SetCursor 函数为一个给定的鼠标设置一个指针。它以鼠标的编号,一个指针和指针的焦点为参数。UpdateCursors 函数为鼠标重绘指针。这些都在一个鼠标移动的时候被自动调用,但是它也可能通过刷新屏幕的方式被手动调用。

    假如LockCanvas 被调用,UpdateCursors 将不能被自动调用,直到UpdateCursors 被此线程内的LockCanvas手动调用。这个函数应该仅仅在应用程序重绘显示上下文前被调用,UpdateCursors 应该仅仅在此更新后被调用。

 

 

3          应用和未来的工作

 

    设备驱动程序和相对应的API都已经被集成进Octopus框架程序,从而CPN Tools也进行了应用。Octopus是一个从示意图1左边那一列描述的框架程序的例子,而CPN Tools是一个应用程序,也是从左边那一列开发而来。接下来的观点大体都是基于Octopus框架开发的经验而来。

    这个框架必须负责完全抽象位置和按键点击的概念。它必须提供提供一个和今天其他应用程序框架服务类似的服务。它还必须有一组控件,而且当任何鼠标动作发生的时候发送事件给它们。例如在用BETA语言编写的Lidskjalv用户接口框架(User Interface Framework)中 [9],一个pushButton有一个eventHandler,同时也有一个onClicked事件。当用户点击鼠标的时候,这可能通过继承被绑定来接收事件通知。相似的,Octopus框架为使用多鼠标提供了一些控件和服务。它提供了一个与onClicked事件类似的机制,但是它必须也通知接收者一个事件以传递其他鼠标目前正在做什么,这是为了允许应用程序开发者来特殊化一些多手交互操作,比如像第1节描述的双手调整那样。因为操作系统不支持多鼠标,这个框架程序必须也开发一个更高级的有效机制,以便当需要的时候可以控制系统鼠标。我们计划当任何鼠标离开应用程序窗口的时候,它就被挂起(也就是说,它现在控制Windows系统鼠标),而且当任何以前加过钩子的鼠标进入应用程序窗口,它就被激活,此时Windows的鼠标没有鼠标操作。最后,一个框架可能也支持在运行的时候增加和移除鼠标,但是因为我们不认为增加和移除鼠标是一个非常常用的操作,暂时省去。

    Octopus目前使用本文描述的驱动程序和API。驱动程序是独立于Octopus框架和CPN Tools应用程序的,但是已经实现的API的仅仅是我们在此刻开发Octopus和CPN Tools中看到需要的。因为这个原因,此API还有很大提升的空间。一个例子是应用程序开发者不能控制在使用GetMice函数时究竟为哪个鼠标加载钩子。一个明显的提升是提供一些函数让应用程序基于文字描述选择鼠标,文字可以描述这个鼠标的性能(2键或3键,有无滚轮等)或者在总线上的位置等信息。

    目前设备驱动程序和API的实现并不能很好的支持多鼠标应用程序同时使用鼠标。就像所描述的,设备驱动程序实际上避免了多鼠标应用程序同时使用相同的鼠标。这个行为在只有很少的应用程序使用这些设备驱动程序和API时是可以接受的,但是当使用多鼠标变得更平常时就不能接受了。因此我们提议制作一个分享机制可能是有效的。此机制可能将被安装进系统的服务而且简单的发送鼠标事件给鼠标目前所在的窗口。

这个解决方案特意在本文中进行描述。它非常容易创建在设备驱动程序顶端的其他API,也可以移植目前的API到其它的程序语言。本文里的描述的高层API是最满足Octopus需要的一个,但是其他框架程序可能有其他的需要。比如说,可能实现一个与Windows控件系统兼容的框架程序,它仅仅在Windows里面加入多指针,这样就可以通过多用户同时使用不同的应用程序的方式被现存的程序使用。进一步的说,微软是否会决定使分辨不同鼠标的输入成为可能,我们就可以不要设备驱动程序,并且改变低层API来使用这个新的功能,而其余的实现将继续正常工作,没有任何问题。

    想利用这个设备驱动程序改变MAME:Analog+ 和 MID实现的人可能也会对此感兴趣,此实现能允许在更多环境下使用多鼠标,比如在Windows 2000下,或者取消只使用USB设备的限制,以此就能很大地扩展它们的可用性。

 

 

4          结论

 

    在本文中我们描述了让多鼠标有各自独立指针的主要问题,这个事实是Windows在应用程序级别上不能分辨不同的鼠标。

我们已经描述了此技术的一个解决方案,此方案是通过使用一个设备驱动程序和两组API来实现的,其中每一部分都移除了一些很复杂的工作,并且提供给程序员一个更干净的接口来使用多鼠标。

    最后,我们简短的描述了一下未来的路。一些观点必须放在开发支持多鼠标的框架中时才成立。主要的,它必须与现有事物(带事件的控件)基本相似,但是它也必须提供了其他鼠标正在做什么的信息。

    这个设备驱动程序和支持其的API将免费提供。感兴趣的人可以联系作者以获得更多信息。

 

 

5   致谢: 作者想感谢Kurt Jensen, Jens Bæk Jørgensen, Thomas Mailund和提供了建设性意见的审阅者。

 

 

 

 

 

6 参考文献

1.A MAME site For Improved Analog Input.http://www.urebelscum.speedhost.com/.

2.M. Beaudouin-Lafon and M. Lassen. The Architecture and Implementation of CPN2000, A Post-WIMP Graphical Application.  In Proceedings of ACM Symposium on User Interface Software and Technology. ACM Press, November 2000.

3.L. Bier, M. Stone, K. Pier, and W. Buxton T. De Rose.Toolglass and magic lenses:the see-through interface.In Proc. ACM SIGGRAPH, pages 73–80. ACM Press, 1993.

4.University of Aarhus CPN group. CPN Tools homepage.

http://www.daimi.au.dk/CPNTools/.

5.M.B. Enevoldsen and M. Lassen.  Octopus: A PostWIMP Framework for New InteractionTechniques.http://www.ecoop2002.lcc.uma.es/tpDemonstrations.htm#d13.

6.J. Finnegan. Nerditorium.

http://msdn.microsoft.com/library/en-us/dnmsj99/html/nerd0799.asp, July 1999.

7.J.P. Hourcade and B. Bederson. Multiple input devices: Mid.

http://www.cs.umd.edu/hcil/mid/.

8.J.P. Hourcade and B.B. Bederson.  Architecture and Implementation of a Java Package for Multiple Input Devices (MID).  Technical report, Institute for Advanced Computer Studies, Computer Science Department, University of Maryland, May 1999.

9.Mjølner Informatics.  Lidskjalv: User Interface Framework – Reference Manual.  Mjølner Informatics Report, MIA 94-27, available at

http://www.mjolner.dk/mjolner-system/documentation/lidskjalv- ref/index.html.

10.Mjølner Informatics. The Mjølner System: BETA Language.

http://www.mjolner.dk/mjolner-system/beta_en.php.

11.K. Jensen.Coloured Petri Nets. Basic Concepts,Analysis Methods and Practical Use. Volume 1,Basic Concepts.Monographs in Theoretical Computer Science.Springer-Verlag, 1992.

12.O.L. Madsen, B. Møller-Pedersen, and K. Nygaard.Object-Oriented Programming in the

BETA Programming Language.Addison-Wesley, June 1993.

13.Microsoft Corporation. Device Input and Output Control (IOCTL).

http://msdn.microsoft.com/library/en-us/devio/devio_7pb3.asp.

14.Microsoft Corporation. Microsoft Windows Driver Development Kits.

http://www.microsoft.com/ddk/W2kDDK.asp.

15.P.G. Viscarola and W.A Mason.  Windows NT Device Driver Development, pages 235–239.Macmillan Computer Publishing, 1999.

16.Anatoly Vorobey.User mode APCs.   http://www.cmkrnl.com/arc- userapc.html, May 1997.

17.WorkSPACE: Distributed Work support through component based SPAtial Computing En- vironments. http://www.daimi.au.dk/workspace/index.shtml.

 

阅读全文....

Windows中多指针输入技术的实现与应用(说在前面的话(1))

Windows中多指针输入技术的实现与应用(说在前面的话(1))

首先要说的是,此文的主要内容都来自本人 谢祁衡  湖南大学 郑善贤 老师指导下写的 毕业论文,以前有一些网友表示过兴趣,他们有的是想要学习利用此种技术的,有的是想与我合作发觉商业价值的,也有一位和我一样是准备做毕业论文的,由于当心过早在网上传播此文,学校方面会误解我论文的来历,所以一直没有敢在网上透露太多细节,而近一段时间又开始找工作,非常的忙,所以在论文完成后自己的进一步完善的MFC的多鼠标类又没有真正的结果,而明天我就要去北京找工作了,可能又有比较长一段时间不能做出更多的成果,甚至很容易把近期所学都忘了,所以干脆早点把此文在网上发布吧,至于最后部分的完善就有待我找到工作以后去了。

      另外要声明的是,我的毕业论文实际完成于去年7月份左右,当时我从网络上甚至查不到任何相关的中文资料,更别说是相关的完整的文章了,希望不是我搜索技巧太不到家的原因吧,而现在离那时已经过去快一年了,可能中间有些信息有所变化,因为没有时间,我也不可能一一查找补充了,希望大家原谅。假如其间有类似本文的论文发表,我也只能说是碰巧了。

      此文在这里发布的时候实际经过了我的修改,因为在论文交上去后我自己又有了新的结论或者新的想法,我都加了进来,但是,此文的主要部分仍然是我的毕业论文,在这里说明一下。

      在这里我要对我的拖延发布表示歉意,至于和我合作等事情,可能近期真的是没有什么时间了,我连工作都还没有找到呢,找到工作后肯定也是先以工作为主吧,这些爱好只能放到这里供更多的有时间的同志们继续发扬光大了。在此仅作为抛砖引玉,我特别希望此技术能更好的在教育软件上利用,有时间的同志们,为我国的教育事业做点贡献吧。特别是为那些生活在水深火热中的初中高中同学们带去点乐趣吧。

   在http://groups.google.com/group/Single-Display-Groupware中你可以提问,或者讨论,里面也有我参考的大部分文献资料,你可以到那里下载。

还有就是因为本文是我的毕业论文,而不希望学校方面有任何疑问,所以请转载的时候一定要注明 湖南大学 谢祁衡, 指导老师 善贤。

      在此再次感谢我们院的领导,和善贤老师!

阅读全文....