zengl v1.2.5 , 该版本在原来普通异或加密方式的基础上,添加了RC4加密方式,RC4可以看成是异或运算的升级版,安全性和实用性都很高,只需要很短的初始密钥就可以产生和明文一样长的伪随机密钥流...

    v1.2.5支持windows ,linux ,mac os x 三个系统下的源码编译,同时支持windows和linux下的64位编译,mac 64位理论上应该也可以,没测试。

    该版本的下载地址如下:

    github项目地址为:https://github.com/zenglong/zengl_language/  选择右侧的Download Zip或Clone in Desktop

    百度盘共享链接地址:http://pan.baidu.com/s/1io3iL 这是一个zip压缩包,解压后可以看到linux ,windows和mac目录,分别为三个操作系统环境下的源码和编译器配置,windows目录中既包含用于vs2008的sln解决方案,还包含用于vc6的dsw工作空间。

    在linux中进行make之前,如果不是root用户,就请用 "su" 之类的命令提权,因为make生成libzengl.so后,会copy这个.so文件到/usr/lib目录中(该目录需要root权限才能进行写入操 作),这样可执行文件zengl和encrypt才能链接和使用libzengl.so的动态链接库,详情可以查看makefile,只有 libzengl.so是zengl语言的核心文件,main.c和encrypt.c生成的zengl和encrypt可执行文件都只是 libzengl.so的测试C程序 (ubuntu下直接使用sudo make) 。

    mac目录中只包含makefile文件和测试用的zengl脚本文件,所以在mac下直接make即可,需要清理中间文件时,可以使用make clean来清理。

    sourceforge.net上的项目地址为:https://sourceforge.net/projects/zengl/files/ (里面有所有历史版本的压缩包,包括v1.2.5的zip包)。

    下面看下v1.2.5所做的修改(这些信息可以在readme或linux的usage.txt中查看到,也可以直接在上面的github链接中查看到):

    zengl v1.2.5 , 该版本在原来普通异或加密方式的基础上,添加了RC4加密方式,RC4可以看成是异或运算的升级版,安全性和实用性都很高,只需要很短的初始密钥就可以产生和明文一样长的伪随机密钥流,rc4对应的zengl接口为:zenglApi_SetSourceRC4Key

    该版本还规范了API的调用位置,通过引入API状态枚举值,当某个API处于不正确的调用位置时,会忽略掉该API,同时返回API调用位置不正确的信息,可以使用zenglApi_GetErrorString接口来获取出错信息。

    所有API接口都可以返回出错信息,所以将原来一些返回值为VOID类型的API接口改为返回值为ZL_EXP_INT即int类型,这样当该API接口出错时,就可以返回-1,用户就可以根据此来判断API调用是否出错,如果是返回指针类型的API接口,则返回ZL_EXP_NULL即0的空指针来表示API接口是否出错,除了zenglApi_GetMemBlock这个API,该API由于原来返回的是一个结构体,所以只有当返回的ZENGL_EXPORT_MOD_FUN_ARG结构体中type成员为ZL_EXP_FAT_INVALID时才表示该API调用出错。

    新增了一个zenglApi_GetExtraDataEx接口,该接口是zenglApi_GetExtraData接口的扩展版,扩展后的函数可以用于检测某额外数据是否存在,这种检测在使用额外数据替代模块函数里的静态变量和全局变量时就可以起到作用(详情可以查看zenglApi_BltModFuns.c里的zenglApiBMF_bltRandom函数,静态变量和全局变量在多线程中容易发生访问冲突)。

    上面提到了zenglApi_BltModFuns.c文件,该文件是v1.2.5中新增的,它里面定义了虚拟机为用户提供的内建模块函数(只是一些很基本的脚本模块函数,像array之类的),用户可以根据需要选择是否使用这些内建模块函数,还可以根据此文件里的模块函数为模板,复制到自己的程序中进行修改替换。因为有时候,像array这样的模块函数,用户一般不会对它进行扩展,所以就可以直接使用该文件里定义的array模块函数,只有当有需要修改时,再从该文件中复制对应的模块函数到自己的应用程序中进行修改替换,这些内建模块函数的设置方法和用户自己定义的模块函数的设置方法是一样的,都是用的zenglApi_SetModFunHandle接口来进行设置,详情可以参考main.c测试程序里的main_builtin_module_init模块初始化函数。

    最后就是作者根据zengl.com上用户Raindy的评论,在64位Ubuntu系统下对zengl进行编译测试,发现之前的版本在64位下有些类型转换问题,还有些链接问题,所以在该版本中将某些地方出现的指针到int类型的转换改为指针到long类型的转换,另外还进行了一些其他的64位的处理。

    v1.2.5版本新增了一个mac目录,该目录里有MAC OS X苹果系统下的makefile文件,下面是作者测试过的编译平台:

    Ubuntu Studio 12.04.3 64位系统下 系统默认编译器:gcc(4.6.3版本)
    Ubuntu Studio 12.04.3 32位系统下 系统默认编译器:gcc(4.6.3版本) gcc(4.5.3版本) gcc(4.4.7版本) gcc(4.1.2版本)
    Slackware-13.37 32位系统下 gcc(4.5.2版本)
    苹果雪豹操作系统 Snow Leopard 10.6 (i386内核) gcc(4.2.1版本)
    winXP系统 vs2008
    winXP系统 VC6
    win7 64位系统 vs2010 (直接导入vs2008的解决方案,32位编译和64位编译,其中64位编译需要在32位项目的基础上,手动配置成x64的平台)

    作者:zenglong
    时间:2013年11月28日
    官网:www.zengl.com

    先看下win7 64位系统下使用vs2010编译zengl为64位可执行文件的情况。

    先用vs2010打开windows目录中的zengl.sln文件,系统会自动完成vs2008到vs2010的转换工作。

    一开始,默认是win32即32位平台编译模式,win7 64位下是支持直接编译运行32位程序的,所以可以先直接在win32模式下编译测试,在测试时,对于main项目请设置命令行参数为test.zl ,对于encrypt项目,命令行参数可以设置为 test.zl test_rc4_encrypt.zl rc4key_334566_hello_world -t rc4 用于测试rc4加密方式(test.zl为原脚本文件,test_rc4_encrypt.zl为加密后的目标文件,rc4key_334566_hello_world为RC4初始化密钥,-t rc4用于指定RC4加密方式,这里的密钥是和main.c里保持一致的) ,也可以将命令行参数设置为 test.zl test_xor_encrypt.zl xorkey_334566_hello_world -t xor 来测试xor普通异或加密。

    要转为64位编译,则可以在配置管理器中新建一个x64的平台:


