VB6拾遗:数组的内部实现

标签: , , , , ,

从最底层来讲,数组只不过是一块连续的内存。

然而一个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肯定不一样),两者在内存中是一样的。但是定长数组描述符是在堆栈上分配的,效率比变长数组要高一些。

随机文章:

  1. 用VBS获取Unix时间戳
  2. Beyond Compare 3.3.5 注册码
  3. VBScript实现ZIP文件的压缩或解压(ZipCompressor)
  4. 批处理技术内幕:随机数%RANDOM%
  5. VBS实现“多线程”

一条评论 发表在“VB6拾遗:数组的内部实现”上

  1. […] 曾经在《VB6拾遗:数组的内部实现》里写过VB数组的内部实现,而VBS是VB的子集,所以VBS数组的内部实现跟VB的大同小异。 […]

留下回复