第二十二章:SB Examples, 改进Lookup

在这一段剩下的章节中, 你将会聚焦于Python脚本上.
正如前一章中指出的, image lookup -rn命令正在被淘汰的路上. 是时候来创建一个漂亮的脚本来显示内容了.
下面是你现在用image lookup -rn命令能够获取到的内容:

图片.png

当你学完这一章之后, 你将会有一个可以查的清除的叫做lookup的脚本.
图片.png

此外, 你将会给lookup命令添加一组参数来为新的搜索添加一些提示.

脚本创建的自动化

包含在本章starter目录下的这个项目是两个能让你在更轻松的创建LLDB脚本是Python 脚本. 这两个脚本如下:
generate_new_script.py: 这将会用你给的名字创建一个新的纲要脚本然后将它粘贴到generate_new_script所在的同一个目录下.
lldbinit.py: 这个脚本将会枚举它所在的目录下的所有脚本(以.py结尾的文件)然后尝试将它们加载到LLDB中. 此外, 如果这个目录下有.txt拓展名的文件, LLDB将会尝试通过command import加载这些文件的内容.
将本章starter目录下的两个文件粘贴到你的~/lldb/目录里.
把这些文件放在正确的位置之后, 进到你的~/.lldbinit文件里然后添加下面一行代码:

command script import ~/lldb/lldbinit.py

这将会加载lldbinit.py文件, lldbinit.py文件会枚与它同目录下的所有的.py文件和.txt文件容纳后将这些文件加载到LLDB中. 这就意味着从现在开始, 只需要简单的将脚本添加到~/lldb目录下面就可以在LLDB启动的时候自动加载到LLDB中.

创建lookup command

将你的新工具设置完成之后, 打开一个终端窗口. 启动一个新的LLDB实例:

lldb

正如期望的那样, 你会迅速的看到LLDB的问候语.
确保你已经存在的LLDB脚本没有任何的编译错误:

(lldb) reload_script

如果输出了一些错误, 是时候试一下你的新命令了__generate_script(**generate_new_script.py **文件里实现的命令).
在LLD中, 输入:

(lldb) __generate_script lookup

如果一切都如我们期望的那样, 你将会得到一些类似下面的输出:

Opening "/Users/derekselander/lldb/lookup.py"...

此外, 一个Finder窗口将会弹出来向你展示当前文件所在的位置. 你可以用这些脚本做的事情是十分疯狂的, 是吧?
将Finder窗口保留几秒钟--不要关闭它. 回到LLDB终端窗口中容纳后使用reload_script命令.
因为lookup.py脚本与lldbinit.py脚本在同一个目录下并且你刚刚重新加载了~/.lldbinit中的内容, 现在你要试一下lookup.py文件是否正常工作. 输入下面这个命令:

(lldb) lookup

你将会得到类似下面的输出:

Hello! the lookup command is working!

现在你可以创建并使用至少两个LLDB自定义命令. 耶, 你可以在一个命令里设置所有的内容, 但是我现在手动控制我脚本的加载.

lldbinit目录结构建议

我自己的lldbinit文件结构也许是有参考价值的. 尽管这一部分的内容不是必要的, 但是更多的是建议你如何组织你所有的LLDB的自定义脚本和内容.
我更倾向于保持我的~/.lldbinit文件尽可能简单并且使用一个像lldbinit.py一样的脚本加载特定目录下的所有内容. Facebook的fblldb.py做了同样的事情(具有同样的作用). 如果你有兴趣的话就把它下载下来看看.
我将那个目录添加到版本控制里这样在我需要将同样的逻辑转移到不同的电脑上的时候, 或者我做错了事情需要回滚的时候就很方便.
例如, 我实际的~/.lldbinit文件(在实际工作中而不是写这本书的时候)仅仅包含下面的内容:

command script import /Users/derekselander/lldb_repo/lldb_commands/
lldbinit.py
command script import /Users/derekselander/chisel/chisel/fblldb.py

