`
RednaxelaFX
  • 浏览: 3052565 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

[脚本分析] Quartett!的二进制脚本分析

阅读更多
我前两天在NetOA方面确实是有点懈怠了。不为别的,正是为了这篇将提到的脚本的分析。虽然没把分析做彻底,不过我觉得现在已经足够使用,顺便拿出来说说。

上个周末,汉公突然跟我提起FFDSystem的话题,然后有人联系我做Quartett!的汉化。自从跟汉公和明大合作参与汉化以来,我基本上就是做脚本处理的相关工作比较多;汉公解决破解的棘手问题,而明大主要完成打包问题,也兼做脚本编辑器,视具体分工而定。这次也不例外,汉公主攻了资源文件的破解和资源抽取,资源的打包还没做,脚本这块就暂时交给了我。一般,如果脚本是没经过处理的文本,那也就没我什么事了;这次遇到的果然还是经过处理了的二进制脚本。

一拿到已经从Script.dat中提取出来的脚本文件,我吓了一跳:文件名居然都是MD5……汉公那边果然还没把资源破解完善。不过没关系,只要文件内容是对的就能开工。可以确认的是,脚本(准确说是给到我手上的脚本)的后缀名是tkn。

打开其中的第一个文件,0a69b4afebd6d64527a21e3f1aa993f9.tkn。内容如下:
Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F
00000000   54 4F 4B 45 4E 53 45 54  64 00 00 00 76 08 00 00   TOKENSETd...v...
00000010   0C 00 00 00 85 23 00 0C  00 00 00 81 62 61 73 65   ....?.....|ase
00000020   5F 70 61 74 68 00 0C 00  00 00 83 2E 2E 2F 00 16   _path.....?./..
00000030   00 00 00 85 23 00 16 00  00 00 81 69 6E 63 6C 75   ...?.....(nclu
00000040   64 65 00 16 00 00 00 83  53 63 72 69 70 74 2F 42   de.....ゴcript/B
00000050   61 73 65 49 6E 73 74 72  75 63 74 69 6F 6E 2E 74   aseInstruction.t
00000060   78 74 00 20 00 00 00 81  6D 6F 74 69 6F 6E 00 20   xt. ...[otion. 
00000070   00 00 00 81 4D 61 69 6E  00 20 00 00 00 85 28 00   ...`ain. ...?.


读起来似乎很郁闷(?),其实看到有那么多ASCII字符我已经很开心了。可以辨认出最开头的TOKENSET(但此时还无法判断那个d是什么)、ase_path、nclude等等。进一步观察可以发现那些看似被剪掉了的字符都在,前面的base_path、include就是如此。编辑器里显示不出来只是因为大于0x7F的字节被解释成双字节字符编码(DBCS)中一个双字节字符的首字节,也就是例如说0x81把base_path中的b(0x62)给“吃”了。
在上述截图范围内,我总共识别出了这些:base_path、include、Script/BaseInstruction.txt、motion、Main等字串。观察它们前后的规律:这些字串总是以0结尾,是标准的C string;这些字串的前面总是有一个大于0x7F的字节(留意到0x81和0x83),而在那个字节之前似乎总是有3个00字节,前面又是一个非00的字节。
为了方便分析,我写了一个小程序来抽取出我感兴趣的信息,辅助分析。
对应上面内容而提出出来的内容:
(格式是:字符串起始地址 一个奇怪的数字 字符串之前的那个字节 字符串内容)
0x1C 0xC 0x81 base_path
0x2B 0xC 0x83 ../
0x3B 0x16 0x81 include
0x48 0x16 0x83 Script/BaseInstruction.txt
0x68 0x20 0x81 motion
0x74 0x20 0x81 Main

经观察,发现字符串之前的那个字节似乎是某种操作码或者类型,而再前面的那个似乎是个什么奇怪的数字,会连续有好几个相同的,然后又增大一点。

