DWARF & Symbol

1. DWARF与dSYM的关系

DWARF (Debug With Arbitrary Record Format) 标准调试信息格式。单独保存下来就是dSYM (Debug Symbol File) 文件 。使用MachOView打开一个dSYM,能看到很多DWARF的section。

对比编译日志可以发现,Generate Debug Symbols开关实际上就是控制clang的-g 以及 -gmodules参数,查看clang文档可以得知,该参数就是用于生产debug信息的:

-gmodules
Generate debug info with external references to clang modules or precompiled headers

-g, --debug, --debug=<arg>
Generate source-level debug information

找到Clang的源码对应部分,可以看出如果配置了-gdwarf-X,则使用对应X的Dwarf版本,否则判断参数fdebug-default-version=是否存在对应版本号,如果无则默认指定为DWARF4(我使用的源码是最新的LLVM12)

  bool WantDebug = false;
  unsigned DwarfVersion = 0;
  Args.ClaimAllArgs(options::OPT_g_Group);
  if (Arg *A = Args.getLastArg(options::OPT_g_Group)) {
    WantDebug = !A->getOption().matches(options::OPT_g0) &&
                !A->getOption().matches(options::OPT_ggdb0);
    if (WantDebug)
      DwarfVersion = DwarfVersionNum(A->getSpelling());
  }
  
    unsigned DefaultDwarfVersion = ParseDebugDefaultVersion(getToolChain(), Args);
  if (DwarfVersion == 0)
    DwarfVersion = DefaultDwarfVersion;

  if (DwarfVersion == 0)
    DwarfVersion = getToolChain().GetDefaultDwarfVersion();

打包上线的时候会把调试符号裁剪掉,但是线上统计到的堆栈仍然要能够知道对应的源代码,这时候就需要把符号写到一个单独的dSYM文件中。

Debug符号表是一个映射表,它把每一个编译好的二进制中的机器指令映射到生成它们的每一行源代码中。这些Debug符号表要么被存储在编译好的二进制中,要么单独存储在Debug Symbol文件中(也就是dSYM文件):一般来说,debug模式构建的App会把Debug符号表存储在编译好的二进制中,而release模式构建的App会把Debug符号表存储在dSYM文件中以节省二进制体积。通过Xcode编译日志可以看到,dSYM文件是由 dsymutil 工具生成的。

2. Clang 生成 DWARF 调试信息

  • 对于 GCC 及 Clang 编译器, 使用参数 -gdwarf-4 即可生成 DWARF4 调试信息,修改对应的数字1-5可生成对应版本的调试信息

  • 创建 foo.c

int foo(int a, int b) {
    int c;
    static double d = 5.0;
    c = a + b;
    return c;
}

int main() {
    int r;
    r = foo(2, 3);
    return 0;
}
  • 使用 Clang 编译并生成 DWARF4 编译信息
clang -O0 -gdwarf-4 foo.c -o foo

编译完成后,本地会多了 foo.dSYM 和 foo可执行文件

  • 使用 lldb 调试 foo 并观察 foo.dSYM 是否起作用
    1. lldb foo 启动 lldb 并设置被调试程序为 foo
    2. 在 lldb 交互命令输入: b foo 设置函数 foo 为断点
    3. 在 lldb 交互命令输入: run 开始运行被调试程序
GIH-D-21687:Release-iphoneos n14637$ clang -O0 -gdwarf-4 foo.c -o foo
GIH-D-21687:Release-iphoneos n14637$
GIH-D-21687:Release-iphoneos n14637$ lldb foo
(lldb) target create "foo"
Current executable set to '/Desktop/bcTest/foo' (x86_64).
(lldb) b foo
Breakpoint 1: where = foo`foo + 10 at foo.c:4:9, address = 0x0000000100003f6a
(lldb) run
Process 15959 launched: '/Desktop/bcTest/foo' (x86_64)
Process 15959 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003f6a foo`foo(a=2, b=3) at foo.c:4:9
   1    int foo(int a, int b) {
   2        int c;
   3        static double d = 5.0;
-> 4        c = a + b;
   5        return c;
   6    }
   7