lldb_repo是一个公有的git仓库https://github.com/DerekSelander/lldb, 这个仓库里包含了一些为你想工程设计的LLDB脚本.
我同样有Facebook的Chisel在版本控制上, 因此无论什么时候那些开发者更新了一些内容, 发布了一些有趣的版本, 我只需要pull一下Chisel仓库 https://github.com/facebook/chisel的最新版本我就可以拥有下次运行LLDB时需要的一切, 或者通过reload_script重新加载我的脚本.
在我的lldb_commands目录里, 我所有的Python脚本都作为两个text文件存放起来. 一个text文件叫做cmds.txt并保存着我所有的command regexcommandalias. 另一个text文件叫做settings.txt, 这个文件是我用来补充一些LLDB设置的.
例如, 此刻我的settings.txt文件的内容如下:

settings set target.skip-prologue false
settings set target.x86-disassembly-flavor intel

在这本书前面的章节你已经将这些设置添加到你的~/.lldbinit文件里, 但是我更愿意将这些LLDB的设置与我自定义的LLDB命令分开, 以便我检索~/.lldbinit文件里的命令的时候不会丢失.
然而, 在这本书里, 我选择将每一章的内容都单独作为一个脚本安装.这就意味着你需要将内容手动的添加到你的~/.lldbinit文件里以便你知道发什么事情. 当你看完这本书的时候你应该审视新的结构实现, 因为按照建议的这种布局做有几个好处. 这些好处如下:

  1. 调用reload_script仅显示这个命令~/.lldbinit正在加载; 它不会显示子脚本被加载了. 例如 这将会反馈lldbinit.py被加载了, 到那时不会反馈lldbinit.py文件本身被加载的内容.这让创建脚本变的更容易因为我经常用reload_script作为检查我最后使用的脚本的任何错误消息的方式. 执行reload_script后产生的输出越少, 在控制台中需要检查的错误就越少.
  2. 正如你注意到的, ~/.lldbinit文件里的内容越少你i就可以越轻松的在不同电脑间切换, 尤其是将它的内容添加到版本控制之后.
  3. 最后, 用这样的实现尽可能简单的添加新的脚本. 只需要将它们添加到lldbinit.py文件所在的目录里然后下一次它就会被加载. 备选方案是你手动的将脚本的路径添加到~/.lldbinit文件里, 如果你经常做这件事的话你会觉得它很烦人.
    在本章中你将使用将脚本粘贴到~/lldb目录下的方式然后将它们加载到LLDB中的这种实现策略去保存脚本...这一种方法更好一点, 对吧?
实现lookup 命令

正如你在前一章中看到的, 在lookup命令的背后foundation其实相当简单. 最主要的秘密是使用SBTarget的FindGlobalFunctions API. 在那之后, 你所需要做的就是用你喜欢的方式格式化这些输出.
你将继续使用Allocator项目, 在本章的starter目录下可以找到这个项目.
打开这个项目, 构建并在iPhone 7 plus模拟器上运行. 你将会使用这个项目测试本章中你新的lookup命令的查询功能.
项目运行起来之后, 暂停这个应用程序并进到LLDB中.
我的记忆有点模糊不清了. 这个FindGlobalFunctions需要指定哪个参数? 在LLDB中输入下面内容:

(lldb) script help(lldb.SBTarget.FindGlobalFunctions)

你将会得到下面的输出, 输出展示了方法的签名:

FindGlobalFunctions(self, *args) unbound lldb.SBTarget method
    FindGlobalFunctions(self, str name, uint32_t max_matches, MatchType
matchtype) -> SBSymbolContextList

