iOS高级调试&逆向技术-汇编寄存器调用约定教程

前言

本文翻译自[Assembly Register Calling Convention Tutorial](https:/
![Uploading DebuggingBookFreeTut_1-feature-1_447331.png . . .]
/www.raywenderlich.com/161938/assembly-register-calling-convention-tutorial)
翻译的不对的地方还请多多包涵指正,谢谢~

序言


通过本教程,你会可以看到CPU使用的寄存器,并探索和修改传递给函数调用的参数。还将学习常见的苹果计算机架构以及如何在函数中使用寄存器。这就是所谓架构的调用约定
了解汇编是如何工作的,以及特定架构调用约定是如何工作是一项极其重要的技能。它可以让你在没有源码的情况下,观察和修改传递给函数的参数。此外,因为源码存在不同或未知名称的变量情况,所以有时候更适合使用汇编。
比如说,假设你总想知道调用函数的第二个参数,不管参数的名称。汇编知识为你提供一个很好的基础层来操作和观察函数中的参数。

汇编

等等,汇编是什么?
你有没有停在一个没有源码的函数中,你会看到一系列内存地址,后面跟着一些吓人的短命令?你拥抱成球轻声在耳边私语告訴自己你从來不看这些东西?嗯...这些东西就是所谓的汇编!

这是一张Xcode里的回溯图片,它展示了模拟器里的汇编函数。

xcode_assembly

看上面的图片,这个汇编可以分成几个部分部分。每一行的汇编指令都包含一个操作码,它可以被认为是非常简单的计算机指令。
那么操作码看起来像什么样子呢?一个操作码执行计算机中的一个简单的任务的指令。比如,思考下面的汇编代码段:

pushq   %rbx           
subq    $0x228, %rsp            
movq    %rdi, %rbx             

在这个汇编块中,你会看到三个操作码,pushqsubqmovq。思考下这些操作码执行的动作。操作码后面是来源和目标的标签。这些就是操作码行为项。
在上一个例子中,有一系列 寄存器 ,分别是 rbxrsprdi,在每个%后面的都称为寄存器。
另外,你可以找到16进制的常量如0x228。这个$后面的常量都为绝对数。
目前都不需要知道这些代码在做什么,因为你首先需要了解函数的寄存器和调用约定。

注意:在上面例子中,寄存器和常量之前有一堆%和$。这是一种怎样的表达方式。然而,有两种主要方式展示汇编 。第一种是Inter汇编,第二种是AT&T汇编。

默认的,苹果反汇编工具库显示的是AT&T格式。正如上面例子中,虽然这是一种很好的格式,但可以肯定它有一点困难。

x86_64 vs ARM64

作为apple平台的开发者,当你学习汇编时,将会处理两种主要的汇编架构:x86_64 架构和 ARM64 架构,x86_64可能是你的macOS计算机架构,除非你运行在比较旧的电脑上。
x86_64是一种64-bit的架构,意味着每个地址可以容纳64个1和0。另外,老的苹果电脑使用32-bit架构,但苹果在2010年已经停止生产32位的计算机了。程序运行在MacOS下可以兼容64位,包括模拟器程序。也就是说,即使你是x86_64的MacOS,它仍然可以运行32位程序。
如果你对工作的硬件的架构表示任何的疑惑,可以在终端运行如下命令:

unmae -m

ARM64 架构使用在移动设备如iPhone,控制电量消耗是最重要的。
ARM 强调电源保护,所以它减少了一些操作码,这助于在复杂汇编指令下的能源消耗减少。这对你来说是个好消息,因为在ARM架构上学习的指令更少。
下面是前面显示的相同方法的截图,这一次是跑在iPhone 7的ARM64位汇编下:

xcode_arm_assembly

在他们的这么多设备中,但后来都移动到64位ARM处理器。32位设备几乎过时了,因为Apple已经通过各种iOS版本淘汰了他们。比如iPhone 4s是32位设备已经不支持iOS 10。在32位iPhone系列中剩下的只有iPhone 5支持iOS 10。
有意思的是,所有的Apple手表目前都是32位。这很可能是因为32位ARM CPU通常比它们的64位兄弟有更小的功率。这对手表很重要,因为电池很小。

x86_64 寄存器调用约定