Target 0: (foo) stopped.
(lldb)
  • 进入到 foo.dSYM 目录并且找到调试信息文件 foo,并使用 file 命令查看 foo 文件描述可以看出是一个 Mach-O 文件

    GIH-D-21687:bcTest n14637$ cd foo.dSYM/Contents/Resources/DWARF/
    GIH-D-21687:DWARF n14637$ file foo
    foo: Mach-O 64-bit dSYM companion file x86_64
    
  • 使用 size 命令查看 foo 可执行文件包含的 Segment 和 Section

GIH-D-21687:DWARF n14637$ size -x -m -l foo
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x4000 (vmaddr 0x100000000 fileoff 0)
    Section __text: 0x4b (addr 0x100003f60 offset 0)
    Section __unwind_info: 0x48 (addr 0x100003fac offset 0)
    total 0x93
Segment __DATA: 0x4000 (vmaddr 0x100004000 fileoff 0)
    Section __data: 0x8 (addr 0x100004000 offset 0)
    total 0x8
Segment __LINKEDIT: 0x1000 (vmaddr 0x100008000 fileoff 4096)
Segment __DWARF: 0x1000 (vmaddr 0x100009000 fileoff 8192)
    Section __debug_line: 0x69 (addr 0x100009000 offset 8192)
    Section __debug_pubnames: 0x29 (addr 0x100009069 offset 8297)
    Section __debug_pubtypes: 0x25 (addr 0x100009092 offset 8338)
    Section __debug_aranges: 0x40 (addr 0x1000090b7 offset 8375)
    Section __debug_info: 0xc2 (addr 0x1000090f7 offset 8439)
    Section __debug_abbrev: 0x7e (addr 0x1000091b9 offset 8633)
    Section __debug_str: 0xd0 (addr 0x100009237 offset 8759)
    Section __apple_names: 0x74 (addr 0x100009307 offset 8967)
    Section __apple_namespac: 0x24 (addr 0x10000937b offset 9083)
    Section __apple_types: 0x72 (addr 0x10000939f offset 9119)
    Section __apple_objc: 0x24 (addr 0x100009411 offset 9233)
    total 0x435
total 0x10000a000

可以看到 __DWARF Segment下包含 __debug_line, __debug_pubnames, __debug_pubtypes 等多个Section。
这些 Section 便是 DWARF 在 .dSYM 中的存储方式

  • 接下来再使用 dwarfdump 探索 DWARF 内容
    输入命令 dwarfdump foo --debug-info 可展示 __debug_line Section 下的内容
GIH-D-21687:DWARF n14637$ dwarfdump foo.dSYM/Contents/Resources/DWARF/foo --debug-info
foo:    file format Mach-O 64-bit x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x000000be version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x000000c2)

0x0000000b: DW_TAG_compile_unit
              DW_AT_producer    ("Apple clang version 12.0.0 (clang-1200.0.32.28)")
              DW_AT_language    (DW_LANG_C99)
              DW_AT_name    ("foo.c")
              DW_AT_LLVM_sysroot    ("/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk")
              DW_AT_APPLE_sdk   ("MacOSX10.15.sdk")
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos")
              DW_AT_low_pc  (0x0000000100003f60)
              DW_AT_high_pc (0x0000000100003fab)

0x00000032:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000100003f60)
                DW_AT_high_pc   (0x0000000100003f78)
                DW_AT_frame_base    (DW_OP_reg6 RBP)
                DW_AT_name  ("foo")
                DW_AT_decl_file ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                DW_AT_decl_line (1)
                DW_AT_prototyped    (true)
                DW_AT_type  (0x000000ba "int")
                DW_AT_external  (true)

0x0000004b:     DW_TAG_variable
                  DW_AT_name    ("d")
                  DW_AT_type    (0x0000008b "double")
                  DW_AT_decl_file   ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                  DW_AT_decl_line   (3)
                  DW_AT_location    (DW_OP_addr 0x100004000)

