Class文件结构&字节码指令

class文件结构

Class文件存储的内容称为字节码(ByteCode),包含了JVM指令集和符号表以及若干其他辅助信息。

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符,整个Class文件中存储的内容几乎全部是程序运行的必要的数据,没有空隙存在。

当遇到8位字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件中有两种数据类型,分别是无符号数和表。

无符号数

无符号数属于基本的数据类型,以u1、u2、u4、u8来表示一个字节、两个字节...的无符号数;无符号数用来描述数字、索引引用、数量值或UTF-8编码构成的字符串值。

表是由多个无符号数或其他表作为数据项构成的复合数据类型,一般以"_info"结尾,用来描述class文件的数据结构。

特点:节省存储空间,提高处理性能

ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; 
    field_info fields[fields_count]; 
    u2 methods_count; 
    method_info methods[methods_count]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}
  • 魔数
  • Class文件版本
  • 常量池
  • 访问标志
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

u2表示无符号数2个字节
u4表示无符号数4个字节

Class文件设计理念和意义

1. 魔数magic

魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。

证明magic作用

创建一个class文件 magic.class ,内容是magic test,直接运行java magic操作:

84407@FantJ MINGW64 ~/Desktop
$ java magictest
▒▒▒▒: ▒▒▒▒▒▒▒▒ magictest ʱ▒▒▒▒ LinkageError
        java.lang.ClassFormatError: Incompatible magic value 1886741100 in class file magictest

报错意思是:magic矛盾,然后给了个magic value的十进制数,那么可以识别的magic十进制应该是多少呢。


应该是3405691582

那么,然后我用javac编译的正常java文件生成class文件,用binary viewer 查看:


minor_version、major_version

魔数往后后面四位:表示字节码版本,分别表示Class文件的副、主版本。当今用的最广的几个版本:
jdk1.8:52
jdk1.7:51
jdk1.6:50




对应版本号是52,是jdk1.8


版本向下兼容

2. constant_pool_count

常量池计数器,值等于constant_pool表中的成员数加1,占用两个字节

3. constant_pool[]常量池

Java虚拟机指令执行时依赖常量池(constant_pool)表中的符号信息。

所有的常量池项都具有如下通用格式:

cp_info {
    u1 tag; 
    u1 info[]; 
}

info[]项的内容tag由的类型所决定。tag有效的类型和对应的取值在下表列出

常量类型
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18
3.1 CONSTANT_Class_info结构

表示类或接口

CONSTANT_Class_info {
    u1 tag; 
    u2 name_index;
}

name_index必须是对常量池的一个有效索引

3.2 CONSTANT_Fieldref_info, CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构

字段:

CONSTANT_Fieldref_info {
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

方法:

CONSTANT_Methodref_info { 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

接口方法:

CONSTANT_InterfaceMethodref_info {
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

class_index必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口,当前字段或方法是这个类或接口的成员。

CONSTANT_Methodref_info结构的class_index项的类型必须是类(不能是接口)。CONSTANT_InterfaceMethodref_info结构的class_index项的类型必须是接口(不能是类)。CONSTANT_Fieldref_info结构的class_index项的类型既可以是类也可以是接口。

name_and_type_index必须是对常量池的有效索引,表示当前字段或方法的名字和描述符。
在一个CONSTANT_Fieldref_info结构中,给定的描述符必须是字段描述符。而CONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info中给定的描述符必须是方法描述符。

3.3 CONSTANT_String_info结构

用来表示String的结构

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

string_index必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一组Unicode码点序列,这组Unicode码点序列最终会被初始化为一个String对象。

3.4CONSTANT_Integer_info和CONSTANT_Float_info结构

表示4字节(int和float)的数值常量:

CONSTANT_Integer_info {
    u1 tag; 
    u4 bytes; 
} 
CONSTANT_Float_info { 
    u1 tag; 
    u4 bytes;
}
3.5CONSTANT_Long_info和CONSTANT_Double_info结构

表示8字节(long和double)的数值常量

CONSTANT_Long_info {
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
} 

CONSTANT_Double_info { 
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
}
3.6 CONSTANT_NameAndType_info结构

表示字段或方法,但是和前面介绍的3个结构不同,CONSTANT_NameAndType_info结构没有标识出它所属的类或接口

CONSTANT_NameAndType_info { 
    u1 tag; 
    u2 name_index; 
    u2 descriptor_index;
}

name_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构要么表示特殊的方法名,要么表示一个有效的字段或方法的非限定名(Unqualified Name)。

descriptor_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构表示一个有效的字段描述符或方法描述符。

3.7 CONSTANT_Utf8_info结构

用于表示字符串常量的值

CONSTANT_Utf8_info {
    u1 tag; 
    u2 length; 
    u1 bytes[length]; 
}

CONSTANT_Utf8_info结构中的内容是以length属性确定长度的

3.8 CONSTANT_MethodHandle_info结构

表示方法句柄

CONSTANT_MethodHandle_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}

reference_kind项的值必须在1至9之间(包括1和9),它决定了方法句柄的类型。

  1. 如果reference_kind项的值为1(REF_getField)、2(REF_getStatic)、3(REF_putField)或4(REF_putStatic),那么常量池在reference_index索引处的项必须是CONSTANT_Fieldref_info结构,表示由一个字段创建的方法句柄。
  2. 如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或8(REF_newInvokeSpecial),那么常量池在reference_index索引处的项必须是CONSTANT_Methodref_info结构,表示由类的方法或构造函数创建的方法句柄。
  3. 如果reference_kind项的值是9(REF_invokeInterface),那么常量池在reference_index索引处的项必须是CONSTANT_InterfaceMethodref_info结构,表示由接口方法创建的方法句柄。
  4. 如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或9(REF_invokeInterface),那么方法句柄对应的方法不能为实例初始化()方法或类初始化方法()。
  5. 如果reference_kind项的值是8(REF_newInvokeSpecial),那么方法句柄对应的方法必须为实例初始化()方法。
3.9 CONSTANT_MethodType_info结构

表示方法类型

CONSTANT_MethodType_info { 
    u1 tag; 
    u2 descriptor_index; 
}
3.10 CONSTANT_InvokeDynamic_info结构

表示invokedynamic指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name)、参数和请求返回类型、以及可以选择性的附加被称为静态参数(Static Arguments)的常量序列。

CONSTANT_InvokeDynamic_info { 
    u1 tag; 
    u2 bootstrap_method_attr_index; 
    u2 name_and_type_index; 
}

bootstrap_method_attr_index项的值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引。

name_and_type_index项的值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符。

4. access_flags:访问标志

访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags的取值范围和相应含义见下表。

标记名 含义
ACC_PUBLIC 0x0001 可以被包的类外访问。
ACC_FINAL 0x0010 不允许有子类。
ACC_SUPER 0x0020 当用到invokespecial指令时,需要特殊处理的父类方法。
ACC_INTERFACE 0x0200 标识定义的是接口而不是类。
ACC_ABSTRACT 0x0400 不能被实例化。
ACC_SYNTHETIC 0x1000 标识并非Java源码生成的代码。
ACC_ANNOTATION 0x2000 标识注解类型
ACC_ENUM 0x4000 标识枚举类型

5. this_class:类索引

this_class的值必须是对constant_pool表中项目的一个有效索引值。

是一个对constant_pool表中项目的一个有效索引值,表示指向常量池的第几个位置。

6. super_class:父类索引

表示这个Class文件所定义的类的直接父类,如果Class文件的super_class的值为0,那这个Class文件只可能是定义的是java.lang.Object类,只有它是唯一没有父类的类

是一个对constant_pool表中项目的一个有效索引值,表示指向常量池的第几个位置。

7. interfaces_count:接口计数器

表示有这个类有几个接口。

8. interfaces[]:接口表

成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。

是一个对constant_pool表中项目的一个有效索引值,表示指向常量池的第几个位置。

表示当前类或接口的直接父接口数量

9. fields_count:字段计数器

表示当前Class文件fields[]数组的成员个数

10. fields[]:字段表

每个成员都必须是一个fields_info结构的数据项,描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。

用于表示当前类或接口中某个字段的完整描述

field_info {
    u2 access_flags; 
    u2 name_index;      //对常量池的一个有效索引
    u2 descriptor_index;     //对常量池的一个有效索引
    u2 attributes_count;     //当前字段的附加属性的数量
    attribute_info attributes[attributes_count];
}

access_flags项的值是用于定义字段被访问权限和基础属性的掩码标志。access_flags的取值范围和相应含义见下表所示:

标记名 说明
ACC_PUBLIC 0x0001 public,表示字段可以从任何包访问。
ACC_PRIVATE 0x0002 private,表示字段仅能该类自身调用。
ACC_PROTECTED 0x0004 protected,表示字段可以被子类调用。
ACC_STATIC 0x0008 static,表示静态字段。
ACC_FINAL 0x0010 final,表示字段定义后值无法修改。
ACC_VOLATILE 0x0040 volatile,表示字段是易变的。
ACC_TRANSIENT 0x0080 transient,表示字段不会被序列化。
ACC_SYNTHETIC 0x1000 表示字段由编译器自动产生。
ACC_ENUM 0x4000 enum,表示字段为枚举类型。

attributes表的每一个成员的值必须是attribute结构,一个字段可以有任意个关联属性。

11. methods_count:方法计数器

methods_count的值表示当前Class文件methods[]数组的成员个数,Methods[]数组中每一项都是一个method_info结构的数据项。

12. methods[]:方法表

method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法。methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

methods[]数组中的每个成员都必须是一个method_info结构的数据项,用于表示当前类或接口中某个方法的完整描述。

method_info { 
    u2 access_flags; 
    u2 name_index; 
    u2 descriptor_index; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}

access_flags项的值是用于定义当前方法的访问权限和基本属性的掩码标志,access_flags的取值范围和相应含义见下表所示。

标记名 说明
ACC_PUBLIC 0x0001 public,方法可以从包外访问
ACC_PRIVATE 0x0002 private,方法只能本类中访问
ACC_PROTECTED 0x0004 protected,方法在自身和子类可以访问
ACC_STATIC 0x0008 static,静态方法
ACC_FINAL 0x0010 final,方法不能被重写(覆盖)
ACC_SYNCHRONIZED 0x0020 synchronized,方法由管程同步
ACC_BRIDGE 0x0040 bridge,方法由编译器产生
ACC_VARARGS 0x0080 表示方法带有变长参数
ACC_NATIVE 0x0100 native,方法引用非java语言的本地方法
ACC_ABSTRACT 0x0400 abstract,方法没有具体实现
ACC_STRICT 0x0800 strictfp,方法使用FP-strict浮点格式
ACC_SYNTHETIC 0x1000 方法在源文件中不出现,由编译器产生

name_indexdescriptor_index 两属性是对常量池的一个有效索引
attributes_count的项的值表示这个方法的附加属性的数量。
attributes 表的每一个成员的值必须是attribute结构,一个方法可以有任意个与之相关的属性。

13. attributes_count:属性计数器

attributes表中每一项都是一个attribute_info结构的数据项。

attributes_count的值表示当前Class文件attributes表的成员个数。

14. attributes[]:属性表

attributes表的每个项的值必须是attribute_info结构,在Class文件格式中的ClassFile结构、field_info结构,method_info结构和Code_attribute结构都有使用,所有属性的通用格式如下:

attribute_info {
    u2 attribute_name_index; 
    u4 attribute_length; 
    u1 info[attribute_length];
}

attribute_name_index必须是对当前Class文件的常量池的有效16位无符号索引。表示当前属性的名字。

attribute_length项的值给出了跟随其后的字节的长度,这个长度不包括attribute_name_indexattribute_name_index项的6个字节。

14.1 ConstantValue属性

ConstantValue属性是定长属性,位于field_info结构的属性表中。如果该字段为静态类型(即field_info结构的access_flags项设置了ACC_STATIC标志),则说明这个field_info结构表示的常量字段值将被分配为它的ConstantValue属性表示的值,这个过程也是类或接口申明的常量字段(Constant Field)初始化的一部分。这个过程发生在引用类或接口的类初始化方法执行之前。

ConstantValue_attribute { 
    u2 attribute_name_index; 
    u4 attribute_length; 
    u2 constantvalue_index; 
}

attribute_name_index项的值,必须是一个对常量池的有效索引。
attribute_length项的值固定为2。
constantvalue_index项的值,必须是一个对常量池的有效索引。

14.2 Code属性

Code属性是一个变长属性,位于method_info结构的属性表。一个Code属性只为唯一一个方法、实例类初始化方法或类初始化方法保存Java虚拟机指令及相关辅助信息。所有Java虚拟机实现都必须能够识别Code属性。如果方法被声明为native或者abstract类型,那么对应的method_info结构不能有明确的Code属性,其它情况下,method_info有必须有明确的Code属性。

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length; 
    u2 max_stack;
    u2 max_locals;
    u4 code_length; 
    u1 code[code_length]; 
    u2 exception_table_length; 
    {   u2 start_pc;
        u2 end_pc; 
        u2 handler_pc; 
        u2 catch_type; 
    } exception_table[exception_table_length]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count];
}