你的CPU使用一组寄存器处理运行中的数据。这些是存储设备,就像你计算机里的内存。然而它们的位于CPU本身,非常接近CPU部分。所以CPU访问它们的时候非常快。
大多数指令涉及一个或多个寄存器,并执行操作。就像写寄存器到内存中,读内存的内容到寄存器,或在两个寄存器上执行算术操作(加减等等)。
x64(这里开始,x64是x86_64的缩写),有16个通用寄存器的机器用来操纵数据。
这些寄存器分别是 RAXRBXRCXRDXRDIRSIRSPR8R15。你现在可能并不清楚这些名字的含意,但你很快就会探索这些重要的寄存器。
当你在x64下调用函数,这种方式和使用寄存器,后面有非常具体的约定。这决定了函数的参数应该在哪里,在函数完成时函数的返回值在哪里。这很重要,因为用一个编译器编译的代码可以使用另一个编译器编译的代码。
举个例子,看一下下面这个OC代码:

NSString *name = @"Zoltan";
NSLog(@"Hello world, I am %@. I'm %d, and I live in %@.", name, 30, @"my father's basement");

它有四个参数传递到NSLog函数调用,有些变量是直接访问的,有一个参数是定义在本地变量中,然后引用参数在函数里。然而,通过汇编看代码时候,计算机不会关心变量的名称,它只关心内存中的地址。
下面的寄存器在x64汇编下作为函数调用时的参数。试着把这些内存提交他们到内存中,因为将来,你会经常使用这些内存。

  • 第一个参数:RDI
  • 第二个参数:RSI
  • 第三个参数:RDX
  • 第四个参数:RCD
  • 第五个参数:R8
  • 第六个参数:R9

如果超过六个参数,在函数里就会通过栈来访问额外的参数。
返回到上面的OC例子中,你可以重新定义寄存器就像下面的伪代码:

RDI = @"Hello world, I am %@. I'm %d, and I live in %@.";
RSI = @"Zoltan";
RDX = 30;
RCX = @"my father's basement";
NSLog(RDI, RSI, RDX, RCX);

当NSLog函数开始,这些寄存器会包含适当的值。如上图所示。
不管如何,当函数序言(function prologue)(准备栈和寄存器的函数开始部分)完成执行,这些寄存器上的值很有可能就会改变。通常在代码不需要它们的时候,汇编将会重写这些值,或简单的丢弃引用。
意味着当你离开函数时开始(通过stepping over,stepping in, or stepping out),你再也不能假设寄存器将保留你希望观察到的值,除非你实际看到汇编代码它正在做什么。
这个函数调用严重影响你的调试(断点)策略,你是否想自动化任何类型的中断去探索,你应该停止在函数调用之前,以便检查或修改参数,而不是真正到达汇编里。

Objective-C 和 寄存器

寄存器使用具体的调用约定。你可以使用相同的知识应用在其它语言中。
当OC执行方法内部,其实是通过一个具体的名为 objc_msgSend 的C函数来执行。这实际上函数有几种不同的类型,稍后再谈。这是消息转发的核心。第一个参数,objc_msgSend引用发送消息的对象。然后是 selector,这是一个简单的char *指定的在对象上执行的函数名称。最后,objc_msgSend采用可变参数在函数里。
让我们看个iOS环境上的实际例子:

[UIApplication sharedApplication];

编译器会把代码转成如下伪代码:

id UIApplicationClass = [UIApplication class];
objc_msgSend(UIApplicationClass, "sharedApplication");

第一个参数引用是UIApplication类,紧接着是 sharedApplication 的selector。
告诉参数的一个简单方法是检查selector的冒号。每个冒号代表跟随一个参数。
这是另一个OC例子:

NSString *helloWorldString = [@"Can't Sleep; " stringByAppendingString:@"Clowns will eat me"];

编译器会转成如下伪代码:

NSString *helloWorldString; 
helloWorldString = objc_msgSend(@"Can't Sleep; ", "stringByAppendingString:", @"Clowns will eat me");

第一个参数是实例NSString(@"Can't Sleep; "),紧接着是selector,最后是一个参数,也是NSString实例。
使用objc_msgSend知识,你可以使用x64寄存器帮助探索上下文,这是一种捷径。

理论到实际

你可以下载教程项目在这
在这章,你将使用项目提供的教程资源bundle调用寄存器,打开项目在Xcode里,并运行它。

registers

这是一个相当简单的应用程序,仅仅显示x64寄存器的内容。重要的是要注意,这个应用程序不能在任何给定的时刻显示寄存器的值,它只能显示在指定函数调用时寄存器的值。意味着当函数使用寄存器的值进行调用时,你不会看到太多寄存器变化的值。
现在你将会理解macOS应用程序功能行为的寄存器,创建一个NSViewControllerviewDidLoad方法符号断点。推荐使用"NS"代替"UI",因为你正在运行Cocoa程序。