因为他是一个Python的类, 所以你可以忽略第一个self参数. str参数的名字叫做name将会是你lookuo查询的内容. max_matches 指出你想要触发的最大数量. 如果你指定的数字是0, 它将会返回所有可用的匹配. matchType 参数是一个lldb Python枚举值, 用这个枚举值你可以执行不同类型的搜索, 比如正则或非正则.
因为正则搜索是唯一能行的通的方法, 所以你将要使用LLDB的枚举值lldb.eMatchTypeRegex.其他的枚举值可以在https://lldb.llvm.org/python_reference/_lldb%27-module.html#eMatchTypeRegex上找到. 是时候将这些功能实现在lookup.py脚本上了. 用你最爱的编辑器打开~/lldb/lookup.py. 在handle_command的末尾找到下面的代码:

# Uncomment if you are expecting at least one argument
# clean_command = shlex.split(args[0])[0]
result.AppendMessage('Hello! the lookup command is working!')

删除上面的代码, 用下面的代码替代它, 确保你的缩进保持原状:

#1
clean_command = shlex.split(args[0])[0]
#2
target = debugger.GetSelectedTarget()
#3
contextlist = target.FindGlobalFunctions(clean_command, 0, lldb.eMatchTypeRegex)
#4
result.AppendMessage(str(contextlist))

下面是对上面代码的解读:

  1. 包含传到这个脚本里的命令的干净的版本, 使用你在第二十章看到的同样的魔法.
  2. 通过SBDebugger抓取SBTarget实例.
  3. clean_command传递给FindGlobalFunctions API并调用FindGlobalFunctions. 你传入了0用了设置了结果没有数量上的上限并且传入了eMatchTypeRegex类型的匹配来使用正则表达式搜索.
  4. 你正在将contextlist转换到一个Python str然后将它添加到SBCommandReturnObject.
    回到Xcode, 用LLDB控制台重新加载脚本的内容:
(lldb) reload_script

试着运行一下lookup命令. 还记得你在上一章中研究的DSObjectiveCObject类吗? 通过LLDN提取出相关的所有内容:

lookup DSObjectiveCObject

你将会得到一些实际上看起来比image lookup -rnDSObjectiveCObject更糟糕的输出:

图片.png

使用LLDB的脚本命令来弄清楚进一步浏览了哪一个API:

(lldb) script k = lldb.target.FindGlobalFunctions('DSObjectiveCObject',0, lldb.eMatchTypeRegex)

这个命令将会重复lookup.py脚本所做的事情并且将SBSymbolContextList实例的值赋值给k. 在浏览API名字的时候我是短变量名字的粉丝--如果你没有注意到的话.
浏览SBSymbolContextList的文档:

(lldb) gdocumentation SBSymbolContextList

这条命令会提取出SBSymbolContextList实现的或者重写的搜索方法. 这里会有很多方法. 但是请将注意力放在__iter____getitem__.

图片.png

这对你的脚本来说是一个好事, 因为这意味着SBSymbolContextList是可迭代以及可索引的. 几秒钟之前, 你刚刚通过LLDB把一个SBSymbolContextList的实例赋值给了名字叫做k的变量.
在LLDB控制台中, 使用索引来抓取k对象里的子项.

(lldb) script k[0]

这相当于输入了script k.__getitem__(0).
你将会得到一些类似下面的输出:

 <lldb.SBSymbolContext; proxy of <Swig Object of type
'lldb::SBSymbolContext *' at 0x113a83780> >

很好理解! SBSymbolContextList持有一个由SBSymbolContext的组成的数组.
使用print命令获取这个SBSymbolContext的上下文:

(lldb) script print k[0]

你的输出与我的可能不同, 但是我获取到了代表[DSObjectiveCObject setLastName:]SBSymbolContext, 像下面这样:

 Module: file = "/Users/derekselander/Library/Developer/Xcode/
DerivedData/Allocator-czsgsdzfgtmanrdjnydkbzdmhifw/Build/Products/Debug-
iphonesimulator/Allocator.app/Allocator", arch = "x86_64"
CompileUnit: id = {0x00000000}, file = "/Users/derekselander/iOS/dbg/s4-
custom-lldb-commands/22.  Ex 1, Improved Lookup/projects/final/Allocator/
Allocator/DSObjectiveCObject.m", language = "objective-c"
   Function: id = {0x100000268}, name = "-[DSObjectiveCObject
