技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 1459|回复: 7
收起左侧

【VB6】在VB6里实现“指针类型”——像C语言的[]那样用()来读写内存中的数组!

[复制链接]

1044

主题

2345

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
218
威望
294 点
宅币
18328 个
贡献
37618 次
宅之契约
0 份
在线时间
1749 小时
注册时间
2014-1-26
发表于 2017-10-18 05:10:34 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
以前我提到过各种VB6里面使用指针的方法比如用VarPtr取得变量的地址然后用CopyMemory(实际上是RtlMoveMemory)来把指定地址的数据复制到自己的地方或者把自己的数据复制到指定地方。现在可以不用那么麻烦了:通过设置SAFEARRAY来直接像访问自家数组一样去读写一个指定的内存区域的数据。

VB6的数组是SAFEARRAY安全数组,而实际的数组的数据是存储在SAFEARRAY结构体的pvData成员指向的地方的。SAFEARRAY的结构如下:
[C] 纯文本查看 复制代码
typedef struct tagSAFEARRAYBOUND {
  ULONG cElements;
  LONG  lLbound;
} SAFEARRAYBOUND, *LPSAFEARRAYBOUND;

typedef struct tagSAFEARRAY {
  USHORT         cDims;
  USHORT         fFeatures;
  ULONG          cbElements;
  ULONG          cLocks;
  PVOID          pvData;
  SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY, *LPSAFEARRAY;
我就想:如果我自己搞个傀儡数组,然后我修改它的pvData成员的值,是不是可以指哪打哪了?经过我的测试发现它还真是这样。

那么在VB6里面如何才能找出一个数组它的SAFEARRAY结构体的存储位置呢?经过多次尝试我发现:

声明API的时候,如果你把某个参数的定义写成“xxxx() As Any”,那么VB6就会在调用这个API的时候把你提供的数组它的SAFEARRAY结构体的地址传给这个API。所以又到了使用某个傀儡函数的时候:VarPtr。
VB6的msvbvm60.dll导出了一个傀儡函数VarPtr,它的实现其实就是把自己参数列表的第一个参数原样返回给你。IDA里面已经看到了,这个VarPtr的反汇编是这么写的:
[Asm] 纯文本查看 复制代码
mov eax,[esp+4]
ret 4
所以我们可以手动声明它的API,并且修改它的名字(别名)和参数的定义。我是这样写的:
[Visual Basic] 纯文本查看 复制代码
Declare Function ArrayPtr Lib "msvbvm60.dll" Alias "VarPtr" (ptr() As Any) As Long
为了观察VB6自身是如何分配地址存储SAFEARRAY结构体的内容和数据的内容,我写了个测试程序,用了这样的代码来测试:
[Visual Basic] 纯文本查看 复制代码
Private Sub Command3_Click()
Cls

Dim Arr_Int_Fixed(111) As Integer '固定大小Integer数组
Dim Arr_Int_Alloc() As Integer '可变大小Integer数组
ReDim Arr_Int_Alloc(111)

Dim Arr_Long_Fixed(111) As Integer '固定大小Long数组
Dim Arr_Long_Alloc() As Integer '可变大小Long数组
ReDim Arr_Long_Alloc(111)

Dim Arr_Long_Member As TestType '在结构体里定义固定大小Long数组
'结构体声明:
'Private Type TestType
'    Something As Long
'    SomeArr(111) As Long
'End Type

Dim Arr_VarPtr As Long '数组变量自身的地址
Dim Arr_Ptr As Long 'SAFEARRAY结构体的地址
Dim Arr_Body As SAFEARRAY 'SAFEARRAY结构体的内容

Arr_VarPtr = ArrayPtr(Arr_Int_Fixed)
CopyMemory Arr_Ptr, ByVal Arr_VarPtr, 4
CopyMemory Arr_Body, ByVal Arr_Ptr, Len(Arr_Body)

Print "Arr_Int_Fixed:"
GoSub PrintArrData

Arr_VarPtr = ArrayPtr(Arr_Int_Alloc)
CopyMemory Arr_Ptr, ByVal Arr_VarPtr, 4
CopyMemory Arr_Body, ByVal Arr_Ptr, Len(Arr_Body)

Print "Arr_Int_Alloc:"
GoSub PrintArrData

Arr_VarPtr = ArrayPtr(Arr_Long_Fixed)
CopyMemory Arr_Ptr, ByVal Arr_VarPtr, 4
CopyMemory Arr_Body, ByVal Arr_Ptr, Len(Arr_Body)

Print "Arr_Long_Fixed:"
GoSub PrintArrData

Arr_VarPtr = ArrayPtr(Arr_Long_Alloc)
CopyMemory Arr_Ptr, ByVal Arr_VarPtr, 4
CopyMemory Arr_Body, ByVal Arr_Ptr, Len(Arr_Body)

Print "Arr_Long_Alloc:"
GoSub PrintArrData

Arr_VarPtr = ArrayPtr(Arr_Long_Member.SomeArr)
CopyMemory Arr_Ptr, ByVal Arr_VarPtr, 4
CopyMemory Arr_Body, ByVal Arr_Ptr, Len(Arr_Body)

Print "Arr_Long_Member:"
GoSub PrintArrData

Exit Sub
PrintArrData:
    '打印数组变量自身的地址和这个数组变量所描述的SAFEARRAY结构体的地址
    Print "Address", Hex$(Arr_VarPtr); "->"; Hex$(Arr_Ptr)
    Print "cDims", Arr_Body.cDims '维数
    Print "fFeatures", Hex$(Arr_Body.fFeatures) '特性
    Print "cbElements", Arr_Body.cbElements '单个元素的大小
    Print "cLocks", Arr_Body.cLocks '是否有锁
    Print "pvData", Hex$(Arr_Body.pvData) '实际的数据的指针
    Print "cElements(0)", Arr_Body.rgsabound(0).cElements '1维元素总数
    Print "lLbound(0)", Arr_Body.rgsabound(0).lLbound '上标
    Return
End Sub
注意在VB6里面你的数组变量本身相当于一个SAFEARRAY*,也就是一个指针,指向一个SAFEARRAY结构体。但你自己声明的结构体内的固定大小数组则不是这样的情况。

这个测试的结果如下图所示:
20171016223631.png

可以发现以下特征:(有些虽然看不出,但结合我的测试它就是这种效果)
  • 固定大小数组变量自身和它指向的SAFEARRAY结构体都在栈上,但数据在堆上。
  • 可变大小数组变量在栈上,但它指向的SAFEARRAY结构体在堆上,经过测试我发现VB6会在这个数组变量生命周期结束后对这个SAFEARRAY结构体所占的内存进行了释放内存的操作,也就是类似C语言的“free()”的操作。此外可变大小数组的数据也是在堆上的,并且是单独分配的,而不是和SAFEARRAY结构体一起分配的。
  • 对于结构体内的固定大小数组,这个变量自身和它指向的SAFEARRAY结构体都在栈上,而数据则在结构体里。图中可以看到这个结构体自身是在栈上的。

其中我说的在堆上的玩意儿,经过我的测试就是不停地点按钮的话,那几个值特别大的数字它是不停地变化的。也就是它确实有个内存的分配和释放的操作在里面。

那么……如果我在某个地址上有个数据但我想要直接访问它而不经过CopyMemory的话,我是不是可以自己构建一个傀儡SAFEARRAY,来让我的数组“一出生”它就指向我要的数据,并且可以直接读写呢?经过我的尝试:这是完全可行的!
但要注意VB6会对生命周期结束的SAFEARRAY进行回收操作,一个阻止回收操作的方法就是把cLocks成员的值设为非零。这样它就会因为上了锁而不再尝试回收它。

我写了个一个Module,照抄里面的代码就能构建自己的指针类型。实测还是很方便的。

我通过构造一个结构体,前面做了个傀儡SAFEARRAY结构,后面是数组变量。用法就是直接用对应的函数初始化就行。对应的函数是xxxxPtr_Setup,其中xxxx表示类型。你自己可以通过照葫芦画瓢的方式抄我的结构体和这个函数的实现来实现自己的自定义类型的指针类型。
modArrayPtr.bas (3.67 KB, 下载次数: 4)

36

主题

146

帖子

7193

积分

用户组: 管理员

UID
77
精华
11
威望
115 点
宅币
6630 个
贡献
132 次
宅之契约
0 份
在线时间
108 小时
注册时间
2014-2-22
发表于 2017-10-18 10:47:32 | 显示全部楼层
膜拜LZ的超神技术。

0

主题

10

帖子

4733

积分

用户组: 技术宅的结界VIP成员

UID
2021
精华
0
威望
2 点
宅币
4719 个
贡献
0 次
宅之契约
0 份
在线时间
6 小时
注册时间
2016-10-21
发表于 2017-10-18 15:57:18 | 显示全部楼层
大神好腻害!

272

主题

446

帖子

4823

积分

用户组: 真·技术宅

UID
2
精华
61
威望
148 点
宅币
3645 个
贡献
131 次
宅之契约
0 份
在线时间
620 小时
注册时间
2014-1-25
发表于 2017-10-19 21:34:16 | 显示全部楼层
厉害,创造了一个VB-C语言!!

1

主题

6

帖子

32

积分

用户组: 初·技术宅

UID
2920
精华
0
威望
2 点
宅币
22 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2017-10-2
发表于 2017-10-19 21:35:55 | 显示全部楼层
元始天尊 发表于 2017-10-19 21:34
厉害,创造了一个VB-C语言!!

我过来看看学长们在干啥

1

主题

15

帖子

15

积分

用户组: 初·技术宅

UID
2735
精华
0
威望
0 点
宅币
0 个
贡献
0 次
宅之契约
0 份
在线时间
6 小时
注册时间
2017-7-28
发表于 2017-10-21 18:12:53 | 显示全部楼层
学习一下
回复

使用道具 举报

17

主题

184

帖子

1303

积分

用户组: 上·技术宅

UID
3808
精华
5
威望
31 点
宅币
972 个
贡献
60 次
宅之契约
0 份
在线时间
189 小时
注册时间
2018-5-6
发表于 2019-8-26 22:02:16 | 显示全部楼层
厉害,看不懂!
菜鸟一枚,直接指正,不必留情

3

主题

31

帖子

384

积分

用户组: 中·技术宅

UID
4293
精华
3
威望
11 点
宅币
251 个
贡献
65 次
宅之契约
0 份
在线时间
30 小时
注册时间
2018-9-19
发表于 2019-9-2 20:42:30 | 显示全部楼层
这个太麻烦了点,msvbvm60.dll其实导出了GetMemX、PutMemX、SetMemX这些函数(X对应字节数或类型,比如读写4字节整数和浮点数都是用GetMem4和PutMem4,而同为4字节的字符串用GetMemStr和PutMemStr),GetMemX即内存取值函数,PutMemX即内存Let赋值函数,SetMemX即内存Set赋值函数,VB6在类模块中定义的Public成员变量,编译器就是用这三类函数去分别实现它们的Property Get、Property Let、Property Set三个过程。
根据这个原理,我在2015年的时候写过一个msvbvm60.tlb,把这三类函数声明进去了,并把它们声明成了带参属性,用它们来实现模拟指针操作:
地址 = VarPtr(变量) '取变量地址
旧值 = PtrLng(地址) '读取地址指向的Long值(VB6取地址函数名称是Ptr在后面,反过来Ptr放前面表示反向操作)
PtrLng(地址) = 新值 '给地址指向内存赋一个Long值
地址 = StrPtr(字符串) '取字符串的缓冲区地址
旧地址 = StrPtrEx(字符串变量) '取字符串变量的缓冲区地址扩展版(扩展的是赋值功能,不是取值功能)
StrPtrEx(字符串变量) = 新地址 '修改字符串变量的缓冲区地址(跟StrPtr的区别是,StrPtr不支持修改地址)
字符串变量地址 = VarPtr(字符串变量) '取字符串的变量地址
字符串缓冲区地址 = StrPtr(字符串) '取字符串的缓冲区地址
旧值 = PtrStr(字符串变量地址) '读取地址指向的String值(注意:PtrStr同PtrLng是对VarPtr的反向,不是对StrPtr的反向)
PtrStr(字符串变量地址) = 新值 '给地址指向内存赋一个String值
字符串值 = GetPtrStr(字符串缓冲区地址) '读取地址指向的字符串缓冲区值(这个才是对StrPtr的反向,但是这个不能赋值,所以就干脆加个Get前缀来区分)

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2019-11-21 17:07 , Processed in 0.284100 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表