0x00000060:     DW_TAG_formal_parameter
                  DW_AT_location    (DW_OP_fbreg -4)
                  DW_AT_name    ("a")
                  DW_AT_decl_file   ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                  DW_AT_decl_line   (1)
                  DW_AT_type    (0x000000ba "int")

0x0000006e:     DW_TAG_formal_parameter
                  DW_AT_location    (DW_OP_fbreg -8)
                  DW_AT_name    ("b")
                  DW_AT_decl_file   ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                  DW_AT_decl_line   (1)
                  DW_AT_type    (0x000000ba "int")

0x0000007c:     DW_TAG_variable
                  DW_AT_location    (DW_OP_fbreg -12)
                  DW_AT_name    ("c")
                  DW_AT_decl_file   ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                  DW_AT_decl_line   (2)
                  DW_AT_type    (0x000000ba "int")

0x0000008a:     NULL

0x0000008b:   DW_TAG_base_type
                DW_AT_name  ("double")
                DW_AT_encoding  (DW_ATE_float)
                DW_AT_byte_size (0x08)

0x00000092:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000100003f80)
                DW_AT_high_pc   (0x0000000100003fab)
                DW_AT_frame_base    (DW_OP_reg6 RBP)
                DW_AT_name  ("main")
                DW_AT_decl_file ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                DW_AT_decl_line (8)
                DW_AT_type  (0x000000ba "int")
                DW_AT_external  (true)

0x000000ab:     DW_TAG_variable
                  DW_AT_location    (DW_OP_fbreg -8)
                  DW_AT_name    ("r")
                  DW_AT_decl_file   ("/Users/n14637/Desktop/bcTest/app/Release-iphoneos/foo.c")
                  DW_AT_decl_line   (9)
                  DW_AT_type    (0x000000ba "int")

0x000000b9:     NULL

0x000000ba:   DW_TAG_base_type
                DW_AT_name  ("int")
                DW_AT_encoding  (DW_ATE_signed)
                DW_AT_byte_size (0x04)

0x000000c1:   NULL

3. info section

info section 是DWARF的核心,其用来描述程序结构,为此 DWARF 提出了 The Debugging Information Entry (DIE) 来以统一的形式描述这些信息,以下是官方文档的部分描述:

DWARF uses a series of debugging information entries (DIEs) to define a low-level representation of a source program. Each debugging information entry consists of an identifying tag and a series of attributes. An entry, or group of entries together, provide a description of a corresponding entity in the source program. The tag specifies the class to which an entry belongs and the attributes define the specific characteristics of the entry.
The debugging information entries are contained in the .debug_info and .debug_types sections of an object file
Each attribute value is characterized by an attribute name. No more than one attribute with a given name may appear in any debugging information entry. There are no limitations on the ordering of attributes within a debugging information entry.
  A variety of needs can be met by permitting a single debugging information entry to “own” an arbitrary number of other debugging entries and by permitting the same debugging information entry to be one of many owned by another debugging information entry. This makes it possible, for example, to describe the static block structure within a source file, to show the members of a structure, union, or class, and to associate declarations with source files or source files with shared objects.
The ownership relation of debugging information entries is achieved naturally because the debugging information is represented as a tree. The nodes of the tree are the debugging information entries themselves. The child entries of any node are exactly those debugging information entries owned by that node.