setLastName:]", range = [0x0000000100001c00-0x0000000100001c37)
   FuncType: id = {0x100000268}, decl = DSObjectiveCObject.h:33,
compiler_type = "void (NSString *)"
     Symbol: id = {0x0000001e}, range =
[0x0000000100001c00-0x0000000100001c40), name="-[DSObjectiveCObject
setLastName:]"

你将会使用属性或者getter方法从SBSymbolContext里抓取函数的名字.
做这件事情最简单的方式是通过SBSymbolContextsymbol属性抓取SBSymbol.这里的SBSymbol里包含一个name属性, 这个属性会返回一个让你快乐的Python字符串.
在你的LLDB控制台中验证一下这个逻辑:

(lldb) script print k[0].symbol.name

在我这里, 我收到了下面的内容:

-[DSObjectiveCObject setLastName:]

这些信息足够构建出你的脚本了. 你将会迭代SBSymbolContextList里的内一个子项并打印出它找到的函数名字.
回到lookup.py脚本的头部并修改handle_command函数的内容. 找到下面的代码:

#3
contextlist = target.FindGlobalFunctions(clean_command, 0, lldb.eMatchTypeRegex)
#4
result.AppendMessage(str(contextlist))

用下面的代码来替换(缩进要正确!):

contextlist = target.FindGlobalFunctions(clean_command, 0,
lldb.eMatchTypeRegex)
output = ''
for context in contextlist:
    output += context.symbol.name + '\n\n'
result.AppendMessage(output)

现在你正在迭代SBSymbolContextList里返回的所有SBSymbolContext, 提取出函数的名字并用两个换行符来分割它们.
回到Xcode中, 然后重新加载你的脚本:

(lldb) reload_script

然后在LLDB中测试一下你更新以后的lookup命令:

(lldb) lookup DSObjectiveCObject

你将会得到一些比之前更漂亮的输出:

-[DSObjectiveCObject setLastName:]
-[DSObjectiveCObject .cxx_destruct]
-[DSObjectiveCObject setFirstName:]
-[DSObjectiveCObject eyeColor]
-[DSObjectiveCObject init]
-[DSObjectiveCObject lastName]
-[DSObjectiveCObject setEyeColor:]
-[DSObjectiveCObject firstName]

这就是全部的好东西, 但是我想看看这些函数在进程里的什么位置. 我想将所有的函数组合起来放到一个特定的模块里(一个SBModule), 当它们被打印出来的时候通过将模块的名字和这个模块被调用的次数放在头部分割开来.
回到lookup.py文件中. 现在你要创建两个新的函数.
第一个函数叫做generateFunctionDictionary, 这个函数将会带一个SBBreakpointContextList参数并且生成一个Python 列表的字典. 这些字典包含每个模块的keys. 字典中的value, 将会是每一个被调用的SBSymbolContext的Python列表.
第二个函数叫做generateOutput, 这个函数将会沿着你从OptionParser实例收到的options解析这些你刚才创建的字典.这个方法将会返回一个字符串并打印才控制台中.
lookup.py脚本中的handle_command函数下面开始按照下面的方式实现generateModuleDictionary函数:

def generateModuleDictionary(contextlist):
    mdict = {}
for context in contextlist: #1
        key = context.module.file.fullpath
#2
        if not key in mdict:
            mdict[key] = []
#3
        mdict[key].append(context)
    return mdict

下面是对代码的解读:

  1. SBSymbolContext里开始, 你正在抓取SBModule(module), 然后是SBFileSpec(file), 然后是fullPath的Python字符串并且将它赋值给名字叫做key的变量. 抓取fullPath是重要的(取而代之的是, say, SBFileSpecbasename属性, 因为这里同样的basename可能有多个模块).
  2. 那个mdict变量准备保存发现的所有符号, 通过模块隔开. 字典里的key将会是模块的名字, value将会是在那个模块里发现的符号的数组.在这一行, 你正在检查这个字典是否已经包含了这个模块的列表. 如果没有, 一个空的数组会被设置到这个模块的key上.
  3. 你正在将SBSymbolContext实例添加到这个模块合适的列表里. 你可以放心的假设mdict变量里的每一个key, 这里将至少有一个或者多个SBSymbolContext实例.
