分析Mach-o文件获取无用代码和类

Mach-O

Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序、库的标准格式
属于Mach-O格式的文件类型有

image

常见的Mach-O文件类型

Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序、库的标准格式
属于Mach-O格式的文件类型有

MH_OBJECT MH_EXECUTE MH_DYLIB MH_DYLINKER MH_DSYM
目标文件(.o) 可执行文件 动态库文件 动态链接编辑器 存储着二进制文件符号信息的文件
静态库文件(.a),静态库其实就是N个.o合并在一起 .app/xx .dylib .framework/xx /usr/lib/dyld .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
  • 目标文件是代码文件和可执行文件的中间产物.C -> .O -> 可执行文件(clang -c 文件名)
  • clang -o 生成文件名 代码文件名 直接生成可执行文件
  • cd usr/bin 查找动态库
  • file 文件名 查看文件类型

Mach-O的基本结构

官方描述https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html
一个Mach-O文件包含3个主要区域:

  • Header

  • 文件类型、目标架构类型等

  • Load commands

  • 描述文件在虚拟内存中的逻辑结构、布局

  • Raw segment data

  • 在Load commands中定义的Segment的原始数据

    image
Section 用途
__TEXT.__text 主程序代码
__TEXT.__cstring C 语言字符串
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname Objective-C 方法名称
__TEXT.__objc_methtype Objective-C 方法类型
__TEXT.__objc_classname Objective-C 类名称
__DATA.__data 初始化过的可变数据
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const 没有初始化过的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common 没有初始化过的符号声明
__DATA.__objc_classlist objc类列表
__DATA.__objc_protolist objc协议列表
__DATA.__objc_imginfo objc 镜像信息
__DATA.__objc_selfrefs 引用到的objc方法
__DATA.__objc_protorefs 引用到的objc协议
__DATA.__objc_superrefs objc超类引用

窥探Mach-O的结构

命令行工具
file:查看Mach-O的文件类型
file 文件路径

otool:查看Mach-O特定部分和段的内容

lipo:常用于多架构Mach-O文件的处理
查看架构信息:lipo -info 文件路径
导出某种特定架构:lipo 文件路径 -thin 架构类型 -output 输出文件路径
合并多种架构:lipo 文件路径1 文件路径2 -output 输出文件路径

