喜多郎 大蛇 下载:在VB6中用CopyMemory拷贝字符串的种种猫腻(二)

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 19:58:05

在VB6中用CopyMemory拷贝字符串的种种猫腻(二) 


出处:http://blog.csdn.net/slowgrace/archive/2009/09/14/4550116.aspx

第三节 经典错误代码集锦好的,现在我们可以来看看VB妈妈好心没做好事的几个例子了。先说明一下,以下所有这些例子都来自这个帖子热心朋友的回复,它们都共享第一节里声明的模块级变量和常数。3.1 我在0楼的代码——结果为何变短
  1. Sub test5()  
  2.     String1 = STR_E  
  3.     String2 = String$(7, 0)  
  4.       
  5.     CopyMemory pString1, ByVal VarPtr(String1), 4  
  6.     CopyMemory ByVal String2, ByVal pString1, 14  
  7.   
  8.     Debug.Print String2 '得到的不是PowerVB,而是“P o w e”?  
  9. End Sub  
这个例子的运行过程如下:(1)第1个CopyMemory得到的是String1的地址,并把这个地址作为源地址传给第2个CopyMemory
(2)第2个CopyMemory从String1的地址拷贝14个字节。由于VB中字符串的内部表示是Unicode,所以这时得到的14个字节的内容是“P-\0-o-\0-w-\0-e-\0-r-\0-V-\0-B-\0-”(注意,其中的“-”是我加入用来分割字符的,并不真的包括在字符串内存中)。
(3)由于CopyMemory的第一个参数是ByVal String2,是一个字符串,而VB会自动对API函数中的字符串参数做UA转换。所以,系统会把14个字节的Unicode空字符串String2转为7个字节的ANSI空字符串,并存在一个临时变量中,假设叫_tmp。
(4)然后系统把拷来的14字节数据“P-\0-o-\0-w-\0-e-\0-r-\0-V-\0-B-\0-”向_tmp拷。注意_tmp只有7字节,所以这里有溢出的危险。
(5)由于_tmp只有7字节,所以_tmp实际只得到头7个字节的数据,就是“P-\0-o-\0-w-\0-e-”
(6)最后VB要把ANSI字串再转回Unicode字串,并把转回的结果赋给String2。AU转换就是是将英文的 1 个字节扩张为 2个字节,这样String2最终的内容是“P-\0-\0-\0-o-\0-\0-\0-w-\0-\0-\0-e-\0-”,用Debug打印出来,可不就是“P o w e”么? 下面这个表总结了上述过程:  另外,我还做了下图来说明上面的过程:哎,这个图画得太杰出了。一目了然啊!你看,第②步的Unicode字符串硬是被当做ANSI字串在第③步被强行AU了,所以结果“浮肿”了很多(被插入好些空格)。 这个例子如何改对呢?中间两句不改,前后各改/增加一句,如下: String2 = String$(LenB(String1), 0) '先确保_tmp2长度足够
CopyMemory pString1, ByVal VarPtr(String1), 4
CopyMemory ByVal String2, ByVal pString1, LenB(String1)
String2 = StrConv(String2, vbFromUnicode) '再做UA转换以抵消VB多做的一次AU转换 3.2 阿勇在11楼的代码——结果为何变胖
  1. '阿勇11楼  
  2. Sub test8_Yong()  
  3.     Dim pString1 As Long  
  4.       
  5.     String1 = STR_E  
  6.     String2 = String$(14, 0)  
  7.       
  8.     CopyMemory pString1, VarPtr(String1), 4  
  9.     CopyMemory String2, ByVal pString1, 4  
  10.   
  11.     Debug.Print String2, StrConv(String2, vbFromUnicode)  
  12. End Sub  