可以看出,调试信息以树的形式表示,而每个DIE作为树的节点,一个 DIE 可以包含几个子DIE,正如一个文件可以有 N 个函数,一个函数可以包含 X 个形式参数和 Y 个局部变量。而对于DIE本身其包含:

  • 一个 TAG 属性,表达描述什么类型的东西,如: TAG_subprogram(函数)、TAG_formal_parameter(形式参数)、TAG_variable(变量)、TAG_base_type(基础类型)

  • N 个属性(attribute),用于具体描述一个DIE,例如 DWARF info 示例 中对函数 foo 的描述:

                     AT_low_pc( 0x0000000100000f60 )
                     AT_high_pc( 0x00000018 )
                     AT_frame_base( rbp )
                     AT_name( "foo" )
                     AT_decl_file( "foo.c" )
                     AT_decl_line( 1 )
                     AT_prototyped( true )
                     AT_type( {0x000000b2} ( int ) )
                     AT_external( true )
    
  • AT_low_pc, AT_high_pc 分别代表函数的 起始/结束 PC地址

  • AT_frame_base 表达函数的栈帧基址(frame base) 为寄存器 rbp 的值

  • AT_name 描述函数的名字为 foo

  • AT_decl_file 表明这个函数在 foo.c 文件中声明

  • AT_decl_line 表明这个函数在 foo.c 第几行声明

  • AT_prototyped 为一个 Bool 值,为 True 代表这是一个子程序/函数(subroutine)

  • AT_type 描述这个函数返回值的类型是什么,对于 foo 函数来说,为 int

  • AT_external 表明这个函数是否为全局可访问

4. Symbol结构

struct nlist_64 存储了symbol的数据结构。而符号的name不在符号表中,而在 String Table 中,所有的字符串都存储在那里。需要根据 n_strx 找到符号的name位于 String Table 中的下标位置,才能找到正确的符号名

struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */ // 符号的name在String Table中的下标。
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

Symbol Table

符号表存储了符号信息。ld和dyld都会在link的时候读取符号表

Dynamic Symbol Table

动态符号表,Dynamic Symbol Table ,其中仅存储了符号位于Symbol Table中的下标,而非符号数据结构,因为符号的结构仅存储在 Symbol Table 而已,使用 otool 命令可以查看动态符号表中的符号位于符号表中的下标。因此动态符号也叫做 Indirect symbols

Dynamic Symbol Table.png

__la_symbol_ptr

上边的otool命令输出中,有 Indirect symbols for (__DATA,__la_symbol_ptr) 9 entries__la_symbol_ptr 是懒加载的符号指针,即第一次使用到的时候才加载。

首先会在__DATA, __la_symbol_ptr创建一个指针,这个指针编译期会指向__TEXT,__stub_helper,第一次调用的时候,会通过dyld_stub_binder把指针绑定到函数实现,下一次调用的时候就不需要再绑定了。

section_64的结构中有个reserved字段,若该section是 __DATA,__la_symbol_ptr ,则该reserved1字段存储的就是该 __la_symbol_ptr 在Dynamic Symbol Table中的偏移量,也可以理解为下标。

__la_symbol_ptr.png

查找 __la_symbol_ptr 的符号流程如下:

遍历load command,如果发现是__DATA,__la_symbol_ptr,那么读取reserved1,即__la_symbol_ptr的符号位于Dynamic Symbol Table的起始地址。
遍历__DATA,__la_symbol_ptr处的指针,当前遍历的下标为idx,加上reserved1就是该指针对应的Dynamic Symbol Table下标
通过Dynamic Symbol Table,读取Symbol Table的下标
读取Symbol Table,找到String Table的Index
找到符号名称

5. 符号命名规则

C的符号生成规则比较简单,一般的符号都是在函数名上加上下划线。

C++因为支持命名空间,函数重载等高级特性,为了避免符号冲突,所以编译器对C++符号做了Symbol Mangling(不同编译器的规则不一样)。

一般如下规则生成:

  • _Z开头
  • 跟着C语言的保留字符串N
  • 对于namespace等嵌套的名称,接下依次拼接名称长度,名称
  • 然后是结束字符E
  • 最后是参数的类型,比如int是i,double是d

Objective C的符号更简单一些,比如方法的符号是+-[Class_name(category_name) method:name:],除了这些,Objective C还会生成一些Runtime元数据的符号

6. 符号的种类

按照不同的方式可以对符号进行不同的分类,比如按照可见性划分

全局符号(Global Symbol) 对其他编译单元可见
本地符号(Local Symbol) 只对当前编译单元可见
按照位置划分:

外部符号,符号不在当前文件,需要ld或者dyld在链接的时候解决
非外部符号,即当前文件内的符号
nm命令里的小写字母对应着本地符号,大写字母表示全局符号;U表示undefined,即未定义的外部符号

7. 实战

我们在拿到一个Crash日志之后着重看到:

Last Exception Backtrace:
0   CoreFoundation                  0x187b9186c __exceptionPreprocess + 220
1   libobjc.A.dylib                 0x19cbacc50 objc_exception_throw + 59
2   CoreFoundation                  0x187c01e1c _CFThrowFormattedException + 115
3   CoreFoundation                  0x187a6f8a8 -[__NSArrayM objectAtIndex:] + 219
4   TestCase                        0x104ee5e40 _hidden#4_ + 24128 (__hidden#44_:48)
5   libdispatch.dylib               0x187785db0 _dispatch_client_callout + 19

Binary Images:
0x104ee0000 - 0x104ee7fff TestCase arm64  <b22862e527c93aa3b12c9f0cdc950ddf> /var/containers/Bundle/Application/D9E40942-5FC9-4811-BCAF-66EFCC53A9B9/TestCase.app/TestCase
  1. 0x104ee0000 - 0x104ee7fff: 是ASLR后的开始和结束地址,通过该地址可以计算出函数在安装包中的地址;

  2. TestCase: 应用的名称

  3. arm64: 应用的架构

  4. b22862e527c93aa3b12c9f0cdc950ddf: uuid的值,这个用来和dysm一一对应;

  5. /var/containers/Bundle/Application/D9E40942-5FC9-4811-BCAF-66EFCC53A9B9/TestCase.app/TestCase:应用的安装路径

对于arm64结构如果没有ASLR的话开始地址是0x100000000,这是由__PAGEZERO段大小决定的。运行内存的开始地址是0x104ee0000,所以偏移了0x4ee0000 = 0x104ee0000 - 0x100000000

所以 0x104ee5e40 在二进制包中的地址为0x100005E40 = 0x104ee5e40 - 0x4ee0000

然后使用Hopper打开dSYM文件,G togo 0x100005E40 ,即可定位到崩溃的方法。

另一种方法可通过 dwarfdump --arch arm64 TestCase.app.dSYM --lookup 0x100005E40

可以看到输出结果:

0x0004847b: DW_TAG_compile_unit
              DW_AT_producer    ("Apple clang version 12.0.0 (clang-1200.0.32.28)")
              DW_AT_language    (DW_LANG_ObjC)
              DW_AT_name    ("/Users/n14637/Desktop/TestCase/TestCase/ViewController.m")
              DW_AT_LLVM_sysroot    ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.3.sdk")
              DW_AT_APPLE_sdk   ("iPhoneOS14.3.sdk")
              DW_AT_stmt_list   (0x0000aecd)
              DW_AT_comp_dir    ("/Users/n14637/Desktop/TestCase")
              DW_AT_APPLE_optimized (true)
              DW_AT_APPLE_major_runtime_vers    (0x02)
              DW_AT_low_pc  (0x0000000100005ccc)
              DW_AT_high_pc (0x000000010000601c)

0x0004869c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000100005d8c)
                DW_AT_high_pc   (0x0000000100005ec0)
                DW_AT_frame_base    (DW_OP_reg29 W29)
                DW_AT_object_pointer    (0x000486b6)
                DW_AT_call_all_calls    (true)
                DW_AT_name  ("-[ViewController addArray]")
                DW_AT_decl_file ("/Users/n14637/Desktop/TestCase/TestCase/ViewController.m")
                DW_AT_decl_line (45)
                DW_AT_prototyped    (true)
                DW_AT_APPLE_optimized   (true)
Line info: file 'ViewController.m', line 0, column 21, start line 45

或者使用atos不需要计算地址直接查看行数:

// 0x100868000 是 Binary Images 起始地址,0x10086de40为崩溃的栈地址
atos -o TestCase.app.dSYM/Contents/Resources/DWARF/TestCase -arch arm64 -l 0x100868000 0x10086de40
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容