GUI工具
MachOView(https://github.com/gdbinit/MachOView

Universal Binary(通用二进制文件)

  • 通用二进制文件
  • 同时适用于多种架构的二进制文件
  • 包含了多种不同架构的独立的二进制文件
  • 因为需要储存多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大
  • 由于两种架构有共同的一些资源,所以并不会达到

otool

image.png
  • -f print the fat headers 查找通用二进制文件header
  • -h print the mach header 打印armv7,arm64里面的头信息
  • -l print the load commands 打印段信息
  • -L print shared libraries used 打印引用动态库

查找无用类

Mach-o文件中 __DATA __objc_classrefs段记录了引用类的地址,__DATA __objc_classlist段记录了所有类的地址,取差集可以得到未使用的类的地址,然后进行符号化,就可以得到未被引用的类信息。
1、通过file命令获取到arch。

#binary_file_arch: distinguish Big-Endian and Little-Endian
#file -b output example: Mach-O 64-bit executable arm64
binary_file_arch = os.popen('file -b ' + path).read().split(' ')[-1].strip()

2、在取类地址的时候区分x86_64和arm

def pointers_from_binary(line, binary_file_arch):
    if len(line) < 16:
        return None
    line = line[16:].strip().split(' ')
    pointers = set()
    if binary_file_arch == 'x86_64':
        #untreated line example:00000001030cec80    d8 75 15 03 01 00 00 00 68 77 15 03 01 00 00 00
        if len(line) != 16:
            return None
        pointers.add(''.join(line[4:8][::-1] + line[0:4][::-1]))
        pointers.add(''.join(line[12:16][::-1] + line[8:12][::-1]))
        return pointers
    #arm64 confirmed,armv7 arm7s unconfirmed
    if binary_file_arch.startswith('arm'):
        #untreated line example:00000001030bcd20    03138580 00000001 03138878 00000001
        if len(line) != 4:
            return None
        pointers.add(line[1] + line[0])
        pointers.add(line[3] + line[2])
        return pointers
    return None

3、通过otool -v -s __DATA __objc_classrefs获取到引用类的地址

def class_ref_pointers(path, binary_file_arch):
  ref_pointers = set()
  lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classrefs %s' % path).readlines()
  for line in lines:
    pointers = pointers_from_binary(line, binary_file_arch)
    ref_pointers = ref_pointers.union(pointers)
  return ref_pointers

4、获取所有的类

def class_list_pointers(path, binary_file_arch):
  list_pointers = set()
  lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classlist %s' % path).readlines()
  for line in lines:
    pointers = pointers_from_binary(line, binary_file_arch)
    list_pointers = list_pointers.union(pointers)
  return list_pointers

5、取差集
用所有类信息减去引用类的信息,此时我们可以拿到未使用类的地址信息。

unref_pointers = class_list_pointers(path, binary_file_arch) - class_ref_pointers(path, binary_file_arch)

6、符号化
通过nm -nm命令可以得到地址和对应的类名字

def class_symbols(path):
  symbols = {}
  #class symbol format from nm: 0000000103113f68 (__DATA,__objc_data) external _OBJC_CLASS_$_EpisodeStatusDetailItemView
  re_class_name = re.compile('(\w{16}) .* _OBJC_CLASS_\$_(.+)')
  lines = os.popen('nm -nm %s' % path).readlines()
  for line in lines:
    result = re_class_name.findall(line)
    if result:
      (address, symbol) = result[0]
      symbols[address] = symbol
  return symbols

7、过滤
在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs这个段里,在未使用的类中需要将这一部分父类过滤出去。使用otool -oV可以获取到类的继承关系。

def filter_super_class(unref_symbols):
  re_subclass_name = re.compile("\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)")
  re_superclass_name = re.compile("\s*superclass 0x\w{9} _OBJC_CLASS_\$_(.+)")
  #subclass example: 0000000102bd8070 0x103113f68 _OBJC_CLASS_$_TTEpisodeStatusDetailItemView
  #superclass example: superclass 0x10313bb80 _OBJC_CLASS_$_TTBaseControl
  lines = os.popen("/usr/bin/otool -oV %s" % path).readlines()
  subclass_name = ""
  superclass_name = ""
  for line in lines:
    subclass_match_result = re_subclass_name.findall(line)
    if subclass_match_result:
      subclass_name = subclass_match_result[0]
    superclass_match_result = re_superclass_name.findall(line)
    if superclass_match_result:
      superclass_name = superclass_match_result[0]
 
    if len(subclass_name) > 0 and len(superclass_name) > 0:
      if superclass_name in unref_symbols and subclass_name not in unref_symbols:
        unref_symbols.remove(superclass_name)
      superclass_name = ""
      subclass_name = ""
  return unref_symbols

8、过滤
为了防止一些三方库的误伤,还可以去过滤一些前缀,或者是是仅保留带有某些前缀的类

for unref_pointer in unref_pointers:
   if unref_pointer in symbols:
     unref_symbol = symbols[unref_pointer]
     if len(reserved_prefix) > 0 and not unref_symbol.startswith(reserved_prefix):
       continue
     if len(filter_prefix) > 0 and unref_symbol.startswith(filter_prefix):
       continue
     unref_symbols.add(unref_symbol)

9、保存
最终结果保存在脚本目录下

script_path = sys.path[0].strip()
f = open(script_path+"/result.txt","w")
f.write( "unref class number:  %d\n" % len(unref_symbles))
f.write("\n")
for unref_symble in unref_symbles:
  f.write(unref_symble+"\n")
f.close()

LinkMap使用

1、XCode开启编译选项Write Link Map File
XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为YES,并指定好linkMap的存储位置
特别提醒:打包发布前记得还原为NO

image.png

2、编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File位于
~/Library/Developer/Xcode/DerivedData/XXX-XXXXXXXXXXXX/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/
这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。

LinkMap结构
image.png
  • Object File:包含了代码工程的所有文件
  • Section:描述了代码段在生成的 Mach-O 里的偏移位置和大小
  • Symbols:会列出每个方法、类、Block,以及它们的大小

1、首先列出来的是目标文件列表(中括号内为文件编号):

# Path: /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Products/Debug-iphoneos/JRAPP.app/JRAPP
# Arch: arm64
# Object files:
[  0] linker synthesized
[  1] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRRepaymentBankBindingPTwoPView.o
[  2] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRApplyAddMountTypeSelectViewController.o
[  3] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/MQTTSSLSecurityPolicyEncoder.o
[  4] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRICBCFaceSignedShowModel.o
[  5] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRUsersSignUpViewController.o
[  6] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRUrgentInfoManager.o
[  7] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRPersonInfoCityModel.o
[  8] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRPayDownPaymentInputPayMoneyCell.o
[  9] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRDigitalCompassSearchRangeSelectTableViewCell.o

2、接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)

