Java字节码解析

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

1. 字节码整体结构

02.jpg
03.jpg

04.jpg

1.0 class字节码数据类型

  • 字节数据直接量:这是基本的数据类型,细分为u1,u2,u4,u8四种,分别代表1个字节,2个字节,4个字节,8个字节组成的整体数据
  • 表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表示有结构的,它的结构体:组成表的成分所在的位置和顺序都是已经严格定义好的。

1.1 魔数(Magic)

  • magic:u4,魔数,代表本文件是.class文件

1.2 版本号(version)

  • minor_version:u2,次版本号
  • major_version:u2,主版本号,1.6(50) 1.7(51) 1.8(52)

1.3 常量池(constant pool)

  • constant_pool_count,u2,常量池个数

  • 常量池数组(常量表),n,它与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同,值得注意的是,常量池数组元素的个数=( constant_pool_count-1 ),0位暂时不使用。根本原因是,索引0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以常量池的索引从1开始的

  • 在jvm规范中,每个变量/字段都有描述信息,描述信息主要作用是描述字段的数据类型、方法的参数列表(数量、类型与顺序)与返回值。

  • 根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对对象的全限定名称来表示,jvm都只使用一个大写字符来表示,如下所示:B-byte,C-char,D-double,F-float,I-int,J-long,S-short,Z-boolean,V-void,L-对象类型,如Ljava/lang/String

  • 对于数组类型来说,每个维度使用一个前置的[来表示,如int[]被记录为[I,String[][],被记录为[[Ljava/lang/String;

  • 描述符描述方法时,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内,如方法:String getByIdAndName(int id,String name),的描述符:(I, Ljava/lang/String;)Ljava/lang/String

  • 常量池通常存两种常量:
    -- 字面量:如字符串、final修饰的常量等;
    -- 符号引用:如类/接口的全限定名、方法的名称和描述、字段的名称和描述等。
    -- 常量池每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节,jvm解析常量池时,会根据这个u1类型来获取元素的具体类型(如下表),

    常量池.jpg

1.4 访问控制符

  • access_flags,u2,主要目的是标记该类是类还是接口,访问权限是否public,abstract,final


    访问控制符.jpg

1.5 this class

  • u2,常量索引,用于确定类的全限定名。

1.6 super class

  • u2,的父类索引,用于确定直接父类的全限定名

1.7 interfaces

  • interfaces_count:u2,表示当前类实现的接口数量,注意是直接实现的接口数量。
  • Interfaces:表示接口的全限定名索引。每个接口u2,共interfaces_count个

1.8 fields

  • fields_count:u2,表示类变量和实例变量总的个数。
  • fields:fileds的长度为filed_info,filed_info是一个复合结构,
filed_info: {
  u2        access_flags;   
  u2        name_index;
  u2        descriptor_index;      
  u2        attributes_count;
  attribute_info   attributes[attributes_count];
}
字段访问控制符.jpg

1.9 methods

  • methods_count:u2,表示方法个数。
  • methods:methods的长度为一个method_info结构
  • Java类中的每一个实例方法(非static方法),在编译生成的字节码中,方法的参数数量总比源代码的参数数量多一个(this),它位于方法的第一个参数位置处:这样我们就可以在Java的实例方法中使用this来访问当前对象的属性及方法
  • 这个操作是在编译期间完成的,由javac编译器在编译的时候将对this的访问转换为对一个普通实例方法的访问,接下在运行期间由jvm在调用实例方法是,自动向实例方法中传入该this参数,所以在实例方法的局部变量表中,至少有一个指向当前对象的局部变量
  • Java字节码对于异常的处理,
    -- 统一采用异常表的方式来对异常进行处理
    -- 在jdk1.4.2之前的版本中,是采用特定的指令方式
    -- 当异常处理存在finally语句时,现代化的jvm采取的处理方式是将finally语句块的字节码拼接到每一个catch块的后面,也就是程序中有多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码
方法访问控制符.jpg
method_info {
  u2        access_flags;         
  u2        name_index;         
  u2        descriptor_index;       
  u2        attributes_count;       
  attribute_info   attributes[attributes_count];  
}
通用的
attribute_info {
  u2  attribute_name_index;  
  u4  attribute_length;
  u1  info[attribute_length];
}


Code_attribute {  //Code_attribute包含某个方法、实例初始化方法、类或接口初始化方法的Java虚拟机指令及相关辅助信息
  u2  attribute_name_index;
  u4  attribute_length;
  u2  max_stack;//当前方法的操作数栈在方法执行的任何时间点的最大深度
  u2  max_locals;//分配在当前方法引用的局部变量表中的局部变量个数
  u4  code_length;//给出当前方法code[]数组的字节数
  u1  code[code_length];//给出了实现当前方法的Java虚拟机代码的实际字节内容 (这些数字代码实际对应一些Java虚拟机的指令)
  u2  exception_table_lentgh; //异常的信息
  {
    u2  start_pc;   //这两项的值表明了异常处理器在code[]中的有效范围,即异常处理器x应满足:start_pc≤x≤end_pc
    u2  end_pc;   //start_pc必须在code[]中取值,end_pc要么在code[]中取值,要么等于code_length的值
    u2  handler_pc; //表示一个异常处理器的起点
    u2  catch_type; //表示当前异常处理器需要捕捉的异常类型。为0,则都调用该异常处理器,可用来实现finally。
  } exception_table[exception_table_lentgh];
  u2  attribute_count;  //表示该方法的其它附加属性,本类有1个
  attribute_info  attributes[attributes_count]; //LineNumberTable、LocalVariableTable
}

LineNumberTable和LocalVariableTable又是两个预定义的attribute,其结构如下:

LineNumberTable_attribute { //被调试器用来确定源文件中由给定的行号所表示的内容,对应于Java虚拟机code[]数组的哪部分
  u2  attribute_name_index;    
  u4  attribute_length;       
  u2  line_number_table_length; 
  {  u2  start_pc;    
     u2  line_number;//该值必须与源文件中对应的行号相匹配
  } line_number_table[line_number_table_length];
}

以及:
LocalVariableTable_attribute {
  u2  attribute_name_index;     
  u4  attribute_length;       
  u2  local_variable_table_length;  
  {  u2  start_pc;   
     u2  length;    
     u2  name_index; 
     u2  descriptor_index;//表示源程序中局部变量类型的字段描述符
    u2   index;
  } local_variable_table[local_variable_table_length];
}

1.10 attributes

  • attributes_count:u2,这里的attribute表示整个class文件的附加属性,和前面方法的attribute结构相同
  • attributes:class文件附加属性,本类中为0017,指向常量池#17,为SourceFile,SourceFile的结构如下:
SourceFile_attribute {
  u2  attribute_name_index;
  u4  attribute_length;
  u2  sourcefile_index;//表示本class文件是由ByteCodeTest.java编译来的
}

2. 案例分析

public class Test1 {
    private int a = 1;
    public int getA() {
        return a;
    }
    public void setA(int a) {
        this.a = a;
    }
}


CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 09 00 03 00 15 07 00 16 07 00 17 01 00 01 61 01 00 
01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 
75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 
00 04 74 68 69 73 01 00 15 4C 63 6F 6D 2F 68 75 69 2F 63 6C 61 7A 7A 2F 54 65 73 74 31 3B 01 00 
04 67 65 74 41 01 00 03 28 29 49 01 00 04 73 65 74 41 01 00 04 28 49 29 56 01 00 0A 53 6F 75 72 
63 65 46 69 6C 65 01 00 0A 54 65 73 74 31 2E 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 13 
63 6F 6D 2F 68 75 69 2F 63 6C 61 7A 7A 2F 54 65 73 74 31 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 
4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 06 00 00 00 03 00 01 00 07 00 08 
00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 
00 00 00 0A 00 02 00 00 00 07 00 04 00 09 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00 
00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 
0A 00 00 00 06 00 01 00 00 00 0C 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 00 01 00 
10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 
00 00 0A 00 02 00 00 00 10 00 05 00 11 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 
00 00 06 00 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13


命令: javap -verbose -p Test1.class 

Classfile /Users/zmhui/code/web/target/classes/com/hui/clazz/Test1.class
  Last modified 2020-5-17; size 467 bytes
  MD5 checksum 3bef9b467a2cb9f695390fa753edc5a1
  Compiled from "Test1.java"
public class com.hui.clazz.Test1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#21         // com/hui/clazz/Test1.a:I
   #3 = Class              #22            // com/hui/clazz/Test1
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/hui/clazz/Test1;
  #14 = Utf8               getA
  #15 = Utf8               ()I
  #16 = Utf8               setA
  #17 = Utf8               (I)V
  #18 = Utf8               SourceFile
  #19 = Utf8               Test1.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               com/hui/clazz/Test1
  #23 = Utf8               java/lang/Object
{
  private int a;
    descriptor: I
    flags: ACC_PRIVATE

  public com.hui.clazz.Test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 7: 0
        line 9: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/hui/clazz/Test1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hui/clazz/Test1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 16: 0
        line 17: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/hui/clazz/Test1;
            0       6     1     a   I
}
SourceFile: "Test1.java"

2.0 对上方class文件进行分析

2.1 魔数-magic,u4

  • CA FE BA BE

2.2 版本号-version,u2+u2

  • 00 00 00 34
    -- 34(16进制)=52,Java1.8版本

2.3 常量池-constant pool,u2+n

  • 00 18
    -- 代表:24-1 个常量
  • 0A 00 04 00 14
    -- 第一个常量:0A=值10,查常量表可知常量类型为:constant_methodref_info,可知00 04和00 14都是分别指向常量4和常量20的索引值
    -- 即为:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
  • 以此方式进行分析
-第一个
0A 00 04 00 14 
09 00 03 00 15 
07 00 16
07 00 17 
01 00 01 61 
01 00 01 49 
01 00 06 3C 69 6E 69 74 3E 
01 00 03 28 29 56 
01 00 04 43 6F 64 65 
01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 
01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 
01 00 04 74 68 69 73 
01 00 15 4C 63 6F 6D 2F 68 75 69 2F 63 6C 61 7A 7A 2F 54 65 73 74 31 3B 
01 00 04 67 65 74 41 
01 00 03 28 29 49 
01 00 04 73 65 74 41 
01 00 04 28 49 29 56 
01 00 0A 53 6F 75 72 63 65 46 69 6C 65 
01 00 0A 54 65 73 74 31 2E 6A 61 76 61 
0C 00 07 00 08 
0C 00 05 00 06 
01 00 13 63 6F 6D 2F 68 75 69 2F 63 6C 61 7A 7A 2F 54 65 73 74 31 
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 
-最后一个

2.4 访问控制符-access flags,u2

  • 00 21,根据上图<访问控制符.jpg>进行查找并没有00 21值,
  • 访问标志可以是由多个标志名称组成的,也就是字节码标志值00 21可以是多个值进行《或运算》的结果,由此可以得出 00 21 是ACC_PUBLIC,ACC_SUPER的或运算结果,

2.5 类索引-this class,u2

  • 00 03, 用来确定这个类的全限定名,00 03 指向了常量池的第三个常量,
  • 即: #3 = Class #22 // com/hui/clazz/Test1

2.6 父类索引-super class,u2

  • 00 04,用于确定这个类的父类的全限定名,00 04 指向常量池第四个常量
  • 即:#4 = Class #23 // java/lang/Object

2.7 接口-interfaces,u2+n

  • 00 00,接口索引集合就用来描述类实现了哪些接口,00 00 (interfaces_count)接口数量,表示没有接口

2.8 字段表-fields,u2+n

  • 00 01,fields_count表示有一个字段
  • 00 02 00 05 00 06 00 00,
  • 00 02,查询字段控制符可知为private
  • 00 05,名称,指向常量5,即:#5 = Utf8 a
  • 00 06,描述,指向常量6,即:#6 = Utf8 I
  • 00 00,属性数量,这里为0

2.9 方法表-methods,u2+n

  • 00 03,methods_count,表示有3个方法
第一个方法
  • 00 01 00 07 00 08 00 01
  • 00 01,访问控制符查看<方法访问控制符.jpg>可知为ACC_PUBLIC
  • 00 07,方法名索引,指向常量7,即:#7 = Utf8 <init>
  • 00 08,方法描述索引,指向常量8,即:#8 = Utf8 ()V
  • 00 01,表示有一个属性
第一个属性
  • 00 09,attribute_name_index指向常量9,即:#9 = Utf8 Code
  • 说明此属性是方法的字节码描述Code_attribute,
  • 00 00 00 38,attribute_length说明属性长度为56,(不包括前6),即以下56个字节
  • 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 07 00 04 00 09 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00
  • 00 02,max_stack操作数栈深度的最大值
  • 00 01,max_locals局部变量表所需的存储空间为 1 个 Slot,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
  • 00 00 00 0A,code_length生成字节码长度,即为10,那么紧接着10个字节就是对应的数据,2A B7 00 01 2A 04 B5 00 02 B1,
    -- 可以利用idea jclasslib Bytecode viewer插件
    -- 读入2A,官网查表可知:aload_0 = 42 (0x2a),即aload_0
    -- 读入B7,官网查表可知:invokespecial = 183 (0xb7),作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。后面两个字节u2类型的参数说明具体调用哪一个方法
    -- 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器“”方法的符号引用。即:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
    -- 读入2A,查表可知:aload_0 = 42 (0x2a),即aload_0
    -- 读入04,官网查表可知:iconst_1 = 4 (0x4)
    -- 读入B5,官网查表可知:putfield = 181 (0xb5)
    -- 读入00 02,这个是putfield的参数,查常量池对应的常量为: #2 = Fieldref #3.#21 // com/hui/clazz/Test1.a:I
    -- 读入B1,官网
    查表可知:return = 177 (0xb1)
  • 00 00,exception_table_lentgh,异常信息这里为0
  • 00 02,attribute_count表示两个属性
第一个attribute_info:00 0A 00 00 00 0A 00 02 00 00 00 07 00 04 00 09
  • 00 0A,attribute_info的attribute_name_index索引,指向常量池10=LineNumberTable,即此属性表为LineNumberTable,即: #10 = Utf8 LineNumberTable
  • 00 00 00 0A,attribute_length即属性长度为10,
  • 00 02 00 00 00 07 00 04 00 09,
  • 00 02,line_number_table_length,表示有个表信息
  • 00 00 00 07,start_pc为0,line_number为7代码的实际位置
  • 00 04 00 09,start_pc为4,line_number为9
第二个attribute_info:00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00
  • 00 0B:attribute_info的attribute_name_index索引,指向常量池11=LocalVariableTable
    ,即 #11 = Utf8 LocalVariableTable
  • 00 00 00 0C,attribute_length即12
  • 00 01,local_variable_table_length即1个
  • 00 00,start_pc
  • 00 0A,length,10
  • 00 0C,name_index,即: #12 = Utf8 this
  • 00 0D,descriptor_index, #13 = Utf8 Lcom/hui/clazz/Test1;
  • 00 00,index
第二/三个方法
  • 00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 0C 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00
  • 00 01 00 10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 10 00 05 00 11 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01

2.10 类属性-attributes,u2+n

  • 00 01 00 12 00 00 00 02 00 13
  • 00 01,attributes_count附加属性个数
  • 00 12 00 00 00 02 00 13,
  • 00 12,attribute_name_index指向 #18 = Utf8 SourceFile
  • 00 00 00 02,attribute_length长度2
  • 00 13,sourcefile_index指向 #19 = Utf8 Test1.java
来自网络
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343