c++的java虚拟机的设计与实现(源码)

本文通过对Java虚拟机结构和功能的分析,利用go语言实现了一个简单的Java虚拟机,通过分析和建立class文件寻址和加载的方法以及对内存分配和指令系统高度的仿真,模拟了java虚拟机先获得环境参数和处理用户设置参数,还原了class文件进入Java虚拟机后所涉及到的数据结构和方法调用,并依次将该类装载、链接、初始化的过程。当java虚拟机结束时,模拟其内存环境的清除和更新。更好更认真地分析了Java语言与平台无关的特性,也对虚拟机的认识更进一步。 关键词 Java虚拟机;class文件;虚拟机结构Simple Java Virtual Machine Design Simulation ImplementationStudent majoring in Computer Science and Technology Zhang Haochen Tutor Xie YuanchengAbstract : This article through the analysis of the Java virtual machine structure and function, using go language to achieve a simple Java virtual machine, through the analysis and establishment of the class file addressing and loading methods and memory allocation and instruction system, a high degree of simulation, simulation of java The virtual machine first obtains the environment parameters and processes the user setting parameters, restores the data structure and method calls involved in the entry of the class file into the Java virtual machine, and sequentially loads, links, and  *好棒文|www.hbsrm.com +Q: &351916072& 
initializes the class. When the java virtual machine ends, it simulates the removal and update of its memory environment. Better and more serious analysis of the Java language and platform-independent features, but also a further understanding of the virtual machine.如果经常关注世界程序的发展,就会知道,Java语言已是世界上使用人数最多的语言了。为什么Java语言有着如此广泛的适用范围,经过查找资料,我发现Java有几大优势,其中之一就是可以跨平台的Java虚拟机,所以,我决定好好学习有关Java虚拟机的有关知识。选题背景Java的现状以及广泛应用面向对象技术是程序设计思想上历史性的一次革命,而Java语言则是面向对象语言中的翘楚。目前在我国,Java应用的主要有两方面:(1)应用于企业发展。Java语言是许多系统的理想开发语言,它既能够链接网页,又可以搭建服务器,还有数据库操作,无疑是一种功能强大的语言。应用Java能够扩大企业市场份额,积极的发挥Java技术的优势,为企业的发展提供更加广阔的平台,从而实现企业经济效益和社会效益的最大化。(2)应用于PC领域。随着科技的不断发展,Java的社会市场需求也在不断的增加,JDK、HTML、CSS、JavaScript核心、Oracle数据库、JavaSE、XML、Java数据结构、JDBC、Servlet/Jsp、Ajax、Hibernate、Spring、Web系统架构、Struts开发。这些技术都是Java语言应用上的的助力,也是Java传播广阔的重要凭证。为什么要使用Java虚拟机有人说,学习Java语言主要掌握Java的三个重点socket编程、多线程和Java虚拟机。Java虚拟机就是Java能在多平台运行的关键。对于Java程序来说,它只能在这个环境上运行,这个环境叫做Java虚拟机。Java虚拟机能生成“字节码”,一种特殊的代码。在程序设计中,可以把Java虚拟机看作一种操作系统,当虚拟机运行的时候,能够翻译字节码直接转换为线程指令操作,避开了操作系统的控制,所以Java能够在多种平台上运行。虽然对程序效率造成了一些影响,但是Java虚拟机成就了Java语言与平台无关的特性。Java虚拟机概述Java虚拟机(Java Virtual Machine 简称JVM)是一个能够运行Java程序的虚拟处理器,是Java语言的运行环境,它有着计算机设计一样的虚拟硬件,可以在真正的计算机上仿真模拟计算机的各种功能,方便Java代码的运行。Java虚拟机的硬件仿真十分完善,如处理器,解释器,内存空间等,还具有相应的指令系统。它是Java 最具吸引力的特性之一。Java虚拟机可以分成多个模块,各个部分各司其职,最重要的是内存管理,解释器和指令系统,另外还有常量池,方法调用,异常处理等。Java虚拟机学习意义关于Java虚拟机的学习,有人会问,用一种语言编写完程序,程序可以运行,得出正确的结果,理解程序语言,这就够了,学习底层运行有什么意义。答案是对解决问题和更深层次的理解一行代码的原理有帮助。理解了底层的东西之后,你能看到编程的原理,能看到机器层面的优点和缺点,进而写出来的代码质量和效率肯定就不同。要想深入了解Java,就要求你懂得底层原理,就像编程要不要理解操作系统原理一样,不理解,就无法解决,但了解底层,很多问题在另一个角度,在底层的角度,就会有不同的观点,就会对问题理解的更透彻。如果在Java程序跨平台时遇到一些错误,这时,发现并纠正错误就要先理解JVM,再去修正代码。Java虚拟机设计命令行Java虚拟机的工作是运行Java应用程序。和其他应用程序一样,Java应用程序也需要一个入口点,这个入口点就是大家熟知的main()方法。但是如何去进入这个入口点Java规范里则没有明确要求。所以本次设计设计用windows自带的命令行执行入口命令。因此先在程序中定义cmd结构体,然后再结构体中加入各种各样的指令符号,如果检测到指令时,能够将结构体中的信息输出并调用各种预计的方法,完成Java虚拟机的各种功能。加载class文件完成了最初的工具,虚拟机就要开始进入原理的第一步了,那就是读取和解析.class文件。首先是寻找class文件,这里本次设计按照实现类路径来寻找class文件。类路径由启动类路径、扩展类路径和用户类路径组成。设计一个classpath结构体,包含三种小的类路径启动类路径、扩展类路径和用户类路径。结构体包含的readclass方法通过依次从三种类路径里搜索class文件。考虑到类路径以及class文件的多样化,将寻址方式分为四种绝对路径、.ZIP或.JAR文件的路径、组合路径、最后是.ZIP或.JAR文件的组合路径。虽然后两种不常用,但是仍要考虑到,建立四个结构体对应四种路径表示方式。之后判断类路径属于的类型,依次寻址,将class文件读入内存中。如果找不到class文件,就退出并返回err。找到class文件,着手准备解析。class文件的内容是字节码。封装一个结构体,在结构体中定义存放读取class文件的方法,按照变量大小的不同量身设计不同的方法。然后定义classfile结构体,将读取出来的数据存放进结构体,其中最难读取的是常量池和属性表。常量池在Java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量。因此,本次设计定义Constantpool和Attributetable两个结构体和在结构体里面的方法,就是读取字节码并翻译的方法。相比较而言,Java虚拟机规范对常量池定义是严谨的,而对属性表的要求则没有那么死板。所以读取常量池要严格按照规定,而属性表就要根据虚拟机自己定义。读取完成后,能够在命令行界面进行输出。内存分配内存分配是Java虚拟机的一大难点,因为里面的结构多而复杂,设计不好的话容易分配错误,从而导致程序不能正常运行。Java虚拟机的内存使用大致分为两类一类是多线程共享的数据区,另一类是线程私有的数据区。多线程共享的数据区是用来保存两种数据,类数据和类实例,简而言之就是新建的类对象和类本身的要求。线程私有数据区用于辅助执行Java字节码,里面包括线程运行时必须的数据结构比如虚拟机栈和pc计数器。同时,Java虚拟机栈又由栈帧、局部变量表和操作数栈构成。在程序中定义的结构体,包括线程、虚拟机栈、栈帧、局部变量表和操作数栈等。前三种其实其他的高级语言也会这么设计,是为了更好地执行程序,并且能够清晰地表示程序结构,为并行打下基础。而局部变量表和操作数栈是为了辅助Java虚拟机执行Java字节码的栈,当有字节码被编译成功并加入指令栈时,最基本的操作就在这里进行。简单的数学运算都在操作数栈中进行,局部变量表可以存储程序执行中所得到的一些结果和变量。而将线程共享数据封装为一个结构体后,可以定义两个字节数组代表方法区和运行时常量池,方法区里存放着对多线程运行很必要的信息;而常量池里存放字面量和符号引用。按照顺序定义结构体,这个数据区再类的加载和方法调用中会使用。指令集和解释器到Java虚拟机的第八版为止,规范已经定义了205条指令,这205条指令构成了现有虚拟机的指令集,每个指令都有它的助记符。Java虚拟机使用的是变长指令,操作码后面可以跟零个字节到多个字节的操作数。这205条指令按照用途被定义为11大类,包括常量,加载,存储,操作数栈,数学,转换,比较,控制,引用指令等,本虚拟机计划实现上述里的六类比较简单的指令。通过网上查阅资料,本次设计发现可以有两种方法实现指令集合,第一种是简单的遍历语句,switch:case来实现,但是完成后程序的篇幅会很长,可读性很差,所以本次设计选择用接口的方法来解决这个问题定义一个接口类,然后实现接口的方法中定义最普遍的指令操作,具体的指令操作先调用接口,再定义具体操作。比如简单的数学指令就是先调用接口的存取栈操作,而数学运算操作则至最后的指令类中实现。实现解释器前,要将之前写的东西进行一个总结,解释器是指令的实现方法,首先要获得字节码的信息,然后找到局部变量表和操作数栈,创建一个线程的同时创建一个帧并压入栈顶,最后将方法实行,方法实行中,也要修改pc寄存器。Java虚拟机实现加载class文件部分类路径实现类路径的查找是Java虚拟机实现的基础,也是比较简单的一步。先将类路径的四种表示方式定义出来,分别是DirE绝对路径,ZipE含有压缩文件形式的路径,CompE组合路径和WildE带有压缩文件的组合路径。实现的方法很简单,先定义一个抽象类,然后利用继承的思想,将四种不同的路径一一设计出格式和寻址方法。struct Entry {string name;readClass();};//总的路径类struct DirE public Entry {string absDir;readClass(string className);}//绝对路径类struct ZipE public Entry{string absZip;readClass(string className);}//含有压缩文件形式的路径struct CompE Entry[];//由于剩下两种组合路径都是更小的路径集合,所以可以用数组来表示。使用readClass方法寻址来找到目标文件readClass (string className){fileName = absDir + className;Readfile(filename);return fileName;}//先将路径名和class文件名拼凑成一个完整的路径名,然后调用读取函数将文件读入内存readCompE(string className){for(){Readfile(Entry[n] + className);if (can find){return n;}}return error;}//组合路径读取class文件时,则要按照顺序遍历每一个小的路径,如果查找到,则将文件读入内存,未查找到时则返回错误然后实现三种类路径,定义到一个结构体中struct classpath{Entry bootpath;Entry extpath;Entry userpath;}//三种类路径classpath(){if(getuser() != null){bootpath = getuser() + lib + *;extpath = getuser() + lib +ext + *;//用户有输入时,使用用户输入的路径作为启动和扩展类路径}else {find (Java_HOME);//没有输入则使用环境变量}if (getuser().include(“-cp”)){userpath = getuser();//用户输入指令时用户类路径为用户输入的路径}else{}}这时,已经完全实现了三种类路径,程序开始寻找class文件。findclass(className){if(readfile(bootpath) = false){if(readfile(extpath) = false){readfile(userpath);}//依次按照启动类路径,扩展类路径,用户类路径的顺序读取class文件}}解析class文件本次设计的Java虚拟机使用结构体来描述整个需要翻译的文件,经过翻阅资料,决定设计出结构体里面的变量与实际文件中字节码的意义相同,对于不同的变量就通过不同的函数将不同的结构读入内存中。为了更精确的读入字节码,设置多种读取不同长度字节码的方法uint8 readuint8();//读取单个字节无符号整数uint16 readuint16();//读取二个字节无符号整数uint32 readuint32();//读取四个字节无符号整数*uint16 readuint16s();//读取uint16类型表*bytes readbytes(int n);//读取指定数量的字节之后定义classFile结构体,代表读入内存的class文件struct ClassFile { uint32 magic; //魔数 uint16 minorVersion; //次版本号 uint16 majorVersion ;//主版本号 *ConstantPool constantPool; //常量池 uint16 accessFlags ;//类访问标志 uint16 thisClass ;//类索引 uint16 superClass; //超类索引 []uint16 interfaces ;//接口索引表 fields []*MemberInfo; //字段表 methods []*MemberInfo ;//方法表AttributeTable; //属性表}设计方法保证能够读取出对应的值。可以用到上面读取字节码的方法。bool MandMversion() {minorVersion = readuint16();//读取副版本号majorVersion = readuint16();//读取主版本号if(majorVersion<45||majorcersion>52){return error;}//虚拟机不支持45以下,52以上Java版本}其他的变量只要按照方法读取即可。但还有一点特殊的地方在于方法表和字段表,因为其实字段表和方法表是用来存储阻断和方法,二者结构基本相同,差别仅仅是接口表的不同,所以可以定义一个结构体代表两个表,读取基本类型时按照之前的方法,而差异在属性表中去区分struct MemberInfo { *ConstantPool cp//常量池 uint16 accessFlags uint16 nameIndex uint16 descriptorIndex AttributeTable//属性表}常量池常量池里面有着各种各样的常量信息,包括数字、字符、类、字节码、接口等等。而从class文件中读取常量池要注意的地方有两点,一是常量池的结构体定义,本次设计选择用一个数组,其实也就是一张表来读取常量池,所以这个数组的上界其实要比常量池的真实大小大一;第二,根据规定常量池的索引只能从1 - n-1,0是无效索引,不指向任何常量。struct constantpool {interface constantinfo[];}//常量池其实是一张表struct constantinfo{*reader readinfo();}//常量接口bool readconstantpool{count = readuint16();// 表头代表常量池大小constantpool cp[count];for (i=1; i= maxsize { error(); } if top != null { frame.lower = top; }//注意链表链接 top = frame; size++;}//压帧入栈*frame pop() { if self._top == nil { error(); } top = top.lower; top.lower = nil; size--; return top;}//出栈*stack clear() { for !empty() { pop(); }}//清空栈*frame top() { if top == nil { error(); } return top;}//取栈顶帧帧接下来要完成帧操作,由于帧是在线程运行中最基本的结构体,所以要与栈相连的同时还要链接其他结构体。定义帧结构体struct Frame { *Frame lower //实现链表 *LocalVars localVars //保存局部变量表指针 *OperandStack operandStack //保存操作数栈指针}*frame newframe{return frame.new(localvars&&operandstack);}局部变量表局部变量表是为了存放线程运行时所产生的中间变量或者结果而定义的表,可以通过索引来调用其中的值,即可以是用数组来完成,所以创建变量表的结构体struct slot{int num;*Object obj; //一个引用,定义这个引用,是为了方便垃圾回收系统的查找算法。}struct localvars slot[];//利用数组*LocalVars newLocalVars(int size) { if size > 0 { slot = interface(size); return &LocalVars{slots}; } else { return ; }}在存取不同的变量时,尤其要注意的是不同类型不同大小的变量存取方法也不同。由于定义的为int型,所有比int占用小的内存空间的数据类型可以直接读取,而double这种比int占内存大的数据类型,则需要拆分成int型才可以保存。操作数栈操作数栈和局部变量表是有差异的,但是可以用一种接口来实现它。struct OperandStack { int size ; interface var[];} *OperandStack newOperandStack(int size) { if size > 0 { return &OperandStack{0, var};//新构建操作数栈 } else { return; }}对于基本类型的处理与在局部变量表中处理其他基本类型的方法一样,以int类型作为中介点,先完成int类型,想要操作其他的类型,需要将其转换成int型再操作。指令集构建部分常量指令设计虚拟机指令系统,首先要面临的大问题之一就是代码的冗余,因为Java语言中有许多指令是有着非常相似的结构的,操作数也许一样,所以为了节省空间,选择把指令抽象成接口,先通过逻辑实现它,等到具体的指令时,再通过具体的方式将它实现。首先定义接口和方法struct Instr { CatchOperands(); //取操作数 Execute(); //实现指令操作运算,具体指令不同方法}struct NoOperandsInstr { // empty}//什么都不用做的结构体reader* FetchOperands { // nothing to do}//不需要取操作数struct BranchInstr { int Offset; //偏移量 }//跳转指令reader* FetchOperands { Offset = Readint16();//读取偏移量}struct Index8Instr { int Index;//局部变量表索引}//加载指令reader* FetchOperands() { self.Index = Readint8();//从字节码中读取一个字节的数}struct Index16Instr { int Index; //常量池索引}reader* FetchOperands() { Index = Readint16();//从字节码中读取2字节数}而对应到字节码时,Java虚拟机要能够记录读到字节码的位置,所以设计了一个结构体和方法来加载字节码和读取字节码struct codereader {byte code[];//加载字节码int pc;//记录读到的位置}接下来要进行指令的方法设计,但是Java虚拟机一共定义了200多条指令,本文也没有办法将其一条条完美的实现出来,所以选择了比较简单并有代表性的六种类型的指令来实现出来。每种指令在这里举一例说明其实现方式。首先是最基础的常量指令。完成一个指令,先调用上面定义好的接口,然后通过方法实现下面未定义的execute函数。常量指令只有一种系列,const系列指令,都是将一些数压入操作数栈顶struct dconst_0 {nooperation;//没有操作数}frame* execute{push(double(0.0));//将double型0推入操作数栈顶}加载指令加载指令从局部变量表获取变量,然后推入操作数栈顶。加载指令可以分为六类aload系列指令操作引用类型变量,dload系列操作double类型变量等等。下面定义一个iload系列指令struct iload{Index8Instr; //从字节码里读取索引}frame* execute() {get(index);//通过索引在局部变量表里取操作数push(operandstack());//将取得的int类型操作数压入操作数栈}存储指令存储指令与加载指令正好相反,存储指令把变量从操作数栈顶弹出,然后存入局部变量表。与加载指令相对应,存储指令也可以分为六类,按照不同的变量类型有不同的描述。下面定义一个istore系列指令struct istore{Index8Instr; //从字节码里读取索引}frame* execute() {pop(operandstack());//从操作数栈顶取int类型操作数set(index);//按照索引将操作数存入局部变量表}栈指令九条栈指令直接对操作数栈进行操作, dup系列指令能够复制栈顶变量,pop和pop2指令能够将栈顶变量弹出, swap指令可以交换栈顶的两个变量。下面定义一个pop指令和dup指令struct pop{nooperands;}frame* execute() {popslot(operandstack());//从操作数栈顶取出变量,只可以是占据一个栈位置的变量}struct dup{nooperands;}frame* execute() {popslot(operandstack());pushslot(operandstack());pushslot(operandstack());//先出栈再压栈两次,实现复制}数学指令数学指令就有很多,包括最基本的加减乘除,移位运算,布尔运算等指令,共有37条。下面定义一条求余指令和一条位移指令:struct irem{nooperands;}frame* execute() {v1 = popint(operandstack());v2 = popint(operandstack());if (v1 == 0)error();result = v2%v1;pushint(result);}struct ishl{//int位左移nooperands;}frame* execute() {v1 = popint(operandstack());//v1为需要移位的数v2 = popint(operandstack());//v2为位数s = v2 & 0x1f;//取v2后五位表示位移位数result = v1<方法、私有方法和父类方法。 invokevirtual:调用所有的虚方法(私有方法、静态方法、父类方法等都是非虚方法)。 invokeinterface:调用接口方法,会在运行时期再确定一个实现此接口的对象。 invokedynamic:现在运行时期动态解析出调用点限定符所引用的方法,然后再执行该方法。方法调用的实现需要n+1个操作数,其中第一个操作数是uint16索引,通过这个起点,可以从当前类的运行时常量池中找到一个方法符号引用,解析这个符号引用就可以得到一个方法,然后剩下的n个操作数传递给被调用方法的参数,从操作数栈中弹出于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。Class文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。也就是需要在类加载阶段,甚至到运行期才能确定目标方法的直接引用。异常处理部分Java的异常处理本质上是抛出捕获异常。Java虚拟机的抛出异常,本质上是对一个异常对象进行处理,首先封装一个异常对象结构体,就像普通的线程一样。之后确定了两个方向,第一是程序正常运行的方向,pc寄存器正常运行,第二则是异常处理的方向,在catch语句之后的设置变量表,其中定义所有能够判断的异常,然后如果产生异常则检查是否与表中的异常相符。表中的项都有pc计数器的数字,如果异常匹配,则程序自动跳转到编程者想让程序跳转的地方。这时的操作在专门负责异常处理的处理器中进行。设计时可以根据此处理器封装为一个类,再类中定义成功或失败时的方法。如果没有成功匹配,则异常处理会弹出栈帧,返回上一级程序或者异常处理,继续检查。当最后异常匹配不成功时,匹配结束,并结束程序。要理解抛出异常,总结经过这次毕业设计,我学到了很多东西。包括程序能够怎样写更加简洁精炼,怎样能够为自己之后的开发做好基础,对一个大程序的设计应该一步步抽丝拨茧般的进行,不可操之过急,也不能看到困难就打退堂鼓,对自己失去信心。这次毕设充分的锻炼了我的耐心和编程能力。总的来说,我认为这次毕设做的还是很成功的,不仅仅是因为它锻炼了我的编程硬实力,也磨练了我的心智,锻炼了我规划计划的能力,我想,它一定是我人生中的一段生活的终点,也是一个更好的起点致谢参考文献[1]王万森,龚文.Java动态类加载机制研究及应用[J].计算机工程与设计,2011,32(06):2154-2158.[2]张华伟,魏庆.Java运行原理与Java虚拟机[J].光盘技术,2009(10):40-42.[3]王凌飞,王保保.Java虚拟机内存管理分析[J].现代电子技术,2007(05):172-174.[4]夏兵,俞建军.Java虚拟机的研究与实现[J].计算机与信息技术,2006(09):56-58.[5]李超,方潜生.Java虚拟机中类装载机制的原理分析与应用研究[J].安徽建筑工业学院学报(自然科学版),2005(05):72-75.[6]谌宁,覃征.基于嵌入式Java虚拟机的垃圾回收算法[J].计算机应用,2005(01):218-219+223.[7]王立冬,张凯.Java虚拟机分析[J].北京理工大学学报,2002(01):60-63.[8]潘春花.Java与C++垃圾回收机制剖析[J].信息系统工程,2015(12):12-13.[9]夏炜.试论java语言的特征及发展前景[J].信息系统工程,2015(05):16.[10]冯宇.分析Java平台的核心——虚拟机[J].网络安全技术与应用,2015(05):134+138.[11]刘冠梅.JAVA虚拟机技术研究与实践思考[J].科技创新与应用,2015(11):104-105.[12]安美瑶.浅谈JAVA技术[J].计算机光盘软件与应用,2014,17(13):294-295.[13]Patrick Doyle,Carlos Cavanna,Tarek S. Abdelrahman. The design and implementation of a modular and extensible Java Virtual Machine[J]. Software: Practice and Experience,2004,34(3).[14]Oscar Aza?ón Esteire,Juan Manuel Cueva Lovelle. Set of tools for native code generation for the Java virtual machines[J]. ACM SIGPLAN Notices,1998,33(3).[15]G. Chen,R. Shetty,M. Kandemir,N. Vijaykrishnan,M. J. Irwin,M. Wolczko. Tuning garbage collection for reducing memory system energy in an embedded Java environment[J]. ACM Transactions on Embedded Computing Systems (TECS),2002,1(1).
目录
摘要 3
关键词 3
ABSTRACT 3
KEY WORDS 3
1. 选题背景 3
1.1. Java的现状以及广泛应用 3
1.2. 为什么要使用Java虚拟机 4
1.3. Java虚拟机概述 4
1.4. Java虚拟机学习意义 4
2. Java虚拟机设计 4
2.1. 命令行 4
2.2. 加载class文件 4
2.3. 内存分配 5
2.4. 指令集和解释器 5
3. Java虚拟机实现 6
3.1. 加载class文件部分 6
3.1.1. 类路径 6
3.1.2. 解析class文件 7
3.1.3. 常量池 8
3.1.4. 属性表 9
3.2. 内存分配部分 10
3.2.1. 线程 11
3.2.2. 虚拟机栈 11
3.2.3. 帧 12
3.2.4. 局部变量表 12
3.2.5. 操作数栈 13
3.3. 指令集构建部分 13
3.3.1. 常量指令 13
3.3.2. 加载指令 14
3.3.3. 存储指令 14
3.3.4. 栈指令 15
3.3.5. 数学指令 15
3.3.6. 类型转换指令 15
3.4. 解释器 16
4. 虚拟机未完成部分 16
4.1. 方法调用部分 16
4.2. 异常处理部分 17
5. 总结 17
致谢 17
参考文献: 18
基于c++的java虚拟机的设计与实现
计算机科学与技术 张皓琛
引言
目录

版权保护: 本文由 hbsrm.com编辑,转载请保留链接: www.hbsrm.com/jsj/jsjkxyjs/1593.html

好棒文