图1
 
 

图2


图3

    在图2中选择x64平台时,其他选项保持默认不变,如从win32项目中复制设置等,这样就不用在x64下再进行配置了。

    图2确定后,就会得到图3的情形,所有项目都变为了x64平台了,在该平台下,直接编译运行测试即可,运行效果和win32下的没有区别,只是生成的可执行文件是64位的,为了避免64编译时出现不必要的警告信息,在所有项目中都禁用了4267的警告信息,该警告信息是提示size_t到int类型转换可能出现数据丢失(win64位下size_t为64位,而int还是32位),但是本项目中用到size_t的地方,长度都不会超过int类型,所以就禁用了该警告信息。

    然后看下ubuntu 64位系统下的编译情况:
 

图4

    上图中先通过make clean将linux目录中作者生成的中间文件给清理掉,可以看到,该版本下make clean中rm使用了-fv参数,通过f来强制删除,这样当某中间文件不存在时,就不会产生错误而停止clean操作,v表示显示删除的结果,如果某中间文件存在,则会显示删除该文件的信息。

    接着使用sudo make来进行编译,通过sudo来提权,因为编译生成的libzengl.so需要cp拷贝到/usr/lib目录中,该版本的makefile中添加了-std=c99的选项,表明使用1999的C编译标准,当然大部分系统中gcc默认就是c99的标准,如果是c99之前的标准,如89 ,94 ,95的标准的话,就会编译出错,因为这些标准对中文支持很不好,而zengl项目中所有的注释都是中文注释(linux下为utf8的编码)。

    在gcc链接生成libzengl.so动态链接库时,使用了-fPIC的选项,如果没有该选项,则在64位下链接zengl动态链接库时会报错,很多开源的做成动态链接库形式的项目也有这样的64位链接问题,所以加入了fPIC的选项。

    上图中最后两条gcc指令用于生成zengl和encrypt可执行文件,其中zengl为libzengl.so的测试程序,encrypt为rc4和普通异或加密的小程序(上面提到过),在生成这两个文件时,一定要将 -lzengl 这个链接参数放到最后,详情可以参考 http://stackoverflow.com/questions/1517138/trying-to-include-a-library-but-keep-getting-undefined-reference-to-messages 这篇讨论,如果像之前的版本那样将 -lzengl放到前面的话,就会出现 undefined reference to 的链接错误,原因就是那篇讨论中提到的,放在前面的话,ld链接器就会认为后面的.o模块不依赖该库里的函数,就不会到这些库里面查找函数的引用,从而就出现链接错误,所以越是基础的依赖库就越要放在后面,作者在Slackware中使用gcc 4.5.2编译时并没有报这样的错误,所以之前的版本也就没注意到。

    通过uname -p或uname -a输出显示的x86_64可以知道当前处于64位的编译环境中。

    下面看下mac苹果系统下的编译情况:
 