attribute_name_index项的值必须是对常量池的有效索引
attribute_length项的值表示当前属性的长度,不包括开始的6个字节。
max_stack项的值给出了当前方法的操作数栈在运行执行的任何时间点的最大深度。
max_locals项的值给出了分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量。long和double型的局部变量的最大索引是max_locals-2,其它类型的局部变量的最大索引是max_locals-1.
code_length项给出了当前方法的code[]数组的字节数,code_length的值必须大于0,即code[]数组不能为空。
code[]数组给出了实现当前方法的Java虚拟机字节码。
exception_table_length项的值给出了exception_table[]数组的成员个数量。
exception_table[]数组的每个成员表示code[]数组中的一个异常处理器(Exception Handler)。exception_table[]数组中,异常处理器顺序是有意义的(不能随意更改)。
start_pcend_pc两项的值表明了异常处理器在code[]数组中的有效范围。
handler_pc项表示一个异常处理器的起点
如果catch_type项的值不为0,那么它必须是对常量池的一个有效索引
attributes_count项的值给出了Code属性中attributes表的成员个数。
属性表的每个成员的值必须是attribute结构。一个Code属性可以有任意数量的可选属性与之关联。

14.3 StackMapTable属性

StackMapTable属性是一个变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的类型阶段被使用。