# Sections:
# Address   Size        Segment Section
0x100004550 0x014CA9F4  __TEXT  __text
0x1014CEF44 0x00004038  __TEXT  __stubs
0x1014D2F7C 0x00003798  __TEXT  __stub_helper
0x1014D6714 0x0008D528  __TEXT  __gcc_except_tab
0x101563C40 0x0004C9B8  __TEXT  __const
0x1015B05F8 0x00091381  __TEXT  __objc_methname
0x10164197A 0x0000C592  __TEXT  __ustring
0x10164DF10 0x000A9D09  __TEXT  __cstring
0x1016F7C19 0x0000EF1E  __TEXT  __objc_classname
0x101706B37 0x00015537  __TEXT  __objc_methtype
0x10171C070 0x00037C88  __TEXT  __unwind_info
0x101753CF8 0x0001C2FC  __TEXT  __eh_frame
0x101770000 0x00001838  __DATA  __got
0x101771838 0x00002AD0  __DATA  __la_symbol_ptr
0x101774308 0x00000128  __DATA  __mod_init_func
0x101774430 0x00000008  __DATA  __mod_term_func
0x101774440 0x0004D238  __DATA  __const
0x1017C1678 0x0004DB80  __DATA  __cfstring
0x10180F1F8 0x000045E0  __DATA  __objc_classlist

首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。
每一行的数据都紧跟在上一行后面,如第二行__stubs的地址0x10304FD9C就是第一行__text的地址0x100005B00加上大小0x0304A29C,整个可执行文件大致数据分布就是这样。
这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。
3、接着就是按上表顺序,列出具体的按每个文件列出每个对应字段的位置和占用空间

# Symbols:
# Address   Size        File  Name
0x100004550 0x00000080  [  1] +[JRRepaymentBankBindingPTwoPView initRepaymentBankBindingPTwoPViewFrame:]
0x1000045D0 0x000000DC  [  1] -[JRRepaymentBankBindingPTwoPView initWithFrame:]
0x1000046AC 0x00000094  [  1] -[JRRepaymentBankBindingPTwoPView cancelBtnAction]
0x100004740 0x00000234  [  1] -[JRRepaymentBankBindingPTwoPView loadTime]
0x100004974 0x000001E8  [  1] ___43-[JRRepaymentBankBindingPTwoPView loadTime]_block_invoke
0x100004B5C 0x000000C8  [  1] ___43-[JRRepaymentBankBindingPTwoPView loadTime]_block_invoke_2
0x100004C24 0x0000006C  [  1] ___copy_helper_block_e8_32s40r
0x100004C90 0x0000004C  [  1] ___destroy_helper_block_e8_32s40r
0x100004CDC 0x00000044  [  1] ___43-[JRRepaymentBankBindingPTwoPView loadTime]_block_invoke.17
0x100004D20 0x0000004C  [  1] ___copy_helper_block_e8_32s
0x100004D6C 0x00000030  [  1] ___destroy_helper_block_e8_32s
0x100004D9C 0x000004A8  [  1] -[JRRepaymentBankBindingPTwoPView isHaveSendMessage:]
0x100005244 0x000001E0  [  1] -[JRRepaymentBankBindingPTwoPView loadPhone:title:message:]
0x100005424 0x00000094  [  1] -[JRRepaymentBankBindingPTwoPView bottomButtonAction]
0x1000054B8 0x000001E0  [  1] -[JRRepaymentBankBindingPTwoPView dissMissAlertView]
0x100005698 0x00000094  [  1] -[JRRepaymentBankBindingPTwoPView topButtonAction]
0x10000572C 0x00000EE0  [  1] -[JRRepaymentBankBindingPTwoPView creatSubviews]
0x10000660C 0x0000004C  [  1] _CGRectMake
0x100006658 0x00000190  [  1] -[JRRepaymentBankBindingPTwoPView creatButtonFrame:]
0x1000067E8 0x0000004C  [  1] -[JRRepaymentBankBindingPTwoPView dealloc]
0x100006834 0x0000003C  [  1] -[JRRepaymentBankBindingPTwoPView cancelBtnBlock]
0x100006870 0x00000044  [  1] -[JRRepaymentBankBindingPTwoPView setCancelBtnBlock:]
0x1000068B4 0x0000003C  [  1] -[JRRepaymentBankBindingPTwoPView bottomBtnBlock]
0x1000068F0 0x00000044  [  1] -[JRRepaymentBankBindingPTwoPView setBottomBtnBlock:]
0x100006934 0x0000003C  [  1] -[JRRepaymentBankBindingPTwoPView topBtnBlock]
0x100006970 0x00000044  [  1] -[JRRepaymentBankBindingPTwoPView setTopBtnBlock:]