xcode_symbolic

构建然后返回应用程序,第一次断点停止,在LLDB控制台里输入:

(lldb) register read

在执行状态暂停,会显示主要寄存器的列表。无论如何,这些信息在多了。你应该有选择地输出寄存器和修复他们成为OC对象。
如果你重新调用,-[NSViewController viewDidLoad] 将会转换成如下汇编伪代码:

RDI = UIViewControllerInstance 
RSI = "viewDidLoad"
objc_msgSend(RDI, RSI)

记住x64调用约定,了解 objc_msgSend 的执行,你可以找到被加载具体的NSViewController实例。
在LLDB控制台输入:

(lldb) po $rdi

你将会得到输出:

<Registers.ViewController: 0x6080000c13b0>

这将会输出隐藏在RDI寄存器中的NSViewController引用,你知道,对于函数这是第一个参数。
在LLDB里,重要的是$前缀是寄存器,所以LLDB知道你想要寄存器的值,而不是当前源码范围内的变量。是的,这与在反汇编视图中看的汇编不同!讨厌。嗯?

注意:细心观察当你OC停止方法时,你从没看到 objc_msgSend 在LLDB的回溯里,这是因为objc_msgSend这类函数执行是 jmp ,或是是跳转操作码的汇编指令。这个意思是objc_msgSend行动就像跳转函数,一但OC代码开始运行,所有有关 objc_msgSend 历史的栈都会被优化。这种优化称为 尾部调用优化.

尝试输出RSI寄存器,希望包含被调用的selector,输出以下内容在LLDB中:

(lldb) po $rsi

不幸的是,你获得了垃圾输出看起来像这样:

140735181830794

为什么是这样?
OC selector本质上是 char *。这意味着,像所有的C类型,LLDB并不知道应用什么样式来展现数据。结果,你必须明确地转换成你想要的数据类型。
尝试转换成正确的类型:

(lldb) po (char *)$rsi

现在你得到了你的预期:

"viewDidLoad"

当然,你也可以输出Selector类型,产生同样的结果:

(lldb) po (SEL)$rsi

现在,是时候探索OC方法的参数了,从你停止在viewDidLoad,你可以安全的假设NSView实例已经被加载了。一个有趣的方法是mouseUp:方法的实现是在NSView的父类NSResponder。
在LLDB,创新一个NSRespondermouseUp:断点,然后继续执行。如果你不记得怎么做,这里有个命令行你可能需要:

(lldb) b -[NSResponder mouseUp:]
(lldb) continue

现在,点击应用程序窗口,确认点击是NSScrollView的外面,否则会被NSScrollView吞噬你的点击,断点将不会触发。

touch_outside

一量用鼠标或触控板点击,LLDB会停止在mouseUp:断点。通过输出接下来的内容到控制台,来输出引用的NSResponder:

(lldb) po $rdi

接着你会得到类似地输出:

<NSView: 0x608000120140>

无论如何,这是一个有趣的selector,它包含冒号在里面,意味着他有参数可以探索!输出以下内容到LLDB控制台中:

(lldb) po $rdx

你将获得有关NSEvent的描述:

NSEvent: type=LMouseUp loc=(351.672,137.914) time=175929.4 flags=0 win=0x6100001e0400 winNum=8622 ctxt=0x0 evNum=10956 click=1 buttonNumber=0 pressure=0 deviceID:0x300000014400000 subtype=NSEventSubtypeTouch

为什么称它为NSEvent?嗯,你可以看在线文档关于-[NSResponder mouseUp:]或者你可以简单使用OC来获得类型:

(lldb) po [$rdx class]

完美,嗯?
有时候使用寄存器和断点是很有用的,以便获取已经内存中的对象引用。
举例来说,如果你想把前置的NSWindow变成红色,但你代码中没有此视图的引用,同时你也不想重新编译任何代码的改变?你可以简单的创建一个断点,从寄存器和操作实例对象来获得引用。你可以尝试着改变主窗口成红色。

注意:尽管每个 NSResponder 实现了 mouseDown:NSWindow 通过继承重载此方法。你可以不通过源码方式找出所有实现了mouseDown:方法的类,确定哪些继承了 NSResponder 的类。 举个输出所有实现了 mouseDown: 的Objective-C类的例子: image lookup -rn '\ mouseDown:

首先移除所有的之前的断点

(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] 

然后输出以下

(lldb) breakpoint set -o -S "-[NSWindow mouseDown:]"
(lldb) continue

