中心东道小学礼仪博客:e(易精经第二章)

来源:百度文库 编辑:偶看新闻 时间:2024/05/06 05:59:38

 

第二章、字节集

 

 

       字节集是易语言独有的基本数据类型,按字面的意思来理解,所谓“字节集”就是“字节的集合”,其本质就是字节数组。从计算机基础知识中,我们知道,一个字节就是8位(bit),也就是8个“0”或“1”。计算机中所有的东西(指令和数据)都是用0和1表示的,而以字节作为保存数据的最小单位,所以,字节集可以保存任何的数据——数字、文本、声音、图像、可执行文件等等;反过来,一段字节集数据具体表示什么,关键看你是如何解读它——你可以认为它是一段文本、一张图片或是一首mp3中的一段。

       易语言的核心支持库提供了很多字节集相关的函数(2-a),这些函数使得我们对字节集的处理异常方便。我们先来看看易语言本身对这些函数的简要介绍。

 

字节集操作命令

简要说明

取字节集长度

取字节集型数据的长度。

到字节集

将指定数据转换为字节集后返回转换结果。

取字节集数据

取出字节集中指定位置指定数据类型的数据。

取字节集左边

返回一个字节集,其中包含指定字节集中从左边算起指定数量的字节。

取字节集右边

返回一个字节集,其中包含指定字节集中从右边算起指定数量的字节。

取字节集中间

返回一个字节集,其中包含指定字节集中从指定位置算起指定数量的字节。

寻找字节集

返回一字节集在另一字节集中最先出现的位置,位置值从 1 开始。如果未找到,返回 -1。

倒找字节集

返回一字节集在另一字节集中最后出现的位置,位置值从 1 开始。如果未找到,返回 -1。

字节集替换

将指定字节集的某一部分用其它的字节集替换,然后返回替换后的结果。

子字节集替换

返回一个字节集,该字节集中指定的子字节集已被替换成另一子字节集,并且替换发生的次数也是被指定的。

取空白字节集

返回具有特定数目 0 字节的字节集。

取重复字节集

返回一个字节集,其中包含指定次数的字节集重复结果。

分割字节集

将指定字节集进行分割,返回分割后的一维字节集数组。

指针到字节集

返回指定内存指针所指向地址处的一段数据,注意调用本命令前一定要确保所提供的内存地址段真实有效。本命令的最佳使用场合就是在易语言回调子程序和易语言DLL公开子程序用作获取外部数据。

 

这些函数的使用都很简单,但有些函数依然会使人迷惑,或者想更深入地了解其中的相关细节。所以我们先围绕某些函数作一点深入的讨论。

2.1 深入讨论字节集相关函数

 

2.1.1 取字节集长度

       首先我们来研究一下“取字节集长度”函数是如何取得一个字节集长度的,因为它的效率决定了我们是否适合把它放在循环体中执行。系统要计算一段字节集的长度,不外乎有两个方法:①逐一累计,也就是把字节一个一个地数出来。 ②把字节集的长度存放在某个特殊的地方,需要的时候读取出来,在字节集操作的过程中即时更新该长度数据。系统具体采用的是哪一种方法,我们作一个简单的测验便知。

       新建一个易语言程序,在窗体上放一个按钮,为该按钮写如下代码:

 

.版本 2

 

.子程序 _按钮测试计算方式_被单击

.局部变量 数据, 字节集

.局部变量 上次时间

 

数据 = 取空白字节集 (1) ' 字节集的长度不论是1还是1000000,计算的时间不变

上次时间 = 取启动时间 ()

.计次循环首 (1000000, )

    取字节集长度 (数据)

.计次循环尾 ()

输出调试文本 (取启动时间 () - 上次时间)

 

       这段代码先分配一定长度的字节集数据,然后执行“取字节集长度”函数一百万次,我的机器测得所花的时间是31毫秒左右。如果你的机器速度很快,测得的时间是0毫秒,请将循环次数增加。然后我把字节集数据的长度改为1000000,再次运行该程序,测得的结果依然大约是31毫秒。由此可见,易语言的“取字节集长度”函数并不是蠢蠢地一个一个字节字节地数,而是把字节集的长度存放在了某个特殊的地方,需要的时候就把它读出来。那么具体存放在何处呢?这也有几种可能:① 存放在字节集的开始处。② 存放在字节集的末尾处。 ③ 存放在字节集开始处更前面的位置。 ④存放在内存堆栈中的某个表中,然后与指定的字节集变量建立联系。很显然,存放在字节集末尾的可能性很小,不然系统如何知道一段字节集到何处结束?而如果存放在内存中的表中,需要进行额外的查表操作,显得过于烦琐,

       我们先来测试简单的,这也需要做试验。首先我们需要获得字节集数据的内存地址,这个我们可以通过“取变量地址”函数获得——就像第一章中“自定义数据类型的内存存储”一节中那样,如果不太清楚,请先转回去看那一节。不过字节集的变量地址更简单——我们只用取字节集的第一个元素的地址就得到了,不用转来转去那么麻烦。得到地址之后,我们就看该地址的第一个整数型数据是否是字节集的长度,如果不是,那显然没有把长度信息放在开头;如果不在开头,我们再把地址指针向内存的低地址方向移动一个整数长度,也就是4个字节,我猜想长度信息也有可能存放在那里。具体的试验代码如下:

 

.版本 2

.支持库 spec

 

.子程序 _按钮存储位置_被单击

.局部变量 数据, 字节集

.局部变量 地址, 整数型

.局部变量 临时地址, 整数型

.局部变量 临时字节集, 字节集

.局部变量 长度, 整数型

 

数据 = 到字节集 (“abcdefg”)  ' 长度7,你可以更改此字符串,观察其他长度

地址 = 取变量地址 (数据 [1])  ' 获得字节集数据的内存地址

输出调试文本 (指针到文本 (地址))  ' 此处输出正确的字符串,证明地址是正确的

 

临时字节集 = 指针到字节集 (地址, 4)