接下来,突然发觉原来0x85也是个重要的数值;也有以这个数值打头的字符串,但一般都是长度为一的符号,所以先前被忽略了。想了想,干脆把0x80开始到0x88开头的,其之前是三个00的东西全部都扫描一遍。于是在之前的程序上修改了一下判断条件,得到下面代码:
opcode_analysis.cs:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace FFDSystemAnalysis
{
    sealed class Analyzer
    {
        private static readonly byte[ ] SIGNATURE = {
		    ( byte )0x54, ( byte )0x4F, ( byte )0x4B, ( byte )0x45,
		    ( byte )0x4E, ( byte )0x53, ( byte )0x45, ( byte )0x54,
		    ( byte )0x64, ( byte )0x0,  ( byte )0x0,  ( byte )0x0
        };

        static void Main( string[ ] args ) {
            if ( !args[ 0 ].EndsWith( ".tkn" ) ) return;
            if ( !File.Exists( args[ 0 ] ) ) return;

            string infile = args[ 0 ];
            string outfile = infile + ".txt";

            Encoding utf16le = new UnicodeEncoding( false, true );
            Encoding jis = Encoding.GetEncoding( 932 );

            using ( BinaryReader reader = new BinaryReader( File.OpenRead( infile ), jis ) ) {
                using ( BinaryWriter writer = new BinaryWriter( File.Create( outfile ), utf16le ) ) {
                    byte[ ] sig = reader.ReadBytes( SIGNATURE.Length );
                    if ( !Equals( sig, SIGNATURE ) ) {
                        Console.WriteLine( "Wrong signature" );
                        return;
                    }

                    // write UTF-16LE BOM
                    writer.Write( ( ushort ) 0xFEFF );

                    Queue<byte> queue = new Queue<byte>( 3 );
                    queue.Enqueue( reader.ReadByte( ) );
                    queue.Enqueue( reader.ReadByte( ) );
                    queue.Enqueue( reader.ReadByte( ) );

                    byte lastOpcode = 0;
                    while ( reader.BaseStream.Position < reader.BaseStream.Length ) {
                        byte currentByte = reader.ReadByte( );
                        if ( currentByte == 0x080
                            || currentByte == 0x081
                            || currentByte == 0x082
                            || currentByte == 0x083
                            || currentByte == 0x084
                            || currentByte == 0x085
                            || currentByte == 0x086
                            || currentByte == 0x087
                            || currentByte == 0x088 ) {
                            if ( MatchQueueData( queue ) ) {
                                long position = reader.BaseStream.Position;
                                string line = ReadCString( reader );
                                Entry e = new Entry( ) {
                                    position = position,
                                    opcode = currentByte,
                                    lastOpcode = lastOpcode,
                                    line = line
                                };
                                writer.Write(
                                    utf16le.GetBytes(
                                        string.Format( "{0}{1}",
                                            e.ToString( ),
                                            Environment.NewLine )
                                        )
                                    );
                            } // if
                        } // if

                        // re-initialize
                        lastOpcode = queue.Dequeue( );
                        queue.Enqueue( currentByte );
                    } // while
                } // using
            } // using
        } // Main

        static bool Equals( byte[ ] a, byte[ ] b ) {
            int len = a.Length;
            if ( len != b.Length ) return false;
            for ( int i = 0; i < len; i++ ) {
                if ( a[ i ] != b[ i ] ) return false;
            }
            return true;
        }

        static bool MatchQueueData( Queue<byte> queue ) {
            byte[ ] array = queue.ToArray( );
            return Equals( zeros, array );
        }

        static string ReadCString( BinaryReader reader ) {
            StringBuilder builder = new StringBuilder( );
            char c = '\0';

            while ( ( c = reader.ReadChar( ) ) != '\0' ) {
                builder.Append( c );
            }

            return builder.ToString( );
        }

        static readonly byte[ ] zeros
            = new byte[ ] { 0, 0, 0 };
    }

    struct Entry
    {
        public long position;
        public string line;
        public byte opcode;
        public byte lastOpcode;

        public override string ToString( ) {
            return string.Format( "0x{0:X} 0x{1:X} 0x{2:X} {3}",
                this.position, this.lastOpcode, this.opcode, this.line );
        }
    }
}