同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。
4、已废弃&多余重复的字段

# Dead Stripped Symbols:
#           Size        File  Name
<<dead>>    0x0000000B  [  2] literal string: whiteColor
<<dead>>    0x00000014  [  2] literal string: setBackgroundColor:
<<dead>>    0x00000012  [  2] literal string: stringWithFormat:
<<dead>>    0x00000009  [  2] literal string: setText:
<<dead>>    0x00000007  [  2] literal string: length
<<dead>>    0x0000000C  [  2] literal string: addSubview:
<<dead>>    0x0000000F  [  2] literal string: initWithFrame:
<<dead>>    0x0000000E  [  2] literal string: setTextColor:
<<dead>>    0x00000016  [  2] literal string: boldSystemFontOfSize:
<<dead>>    0x00000009  [  2] literal string: setFont:
<<dead>>    0x0000000E  [  2] literal string: .cxx_destruct
<<dead>>    0x00000001  [  2] literal string: 
<<dead>>    0x00000005  [  2] literal string: %@%@
<<dead>>    0x00000008  [  2] literal string: orderId
<<dead>>    0x00000008  [  2] literal string: @16@0:8
<<dead>>    0x0000000B  [  2] literal string: v24@0:8@16
<<dead>>    0x00000011  [  2] literal string: v40@0:8@16@24@32`

得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
Objective-C 中的方法都会通过 objc_msgSend 来调用,而 objc_msgSend 在 Mach-O 文件里是通过 _objc_selrefs 这个 section 来获取 selector 这个参数的。
所以,_objc_selrefs 里的方法一定是被调用了的。_objc_classrefs 里是被调用过的类, objc_superrefs 是调用过 super 的类(继承关系)。通过 _objc_classrefs 和 _objc_superrefs,我们就可以找出使用过的类和子类。

APPCode

通过 AppCode 查找无用代码
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。

image.png

说明:AppCode检测出了实际上需要的大部分场景的问题,但是由于 Objective-C 是一门动态性语言,所以 AppCode 检测出无用的方法等都需要工程师自己再次确认后删除。(在我们的工程中有一些和 H5 交互的桥接方法,因此 AppCode 视为 Unused Method,但是你删除的话,那就自己哭去吧 😭)。使用 AppCode 的时候如果工程比较大,则整个 code inspect 会非常耗时

无用类:Unused class 是无用类,Unused import statement 是无用类引入声明,Unused property 是无用的属性;
无用方法:Unused method 是无用的方法,Unused parameter 是无用参数,Unused instance variable 是无用的实例变量,Unused local variable 是无用的局部变量,Unused value 是无用的值;
无用宏:Unused macro 是无用的宏。
无用全局:Unused global declaration 是无用全局声明。

主意需要人工二次确认

  • JSONModel 里定义了未使用的协议会被判定为无用协议;
  • 如果子类使用了父类的方法,父类的这个方法不会被认为使用了;
  • 通过点的方式使用属性,该属性会被认为没有使用;* 使用 performSelector 方式调用的方法也检查不出来,比如 self performSelector:@selector(arrivalRefreshTime);
  • 运行时声明类的情况检查不出来。比如通过 NSClassFromString 方式调用的类会被查出为没有使用的类,比如 layerClass = NSClassFromString(@“SMFloatLayer”)。还有以 [[self class] accessToken] 这样不指定类名的方式使用的类,会被认为该类没有被使用。像 UITableView 的自定义的 Cell 使用 registerClass,这样的情况也会认为这个 Cell 没有被使用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容