运行上面的程序,你会发现string2的结果也是浮肿的。(0)第1个CopyMemory获得String1变量指针,并存在pString1里;(1)第2个CopyMemory首先从pString1里把String1缓冲区地址拷出来;然后把这个地址拷到临时字符串变量_tmp2里,也就是让_tmp2的字符串缓冲区指针指向String1的字符串缓冲区地址;(2)之后VB把_tmp2的内容做AU转换,并把AU转换的内容拷到一个新分配的字符串缓冲区中,并把String2的字符串缓冲区指针指向这个新分配的地址。 看下面的图,第②步之后_tmp2字符串缓冲区里是来自String1的Unicode字符串,可是它还是在第③步被VB妈妈当初ANSI字符串强行AU,然后再倒手给String2,String2里得到的字符串可不就是浮肿的么:) 这个例子如何改对呢?最后加个String2 = StrConv(String2, vbFromUnicode)把VB多做的那一次AU转换抵消就可以了。 3.2.1 插播:字符串内存的初始化Q:String2 = String$(14, 0)这一句可以不要么?A:这一句是必要的,相当于给它初始申请内存。千万别用没分配内存的指针,否则很容易崩溃的。另外,String$(7,0)相当于加入7个Chr(0)字符(vbNullChar),Space$(7)则相当于加入7个Chr(20)字符(空格)。由于VB字符串允许含Null字符,所以这两种初始化方法都可以的。 3.3 Modest在16楼的方法——错误的代码正确的结果这个方法Modest自己起初的评语是“通俗易懂、还不出错”,打眼一望,我也很赞同这个评语。之后有些细节感觉想不通,请求继续解释。赵老虎分析完之后说“根本就是瞎猫碰上死老鼠”,“实际上是在胡乱操作内存”。刚看到这个评语,我觉得Tiger_Zhao这厮也太那个了,好歹老魏也是VB版一大虾啊。可是看完他的解释之后,我却发现这个评语还真中肯。这里要赞一下Modest,当真好气度,换了别人这么说我,甭管对错,我得先一蹦老高。闲话少说,咱们来看老魏的代码吧。核心的代码如下:
CopyMemory pString1, String1, 4CopyMemory pString2, String2, 4CopyMemory pString2, pString1, LenB (String1)
初看起来,这个代码貌似是分别得到两个字符串缓冲区的指针,然后把String1的字符串缓冲区拷给String2,结果也是正确的。当真“通俗易懂、还不出错”。但是细想想,你会发现这一切都不大靠谱:(1)首先对于头2个CopyMemory而言,既然VB妈妈会做UA转换,那么pString1和pString2得到的应该分别是_tmp1和_tmp2的地址,而这两个临时变量在CopyMemory调用之后会被释放掉,也就是说pString1和pString2得到的其实是无效的地址(见下图)。所以这两个语句应该改成这样;
CopyMemory pString1, VarPtr(String1), 4CopyMemory pString2, VarPtr(String2), 4
(2)其次,就算我们把头两个语句改成上面这样,第3个CopyMemory真的在拷贝字符串缓冲区么?看出来了么?要拷贝字符串缓冲区,第3个语句应该加上ByVal,像这样:
CopyMemory ByVal pString2, ByVal pString1, LenB (String1)
(3)可是,诡异的是,就这么一段漏洞百出的代码,它的运行结果明明是正确的啊?这是为什么呢?看下面Tiger_Zhao的解释。
  1. Sub test9_Modest()  
  2.     Dim String1 As String  
  3.     Dim String2 As String  
  4.     Dim pString1 As Long  
  5.     Dim pString2 As Long  
  6.     '这 4 个变量每个长 4 字节,在栈上按地址从低到高为:  
  7.     '| pString2 | pString1 | String2 | String1 |  
  8.   
  9.     String1 = "PowerVB"  
  10.     String2 = Space$(Len(String1))  
  11.     'String1、String2 分别指向两个字符串  
  12.   
  13.     CopyMemory pString1, String1, 4  
  14.     CopyMemory pString2, String2, 4  
  15.     'pString1 、pString2 值为已被释放的临时变量的地址,无所谓  
  16.   
  17.     CopyMemory pString2, pString1, LenB(String1)  
  18.     '由于不加 ByVal,其实是从变量 pString1 的地址向变量 pString2 的地址复制 14 个字节  
  19.     '等于直接操作栈内存,让变量向左复制(看后面插播的“覆盖模式”),于是  
  20.     '| pString2 : 原 pString1 的值  
  21.     '| pString1 : 原 String2 的字符串指针  
  22.     '| String2 : 原 String1 的字符串指针  
  23.     '| String1 : 不确定 |  
  24.   
  25.     Debug.Print String2  
  26. End Sub  
3.3.1 插播1:关于栈(1)变量都是存放在中的。这个内存由编译器自动分配释放。
(2)对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。所以先声明的变量的内存地址会比后声明的高。比如上面的示例里那样。3.3.2 插播2:CopyMemory自动处理覆盖A: CopyMemory是Copy还是Cut?原来地址中内存的内容还在么?Q: 复制。源和目标内存不交叉,源不变;如果交叉,还会自动处理覆盖情况。A: “自动处理覆盖情况”是什么意思?是说:万一交叉了,还会把被覆盖的复原,而且把目的地址挪开点么?Q: 看下面的例子:
  1. '如果有一个数组 a() : 00-01-02  
  2.   
  3. 'a)  
  4. CopyMemory a(1), a(0), 2  
  5. '结果是 00-00-01,会避免:先用 a(0) 覆盖 a(1),然后再用 a(1) 覆盖 a(2),最终变成 00-00-00  
  6.   
  7. 'b)  
  8. CopyMemory a(0), a(1), 2  
  9. '结果是 01-02-02,会避免:先用 a(2) 覆盖 a(1),然后再用 a(1) 覆盖 a(0),最终变成 02-02-  
3.3.3 插播3:VB挂掉的话会有什么后果?Modest这段代码如果把变量的次序换一下,VB就可能会挂掉。大家可以试一试:PQ: 这种崩溃有可能波及到整个操作系统么?还是甭管我怎么瞎折腾,把VB关掉就万事大吉?A: 不可能波及整个系统。因为VB之所以崩溃,就是因为系统搞的鬼。操作系统为了保护自己,会强行关闭它自认为潜在的“威胁”,所以会把VB搞掉。而一旦保护不了,而且出错,就会出现所谓的“蓝屏”。常规情况的话,重开VB就可以了。Q:在调试状态下用错指针,会导致VB崩溃。那如果是编译成可执行程序,里面有错误的指针操作的话,可能会蓝屏么?A:通常有进程保护,不会蓝屏。除非你的程序操作了系统级资源。