标题: VB6拾遗:数组的内部实现
作者: Demon
链接: https://demon.tw/programming/vb6-repick-array-internal.html
版权: 本博客的所有文章,都遵守“署名-非商业性使用-相同方式共享 2.5 中国大陆”协议条款。
从最底层来讲,数组只不过是一块连续的内存。
然而一个VB数组变量并不是一个直接指向储存数组数据的内存块的指针,而是一个指向被称为数组描述符结构的指针,该结构具有SAFEARRAY类型。换句话说,VB数组内部是用SAFEARRAY来实现的,VB数组变量是指向SAFEARRAY结构的指针SAFEARRAY *。
typedef struct tagSAFEARRAY { USHORT cDims; USHORT fFeatures; ULONG cbElements; ULONG cLocks; PVOID pvData; SAFEARRAYBOUND rgsabound[1]; } SAFEARRAY, *LPSAFEARRAY; typedef struct tagSAFEARRAYBOUND { ULONG cElements; LONG lLbound; } SAFEARRAYBOUND, *LPSAFEARRAYBOUND;
cDims表示数组的维度,一维数组的cDims为1,二维数组的cDims为2,以此类推。
cbElements表示每个数组元素占用的字节,字节型数组Byte()的cbElements为1,长整型数组Long()的cbElements为4,以此类推。
cLocks表示数组被锁定的次数。当一个数组被锁定时,你不能对其ReDim、ReDim Preserve或者Erase;你能够改变数组中元素的值,但是你不能修改或者销毁数组的结构。当你以ByRef方式传递一个数组作为参数,或者当你对数组中某个元素使用With语句时,VB会对数组进行锁定。
pvData是指向数组实际数据的指针,这是SAFEARRAY结构中的主角,SAFEARRAY结构中的其他字段只是为它提供一些描述而已。
cElements表示数组维度中元素的个数。
lLbound表示数组维度的下边界。VB中的LBound函数直接读取这个字段的值,而UBound可以通过计算lLbound + cElements – 1得到。
fFeatures是一个重要的标志,其设置能够使SafeArray API能够正确的销毁数组。这些标志可以分为两类:一类用来指定数组元素的类型,一类用来指定数组数据的内存分配。
第一组标志中包括三个说明内存如何分配的标志FADF_AUTO、FADF_STATIC、FADF_EMBEDDED,还有说明数组是否为定长的标志FADF_FIXEDSIZE。在VB中,变长数组的这4个标志都没有被设置;定长数组设置了FADF_STATIC和FADF_FIXEDSIZE;结构中的定长数组还设置了FADF_EMBEDDED。VB中并不生成设置了FADF_AUTO标志的数组。
另一组标志说明数组元素的类型,FADF_BSTR说明数组元素的类型为BSTR,这样在调用SafeArrayDestroyData函数销毁数组数据时会对每个元素调用SysFreeString函数以释放字符串的内存;FADF_VARIANT则调用VariantClear;FADF_UNKNOWN和FADF_DISPATCH则调用对应的Realese方法。FADF_HAVEVARTYPE说明在SAFEARRAY结构前面4个字节储存有数组元素的变量类型;FADF_HAVEIID说明在SAFEARRAY结构前面16个字节储存有GUID,该标志只有在FADF_UNKNOWN或者FADF_DISPATCH设置时才使用。
说了一大堆理论,下面看一个简单的例子:
Sub Main() Dim FixedArray(1 To 10) As Long ReDim VariableArray(1 To 10) As Long End Sub
用OllyDbg调试,得到的汇编代码如下:
00401791 push 3 ; 数组元素类型 VT_I4 00401793 push ___vba@0A2C7D1C ; 编译时生成的SAFEARRAY结构地址 00401798 lea eax, [ebp-2C] ; 数组变量地址 0040179B push eax ; 0040179C call ___vbaAryConstruct2 ; MSVBVM60.__vbaAryConstruct2 004017A1 push 1 ; 数组下边界 004017A3 push 0A ; 数组上边界 004017A5 push 1 ; 数组维度 cDims 004017A7 push 3 ; 数组元素类型 VT_I4 004017A9 lea eax, [ebp-14] ; 数组变量地址 004017AC push eax 004017AD push 4 ; 每个数组元素的字节数 cbElements 004017AF push 80 ; 标识 fFeatures 004017B4 call @__vbaRedim ; MSVBVM60.__vbaRedim 004017B9 add esp, 1C 004017BC push 004017DE 004017C1 lea eax, [ebp-14] 004017C4 push eax 004017C5 push 0 004017C7 call ___vbaAryDestruct ; MSVBVM60.__vbaAryDestruct 004017CC lea eax, [ebp-2C] 004017CF mov dword ptr [ebp-34], eax 004017D2 lea eax, [ebp-34] 004017D5 push eax 004017D6 push 0 004017D8 call ___vbaAryDestruct ; MSVBVM60.__vbaAryDestruct
可以知道,在VB内部定长数组是用__vbaAryConstruct2函数创建的;而变长数组是用__vbaRedim函数创建的;但是不管是定长数组还是变长数组,都用__vbaAryDestruct函数来销毁。
通过跟进__vbaAryConstruct2和__vbaRedim可知,定长数组的数组描述符SAFEARRAY结构是在编译时生成的,由__vbaAryConstruct2将其拷贝到堆栈中;变长数组的数组描述符SAFEARRAY结构是调用SafeArrayAllocDescriptorEx函数在堆上分配的;但是不管是定长数组还是变长数组,其数据指针pvData指向的内存都是由SafeArrayAllocData在堆上分配的。
; MSVBVM60.__vbaAryDestruct 72A1C1FE push esi 72A1C1FF mov esi, dword ptr [esp+0C] ; esi = SAFEARRAY ** 72A1C203 mov eax, dword ptr [esi] ; eax = SAFEARRAY * 72A1C205 test eax, eax 72A1C207 jz short 72A1C246 ; 如果指针为NULL 72A1C209 mov cx, word ptr [eax+2] ; cx = SAFEARRAY->fFeatures 72A1C20D test cl, 05 72A1C210 jnz short 72A1C24A ; 如果设置了FADF_AUTO或者FADF_EMBEDDED 72A1C212 push esi 72A1C213 push dword ptr [esp+0C] 72A1C217 call __vbaErase ; MSVBVM60.__vbaErase 72A1C21C mov eax, dword ptr [esi] 72A1C21E test eax, eax 72A1C220 jz short 72A1C246 ; 如果指针为NULL 72A1C222 cmp dword ptr [eax+0C], 0 ; SAFEARRAY->pvData == NULL ? 72A1C226 je short 72A1C246 72A1C228 and word ptr [eax+2], FFED ; 去掉FADF_STATIC和FADF_FIXEDSIZE 72A1C22E push eax 72A1C22F call dword ptr [<&OLEAUT32.#39>] ; OLEAUT32.SafeArrayDestroyData 72A1C235 mov esi, dword ptr [esi] 72A1C237 test byte ptr [esi+2], 20 72A1C23B jz short 72A1C246 ; 如果没有设置FADF_RECORD 72A1C23D push 0 72A1C23F push esi 72A1C240 call dword ptr [72A4EEAC] ; OLEAUT32.SafeArraySetRecordInfo 72A1C246 pop esi 72A1C247 retn 8 72A1C24A test cl, 20 72A1C24D jz short 72A1C246 ; 如果没有设置FADF_RECORD 72A1C24F push 0 72A1C251 push eax 72A1C252 jmp short 72A1C240
__vbaAryDestruct在内部调用__vbaErase来销毁数组,无论是定长数组还是变长数组,__vbaErase都调用SafeArrayDestroyData来销毁pvData指向的内存;对于变长数组,还要调用SafeArrayDestroyDescriptor销毁数组描述符。
但是由于定长数组设置了FADF_STATIC标志,SafeArrayDestroyData并不会释放pvData指向的内存,而仅仅是将它清零,所以在__vbaAryDestruct中还要去掉FADF_STATIC和FADF_FIXEDSIZE标志后再调用一次SafeArrayDestroyData才能真正释放掉内存。
最后来看一下两个数组的内存dump:
FixedArray(fFeatures = FADF_STATIC|FADF_FIXEDSIZE|FADF_HAVEVARTYPE):
01 00 92 00|04 00 00 00|00 00 00 00|E8 39 1E 00| 0A 00 00 00|01 00 00 00|
VariableArray(fFeatures = FADF_HAVEVARTYPE):
01 00 80 00|04 00 00 00|00 00 00 00|68 3A 1E 00| 0A 00 00 00|01 00 00 00|
可以看出,除了fFeatures不一样以外(当然了,pvData肯定不一样),两者在内存中是一样的。但是定长数组描述符是在堆栈上分配的,效率比变长数组要高一些。
赞赏微信赞赏支付宝赞赏
随机文章:
[…] 曾经在《VB6拾遗:数组的内部实现》里写过VB数组的内部实现,而VBS是VB的子集,所以VBS数组的内部实现跟VB的大同小异。 […]