注意: 一个获取唯一的key的更简单的方法是使用`SBModule`里的**__str__()**方法(以及LLDB Python模块中的每一个类). 当你在其中一个对象上调用Python的**print**的时候就会被调用. 然而, 如果你只依靠`__str__()`你不会学习进程里的所有这些类, 属性和方法

generateModuleDictionary函数下面, 实现generateOutput函数:

def generateOutput(mdict, options, target): #1
output = ''
separator = '*' * 60 + '\n' #2
for key in mdict:
#3
count = len(mdict[key])
firstItem = mdict[key][0]
#4
moduleName = firstItem.module.file.basename
output += '{0}{1} hits in {2}\n{0}'.format(separator,
#5
      for context in mdict[key]:
          query = ''
          query += context.symbol.name
          query += '\n\n'
          output += query
return output

下面是对代码的解读:

  1. output变量将会是返回的包含传给SBCommandReturnObject的所有内容字符串.
  2. 枚举mdict字典中发现的所有的key.
  3. 这将会抓取array里子项的数量以及每一个数组里的第一个子项. 稍后你将使用这些信息查询模块的名字.
  4. 你正在抓取模块名字以便应用到每一段的头部输出.
  5. 这将会迭代Python数组中搜索的SBSymbolContext然后将名字添加到output变量.
    在你测试这个脚本之前还有最后一点要做.
    补充handle_command函数里的代码以便它可以使用你刚刚创建的这两个新的方法. 找到下面的代码:
output = ''
for context in contextlist:
    output += context.symbol.name + '\n\n'

并用下面的代码代替:

mdict = generateModuleDictionary(contextlist)
output = generateOutput(mdict, options, target)

你知道做什么.回到Xcode中; 重新加载LLDB中的内容:

(lldb) reload_script

检查你新改善的lookup命令:

(lldb) lookup DSObjectiveCObject

你将会得到一些类似下面的输出:

************************************************************
8 hits in Allocator
************************************************************
-[DSObjectiveCObject setLastName:]
-[DSObjectiveCObject .cxx_destruct]
-[DSObjectiveCObject setFirstName:]
-[DSObjectiveCObject eyeColor]
-[DSObjectiveCObject init]
-[DSObjectiveCObject lastName]
-[DSObjectiveCObject setEyeColor:]
-[DSObjectiveCObject firstName]

酷. 然后查看所有壹initWith开头并且包含两个参数的 Objective-C 方法.

(lldb) lookup initWith(\w+\:){2,2}\]

你会得到加载到Allocator进程里的所有的公有模块和私有模块的方法.

为lookup添加选项

你要保持选项很好很简单而且只实现两个不需要其他参数的选项.
你需要实现下面的内容:
• 添加加载地址到每一个查询上. 这是理想如果你想知道函数在内存中的实际位置.
• 仅仅提供一个模块的简介. 不要产生函数名字, 只列出每一个模块调用的数量.
__generate_script为在lookup.py文件底部发现的generateOptionParser方法添加了一些占位符. 在generateOptionParser函数中, 改变这个函数以便它包含下面的代码:

def generateOptionParser():
    usage = "usage: %prog [options] code_to_query"
    parser = optparse.OptionParser(usage=usage, prog="lookup")
    parser.add_option("-l", "--load_address",
          action="store_true",
          default=False,
          dest="load_address",
          help="Show the load addresses for a particular hit")
    parser.add_option("-s", "--module_summary",
          action="store_true",
          default=False,
          dest="module_summary",
          help="Only show the amount of queries in the module")
    return parser