StackMapTable_attribute { 
    u2 attribute_name_index;
    u4 attribute_length; 
    u2 number_of_entries; 
    stack_map_frame entries[number_of_entries];
}

attribute_name_index项的值必须是对常量池的有效索引
attribute_length项的值表示当前属性的长度,不包括开始的6个字节。
number_of_entries项的值给出了entries表中的成员数量。Entries表的每个成员是都是一个stack_map_frame结构的项。
entries表给出了当前方法所需的stack_map_frame结构。

...更多的属性就不在这一一贴了,太多了,需要的时候查官方文档即可:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4

字节码指令

java虚拟机指令由一个字节长度的,代表某种特定操作含义的数字(称之为操作码),以及随后的代表此操作所需参数的操作数而构成。

操作码的长度为1个字节,所以最大只有256条

常量入栈指令
局部变量值转载到栈中指令
将栈顶值保存到局部变量中指令
wide指令
通用(无类型)栈操作指令
类型转换指令
整数运算
浮点运算
逻辑运算——移位运算
逻辑运算——按位布尔运算
控制流指令——条件跳转指令
控制流指令——比较指令
控制流指令——无条件跳转指令
控制流指令——表跳转指令
控制流指令——异常和finally
对象操作指令
数组操作指令
方法调用指令
方法返回指令
线程同步指令

指令参考:https://blog.csdn.net/web_code/article/details/12164733

一个简单的demo分析

Test.java

public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = a+b;
        System.out.println(c);
    }
}

javap -v Test.class

   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #26.#27        // java/io/PrintStream.println:(I)V
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10    //把10扩展成int入栈
         2: istore_1      //将栈顶int类型值保存到局部变量1中
         3: bipush        20     //把20扩展成int入栈
         5: istore_2     //将栈顶int类型值保存到局部变量2中
         6: iload_1      //从局部变量1中装载int类型值入栈  
         7: iload_2     //从局部变量2中装载int类型值入栈  
         8: iadd       // 将栈顶两int类型数相加,结果入栈。
         9: istore_3     //将栈顶int类型值保存到局部变量3中
        10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;获取静态字段的值。#2表示常量池的索引
        13: iload_3
        14: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V 运行时方法绑定调用方法。
        17: return      //void函数返回。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容