图5

    mac系统下只有makefile文件和测试脚本,所以直接make就可以了,一开始不需要make clean,当然make clean也不会报任何错误。

    从gcc编译参数可以看到,mac下的makefile是用的../linux/里的源代码,然后在mac目录下生成可执行文件和动态链接库。

    mac下的动态链接库是以dylib为扩展名的,生成动态链接库时所用的参数也和linux下不同,需要用到-dynamiclib参数,在该参数后面的-install_name参数可以让链接生成的可执行文件直接从当前目录中查找动态链接库文件,所以mac下不需要像ubuntu那样使用sudo提权和拷贝链接库的操作,不过这两个参数是和平台相关的,也就是说只能用在mac系统下。

    在编译成功后,测试方法和linux系统下一样,详细的测试方式请参考mac目录中的usage.txt文件。

    上面是各平台的编译情况,下面看下RC4加密:

    RC4和异或加密一样属于对称加密方式,是由RSA Security的Ron Rivest 在1987年开发出来的,现在的很多网络游戏,还有GoAgent等流行程式中都使用RC4来加密封包数据,因为这种方式本质上还是用到了异或运算,所以执行速度快,它的初始密钥很短,很适合保存和修改,实用性就比普通异或方式要高很多,它由一个初始密钥来初始化一个256元素的状态盒子后,由该状态盒子加上两个索引指针,就可以产生和明文一样长的伪随机密钥流,普通异或加密方式之所以不安全,是因为很多情况下当异或密钥长度小于明文长度时,它就会出现破绽,而RC4状态盒子生成的伪随机密钥流和明文一样长,再加上密钥流里的字符都是伪随机的,让理论上破解这种加密方式变得很困难,所以RC4也具有高安全性。

    不少网络游戏一般是在登录的时候向客户端发送一个十来二十个字节的初始密钥,然后客户端使用该初始密钥来初始化256元素的状态盒子,在登录成功后,就将初始密钥从内存中free释放掉,在游戏过程中,就不会再用到该初始密钥,服务端和客户端通过一收一发保持两边状态盒子的当前状态一致,从而两边会产生相同的伪随机密钥流,从而完成两边的加密解密过程,由于状态盒子随时都在变化,每次产生的加密密钥流基本上都不一样,所以同一段明文会产生不同的加密封包,对于这种加密方式单靠截取封包分析的方法是很难破解的,只有对客户端逆向工程才能真正破解出明文。

    当然zengl并没有使用上面所说的同步状态盒子的方式,因为脚本的执行一般只有一个客户端,不存在什么服务器,所以每次解析加密脚本之前,都会使用初始密钥来初始化一次状态盒子,然后使用状态盒子来解密脚本文件,解析执行完一个zengl脚本后,下次再遇到加密脚本时,还是先用初始密钥来初始化状态盒子,再由状态盒子来解密脚本。

    encrypt.c程式可以看作RC4和普通异或加密的小程式,从它的代码中可以看到RC4的加解密过程:

    在encrypt.c中通过rc4_init函数来初始化状态盒子:

/*rc4初始化状态盒子*/
void rc4_init(unsigned char * state, unsigned char * key, int len)
{
   int i,j=0,t;
   
   for (i=0; i < 256; ++i) //将盒子里的元素用0到255初始化
      state[i] = i;
   for (i=0; i < 256; ++i) { //将盒子里的元素顺序打乱
      j = (j + state[i] + key[i % len]) % 256;
      t = state[i];
      state[i] = state[j];
      state[j] = t;
   }   
}

    上面的代码中先用0到255来初始化state状态盒子,之所以用256个元素,而每个元素为0到255的值,是因为后面加解密时,每次异或运算使用的都是一个字节,而一个字节最大为255 。256个元素刚好把一个字节里的所有可能的值都囊括进去了。

    接着使用key密钥将盒子里的元素打乱,这里打乱使用了两个索引,一个i ,一个j ,i 用于循环对盒子里的每个元素进行处理,j 用于将 i 里的元素和另一个在key作用下指向的元素进行交换,循环下去,从而将盒子里的所有元素打乱。

    encrypt.c文件中main_output_rc4_source函数就使用rc4_init初始化状态盒子,并使用该盒子来加解密文件:

int main_output_rc4_source(char * src_filename,char * dest_filename,char * rc4_key_str)
{
    ...................................... //省略N行代码
    unsigned char state[256]; //rc4用于生成密钥流的状态盒子
    int i,j,t;
    ...................................... //省略N行代码

    rc4_init(state,rc4_key_str,rc4_key_len); //初始化state状态盒子
    i = 0; //将i重置为0
    j = 0; //将j重置为0
    while((buf_len = fread(buf,sizeof(unsigned char),1024,src_pt)) != 0)
    {
        for(cur = 0;cur < buf_len;cur++)
        {
            i = (i + 1) % 256; //将i加一
            j = (j + state[i]) % 256; //得到随机的j
            t = state[i]; //开始对i和j两元素交换,每次交换都会打乱盒子
            state[i] = state[j];
            state[j] = t;
            //下面使用i , j里面的元素得到一个随机的元素,使用该元素来和buf[cur]进行异或
            enc_buf[cur] = state[(state[i] + state[j]) % 256] ^ buf[cur];
        }
        fwrite(enc_buf,sizeof(unsigned char),buf_len,dest_pt);
        totalwrite_len += buf_len;
        cur_percent = (int)(((float)totalwrite_len / (float)src_filesize) * 100);
        if(cur_percent != prev_percent)
            printf("%d%%...",cur_percent);
    }
    printf(" rc4 '%s' to '%s' is ok!\n",src_filename,dest_filename);
    fclose(src_pt);
    fclose(dest_pt);
    return 0;
}

    棕色的注释在encrypt.c中并没有,只是在这里起补充说明的作用。可以看到,每次交换都会增强盒子的随机性,i , j 两元素得到的另一个元素也是随机的,所以就可以得到一串和明文一样长的伪随机密钥流,当然如果buf文件缓存里的内容是密文的话,那么上述异或运算后就会得到明文。

    使用初始密钥初始化状态盒子后,每次使用状态盒子时,如果盒子的里的元素状态和i , j两个索引不一样,那么得到的密钥流理论上也是不一样的,所以网上广为流传的初始化状态盒子后,第一次使用状态盒子得到密文后,再次使用状态盒子就可以还原出明文,这种说法肯定不对,状态盒子每次用都在随机的变化,第一次和第二次用得到的密钥流都不一样,使用异或运算怎么可能还原。所以zengl在解析RC4加密脚本之前都必须对状态盒子重新初始化,同时重置i , j 两个索引指针,以确保解密时的状态盒子和加密时的盒子是从同一个起点出发的,才能确保它们生成相同的伪随机密钥流出来,相同的密钥流进行异或运算才能还原出明文出来。之所以网上有这样的误解,可能是受了"RC4只要初始化一次后,就可以不再需要初始密钥"这句话的影响,要想只初始化一次后,不再使用初始密钥的前提是像前面提到的网游那样,两边的状态盒子和i , j 两个索引指针从同一个起点开始,同步进行加密解密。

    另外在main.c中的main函数里用到了zengl的RC4加密接口:

/**
    用户程序执行入口。
*/

int main(int argc,char * argv[])
{
    ........................ //省略N行代码

    char * rc4_key_str = "rc4key_334566_hello_world";
    int rc4_key_len = strlen(rc4_key_str);

    ........................ //省略N行代码

    //zenglApi_SetSourceXorKey(VM,xor_key_str);
    zenglApi_SetSourceRC4Key(VM,rc4_key_str,rc4_key_len);

    if(zenglApi_Call(VM,"encrypt_script/test.zl","OutIn","clsTest") == -1) //编译执行zengl脚本里的类函数
        main_exit(VM,"错误:编译<test fun call>失败:%s\n",zenglApi_GetErrorString(VM));

    zenglApi_Close(VM);

    ........................ //省略N行代码
}

    上面的代码中使用zenglApi_SetSourceRC4Key来设置虚拟机内部的RC4密钥和初始化状态盒子,zengl内部初始化的方式和上面提到的encrypt.c里的方式在原理上是差不多的。使用该接口设置好密钥后,接着就可以使用zenglApi_Run或zenglApi_Call来解析执行加密脚本了。

    zenglApi_SetSourceRC4Key接口比zenglApi_SetSourceXorKey接口多了一个参数,除了需要指向RC4的密钥指针外,还需要密钥的长度参数,限于篇幅这里就不显示具体的zengl内部和RC4相关的源代码,可以在github上查看v1.2.5和v1.2.4之间的代码变化,然后从这些变化中查看和RC4加密相关的代码,这些代码和前面encrypt.c里显示的代码在原理上是一样的。

    v1.2.5的版本还规范了API的调用位置:

    v1.2.5的zengl_global.h文件中,添加了一个API接口的各种位置状态枚举:

/*API接口的各种状态枚举定义*/
typedef enum _ZENGL_API_STATES{
    ZL_API_ST_NONE,
    ZL_API_ST_OPEN,
    ZL_API_ST_RESET,
    ZL_API_ST_RUN,
    ZL_API_ST_AFTER_RUN,
    ZL_API_ST_MODULES_INIT,
    ZL_API_ST_MOD_INIT_HANDLE,
    ZL_API_ST_MOD_FUN_HANDLE,
}ZENGL_API_STATES;
/*API接口的各种状态枚举定义结束*/

    下面是zenglApi.c里的一段代码:

ZL_EXPORT ZL_EXP_VOID * zenglApi_Open()
{
    ........................  //省略N行代码
    VM->ApiState = ZL_API_ST_OPEN;
    return (ZL_EXP_VOID *)VM;
}

    在zenglApi_Open接口创建一个虚拟机后,就将该虚拟机的ApiState位置状态成员设置为ZL_API_ST_OPEN,这样虚拟机就处于ZL_API_ST_OPEN刚被创建打开的状态,同理当zenglApi_Run接口运行时,虚拟机就会被设置为ZL_API_ST_RUN状态,如果某个API接口只能用在ZL_API_ST_RUN状态的话,那么当该API接口处于ZL_API_ST_OPEN状态位置时,就会返回出错信息,例如zenglApi_GetExtraData接口:

/*API接口,用户可以通过此接口得到额外数据*/
ZL_EXPORT ZL_EXP_VOID * zenglApi_GetExtraData(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * extraDataName)
{
    ........................  //省略N行代码

    ZL_CHAR * ApiName = "zenglApi_GetExtraData";
    if(VM->signer != ZL_VM_SIGNER) //通过虚拟机签名判断是否是有效的虚拟机
        return ZL_NULL;
    switch(VM->ApiState)
    {
    case ZL_API_ST_OPEN:
    case ZL_API_ST_RESET:
    case ZL_API_ST_AFTER_RUN:
        VM->run.SetApiErrorEx(VM_ARG,ZL_ERR_VM_API_INVALID_CALL_POSITION, ApiName , ApiName);
        return ZL_NULL;
        break;
    }

    上面的代码中通过switch(VM->ApiState)结构来判断当前处于哪种状态,如果处于ZL_API_ST_OPEN ,ZL_API_ST_RESET ,ZL_API_ST_AFTER_RUN这三种位置状态时,则通过SetApiErrorEx函数指针设置无效的调用位置的API错误信息,错误信息中还包含了ApiName出错的API接口函数名,设置了出错信息后,返回空指针来表示该接口调用出错,用户可以通过zenglApi_GetErrorString接口来获取具体的出错信息。

    这样当某个API接口处于错误的调用位置时,就会直接返回,而不会执行具体的任务代码。

    最后看下v1.2.5中新增加的zenglApi_BltModFuns.c文件:

    该文件中目前定义了5个内建模块函数:zenglApiBMF_array (array内建模块函数,用于创建zengl脚本的动态数组) ,zenglApiBMF_bltExit (bltExit模块函数,直接退出zengl脚本) ,zenglApiBMF_bltConvToInt (bltConvToInt模块函数,将参数转为整数形式) ,zenglApiBMF_bltIntToStr (bltIntToStr模块函数,将整数转为字符串的形式) ,zenglApiBMF_bltRandom (bltRandom模块函数,产生随机数) 。

    用法和用户自定义的模块函数一样,如main.c中:

ZL_EXP_VOID main_builtin_module_init(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT moduleID)
{
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"printf",main_builtin_printf);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"read",main_builtin_read);
    //zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltRandom",main_builtin_random); //使用main.c中定义的bltRandom
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltRandom",zenglApiBMF_bltRandom); //使用虚拟机zenglApi_BltModFuns.c中定义的bltRandom
    //zenglApi_SetModFunHandle(VM_ARG,moduleID,"array",main_builtin_array); //使用main.c中定义的array
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"array",zenglApiBMF_array);  //使用虚拟机zenglApi_BltModFuns.c中定义的array
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltExit",zenglApiBMF_bltExit);  //使用虚拟机zenglApi_BltModFuns.c中定义的bltExit
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltConvToInt",zenglApiBMF_bltConvToInt);  //使用虚拟机zenglApi_BltModFuns.c中定义的bltConvToInt
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltIntToStr",zenglApiBMF_bltIntToStr);  //使用虚拟机zenglApi_BltModFuns.c中定义的bltIntToStr
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltPrintArray",main_builtin_print_array);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltTestAddr",main_builtin_test_addr);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltSetArray",main_builtin_set_array);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltLoadScript",main_builtin_load_script);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltGetZLVersion",main_builtin_get_zl_version);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltGetExtraData",main_builtin_get_extraData);
}

    都是使用zenglApi_SetModFunHandle接口进行设置。

    v1.2.5版本中将原来void返回类型的API改为int返回类型,以方便返回出错信息,例如:

ZL_EXPORT ZL_EXP_INT zenglApi_SetErrThenStop(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * errorStr, ...);

    zenglApi_SetErrThenStop原来是void类型,现在转为ZL_EXP_INT即int类型,由于API的调整,可能会导致用户原来的程序中调用这些接口出现一些错误,例如在采集器v1.2.2版本中(代码暂没公开),有些地方用到return zenglApi_SetErrThenStop(...)的代码,以前是返回void类型,可以直接return,现在返回类型不是void了,就会在return时出现编译错误,所以只有调整代码,将其写为两行,先 zenglApi_SetErrThenStop(...); 再 return;

    最后,zengl本身不会处理任何文件字符编码问题,所以如果出现乱码,请自行编写函数来转码,或者自行调整zengl的C文件编码 (windows下默认为GBK编码,linux ,mac下默认为utf8编码) 。

    有关v1.2.5版本的介绍就到这里。

    OK,到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: zengl v1.3.0 位运算符 字符串脚本解析 缓存 Bug修复

上一篇: zengl v1.2.4 脚本加密,单目负号,VC6编译,新增API,BUG修复

相关文章

zenglServer v0.1.1 url解码,新增builtin模块实现数组成员的迭代操作

zengl编程语言v0.0.6创建小型计算器

zengl v1.6.0-v1.7.1, zenglServer v0.1.0

zengl编程语言v0.0.19 def宏定义

zengl v1.8.0 缓存内存中的编译数据,跳过编译过程

zengl v1.5.0 移植到zenglOX系统