这段代码本身没什么稀奇,只有第57行到62行的内容有点诡异:居然把变量赋值给自己了?
不不,再怎么说我也不可能犯这种错误。这其实是C# 3.0里的一个有趣语法,initializer。可以通过initializer,在使用new关键字构造新实例的时候指定其中一些字段的值;等号左边的是字段名,右边则是字面量或者变量名(或者表达式)。编译器能够正确识别出看似是同名字的token之间的区别,因而能够正确赋值。好吧我承认这不是好的编程习惯,大家看到了千万不要学,要引以为戒……
另外,那个if里一大堆对currentByte的判断后来也重构到外面一个单独的MatchOpcode()方法里去了。像上面这样写实在太恶心……也要引以为戒哦

虽然没什么稀奇,还是说下这个文件里的流程:
1、检查作为参数文件是否存在,并且是否后缀为tkn。检查不通过则退出程序。
2、获取一个Shift-JIS和一个UTF-16LE字符集的Encoding实例,并使用它们创建Shift-JIS的输入流和UTF-16LE的输出流。
3、校验脚本文件的特征码(signature)。这里假设头12个字节都是特征码。
4、校验成功后,给输出流写出一个字节序标记(BOM,Byte Order Mark)。这本来应该不需要手工做的,但我一直没弄清楚为什么我明明在创建utf16le时指定要BOM系统却不帮我自动做……
5、创建一个队列来记录最近的三个字节。使用一个变量(lastOpcode)来记录最近的第四个字节。
6、扫描文件直到遇到文件结束。如果遇到了连续的3个00,则读入其后的一个字节,并判断是否在[0x80, 0x88]的范围内;满足的话则读入一个C string并输出记录。
7、程序结束。

于是我得到了更新版的记录:
(格式与前面相同)
0x15 0xC 0x85 #
0x1C 0xC 0x81 base_path
0x2B 0xC 0x83 ../
0x34 0x16 0x85 #
0x3B 0x16 0x81 include
0x48 0x16 0x83 Script/BaseInstruction.txt
0x68 0x20 0x81 motion
0x74 0x20 0x81 Main


于是我恍然大悟:那“奇怪的数字”居然是脚本源文件行号!而被认为是操作码或者类型的那个字节,则用于指定后面字符串的类型:可以是符号、十进制数字、十六进制数字、标识符、字符串、符号等。形象点看,如下图:
(红色的是行号,黄色的是类型,绿色的是字符串内容)


但位于脚本的0xC到0xF的那个数字(上图紫色部分)是什么意思还让我伤了下脑筋。观察了一下,发现从0a69b4afebd6d64527a21e3f1aa993f9.tkn提取出来的“东西”一共有1237个,而那意义不明的数字是0x876 = 2166,还差了不少。但总觉得它们应该有关系。突然想起我前面是用了个很糟糕的办法来提取记录,有连续的3个00字节才满足条件。但假如行号超过了0xFF = 255行的话这个条件就不成立了。赶紧把程序修改为第三版,按照新的理解去读入“行号”和“类型”两个数据,确认那个数字确实就是文件里总的token数。

然后我才理解了signature里那TOKENSET的含义……这看似是二进制的脚本其实根本没有编译过的二进制脚本之魂。
编译的前端至少有两部,scan和parse。Scan阶段处理词法分析,会把源文件切分成一个个token,而parse阶段处理文法分析,会根据上下文无关文法来尝试“理解”这些token,构造语法树(进而构造抽象语法树)。但这里我所看到的脚本只对脚本源文件做了scan,然后直接把scan的结果保存成“二进制脚本”了。真够OTL的。

简单点说,这个“二进制脚本”完整保留了脚本源文件的文本信息,而且还多加了些行号、类型等信息进去。缺少的是被去除了的注释。

那就很好办了不是么。于是把所谓的反编译程序写了出来:
ScriptDecompiler.cs
// ScriptDecompiler.cs, 2007/12/18
// by RednaxelaFX