这里不需要深入解读这些代码因为你已经在前面的章节学过这些东西. 你已经创建了两个支持的选项, -s, 或者--module_summary以及-l, 或者--load_address.
首先你要实现加载地址的选项. 在generateOutput函数中, 找到以context in mdict[key]:开头的for循环迭代SBSymbolContext的代码.
按照下面的方式修改for循环:

 for context in mdict[key]:
    query = ''
    #1
    if options.load_address: 
        #2
         start = context.symbol.addr.GetLoadAddress(target)             end = context.symbol.end_addr.GetLoadAddress(target)             #3
startHex = '0x' + format(start, '012x')
endHex = '0x' + format(end, '012x')
query += '[{}-{}]\n'.format(startHex, endHex)
query += context.symbol.name
query += '\n\n'
output += query

下面是这些代码做的事情:

  1. 你添加了一个条件句来判断load_address选项时候被设置了. 如果被设置了, 这将添加内容到输出上.
  2. 这种从SBSymbolContextSBSymbol(symbol属性)到SBAddress(addr或者end_addr)然后通过GetLoadAddress获取Python long. 这里实际有一个load_addr变量给SBAddress, 但是我已经及时的发现了一个bug, 因此我已经默认使用GetLoadAddressAPI替代. 这个方法期望SBTarget作为一个输入参数.
  3. 在你有了Python long的起始表达式和结束表达式之后, 你已经讲他们格式化的很漂亮并且用Python的format函数组合在一起. 如果需要会补上0, 注意它应该是12个数字的长度, 并且将它格式化为十六进制.
    保存你的工作成果并且重新查看Xcode和LLDB控制台. 重新加载.
(lldb) reload_script

测试一下你的新选项:

(lldb) lookup -l DSObjectiveCObject

你将会得到一些类似下面的输出:

************************************************************
8 hits in Allocator
************************************************************
[0x0001099d2c00-0x0001099d2c40]
-[DSObjectiveCObject setLastName:]
[0x0001099d2c40-0x0001099d2cae]
-[DSObjectiveCObject .cxx_destruct]

在这个列表中的一个地址上设置一个断点看看它是否与函数相匹配.
按照下面的方式做, 用你列表中的地址替换下面的地址:

(lldb) b 0x0001099d2c00
Breakpoint 3: where = Allocator`-[DSObjectiveCObject setLastName:] at
DSObjectiveCObject.h:33, address = 0x00000001099d2c00

做得好!你已经实现了一个多选项的命令!
在最后重新看一下generateOutput.找到下面这一行代码:

moduleName = firstItem.module.file.basename

在这一行代码的后面加上下面的内容:

if options.module_summary:
    output += '{} hits in {}\n'.format(count, moduleName)
    continue

这写代码简单的添加了每一个模块调用的次数并且跳过了了添加实际符号.
就是这些.没有更多的代码.保存, 然后回到Xcode中重新加载你的脚本:

(lldb) reload_script

测试一下你的module_summary:

(lldb) lookup -s viewWillAppear

你将会得到一些类似下面的代码:

46 hits in UIKit
1 hits in WebKit
4 hits in Allocator

就是这些!你已经做完了!你已经从草稿中创建了一个更强大的脚本. 在后面的章节中你将使用这个脚本搜索代码. 当你制定一个宽泛的搜索范围的然后想将范围进一步缩小的时候summary选项是一个强大的工具.

我们为什么要学这些?

这里还有更多的选项可以添加到lookup命令上. 你可以通过在SBSymbolContextSBFunction(通过function属性)的后面创建一个-S或者-Swift_only选型
去访问GetLanguage()API.
当你实现了之后, 你也应该添加一个-m或者--module选项过滤出某个模块的内容.
如果你想看看还有哪些选项可用, 可以在https://github.com/DerekSelander/LLDB/blob/master/lldb_commands/lookup.py查看我实现的lookup命令.
享受添加的这些选项吧!

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

推荐阅读更多精彩内容