这会设置一个断点,它只触发一次 —— 一次性断点。

点击应用程序,点击之后立即就会触发断点。然后输入以下在LLDB控制台:

(lldb) po [$rdi setBackgroundColor:[NSColor redColor]]
(lldb) continue 

恢复断点,NSWindow就会变成红色!

red_console

Swift 和 寄存器

当在Swift探索寄存器的时候,相比较OC而言,你将会遇到两个方面的困难。

  1. 首先,寄存器 不能 用在Swift调试上下文。意味着你无论想要获得什么数据,你得使用OC调试上下文输出寄存器。记住你可以使用 expression -l objc -O --命令。幸运的是 register read 命令是可以在Swift环境中使用的。
  2. 第二,Swift相比较OC并不是动态语言。事实上,有时候最好假设Swift像C语言,除了有一个非常非常暴躁和专横的编译器。如果你有内存地址,你需要明确地转换成你期望的对象,否则,Swift调试环境并不知道解释内存地址。

也就是说,Swift也使用了相同的寄存器调用约定。无论如何,这是一个非常重要的不同点。当Swift调用函数,它不需要使用objc_msgSend,除非你标记方面为dynamic。意味着Swift调用函数,之前RSI寄存器关联的是selector,而实际上是函数的第二个参数。
理论足够了--是时候该行动了。
在Registers项目中,导航到ViewController.swift,然后增加相关的函数在类里:

func executeLotsOfArguments(one: Int, two: Int, three: Int,
                            four: Int, five: Int, six: Int,
                            seven: Int, eight: Int, nine: Int,
                            ten: Int) {
        print("arguments are: \(one), \(two), \(three), \(four), \(five), \(six), \(seven), \(eight), \(nine), \(ten)"
    )
}

现在,在 viewDidLoad 中,调用该函数与相应的参数:

override func viewDidLoad() {
  super.viewDidLoad()
  self.executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4,
                              five: 5, six: 6, seven: 7,
                              eight: 8, nine: 9, ten: 10)
}

放置一个断点在 executeLotsOfArguments 函数调用的行上,然后调试器会停在最开始函数的地方。这很重要,否则在函数执行的时候寄存器将会被破坏。

然后移除之前的断点。

构建和运行app,然后等待 executeLotsOfArguments 断点让程序暂停。
再次,一种好的方式是开始调查输出寄存器列表,在LLDB中,输出以下:

(lldb) register read -f d

这会输出寄存器并使用数字格式(-f d)显示。输出看起来是这样:

General Purpose Registers:
       rax = 7
       rbx = 9
       rcx = 4
       rdx = 3
       rdi = 1
       rsi = 2
       rbp = 140734799801424
       rsp = 140734799801264
        r8 = 5
        r9 = 6
       r10 = 10
       r11 = 8
       r12 = 107202385676032
       r13 = 106652628550688
       r14 = 10
       r15 = 4298620128  libswiftCore.dylib`swift_isaMask
       rip = 4294972615  Registers`Registers.ViewController.viewDidLoad () -> () + 167 at ViewController.swift:16
    rflags = 518
        cs = 43
        fs = 0
        gs = 0

正如你看到的,寄存器跟随x64位的调用约定。RDIRSIRDXRCXR8R9 持有了你的六位参数。

你还可能注意到其他参数存储在其它寄存器中。虽然这是真的,但它只是代码的剩余部分,它为其余参数设置到栈上。记住,第六个参数之后的都在栈上。

RAX,保存返回值的寄存器

等等--还有更多!到目前为止,你已经了解如何在函数中调用六个寄存器,但是有关返回值的呢?

幸运的是,只有一个函数返回值的寄存器:RAX。返回到 executeLotsOfArguments 然后修改函数返回为 String ,像这样:

func executeLotsOfArguments(one: Int, two: Int, three: Int,
                            four: Int, five: Int, six: Int,
                            seven: Int, eight: Int, nine: Int,
                            ten: Int) -> String {
    print("arguments are: \(one), \(two), \(three), \(four), \(five), \(six), \(seven), \(eight), \(nine), \(ten)")
    return "Mom, what happened to the cat?"
}

viewDidLoad,修改函数调用接收并忽略字符串的值。

override func viewDidLoad() {
    super.viewDidLoad()
    let _ = self.executeLotsOfArguments(one: 1, two: 2,
          three: 3, four: 4, five: 5, six: 6, seven: 7,
          eight: 8, nine: 9, ten: 10)
}

创建断点在 executeLotsOfArguments 的任意地方。再次构建和运行。然后在函数里等待执行到暂停。下一步,输出以下到LLDB控制台:

(lldb) finish

它会完成执行当前函数并再次暂停调试器。此刻,函数返回值应该已经在 RAX。输出以下内容在LLDB中:

(lldb) register read rax 

你会得到相似的结果:

rax = 0x0000000100003760  "Mom, what happened to the cat?"

找到了!你返回的值!

了解返回值在 RAX 中是非常重要的,你将会在下个段落写函数的调试脚本。

通过寄存器改变返回值

为了巩固对寄存器的理解,你将修改已编译应用程序中的寄存器。

关闭Xcode和Registers项目。打开终端窗口然后运行iPhone 7模拟器,像下面这样输入

xcrun simctl list

你会得到一个很长的设备列表,找到最后一个iOS版本的模拟器。在下面找到iPhone 7设备。看起来像是这样:

iPhone 7 (269B10E1-15BE-40B4-AD24-B6EED125BC28) (Shutdown)

这个UUID你将会在后面用到。使用下面命令打开模拟器并替换你的UUID:

open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID 269B10E1-15BE-40B4-AD24-B6EED125BC28

确认模拟器已经被运行,并到主屏幕中。你可以通过快捷键(Command + Shift + H)到主屏幕。一旦模拟器运行完成。在终端窗口后面写上如下命令设置到SpringBoard程序:

lldb -n Spring Board

这个把正在iOS模拟器中运行的SpringBoard实例,绑定到LLDB上!SpringBoard是一个在iOS上控制主屏幕的应用程序。

输入如下命令在LLDB:

(lldb) p/x @"Yay! Debugging"

你会获得相似的输出:

(__NSCFString *) $3 = 0x0000618000644080 @"Yay! Debugging!"

记录下这个新创建的NSString实例的内存引用地址,你马上就会用到它。现在创建一个断点在UILabelsetText:方法:

(lldb) b -[UILabel setText:]

下一步输入以下:

(lldb) breakpoint command add 

LLDB变成支持多行的编辑模式。这个命令让你增加额外的命令当你想命中断点时来执行。输入以下,并替换内存地址,像这样:

> po $rdx = 0x0000618000644080
> continue
> DONE

回过头来看一下刚刚做的事情,你已经创建了一个断点在 UILabelsetText: 方法上。当此方法被命中,你会替换 RDX —第三个参数—不同的字符串实例"Yay!Debugging!"。

恢复调试使用 continue 命令:

(lldb) continue

尝试探索SpringBoard模拟器app,会看到文本内容发生改变。划动手指从下往上拉出控制中心。然后观察改变:

yay_debugging

尝试浏览其它新弹出的地方,因为这可能会使视图被延迟加载,导致断点行动被命中。

yay_debugging2-1

虽然这可能看起来很酷的编程技术,提供了一种在有限的寄存器和汇编知识情况下,没有源码就可以产生巨大的变化。
从调试观点来看这很有用,你可以快速在视觉上进行验证,当 -[UILabel setText:] 在SpringBoard应用执行和运行在断点条件下去找精确的代码行去设置指定UILabel的文本。
继续这个想法,有些 UILabel 实例的文本并没有改变也告诉你一些事情。比如说,UIButton的文本并没有改变。也许 UILabelsetText:在早期就被调用过?或者可能开发SpringBoard程序的开发者选择使用setAttributtedText:来代替?或者他们使用还未公开给第三方开发者的私有函数?
正如你看到的,使用和操作寄存器可以提供给你很多的观察力,去了解应用程序的函数。:]

何去何从?

好了!这篇文章很长,不是吗?坐下来休息一下喝杯饮料;你获得了它。
你可以下载完整的项目从这篇教程的这里

你学习了什么?

  • 架构定义调用约定了哪个指令哪位参数到函数和它返回的值被保存。
  • 在Objective-C中,RDI 寄存器用来引用调用的对象,RSI 是selector,RDX 是首个参数等等。
  • 在Swift中,RDI 是第一个参数,RSI第 二个参数,然后等等,只要Swift方法不使用动态调用(dynamic dispatch)。
  • RAX 寄存器为函数返回值使用,不管你是用在用OC还是Swift。
  • 当使用$打印寄存器的时候,确认当前环境是OC。

你可以用寄存器做很多事情。尝试探索没有源码的app;这很有趣,为解决调试问题打下了良好的基础。
尝试在iOS模拟器和地图的UIViewControllers一样出现使用汇编、智能的断点和断点命令。

文章已同步到我的博客

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

推荐阅读更多精彩内容