/*
 * Copyright (c) 2007 着作权由RednaxelaFX所有。着作权人保留一切权利。
 * 
 * 这份授权条款,在使用者符合以下三条件的情形下,授予使用者使用及再散播本
 * 软件包装原始码及二进位可执行形式的权利,无论此包装是否经改作皆然:
 * 
 * * 对于本软件源代码的再散播,必须保留上述的版权宣告、此三条件表列,以
 *   及下述的免责声明。
 * * 对于本套件二进位可执行形式的再散播,必须连带以文件以及/或者其他附
 *   于散播包装中的媒介方式,重制上述之版权宣告、此三条件表列,以及下述
 *   的免责声明。
 * * 未获事前取得书面许可,不得使用RednaxelaFX之名称,
 *   来为本软件之衍生物做任何表示支持、认可或推广、促销之行为。
 * 
 * 免责声明:本软件是由RednaxelaFX以现状("as is")提供,
 * 本软件包装不负任何明示或默示之担保责任,包括但不限于就适售性以及特定目
 * 的的适用性为默示性担保。RednaxelaFX无论任何条件、
 * 无论成因或任何责任主义、无论此责任为因合约关系、无过失责任主义或因非违
 * 约之侵权(包括过失或其他原因等)而起,对于任何因使用本软件包装所产生的
 * 任何直接性、间接性、偶发性、特殊性、惩罚性或任何结果的损害(包括但不限
 * 于替代商品或劳务之购用、使用损失、资料损失、利益损失、业务中断等等),
 * 不负任何责任,即在该种使用已获事前告知可能会造成此类损害的情形下亦然。
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace FFDSystemAnalysis
{
    enum TokenType
    {
        Decimal = 0x080,
        Identifier = 0x081,
        Hexadecimal = 0x082,
        String = 0x083,
        Operator = 0x085
    }

    sealed class ScriptDecompiler
    {
        private static readonly byte[ ] SIGNATURE = {
		    ( byte )0x54, ( byte )0x4F, ( byte )0x4B, ( byte )0x45,
		    ( byte )0x4E, ( byte )0x53, ( byte )0x45, ( byte )0x54,
		    ( byte )0x64, ( byte )0x0,  ( byte )0x0,  ( byte )0x0
        };

        static void Main( string[ ] args ) {
            if ( !args[ 0 ].EndsWith( ".tkn" ) ) return;
            if ( !File.Exists( args[ 0 ] ) ) return;

            string infile = args[ 0 ];
            string outfile = Path.GetFileNameWithoutExtension( infile ) + ".txt";

            Encoding utf16le = new UnicodeEncoding( false, true );
            Encoding jis = Encoding.GetEncoding( 932 );

            using ( BinaryReader reader = new BinaryReader( File.OpenRead( infile ), jis ) ) {
                using ( BinaryWriter writer = new BinaryWriter( File.Create( outfile ), utf16le ) ) {
                    byte[ ] sig = reader.ReadBytes( SIGNATURE.Length );
                    if ( !Equals( sig, SIGNATURE ) ) {
                        Console.WriteLine( "Wrong signature" );
                        return;
                    }

                    // write UTF-16LE BOM
                    writer.Write( ( ushort ) 0xFEFF );

                    // process each token
                    int lineNum = 1;
                    int lastLineNum = 1;
                    TokenType tokenType = TokenType.Operator;
                    TokenType lastTokenType = TokenType.Operator;
                    int tabCount = 0;
                    int tokenCount = reader.ReadInt32( );
                    for ( int tokenNum = 0; tokenNum < tokenCount; ++tokenNum ) {
                        // deal with line numbers, insert empty new lines if needed
                        lineNum = reader.ReadInt32( );
                        if ( lastLineNum < lineNum ) { // should write on a newline
                            // write empty lines
                            for ( int i = lastLineNum; i < lineNum; ++i ) {
                                writer.Write( utf16le.GetBytes( Environment.NewLine ) );
                            }
                            // write tabs as indent
                            for ( int tabs = 0; tabs < tabCount; ++tabs ) {
                                writer.Write( utf16le.GetBytes( "\t" ) );
                            }
                            // put a dummy value into tokenType
                            lastTokenType = TokenType.Operator;
                        }

                        // get token tokenType
                        tokenType = ( TokenType ) ( reader.ReadByte( ) & 0x0FF );

                        // get token value
                        string tokenString = ReadCString( reader );

                        // deal with different token types
                        if ( !( lastTokenType == TokenType.Operator
                            || lastTokenType == TokenType.String
                            || lastTokenType == TokenType.Decimal
                            || lastTokenType == TokenType.Hexadecimal ) ) {
                            writer.Write( utf16le.GetBytes( " " ) );
                        }
                        switch ( tokenType ) {
                        case TokenType.Decimal:
                        case TokenType.Identifier:
                        case TokenType.Hexadecimal:
                            writer.Write( utf16le.GetBytes( tokenString ) );
                            break;

                        case TokenType.String:
                            writer.Write(
                                utf16le.GetBytes( string.Format( "\"{0}\"", tokenString ) ) );
                            break;

                        case TokenType.Operator:
                            switch ( tokenString ) {
                            case "#":
                            case "%":
                            case "-":
                            case "@":
                                writer.Write( utf16le.GetBytes( tokenString ) );
                                break;
                            case "{":
                                ++tabCount;
                                writer.Write( utf16le.GetBytes( tokenString ) );
                                break;
                            case "}":
                                --tabCount;
                                writer.BaseStream.Position -= 2; // delete the last tab
                                writer.Write( utf16le.GetBytes( tokenString ) );
                                break;
                            case "(":
                            case ",":
                            case ";":
                            case "=":
                                writer.Write(
                                    utf16le.GetBytes( string.Format( "{0} ", tokenString ) ) );
                                break;
                            case ")":
                                writer.Write(
                                    utf16le.GetBytes( string.Format( " {0}", tokenString ) ) );
                                break;
                            } // switch tokenString
                            break;

                        default:
                            Console.WriteLine( "Unexpected token type {0} at 0x{1}.",
                                tokenType.ToString( "X" ),
                                reader.BaseStream.Position.ToString( "X" ) );
                            return;
                        } // switch tokenType

                        // re-initialize
                        lastLineNum = lineNum;
                        lastTokenType = tokenType;
                    } // for
                }
            }
        }

        static bool Equals( byte[ ] a, byte[ ] b ) {
            int len = a.Length;
            if ( len != b.Length ) return false;
            for ( int i = 0; i < len; i++ ) {
                if ( a[ i ] != b[ i ] ) return false;
            }
            return true;
        }

        static string ReadCString( BinaryReader reader ) {
            StringBuilder builder = new StringBuilder( );
            char c = '\0';

            while ( ( c = reader.ReadChar( ) ) != '\0' ) {
                builder.Append( c );
            }

            return builder.ToString( );
        }
    }
}

中间有些代码是为了插入缩进的,忽略那部分吧……

得到的脚本看起来像是这样:










#base_path "../"









#include "Script/BaseInstruction.txt"









motion Main (  )


中间是有很多空行没错。那些原本应该是有注释的地方,或者本身就是空行(为了让代码好看)。这里我只是把原始脚本的状态尽量复原了出来而已。

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

暂时来说,这样就够用了。这个脚本处理已经让我们能做很多事。要进一步做的话,我可以把文法分析也做出来,方便对脚本更仔细的分析。但这两天肯定是没时间做那种事情咯……

Until then...

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

P.S. 上述代码皆以BSD许可证的形式发布。请有兴趣的人在遵循BSD许可证的前提下自由使用这些代码。

P.S.S. 其实上面代码值得吐槽的地方N多。例如说我完全没使用try-catch语句来处理可能出现的异常,又例如我在第一份代码里把一个Queue转变成数组再做相等性比较(极其恶心,本来自己写个循环数组就解决了)。……这些都是所谓的“原型代码”,目标是尽可能快的写出代码来验证自己的一些设想是否正确。偷懒不加异常处理、宁可别扭的使用标准库里的容器也不自己封装一个,都是出于同一原因。看倌们请多多包涵这些地方 XD

P.S.S.S. 唉,不过我偷懒也真是过分了。后一份代码里居然没把BinaryWriter改回用StreamWriter……
分享到:
评论
3 楼 lwwin 2007-12-23  
偶的意思是说,这些 天分 是有爱………………
2 楼 RednaxelaFX 2007-12-21  
跟天分大概没什么关系,不过经验肯定是有关的 ^ ^
1 楼 lwwin 2007-12-20  
经验啊经验,破解的东西果然还是靠天分的罢?

相关推荐

    Quartett!文本插入程序

    "Quartett!文本插入程序"是一个用于在特定文件中插入文本的工具,它主要处理tkn格式的文件。这个程序可能对程序员或系统管理员非常有用,因为它允许他们自动化一些文本处理任务,例如批量添加注释、修改配置文件或者...

    quartett-app

    "quartett-app" 是一个基于Java开发的项目,它是一个响应式的网页游戏,灵感来源于经典的超级Trumpf游戏。这个游戏的特色在于它结合了蒂罗尔地区城市的文化元素,为玩家提供了一种新颖且富有地方特色的卡牌游戏体验...

    java+sql server项目之科帮网计算机配件报价系统源代码.zip

    sql server+java项目之科帮网计算机配件报价系统源代码

    【java毕业设计】智慧社区老人健康监测门户.zip

    有java环境就可以运行起来 ,zip里包含源码+论文+PPT, 系统设计与功能: 文档详细描述了系统的后台管理功能,包括系统管理模块、新闻资讯管理模块、公告管理模块、社区影院管理模块、会员上传下载管理模块以及留言管理模块。 系统管理模块:允许管理员重新设置密码,记录登录日志,确保系统安全。 新闻资讯管理模块:实现新闻资讯的添加、删除、修改,确保主页新闻部分始终显示最新的文章。 公告管理模块:类似于新闻资讯管理,但专注于主页公告的后台管理。 社区影院管理模块:管理所有视频的添加、删除、修改,包括影片名、导演、主演、片长等信息。 会员上传下载管理模块:审核与删除会员上传的文件。 留言管理模块:回复与删除所有留言,确保系统内的留言得到及时处理。 环境说明: 开发语言:Java 框架:ssm,mybatis JDK版本:JDK1.8 数据库:mysql 5.7及以上 数据库工具:Navicat11及以上 开发软件:eclipse/idea Maven包:Maven3.3及以上

    【java毕业设计】智慧社区心理咨询平台(源代码+论文+PPT模板).zip

    zip里包含源码+论文+PPT,有java环境就可以运行起来 ,功能说明: 文档开篇阐述了随着计算机技术、通信技术和网络技术的快速发展,智慧社区门户网站的建设成为了可能,并被视为21世纪信息产业的主要发展方向之一 强调了网络信息管理技术、数字化处理技术和数字式信息资源建设在国际竞争中的重要性。 指出了智慧社区门户网站系统的编程语言为Java,数据库为MYSQL,并实现了新闻资讯、社区共享、在线影院等功能。 系统设计与功能: 文档详细描述了系统的后台管理功能,包括系统管理模块、新闻资讯管理模块、公告管理模块、社区影院管理模块、会员上传下载管理模块以及留言管理模块。 系统管理模块:允许管理员重新设置密码,记录登录日志,确保系统安全。 新闻资讯管理模块:实现新闻资讯的添加、删除、修改,确保主页新闻部分始终显示最新的文章。 公告管理模块:类似于新闻资讯管理,但专注于主页公告的后台管理。 社区影院管理模块:管理所有视频的添加、删除、修改,包括影片名、导演、主演、片长等信息。 会员上传下载管理模块:审核与删除会员上传的文件。 留言管理模块:回复与删除所有留言,确保系统内的留言得到及时处理。

    计算机系统基础实验LinkLab实验及解答:深入理解ELF文件与链接过程

    内容概要:本文档详细介绍了LinkLab实验的五个阶段,涵盖了ELF文件的组成、符号表的理解、代码节与重定位位置的修改等内容。每个阶段都有具体的实验要求和步骤,帮助学生理解链接的基本概念和链接过程中涉及的各项技术细节。 适合人群:计算机科学专业的本科生,特别是正在修读《计算机系统基础》课程的学生。 使用场景及目标:① 通过实际操作加深对链接过程和ELF文件的理解;② 掌握使用readelf、objdump和hexedit等工具的技巧;③ 实现特定输出以验证实验结果。 阅读建议:实验过程中的每个阶段都有明确的目标和提示,学生应按照步骤逐步操作,并结合反汇编代码和二进制编辑工具进行实践。在完成每个阶段的实验后,应及时记录实验结果和遇到的问题,以便于总结和反思。

    基于关键词的历时百度搜索指数自动采集资料齐全+详细文档+高分项目+源码.zip

    【资源说明】 基于关键词的历时百度搜索指数自动采集资料齐全+详细文档+高分项目+源码.zip 【备注】 1、该项目是个人高分项目源码,已获导师指导认可通过,答辩评审分达到95分 2、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 3、本项目适合计算机相关专业(人工智能、通信工程、自动化、电子信息、物联网等)的在校学生、老师或者企业员工下载使用,也可作为毕业设计、课程设计、作业、项目初期立项演示等,当然也适合小白学习进阶。 4、如果基础还行,可以在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!

    用C语言写出一个简单的圣诞树,让你的朋友们体验一下程序员的浪漫,点开即令哦!

    第一次发文的小白,解释的不好,各位大佬勿怪哦

    免费下载:Hilma af Klint a Biography (Julia Voss)_tFy2T.zip

    免费下载:Hilma af Klint a Biography (Julia Voss)_tFy2T.zip

    屏幕截图 2024-12-21 172527.png

    屏幕截图 2024-12-21 172527

    2024级涉外护理7班马天爱劳动实践总结1.docx

    2024级涉外护理7班马天爱劳动实践总结1.docx

    IndexOutOfBoundsException(解决方案).md

    IndexOutOfBoundsException(解决方案)

    【java毕业设计】智慧社区垃圾分类门户.zip

    有java环境就可以运行起来 ,zip里包含源码+论文+PPT, 系统设计与功能: 文档详细描述了系统的后台管理功能,包括系统管理模块、新闻资讯管理模块、公告管理模块、社区影院管理模块、会员上传下载管理模块以及留言管理模块。 系统管理模块:允许管理员重新设置密码,记录登录日志,确保系统安全。 新闻资讯管理模块:实现新闻资讯的添加、删除、修改,确保主页新闻部分始终显示最新的文章。 公告管理模块:类似于新闻资讯管理,但专注于主页公告的后台管理。 社区影院管理模块:管理所有视频的添加、删除、修改,包括影片名、导演、主演、片长等信息。 会员上传下载管理模块:审核与删除会员上传的文件。 留言管理模块:回复与删除所有留言,确保系统内的留言得到及时处理。 环境说明: 开发语言:Java 框架:ssm,mybatis JDK版本:JDK1.8 数据库:mysql 5.7及以上 数据库工具:Navicat11及以上 开发软件:eclipse/idea Maven包:Maven3.3及以上

    【java毕业设计】智慧社区网端门户(源代码+论文+PPT模板).zip

    有java环境就可以运行起来 ,zip里包含源码+论文+PPT, 系统设计与功能: 文档详细描述了系统的后台管理功能,包括系统管理模块、新闻资讯管理模块、公告管理模块、社区影院管理模块、会员上传下载管理模块以及留言管理模块。 系统管理模块:允许管理员重新设置密码,记录登录日志,确保系统安全。 新闻资讯管理模块:实现新闻资讯的添加、删除、修改,确保主页新闻部分始终显示最新的文章。 公告管理模块:类似于新闻资讯管理,但专注于主页公告的后台管理。 社区影院管理模块:管理所有视频的添加、删除、修改,包括影片名、导演、主演、片长等信息。 会员上传下载管理模块:审核与删除会员上传的文件。 留言管理模块:回复与删除所有留言,确保系统内的留言得到及时处理。 环境说明: 开发语言:Java 框架:ssm,mybatis JDK版本:JDK1.8 数据库:mysql 5.7及以上 数据库工具:Navicat11及以上 开发软件:eclipse/idea Maven包:Maven3.3及以上

    【java毕业设计】智慧社区智慧养老照护系统(源代码+论文+PPT模板).zip

    zip里包含源码+论文+PPT,有java环境就可以运行起来 ,功能说明: 文档开篇阐述了随着计算机技术、通信技术和网络技术的快速发展,智慧社区门户网站的建设成为了可能,并被视为21世纪信息产业的主要发展方向之一 强调了网络信息管理技术、数字化处理技术和数字式信息资源建设在国际竞争中的重要性。 指出了智慧社区门户网站系统的编程语言为Java,数据库为MYSQL,并实现了新闻资讯、社区共享、在线影院等功能。 系统设计与功能: 文档详细描述了系统的后台管理功能,包括系统管理模块、新闻资讯管理模块、公告管理模块、社区影院管理模块、会员上传下载管理模块以及留言管理模块。 系统管理模块:允许管理员重新设置密码,记录登录日志,确保系统安全。 新闻资讯管理模块:实现新闻资讯的添加、删除、修改,确保主页新闻部分始终显示最新的文章。 公告管理模块:类似于新闻资讯管理,但专注于主页公告的后台管理。 社区影院管理模块:管理所有视频的添加、删除、修改,包括影片名、导演、主演、片长等信息。 会员上传下载管理模块:审核与删除会员上传的文件。 留言管理模块:回复与删除所有留言,确保系统内的留言得到及时处理。

    Delphi 12 控件之DevExpressVCLProductDemos-24.2.3.exe

    DevExpressVCLProductDemos-24.2.3.exe

    计算机语言学中并查集数据结构的C++实现

    欢迎下载

    【java毕业设计】智慧社区养老服务平台.zip

    有java环境就可以运行起来 ,zip里包含源码+论文+PPT, 系统设计与功能: 文档详细描述了系统的后台管理功能,包括系统管理模块、新闻资讯管理模块、公告管理模块、社区影院管理模块、会员上传下载管理模块以及留言管理模块。 系统管理模块:允许管理员重新设置密码,记录登录日志,确保系统安全。 新闻资讯管理模块:实现新闻资讯的添加、删除、修改,确保主页新闻部分始终显示最新的文章。 公告管理模块:类似于新闻资讯管理,但专注于主页公告的后台管理。 社区影院管理模块:管理所有视频的添加、删除、修改,包括影片名、导演、主演、片长等信息。 会员上传下载管理模块:审核与删除会员上传的文件。 留言管理模块:回复与删除所有留言,确保系统内的留言得到及时处理。 环境说明: 开发语言:Java 框架:ssm,mybatis JDK版本:JDK1.8 数据库:mysql 5.7及以上 数据库工具:Navicat11及以上 开发软件:eclipse/idea Maven包:Maven3.3及以上

    小米15pro工程固件 可以用于修改参数 修复tee损坏 修复底层分区 会用的下载

    资源描述: 机型代码:haotian 1-----工程固件可以用于修改参数 开启diag端口。可以用于修复tee损坏以及修复底层分区。 2-----此固件是完整官方。不是第三方打包。请知悉 3-----此固件可以解锁bl后fast模式刷写。也可以底层深刷。也可以编程器写入 4-----请会用此固件 了解工程固件常识以及会用的朋友下载。 5-----个别高版本深刷需要授权才可以刷入。需要自己会刷写。 6------资源有可复制性。下载后不支持退。请考虑清楚在下载哦 工程资源常识可以参考博文:https://blog.csdn.net/u011283906/article/details/141815378 了解基本

    JSP论文格式化系统_——后台模块的设计与实现(源代码+论文)(2024gk).7z

    1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于计算机科学与技术等相关专业,更为适合;

Global site tag (gtag.js) - Google Analytics