长度 = 取字节集数据 (临时字节集, #整数型, )

输出调试文本 (长度)  ' 此处输出错误的结果。

 

地址 = 地址 - 4 ' 将内存指针回退一个整数型的数据长度,也就是4个字节

临时字节集 = 指针到字节集 (地址, 4)

长度 = 取字节集数据 (临时字节集, #整数型, )

输出调试文本 (长度)  ' 此处输出正确的字节集长度7

' 由此可见,字节集的长度存在该字节的首地址前面的一个整数中。

 

       试验的结果再一次证实了我的猜想。知道了易语言取字节集长度的核心方式,我们就可以大胆地在循环中使用“取字节集长度”函数而不用担心影响效率了,同时也深入地理解了字节集的存储方式。

 

2.1.2 取字节集数据

       前面我们已经说过,字节集中保存的是什么数据,关键看我们如何转译它。因此我们可以从一个“文本”字节集中读取整数型数据,也可以从一个“整数型”字节集中读取日期时间型数据,也可以在文本和字节集类型中方便地相互转换。此外,系统还提供其他的诸如“到文本”、“到数值”函数,但到底何时采用哪个函数,这是一个问题。

       我的建议是对字节集数据不要使用“到数值”函数,应该使用“取字节集数据”函数,根据需要传递#整数型、#字节型、#小数型等参数。否则你会发现程序运行的结果不对头,但即使是拿了放大镜也看不出源代码问题在哪里,请看下面的代码片断,输出结果是什么:

 

.版本 2

 

.子程序 _按钮字节集到数值_被单击

.局部变量 某字节集, 字节集

 

某字节集 = 到字节集 (1234)

输出调试文本 (到数值 (某字节集))

某字节集 = 到字节集 (“1234”)

输出调试文本 (到数值 (某字节集))

 

       也许你觉得至少有一个要输出1234吧,但很不幸,输出结果却都是0。但使用“输出调试文本(取字节集数据(某字节集,#整数型,))”可以得到可预见的正确数据。

 

       其次是关于“到文本()”和“取字节集数据(...,#文本型,)”的区别问题。对于字节集参数来说,“到文本()”相当于“取字节集数据(...,#文本型,1)”,显然,“取字节集数据()”功能要强大些,可以取指定位置的文本数据。我们知道,在易语言中,文本变量是以0作为文本的结尾的,而一段字节集中可能有多个0分割的文本,“到文本()”函数只能取出第一个文本。请看示例代码:

 

.版本 2

 

.子程序 _按钮字节集到文本_被单击

.局部变量 某字节集, 字节集

 

某字节集 = 到字节集 (“abcdefghijklmnopq”)

输出调试文本 (到文本 (某字节集))  ' 输出全部

 

某字节集 [8] = 0  ' 插入一个0

输出调试文本 (到文本 (某字节集))  ' 文本被截断,只输出abcdefg

输出调试文本 (取字节集数据 (某字节集, #文本型, 1)) ' 与上一行输出结果相同

输出调试文本 (取字节集数据 (某字节集, #文本型, 9)) ' 输出后一半,ijklmnopq

 

       由代码的运行结果可以看出,“取字节集数据(...,#文本型,)”很灵活,可以任意指定从指定位置开始的文本,直到遇到0结束。

 

       此外,“取字节集数据”函数还允许我们像读取文件那样按顺序读取一段数据,请看易语言集成开发环境中对“取字节集数据”函数第三个参数的描述:

 

“参数<3>的名称为“起始索引位置”,类型为“整数型(int)”,可以被省略。指定从字节集的什么地方开始取数据,索引值从1开始。如果被省略,默认为数值1。如果为本参数提供一个整数型变量,则命令执行后将自动修改该变量内容,将其索引值移动到下一个读入位置。如果移动后到达字节集的末尾,将修改该变量的内容为-1。”

 

具体的使用代码如下:

 

.版本 2

 

.子程序 _按钮顺序读取_被单击

.局部变量 某字节集, 字节集

.局部变量 指针位置, 整数型

 

某字节集 = 到字节集 (“http://goomoo.cn”) + { 0 } ' 注意文本后面添加一个0表示文本结束

某字节集 = 某字节集 + 到字节集 (12345)

某字节集 = 某字节集 + 到字节集 ([2003年10月1日])

 

指针位置 = 1

输出调试文本 (取字节集数据 (某字节集, #文本型, 指针位置))  ' 输出 http://goomoo.cn

输出调试文本 (取字节集数据 (某字节集, #整数型, 指针位置))  ' 输出 12345

输出调试文本 (取字节集数据 (某字节集, #日期时间型, 指针位置))  ' 输出 2003年10月1日

输出调试文本 (指针位置)  ' 输出 -1 ,表示到字节集尾。

 

2.1.3 指针到字节集

       易语言自身对此函数的附加说明是这样的:“本命令的最佳使用场合就是在易语言回调子程序和易语言DLL公开子程序用作获取外部数据。”,其实“指针到字节集”函数的实质是读取本进程内指定内存位置的指定长度的数据,因此,我们可以使用此函数遍历本进程的所有内存空间,当然,这需要你对Windows的内存管理机制有足够的了解才行。

       因为还没有涉及到回调函数和DLL编程,这里我们以一个小例子来试验一下“指针到字节集”的神奇之处。Windows在执行一个可执行文件的时候,首先会把该文件映射到该进程内存的0x400000地址处,这是一个十六进制的地址,转换成十进制是4194304,我们就从该地址读取10KB的数据看看是什么,具体代码如下:

 

.版本 2

 

.子程序 _按钮从内存读取本执行文件映像_被单击

.局部变量 某字节集, 字节集

 

某字节集 = 指针到字节集 (4194304, 10 × 1024)  ' 从4194304处读取10KB的数据。

写到文件 (“c:\~asdf.dat”, 某字节集) ' 将该数据写到一个临时文件中

运行 (“notepad.exec:\~asdf.dat”, 假, )  ' 用记事本打开查看

删除文件 (“c:\~asdf.dat”)

 

       运行结果如图2.1.3-a所示:

2.1.3-a 指针到字节集试验运行结果

       如果你是一位有心人,你就会发现这些“乱码”很眼熟——这不就是一个exe文件的内容吗?!正是,这个小试验就证实了“指针到字节集”实际上读取的是本进程的指定内存地址的数据。

       关于“指针到字节集”的更多用法,请参阅后续的DLL编程相关章节。

 

2.2 [例] 十六进制查看器

       首先我们演示一下字节集的字节数组特性,我们来写一个小程序,把任何文件的内容以十六进制的形式显示出来。市面上早就有很多这样的软件了,而且已经非常成熟,比如WinHex,UltraEdit,HexWorkShop 等(图2.1-a是UltraEdit 显示的一个exe文件的部分数据),但有时候我们的软件也需要我们自己来实现这样的功能,比如我们要写串口通信程序、内存查看器等。

       先进行问题分析。

为了便于代码重用,我们决定把这个功能写成一个函数,该函数接收三个参数:

①要转换的字节集。

②是否显示数据地址。

③是否显示文本。

图2.2-aUltraEdit 中显示一个Exe文件的16进制数据

 

函数返回转换后的文本,这样我们就可以很方便地显示到编辑框等控件中了。在转换的过程中,需要把字节集掰成一个个的字节,我们使用易语言自带的“取十六进制文本”把它转换成十六进制的形式。但这里有一个问题:如果字节的值小于或等于16,那么转换后就是一位,而大于16的转换后就是两位,要是文本整齐美观,对于小于一位的,就需要在前面补0;还有,如果要显示数据地址,则也需要补齐长度;我们决定每行显示16个字节,那么字节集的长度有可能不是16的整数倍数,那么也需要在文本的后面补上空白文本以对齐。显然,我们需要写一个这样的函数,该函数能够在指定的文本前后添加重复的字符,使该文本达到指定的长度。这个函数的代码如下:

 

.版本 2

 

.子程序 填充重复文本, 文本型, , 将文本的左侧或右侧添加字符到指定的长度

.参数 参原始文本, 文本型

.参数 参要达到的长度, 整数型

.参数 参填充的字符, 文本型, 可空, 取第一个字符

.参数 参在左侧, 逻辑型, 可空, 不在左侧就在右侧

.局部变量 要增加的长度, 整数型

.局部变量 字符, 文本型

.局部变量 文本, 文本型

 

.如果真 (是否为空 (参填充的字符))

    参填充的字符= “ ”  ' 默认使用空格填充

.如果真结束

.如果真 (是否为空 (参在左侧))

    参在左侧= 真  ' 默认填充左侧

.如果真结束

.如果真 (参填充的字符 = “”)

    返回 (参原始文本)

.如果真结束

要增加的长度 = 参要达到的长度 - 取文本长度 (参原始文本)

.如果真 (要增加的长度 ≤ 0)

    返回 (参原始文本)

.如果真结束

字符 = 取文本左边 (参填充的字符, 1)  ' 只要第一个字符

文本 = 取重复文本 (要增加的长度, 字符)

.如果真 (参在左侧)

    返回 (文本 + 参原始文本)

.如果真结束

返回 (参原始文本 + 文本)

 

       我们的计划是每行显示16个字节,可以指定是否显示地址和文本。所以我们需要把一行文本分为三段:左侧的地址、中间的十六进制文本、右边的普通文本。左侧的地址我们可以直接通过循环的计数器得到;中间的我们把一个个字节转换成16进制文本后累加;右侧的文本则麻烦一些,因为有些控制字符是无法显示出来的(比如空字符、回车、换行、响铃、退格等),这些字符的ASCII码都小于32,这里我们按照惯例,统一把它们转换成“.”,其ASCII码是46。当计数器与16的余数是0的时候,就表示已完成一行,我们在其中插入换行符。具体的实现代码如下:

 

.版本 2

 

.子程序 字节集到十六进制文本, 文本型

.参数 参数据, 字节集

.参数 参是否显示地址, 逻辑型

.参数 参是否显示文本, 逻辑型

.局部变量 数据长度, 整数型

.局部变量 i, 整数型

.局部变量 结果文本, 文本型

.局部变量 一行文本, 文本型

.局部变量 某字节, 字节型

.局部变量 右侧文本, 文本型

 

数据长度 = 取字节集长度 (参数据)

.计次循环首 (数据长度, i)

    某字节= 参数据 [i]

    一行文本= 一行文本 + “ ” + 填充重复文本 (取十六进制文本 (某字节), 2, “0”)

    .如果真 (参是否显示文本)

        .如果真 (某字节 < 32)

            某字节= 46  ' 原点符号

        .如果真结束

        右侧文本= 右侧文本 + 字符 (某字节)

    .如果真结束

    .如果真 (i % 16 = 0)  ' 假定每行16个字节

        .如果真 (参是否显示地址)

            一行文本= 填充重复文本 (取十六进制文本 (i - 16), 4, “0”, ) + “ | ” + 一行文本

        .如果真结束

        .如果真 (参是否显示文本)

            一行文本= 一行文本 + “ | ” + 右侧文本

        .如果真结束

        结果文本= 结果文本 + 一行文本 + #换行符  ' 满一行就插入回车符并添加到结果文本中

        一行文本= “”

        右侧文本= “”

    .如果真结束

 

.计次循环尾 ()

 

' 最后一行有可能数据刚好不能被16整除,则不满足条件i%16=0,所以要补上最后的一段

.如果真 (一行文本 ≠ “”)

    .如果真 (取文本长度 (一行文本) < 48)

        一行文本= 填充重复文本 (一行文本, 48, “ ”, 假)

    .如果真结束

    .如果真 (参是否显示地址)

        一行文本= 填充重复文本 (取十六进制文本 (i - i % 16), 4, “0”, 真) + “ | ” + 一行文本

    .如果真结束

    .如果真 (参是否显示文本)

        一行文本= 一行文本 + “ | ” + 右侧文本

    .如果真结束

    结果文本= 结果文本 + 一行文本 + #换行符

.如果真结束

返回 (结果文本)

 

       在窗体上添加一个按钮,标题为“打开文件”;添加两个选择框,分别命名为“显示地址”和“显示文本”,默认均选中;添加一个编辑框,“是否允许多行”属性为“真”,“滚动条”选择“纵向滚动条”;添加一个通用对话框用来打开文件;添加一个程序集变量“集字节集”,我们需要把读入的字节集保存到其中。为按钮、选择框添加以下的代码:

 

.版本 2

 

.子程序 _按钮打开文件_被单击

.如果真 (通用对话框1.打开 ())

    集字节集= 取字节集左边 (读入文件 (通用对话框1.文件名), 23 × 1024)

    编辑框1.内容 = 字节集到十六进制文本 (集字节集, 选择框显示地址.选中, 选择框显示文本.选中)

.如果真结束

 

=====================================================

.子程序 _选择框显示地址_被单击

编辑框1.内容 = 字节集到十六进制文本 (集字节集, 选择框显示地址.选中, 选择框显示文本.选中)

=====================================================

.子程序 _选择框显示文本_被单击

编辑框1.内容 = 字节集到十六进制文本 (集字节集, 选择框显示地址.选中, 选择框显示文本.选中)

 

       在用户单击“打开文件”按钮后,我们使用“读入文件”函数读取文件,“读入函数”返回的是文件的字节集数据。这里我们考虑到速度的原因,只取其前面的23KB,如果不这样的话,超过100KB的文件就很慢了。而专业的软件如WinHex、UltraEdit等则可以读取多达几百兆甚至上G的文件,而且依然很快,显然是经过优化了的。其优化的方法是这样的:并不是一次将所有的文件读出,而是读取文件的一段数据,比如800个字节,按每行16个字节计,也就是50行。拖动滚动条的时候,将文件的读写指针以每16字节为单位进行偏移读取,转换后更新编辑框。这样每次只处理800个字节,速度当然就很快了(具体的读取方法,请参看下一例:文件分割机)。当然这样的话,就要去掉编辑框的滚动条,然后加上我们自己的滚动条进行额外的处理。这个优化的过程就留给你作为课外练习了,如果有疑问,请上本书的论坛(http://goomoo.cn/)讨论。

       最后我们运行程序,随便打开一个文件,得到如图2.2-b的运行结果,跟UltraEdit显示的结果是一致的。

图2.2-b 文件的16进制显示

 

 

2.3 [例] 无需合并程序的文件分割机

       在软盘流行的时代,也广泛流传着一类软件:文件分割机。这些软件能够把指定的文件分割成指定大小的单个文件,这些单个文件的大小刚好小于或等于软盘的容量,因此可以把它们拷贝入多张软盘。再以软盘为媒介,拷贝到目标机器上,然后用软件自带的功能或仅仅使用DOS批处理文件将它们拼装起来,生成和原始文件一样的文件。虽然现在U盘和大容量移动硬盘的方便、廉价和稳定,早已把软驱和软盘推进了历史,但把写这种软件作为一个练习还是蛮不错的。

       今天我们就来写一个这样的软件,我们的目标是:

              ①可以由用户指定要分割的文件。

              ②可以有用户指定分割后的单个文件的大小。

              ③使用批处理文件来组装文件。其原因是一方面你不能指望目标机器也安装了你的分割机,另一方面批处理文件的尺寸非常小,便于软盘拷贝。

       为了方便生成最后的批处理文件,我们先来练习一个DOS命令:Copy。Copy 命令用来复制文件,其最简单的用法是:

       copy源文件名 目的文件名

       执行之后就将源文件名所指的文件复制到目标文件名所指的文件。Copy命令不光能复制文件,还能够将多个文件合并起来。请执行如下操作:

       ①点击开始菜单,点击“运行...”菜单项。

       ②在运行窗口中,输入“cmd”,如果是Windows98 或 WindowsME,则输入command,输入完毕按回车键。

       ③在弹出的命令提示符窗口中输copy /? ,然后按回车键,我们得到了关于Copy命令的详细帮助,如下:

C:\Documents and Settings\Administrator>copy/?

将一份或多份文件复制到另一个位置。

 

COPY [/D] [/V] [/N] [/Y | /-Y] [/Z] [/A | /B ]source [/A | /B]

     [+source [/A | /B] [+ ...]] [destination [/A | /B]]

 

 source       指定要复制的文件。

  /A           表示一个 ASCII 文本文件。

  /B           表示一个二进位文件。

  /D           允许解密要创建的目标文件

 destination  为新文件指定目录和/或文件名。

  /V           验证新文件写入是否正确。

  /N           复制带有非 8dot3 名称的文件时,

              尽可能使用短文件名。

  /Y           不使用确认是否要覆盖现有目标文件

              的提示。

  /-Y          使用确认是否要覆盖现有目标文件

              的提示。

  /Z           用可重新启动模式复制已联网的文件。

 

命令行开关 /Y 可以在 COPYCMD 环境变量中预先设定。

这可能会被命令行上的 /-Y替代。除非 COPY

命令是在一个批处理脚本中执行的,默认值应为

在覆盖时进行提示。

 

要附加文件,请为目标指定一个文件,为源指定

数个文件(用通配符或 file1+file2+file3 格式)。

 

       在帮助文件的最后,我们看到一句话“要附加文件,请为目标指定一个文件,为源指定

数个文件(用通配符或 file1+file2+file3 格式)。”也就是说,我们可以通过“+”把多个文件合并拷贝为一个文件。在合并拷贝的时候,我们要采用二进制模式,所以还需要加上“/B”开关选项,所以,最后生成的批处理文件应该类似这样的样子:

       copy/b 文件名.扩展名.1+文件名.扩展名.2+...+文件名.扩展名.n 文件名.扩展名

 

开工了,我们新建一个易语言程序,设计如下的窗体:

图2.3-a 文件分割机窗体

 

       其中,长编辑框命名为“编辑框待分割的文件名”;短编辑框的名称为“编辑框块大小”,“输入方式”设为“整数文本输入”;组合框名称为“组合框单位”,有三项:字节、KB和MB,供用户选择;按钮的命名均为“按钮”+ 按钮的标题;“取消”按钮的“禁止”属性设为真。

       在写代码之前,先简要介绍一下文件读写方面的操作。在易语言中,读写文件最简单的是使用“读入文件”和“写出文件”函数,“读入文件”直接把文件内容读入到字节集变量,而“写出文件”则直接把字节集写出到指定的文本,很是方便,当然这两个函数不适合操纵大文件,否则太占内存,效率低。

       读写文件的另一种方式是标准的“打开文件-读取数据-关闭文件”三步曲。该方式需要先使用“打开文件”得到一个文件号,然后围绕该文件号进行“移动读写位置”和读写数据操作,在读写文件操作完毕后要记得关闭文件号,以免影响或许有其他进程对该文件的读写操作。关于读写文件的详细介绍,请参看本书的“文件格式编程”一章。

       此程序中还涉及到文件名、文件大小和目录等操作,这些函数都很简单,看看易语言编程环境提示窗口中的信息就能理解。其中经常需要得到去除路径的文件名,于是写了一个函数“去除文件名路径”(图2.3-b),如果文件名中有“\”,则返回“\”后的文本,否则返回原始文本。

 

图2.3-b 去除文件名路径函数

 

       分割文件整个步骤是这样的:

              ① 创建保存分割后文件的目录。

              ② 打开要分割的文件。

              ③ 从文件读取指定长度的数据块。

              ④ 将数据块保存到按规则生成的文件名的文件中。

              ⑤ 将新生成文件的文件名添加到批处理文本后面,更新进度条。

              ⑥ 循环③至⑤直到文件读完。

              ⑦ 关闭文件。

              ⑦ 生成批处理文件并保存。

 

       下面我们看看整个程序的源代码。

 

.版本 2

 

.程序集 窗口程序集1

.程序集变量 集已取消, 逻辑型

=========================================================

.子程序 _按钮开始分割_被单击

.局部变量 文件大小, 整数型

.局部变量 分块大小, 整数型

.局部变量 分块文件名, 文本型

.局部变量 批处理文本, 文本型

.局部变量 目录, 文本型

.局部变量 文件名, 文本型

.局部变量 文件号, 整数型

.局部变量 临时字节集, 字节集

.局部变量 i, 整数型

 

文件名 = 编辑框待分割的文件.内容

.如果真 (文件名 = “”)

    信息框 (“请指定要分割的文件!”, #错误图标, )

    返回 ()

.如果真结束

.如果真 (文件是否存在 (文件名) = 假)

    信息框 (“指定要分割的文件不存在!”, #错误图标, )

    返回()

.如果真结束

文件大小 = 取文件尺寸 (文件名)

分块大小 = 到数值 (编辑框块大小.内容)

.如果真 (分块大小 = 0)

    信息框 (“请指定分块大小,并选择合适的单位。”, #信息图标, )

    返回 ()

.如果真结束

.判断开始 (组合框单位.内容 = “KB”)

    分块大小= 分块大小 × 1024

.判断 (组合框单位.内容 = “MB”)

    分块大小= 分块大小 × 1024 × 1024

.默认

 

.判断结束

.如果真 (分块大小 ≥ 文件大小)

    信息框 (“文件本身比分块大小还小,不用分割。”, #信息图标, )

    返回 ()

.如果真结束

集已取消 = 假

按钮开始分割.禁止 = 真

按钮取消.禁止 = 假

目录 = 文件名 + “.部分”

创建目录 (目录)  ' 创建一个目录用来保存分割后的文件们。

文件号 = 打开文件 (文件名, #读入, #禁止写)

.如果真 (文件号 = 0)

    信息框 (“打开文件失败!”, #错误图标,)

    返回 ()

.如果真结束

文件名 = 去除文件名路径 (文件名)

.循环判断首 ()

    临时字节集= 读入字节集 (文件号, 分块大小)  ' 读入指定长度的数据,文件读写指针会自动移位

    i = i + 1

    分块文件名= 目录 + “\” + 文件名 + “.” + 到文本 (i)  ' 生成分块文件名

    写到文件 (分块文件名, 临时字节集)  ' 将数据写到该分块文件

    分块文件名= #引号 + 去除文件名路径 (分块文件名) + #引号

    ' 去掉路径,绝对路径在别的机器上可能无法合并;加上引号,防止文件名中有空格

    .如果 (批处理文本 = “”)

        批处理文本= 分块文件名

    .否则

        批处理文本= 批处理文本 + “+” + 分块文件名  ' 拼装批处理文本的中间部分

    .如果结束

    进度条1.位置 = 取读写位置 (文件号) × 100 ÷ 文件大小 ' 计算进度条位置

    处理事件 ()  ' 用户更新进度条和响应用户单击取消按钮。

    .如果真 (集已取消)

        关闭文件 (文件号)

        按钮开始分割.禁止 = 假

        按钮取消.禁止 = 真

        返回 ()

    .如果真结束

 

.循环判断尾 (是否在文件尾 (文件号, ) = 假)

' 组合批处理命令

' 在批处理命令中加上引号,防止文件名中有空格;最后加上pause暂停,让用户可以看清楚屏幕显示

批处理文本 = “copy /b ” + 批处理文本 + “ ” + #引号 + 文件名 + #引号 + #换行符+ “pause” + #换行符

写到文件 (目录 + “\合并文件.bat”, 到字节集 (批处理文本))  ' 直接将字节集写入文件就可以了。

信息框 (“分割文件完毕!”, #信息图标, )

按钮开始分割.禁止 = 假

按钮取消.禁止 = 真

运行 (“explorer.exe ” +目录, 假, )  ' 用资源管理器浏览分割后的文件。

 

=========================================================

.子程序 去除文件名路径, 文本型, , 此函数用来去除文件名中的路径。

.参数 参带路径文件名, 文本型

 

返回 (取文本右边 (参带路径文件名, 取文本长度 (参带路径文件名) -倒找文本 (参带路径文件名, “\”, , 假)))

 

=========================================================

.子程序 _按钮浏览_被单击

 

.如果真 (通用对话框1.打开 ())

    编辑框待分割的文件.内容 = 通用对话框1.文件名

.如果真结束

 

=========================================================

.子程序 _按钮取消_被单击

 

集已取消 = 真

 

 

       图2.3-c文件分割机的执行结果

 

 

2.4 [例] 生成EXE文件并在其中写入配置信息

       假如你使用过木马软件,你一定对其这样的功能不会陌生:木马主程序会根据你填写的某些信息,比如服务器地址、端口号和密码等生成一个特定的EXE文件,该EXE文件能够根据填入的参数执行特定的操作。难道木马主程序有编译功能?! 非也,其实该EXE文件是事先就编译好了的,然后作为资源放入EXE文件中。木马主程序在生成木马的时候,先把该资源写到磁盘文件,然后修改该文件中的某些数据,将配置信息写入。木马程序运行的时候,先从自身把这些配置信息读取出来,然后执行相应的操作。

       一般来说,有两种方法把配置信息写入EXE文件中,① 将配置信息写到EXE文件的末尾,很多木马采用的是这种方法。② 改写EXE文件的中间部分。第一种方法实现起来比较简单,但不幸的是由于易语言独立编译后EXE文件的特殊性,在其后添加数据后就无法运行了,所以此法并不实用。非独立编译的EXE文件虽然可以把数据写在尾部,但运行时要带支持库,那还不如把配置信息存在一个独立的文件中,因此也没有什么意义。这里我们就讨论第二种方法,修改EXE文件的中间数据。但具体修改哪个位置呢?况且EXE文件可不是随便能修改的,修改不正确会导致程序无法运行,还真有点麻烦。这里我给大家介绍一个小技巧。

       还是先来做一个试验。新建一个易程序,在窗体上添加一个按钮。双击该按钮,添加如下代码:

 

.版本 2

 

.子程序 _按钮显示_被单击

.局部变量 配置信息, 文本型

 

配置信息 = “http://goomoo.cn”

信息框 (配置信息, 0, )

 

       运行该程序,单击按钮,就显示出变量的值。

       将该程序编译成EXE文件,再用UltraEdit打开,我们可以看到变量的值就在EXE文件中(图2.4-a 如果没看到,请使用搜索功能)。

 

 

图2.4-a 字符串被编译程序存在EXE文件中

 

       既然能找到它,那就好办了。我们把该字符串修改成别的字符串试试,比如:“易容大师”,注意字符串的长度不要超过原来变量的长度哦,否则程序就有可能不能运行了。如果字符串比原来的短,请在左侧的十六进制输入区用0补足。修改完毕如图2.4-b所示。在修改的过程中,有时中文被拆分成两半而显示成乱码,不去理会。修改完毕保存文件。

       再次运行程序,点击按钮,哈哈,一句代码不写,就在外部修改了变量的内容。

 

 

图2.4-b 修改EXE文件中的字符串

 

       既然能够手动修改了,那么改成程序修改也不是难事,只要进行一个字节集替换就可以了。请按下面的步骤操作:

 

1> 新建一个易程序,添加一个按钮,为按钮写如下代码:

 

.版本 2

 

.子程序 _按钮显示_被单击

.局部变量 配置信息, 文本型

.局部变量 临时文本数组, 文本型, , "0"

 

配置信息=“www.goomoo.cn|4567|admin|goomoo |                           ”

' 注意在后面加些空格占用存储空间,以免替换的信息超过长度

临时文本数组 = 分割文本 (配置信息, “|”, )

信息框 (“主机:” + 临时文本数组 [1] + #换行符 + “端口:” + 临时文本数组 [2] + #换行符 + “用户名:” + 临时文本数组 [3] + #换行符 + “密码:” + 临时文本数组 [4], 0, )

 

       运行该程序,单击按钮,应该得到这样的运行结果:

图2.4-c 程序被配置前的运行结果

 

       将该程序编译成“待修改程序.exe”。

 

2> 再新建一个易程序。点击菜单“插入>资源>图片或图片组...”,在资源表格中双击“内容”下的空白格,出现“图片或图片组资源属性”对话框,点击其中的 [导入新图片] 按钮,出现一个浏览文件对话框,在“文件类型”旁边选择“所有文件(*.*)”,选中刚才编译的“待修改程序.exe”文件,把它作为图片资源添加进来(见图2.4-c)。将该资源重命名为“待修改程序”。

 

图2.4-c 把EXE文件作为图片资源加入

 

3> 在_启动窗口上添加4个标签控件、4个编辑框控件和1个通用对话框控件。布局如图2.4-d所示。

图2.4-d 窗体布局

       其中编辑框的名字设置为“编辑框”+前面标签的名字。通用对话框的“类型”设置为“保存文件”,“过滤器”设置为“EXE文件(*.exe)|*.exe”,“默认后缀”设为“exe”。双击按钮,添加以下代码:

 

.版本 2

 

.子程序 _按钮生成_被单击

.局部变量 文件内容, 字节集

.局部变量 要替换成的文本, 文本型

.局部变量 原始文本, 文本型

.局部变量 长度差, 整数型

 

.如果真 (通用对话框1.打开 () = 假)

    返回 ()

.如果真结束

原始文本 = “www.goomoo.cn|4567|admin|goomoo |                           ”  ' !!!注意这里的文本要和被配置程序里的文本一样!!!

要替换成的文本 = 编辑框主机.内容 + “|”+ 编辑框端口.内容 + “|” + 编辑框用户名.内容 + “|” + 编辑框密码.内容+ “|”

长度差 = 取文本长度 (原始文本) -取文本长度 (要替换成的文本)

.判断开始 (长度差 > 0)

    ' 需要在右侧补空格

    要替换成的文本= 要替换成的文本 + 取重复文本 (长度差, “ ”)

.判断 (长度差 < 0)

    信息框 (“配置信息太长,可能会导致生成的程序无法运行。”, #错误图标, )

    信息框 (“生成文件失败!”, #错误图标,)

    返回 ()

.默认

 

.判断结束

文件内容 = 子字节集替换 (#待修改程序, 到字节集 (原始文本), 到字节集 (要替换成的文本), , )  ' 进行字节集替换

.如果 (写到文件 (通用对话框1.文件名, 文件内容))

    信息框 (“生成文件成功!”, #信息图标,)

.否则

    信息框 (“生成文件出错!”, #错误图标,)

 

       很惊讶只有这么短的代码,是不是?其中最关键的只有一句“文件内容 = 子字节集替换 (#待修改程序, 到字节集 (原始文本), 到字节集 (要替换成的文本), , )” ,其他的只是一些辅助性的工作如打开对话框、错误提示等。这段代码中要注意的是“原始文本”变量的内容一定要和被配置程序中的一致,包括大小写!否则替换无法成功。

       运行生成的程序,点击按钮,我们看到我们在配置程序中填的参数显示出来了(图2.4-e)!

 

       一切就这么简单! ——当魔术的秘密被揭穿的时候,魔术也就不再神奇了。

图2.4-e 运行生成的EXE,我们看到了我们在配置程序中填入的信息

 

 

2.5文件捆绑机

       不知大家有没有用过多媒体制作软件如Authorware、Director、Multimedia Builder 、Flash等,如果没有用过,也一定听说过或使用过EXE文件捆绑器、WinRAR之类的程序,以及其他的电子贺卡之类的小程序。这类程序都有一些共同的特定:① 能够生成EXE文件;② 能够把多个其他的文件捆绑在该EXE文件中,生成的EXE文件在运行时能够解读它们。

       用前面的思路来实现这个功能可就难了:你不知道用户要捆绑多少个文件,你也不知道这些文件有多大。所以用预留空间然后进行替换的方法显然是行不通了,我们得另辟蹊径。

       前面我们说过一个方案,就是把数据附加在EXE文件的尾部,但这个方法在易语言中对于独立编译的EXE文件完全行不通。你可以做一个这样的试验:新建一个空的易语言程序,然后独立编译成EXE文件,用UltraEdit打开,修改尾部的一个字节的数据,或者在尾部添加一个字节,保存,再运行,你就得到这样的信息提示然后程序退出:

 

图2.5-a 修改独立编译的EXE尾部的数据后运行的错误提示

 

       根据日常使用易语言独立编译程序的观察,我们发现易语言独立编译的程序在执行的时候,会把相关的支持库文件释放到临时文件夹中。也就是说,易语言独立编译的EXE文件中既包含了非独立编译的EXE程序,也包含了支持库,同时支持库是压缩了之后捆绑到EXE程序的尾部的。所以一旦我们修改了其尾部的数据,程序就不能正确运行了。既然不能附加到尾部,那么我们能不能把文件插在它们之间呢?如果插在它们之间程序仍能够运行,那我们就几乎成功了。这里关键是要找到它们之间的交界处在哪里,如果找到了,事情就成功了一半。有的朋友可能会说:“既然如此,那我们先编译一个非独立的exe文件,得到其大小,不就可以知道他们在何处交界的吗?”。我也曾这么想过,但通过观察发现事实并没有这么简单。我们可以随便编译一个易程序,一个非独立编译的,一个独立编译的,然后用UltraEdit打开看看,就会发现前面的开始处不远,就有不同的地方。既然如此,就不能简单地依据文件大小来判断了,差之毫厘,谬以千里啊!我猜想文件的头部虽然不同,但文件的尾部应该还是一样的,能够找到非独立编译程序的尾部也是一样的。嗯,不要吝惜你自己的想像力!大胆地猜想吧!“想像力比知识更重要。”,这是爱因斯坦说的。还有的朋友可能会说:“既然非独立编译文件的大小不能确定,为什么不找运行库的头呢?那样不是更简单?”事实上我们根本无法找到运行库的头。易语言的运行库是Krnln.fnr文件,该文件有0.97MB,而独立编译的exe文件一般为500KB左右,显然运行库只是部分放在了文件后面或是被压缩了然后放在后面的,那要找运行库的头是根本找不到的。

    一旦我们找到了它们的交界处,我们就可以把独立编译后的EXE从此处一分为二,然后分别当作图片资源插入到主程序中。在写出的时候先写出非独立编译的那块,然后写一个分割符字符串,这个标志是为了便于生成的EXE程序在读取自身时方便分割自身。紧接着我们在后面写上要插入的文件的相关信息,比如文件名、文件起始位置、文件大小等,再写一个分割字符串,然后就是文件的内容,再写个分割串,最后再把易语言编译时附加的运行库数据加在后面就完事了。所以,最终生成的EXE文件应该具有如下的结构:

 

非独立编译块一

编译程序插入的分割符

非独立编译块二

 

文件信息描述文本

 

 

文件一

文件二

。。

 

文件N

 

压缩的支持库块

 

表2.5-a 最终生成的文件结构,其中灰色块为分割字符串

 

       从表中我们可以看到,文件会被分隔字符串分成5块——连续的白色的部分。如果分隔的块数小于5,则说明未插入任何文件。文件的描述信息在第三块,被插入的文件连续地存放在第4块。最终捆绑的程序在运行的时候,先读取文件描述信息,并将该信息显示于列表框中,用户从列表框中选择要释放的文件,然后点击“释放并打开”按钮,程序就从第四块中读出指定的字节集存到文件中并打开。

       根据这个思路,我们在编程时需要写三个程序:① 要被作为资源包含进另一个程序的程序,我们称之为“被包程序”;②被包程序在被作为资源插入前要被一分为二,所以还要一个“分割程序”;③ 显然还需要一个生成EXE的“主程序”。 在发布时只需要发布主程序就行了。该主程序允许用户添加多个文件,然后生成一个EXE文件。

       1>我们先来写“被包程序”。该程序在运行是先将自身按分隔字符串分块,从数据块中读出被插入文件的信息,然后根据用户的需求来释放文件,并打开释放的文件。

       因此,我们设计如下的窗体(图2.5-b),中间哪个白色的区域是个列表框。

图2.5-b 被包程序的窗体

 

       给程序添加一个常量“分割符”,如图2.5-c所示。我们假定程序的各个部分是通过这个文本分割的。当然你也可以使用自己认为好的分隔字符串,只要保证在编译完毕的EXE文件中的其他位置不会有该文本。

图2.5-c 用来分割文件的分割符

 

       添加一个程序集变量“集字节集数组”,用来保存文件被分割后的各部分。

       在_启动窗口创建完毕后,需要将插入的文件信息读取到列表框中,所以双击_启动窗口,添加以下代码:

 

.版本 2

 

.子程序 __启动窗口_创建完毕

.局部变量 文件信息, 文本型

.局部变量 临时文本数组, 文本型, , "0"

.局部变量 i, 整数型

 

集字节集数组 = 分割字节集 (读入文件 (取执行文件名 ()), 到字节集 (#分割符), )

.如果真 (取数组成员数 (集字节集数组) ≤ 4)

    ' 如果已插入文件,则应该有5段

    信息框 (“该程序尚未包含任何文件。”, 0, )

    结束 ()

.如果真结束

' 从文件结构图中可以知道,文件信息在第3块中

文件信息 = 到文本 (集字节集数组 [3])

' 将文件信息添加到列表框中

临时文本数组 = 分割文本 (文件信息, “|”, ) ' 文件信息之间以“|”分隔

.计次循环首 (取数组成员数 (临时文本数组), i)

    列表框文件列表.加入项目 (临时文本数组 [i], )

.计次循环尾 ()

 

       在用户单击〔释放并打开选中的文件〕的时候,程序从列表框的文本中读取文件名、文件的起始位置和长度等信息,然后从“集字节集数组[4]”中读出文件数据,写到磁盘文件并打开。具体代码如下:

 

.版本 2

 

.子程序 _按钮释放打开_被单击

.局部变量 文件信息, 文本型

.局部变量 临时文本数组, 文本型, , "0"

.局部变量 文件数据, 字节集

 

.如果真 (列表框文件列表.现行选中项 = -1)

    信息框 (“请先选择要释放的文件。”, #信息图标, )

    返回 ()

.如果真结束

 

文件信息 = 列表框文件列表.取项目文本 (列表框文件列表.现行选中项)

临时文本数组 = 分割文本 (文件信息, “*”, ) ' 文件名、起始位置和长度之间以“*”分隔

文件数据 = 取字节集中间 (集字节集数组 [4], 到数值 (临时文本数组 [2]), 到数值 (临时文本数组 [3]))

' 文件数据在第四块字节集中

写到文件 (临时文本数组 [1], 文件数据)

' 运行 (临时文本数组 [1], 假, )'此句只有可执行文件才会被执行。

运行 (“cmd /c start ” +临时文本数组 [1], 假, #隐藏窗口)  ' 此句可以打开任何文件。

' 此处本应使用API ShellExecute ,但未讲到,所以先用此法代替。

 

       被包程序写这么多代码就够了。程序的最后一行“运行”函数的参数“欲执行的命令行”被设置为“cmd /c start ”加上文件名。我们知道,cmd.exe 是WindowsNT、XP、2003的命令行解释程序;“/c”参数表示在执行后续的命令字符串后自动关闭解释程序;“start”是个内部命令,后面带一个文件名,执行该命令会用与该文件关联的程序打开该文件。

现在运行它,会提示“该程序尚未包含任何文件。”,然后退出。当然呢,还没插入文件呢。请将该程序编译成一个独立编译版本的EXE文件,一个非独立编译版本的EXE文件。

 

       2>下面开始写分割程序,该程序需要根据非独立编译的EXE文件的尾部1KB的数据找到分割处,然后把独立编译的EXE文件一分为二。

       新建一个易程序,设计如下的窗体(图2.5-c )。其中上下编辑框的名称分别为“编辑框非独立编译文件名”和“编辑框独立编译文件名”,上下两个“浏览...”按钮的名称分别为“按钮浏览非独立编译EXE”和“按钮浏览独立编译EXE”。

 

图2.5-c 分割程序的窗体

 

       分割程序的代码很简单,全部代码如下:

 

.版本 2

 

.程序集 窗口程序集1

======================================================

.子程序 _按钮浏览非独立编译EXE_被单击

 

.如果真 (通用对话框1.打开 ())

    编辑框非独立编译文件名.内容 = 通用对话框1.文件名

.如果真结束

 

======================================================

.子程序 _按钮浏览独立编译EXE_被单击

 

.如果真 (通用对话框1.打开 ())

    编辑框独立编译文件名.内容 = 通用对话框1.文件名

.如果真结束

 

 

.子程序 _按钮分割_被单击

.局部变量 非独立编译文件数据, 字节集

.局部变量 独立编译文件数据, 字节集

.局部变量 尾部数据, 字节集

.局部变量 交接位置, 整数型

.局部变量 临时数据, 字节集

 

.如果真 (取文件尺寸 (编辑框独立编译文件名.内容) ≤ 0 或 取文件尺寸 (编辑框非独立编译文件名.内容) ≤ 0)

    信息框 (“有一个文件不存在!”, #错误图标,)

    返回 ()

.如果真结束

.如果真 (取文件尺寸 (编辑框非独立编译文件名.内容) ≥取文件尺寸 (编辑框独立编译文件名.内容))

    信息框 (“文件尺寸不对,文件是不是载入反了?”, #询问图标, )

    返回 ()

.如果真结束

非独立编译文件数据 = 读入文件 (编辑框非独立编译文件名.内容)

独立编译文件数据 = 读入文件 (编辑框独立编译文件名.内容)

尾部数据 = 取字节集右边 (非独立编译文件数据,1024)  ' 取尾部1KB的数据

交接位置 = 寻找字节集 (独立编译文件数据, 尾部数据, )

.如果真 (交接位置 ≤ 0)

    信息框 (“找不到交界处,分割失败!”, #错误图标, )

    返回 ()

.如果真结束

交接位置 = 交接位置 + 1024

临时数据 = 取字节集左边 (独立编译文件数据, 交接位置)

写到文件 (“01.dat”, 临时数据)

临时数据 = 取字节集右边 (独立编译文件数据, 取字节集长度 (独立编译文件数据) - 交接位置)

写到文件 (“02.dat”, 临时数据)

信息框 (“分割完毕。”, #信息图标, )

 

       这段代码也全是字节集操作。运行该程序,点击“浏览”按钮们选择你刚才编译的那个非独立的EXE文件和独立编译的EXE文件,点击“分割”按钮把独立编译的EXE文件分割成01.dat和02.dat两个文件。

       我们可以通过一个小试验来检测分割的位置是否正确:用UltraEdit打开01.dat文件,在尾部胡乱增加一些数据(把前面的数据复制粘贴一些到尾部即可),然后保存文件。再在Windows命令提示符窗口中用 “copy /b 01.dat 02.dat asdf.exe”DOS命令把这两个文件合并成一个asdf.exe 文件,再运行asdf.exe ,我们可以发现,程序仍能正确运行。成功!

       【注意】测验完毕,请重新生成01.dat和02.dat文件。

 

       3>接下来写主程序。新建一个易程序,设计如下的窗体(图):

 

图 2.5-d 主程序窗体

 

       窗体上的白色区域是个列表框,用来显示用户要插入的文件名,将其“允许多项选择”属性设为“真”,以方便用户选择多个文件进行删除操作。有两个通用对话框,把其中一个通用对话框的类型改为“保存文件”,“过滤器”设置为“可执行文件(*.exe)|*.exe”,“默认文件后缀”设为“exe”。

       按照2.4 节中所讲述的方法,把分割程序生成的01.dat文件和02.dat文件作为图片资源加入,并分别命名为“文件头”和“文件尾”,如图2.5-e。

 

图2.5-e 把分割的文件作为图片资源加入

 

       还要注意要把那个“分割符”的常量从被包程序中复制粘贴过来,以确保一致。

       这段程序的代码也不多,全部列出如下:

 

.版本 2

 

.程序集 窗口程序集1

======================================================

.子程序 去除文件名路径, 文本型, , 此函数用来去除文件名中的路径。

.参数 参带路径文件名, 文本型

 

返回 (取文本右边 (参带路径文件名, 取文本长度 (参带路径文件名) -倒找文本 (参带路径文件名, “\”, , 假)))

 

======================================================

.子程序 _按钮添加文件_被单击

 

.如果真 (通用对话框1.打开 ())

    列表框1.加入项目 (通用对话框1.文件名, )

.如果真结束

 

======================================================

.子程序 _按钮删除选择_被单击

.局部变量 已选择项目们, 整数型, , "0"

.局部变量 i, 整数型

 

已选择项目们 = 列表框1.取所有被选择项目 ()

.变量循环首 (取数组成员数 (已选择项目们), 1, -1, i)  ' 注意要反向删除

    列表框1.删除项目 (已选择项目们 [i])

.变量循环尾 ()

 

======================================================

.子程序 _按钮捆绑_被单击

.局部变量 插入数据, 字节集

.局部变量 文件信息, 文本型

.局部变量 i, 整数型

.局部变量 文件名, 文本型

.局部变量 文件数据, 字节集

.局部变量 当前位置, 整数型

 

.如果真 (列表框1.取项目数 () < 1)

    信息框 (“你未选择插入任何文件。”, #错误图标, )

    返回 ()

.如果真结束

.如果真 (通用对话框保存文件.打开 () = 假)

    返回 ()

.如果真结束

当前位置 = 1

.计次循环首 (列表框1.取项目数 (), i)

    文件名= 列表框1.取项目文本 (i - 1)

    文件数据= 读入文件 (文件名)

    文件信息= 文件信息 + 去除文件名路径 (文件名) + “*” + 到文本 (当前位置) +“*” + 到文本 (取字节集长度 (文件数据)) + “|”

    ' 文件之间以“|”作为分隔,文件名、起始位置、文件长度之间以“*”分隔

    插入数据= 插入数据 + 文件数据

    当前位置= 当前位置 + 取字节集长度 (文件数据)

.计次循环尾 ()

文件数据 = #文件头 + 到字节集 (#分割符) + 到字节集 (文件信息) +到字节集 (#分割符) + 插入数据 + 到字节集 (#分割符) + #文件尾

.如果 (写到文件 (通用对话框保存文件.文件名, 文件数据))

    信息框 (“捆绑文件成功!”, #信息图标,)

.否则

    信息框 (“写出文件失败!”, #错误图标,)

.如果结束

 

       这段代码的核心部分是“_按钮捆绑_被单击”子程序。该程序顺次读取列表框中的文件,把文件名、文件的起始位置和大小递增地存入到“文件信息”变量中,而文件内容则递增地存入到“插入数据”变量中。最后独立编译EXE的文件头、文件信息、文件数据、独立编译的文件尾全部用分隔符分隔合并起来,存入一个文件中,这个文件就是捆绑后的文件了。

       运行该程序,随便添加几个文件。点击〔捆绑...〕按钮生成一个EXE文件,建议保存在一个空目录中。运行该程序,选中列表框中的一个文件,点击〔释放并打开选中的文件〕按钮,该程序会把指定的文件释放到当前目录下并打开,见图2.5-g。

 

图2.5-f 主程序的运行界面

 

 

图2.5-g 生成的捆绑程序的运行效果

 

       嗯。这个例程到这里就要结束了。从头至尾看一看,想一想,其实代码都是很简单的,关键是如何突破常规思维,出奇制胜。

       当然,这个程序也存在问题,就是捆绑大文件时太占用内存,因为所有的数据要读到字节集变量中进行操作,如果要捆绑几百兆的视频文件,采用字节集来操作的方法显然是不合适的,直接用文件读写操作效率会更高、占用内存会更少。因为本节主要演示字节集的操作技巧,采用文件读写的方式就留给有兴趣的读者作为练习了。

 

2.6 [例] 程序的自校验

       在因特网上举目四望,破解软件泛滥成灾。虽然软件开发者始终应该把软件的功能和易用性放在首位,但基本的防破解措施还是必要的,正所谓“防君子不防小人”。如果说软件开发出来后,因为盗版的泛滥而导致开发商利润微薄或无利润,那么该软件甚至企业的前途都将令人忧虑。虽然要做到完全不被破解是不可能的,但我们仍可以采取一些有效的措施来给破解者施加难度,延迟其破解的时间,或者让一些低水平的破解者放弃破解。在这些防破解措施中,给程序加上自校验无疑是行之有效的方法之一。程序的自校验功能使程序在运行时能够检查自身的完整性与正确性,如果发现自身被修改,就拒绝运行、以某种方式警告用户或自动销毁等。

       下面我们以字节集操作的方式介绍一个简单的给程序加自校验的方法。我们的思路是这样的:把程序编译成非独立编译的EXE文件之后,通过另一个程序计算其校验数据并加到其尾部。校验数据可以通过易语言的“数据操作支持库一”中的“取数据摘要”获得。程序在运行的时候根据特定的字符串将自身分为两部分,前部分为可执行文件数据,后部分为自校验数据。程序计算前半部分的校验数据,并和后半部分的数据进行比较,如果不同则表示程序已被非法修改从而执行相应的操作。

       根据这个思路,我们需要写两个程序,一个是用来给用户使用的“主程序”,另一个则是用来给“主程序”添加自校验信息的“添加校验程序”。这里我们的主程序不实现任何有用的功能,只是在启动时检查自身的完整性。

       1>新建一个空的易语言程序。先增加一个常量,如图2.6-a。我们需要用这个字符串来分割文件。注意此常量会被编译程序编译到EXE文件中,后面我们还要用校验程序加上一个分割符,所以实际上最终的文件会被此分割符分成三段。

图2.6-a 加入分割符文本常量

 

       双击启动窗体,添加以下代码。注意,此段代码中的“取数据摘要”函数和“解压数据”需要用到“数据操作支持库一”,请确认支持库配置对话框中勾中了这个支持库。

 

.版本 2

.支持库 dp1

 

.子程序 __启动窗口_创建完毕

.局部变量 文件数据, 字节集

.局部变量 校验数据, 字节集

.局部变量 临时数据数组, 字节集, , "0"

 

.如果真 (是否为调试版 () = 假) ' 此处加一判断,以方便调试程序

    文件数据= 读入文件 (取执行文件名 ())

    临时数据数组= 分割字节集 (文件数据, 到字节集 (#分割符), )

    .如果真 (取数组成员数 (临时数据数组) < 3)  ' 分割后应该有三块

        信息框 (“程序已被非法修改或感染病毒!”, #错误图标, “警告”)

        结束 ()

    .如果真结束

    ' 把前面两块加上分割符拼接成完整的EXE文件

    文件数据= 临时数据数组 [1] + 到字节集 (#分割符) + 临时数据数组 [2]

    校验数据= 解压数据 (临时数据数组 [3])  ' 第三块为校验数据,已被压缩,所以先解压一下

    .如果真 (取数据摘要 (文件数据) ≠到文本 (校验数据))

        信息框 (“程序已被非法修改或感染病毒!”, #错误图标, “警告”)

        结束 ()

    .如果真结束

 

.如果真结束

 

       现在在易语言编程环境中运行这个程序,程序会正常运行。请将其编译成非独立编译的“主程序.exe”文件,在资源管理器中运行,程序会提示“程序已被非法修改或感染病毒!”,当然罗,我们还没有给它加自校验数据。

 

       2>再新建一个易语言程序,保存为“添加校验码.e”,设计如图2.6-b所示的窗体。

图2.6-b 添加校验信息的窗体

 

       然后添加代码。该程序的代码很短,全部代码如下:

 

.版本 2

.支持库 dp1

 

.程序集 窗口程序集1

=====================================================

.子程序 _按钮浏览_被单击

 

.如果真 (通用对话框1.打开 ())

    编辑框文件名.内容 = 通用对话框1.文件名

.如果真结束

 

=====================================================

.子程序 _按钮添加校验_被单击

.局部变量 文件数据, 字节集

.局部变量 校验数据, 字节集

 

.如果真 (编辑框文件名.内容 = “” 或 取文件尺寸 (编辑框文件名.内容) ≤ 0)

    信息框 (“指定的文件不正确!”, #错误图标,)

    返回 ()

.如果真结束

文件数据 = 读入文件 (编辑框文件名.内容)

校验数据 = 压缩数据 (到字节集 (取数据摘要 (文件数据)))

文件数据 = 文件数据 + 到字节集 (#分割符) +校验数据

.如果 (写到文件 (编辑框文件名.内容, 文件数据))

    信息框 (“添加自校验信息成功!”, #信息图标, )

.否则

    信息框 (“改写文件失败!”, #错误图标,)

 

       运行该程序,点击〔浏览〕按钮,选择刚才编译的“主程序.exe”文件,然后单击〔添加校验〕按钮。程序提示修改成功后再到资源管理器运行主程序,我们发现程序可以正常运行了。使用UltraEdit打开主程序,修改其中的任何一个字节,比如把开始处不远的“This program” 改成 “That program”,保存后再运行主程序,则再次得到警告信息(图2.6-d),

 

图2.6-d 修改程序之后运行的效果

 

       这样这个自校验程序就完成了。当然这还是最简单的自校验方式,你还可以做得更复杂一些:比如你可以把自校验信息写在EXE文件的其他位置;把EXE文件用UPX等可执行文件压缩工具压缩后或其他加壳工具加壳后再加上自校验信息,这样只要一脱壳,程序就无法运行。如果你有更巧妙的方法,别忘记给我发一封邮件啊,我的邮箱是:zeng_goomoo@yahoo.com.cn。

 

2.7 使用字节集的注意事项

       从前面的几个实例中,我们可以看到易语言所提供的字节集的功能是很强大、很方便的。但在使用字节集的时候,也要注意一些问题,除了前面提到的“到数值”函数之外,要注意的就是字节集操作的效率。主要集中在一下两点:

       1>不要用字节集来操作大的文件。字节集型的变量需要把所有的数据读入到内存中进行处理,如果文件大小多达几十上百兆,那么内存的占用是可想而知的。请看下面的代码:

 

.版本 2

 

.程序集 窗口程序集1

.程序集变量 数据, 字节集

===============================================

.子程序 __启动窗口_创建完毕

 

数据 = 取空白字节集 (300 × 1024 × 1024)

 

       这段代码在运行时分配300MB字节集的内存空间,运行之后你看看你的任务管理器的内存占用率吧。注意在运行前请保存你的工作文件,以免意外死机。

 

2> 不要频繁地进行字节集相加。字节集的相加操作会导致内存的重新分配,从而极大地影响运行效率。请看下面的代码:

 

.版本 2

 

.子程序 取随机字节集, 字节集, , 通过字节集相加的方式

.参数 参长度, 整数型

.局部变量 结果, 字节集

 

.计次循环首 (参长度, )

    结果= 结果 + 到字节集 (取随机数 (0, 255))

.计次循环尾 ()

返回 (结果)

 

==========================================================

.子程序 取随机字节集一, 字节集, , 通过预先分配的方式

.参数 参长度, 整数型

.局部变量 结果, 字节集

.局部变量 i, 整数型

 

结果 = 取空白字节集 (参长度)

.计次循环首 (参长度, i)

    结果 [i] = 取随机数 (0, 255)

.计次循环尾 ()

返回 (结果)

 

==========================================================

.子程序 _按钮测试效率_被单击

.局部变量 上次时间, 整数型

.局部变量 长度, 整数型

 

长度 = 15 × 1024

上次时间 = 取启动时间 ()

取随机字节集 (长度)

输出调试文本 (取启动时间 () - 上次时间)  ' 输出2657

上次时间 = 取启动时间 ()

取随机字节集一 (长度)

输出调试文本 (取启动时间 () - 上次时间)  ' 输出0

 

       这段代码中有两个函数实现同样的功能:“取随机字节集”和“取随机字节集一”,都是用来取得指定长度的随机数据,但实现的细节不一样,前一个函数使用字节集相加的方式,而后一个则先分配内存,然后逐一为数据中的每个字节赋值。从运行的结果可以看到,采用字节集相加的方式运行效率低得吓人,取15KB的随机数据居然需要将近3秒。而预先分配内存的函数显然快很多,耗时基本为0。