匯編寄存器調用約定教程

前言

本文翻譯自Assembly Register Calling Convention Tutorial
翻譯的不對的地方還請多多包涵指正,謝謝~

匯編寄存器調用約定教程

在本篇教程,你將看到CPU使用的寄存器并探索改變傳入函數的參數。你將學到普通的蘋果計算機架構及在函數內寄存器是如何使用的。這就是被稱為架構的調用約定。

了解匯編如何工作及特定架構的調用約定的工作方式是一項非常重要的技能。它能讓你觀察沒有源碼的函數參數,并改變其參數。除此外,有些時候探尋匯編層面可能更好,因為你的源碼可能有不同的或者未知的參數名稱。

例如,假設你總是希望知道函數的第二個參數是什么,而不需要知道它的參數名稱。匯編知識能讓你在站在一個極佳的層面來操縱或觀察函數參數。

匯編101

等等,那么匯編又是甚?

你曾在沒有源碼的函數面前束手無策,或看到惡心的內存地址后面跟著一個可怕的短命令?是否曾躲在一個球內悄悄告訴自己再也不要看到這些密集的東西?好吧。。。這些就是所謂的匯編。

下面是一張Xcode的回溯圖,展示的是模擬器內一個函數的匯編。

image

看以上圖片,匯編可以分成若干部分。每行匯編指令包含一個操作符,可以把它想象成計算機的一個極簡的操作指令。

操作符是什么?它是計算機中可以執行簡單任務的指令。例如,看如下匯編片段:

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

在這段匯編塊中,可以看到有三個操作符:pushq, subq, movq。將其當做要執行的動作。操作符后面的是源或結果標簽。這些就是操作符要操縱的對象。

上述例子中,有幾個寄存器,用rbx,rsp,rdi,rbp來表示。%作為前綴表明這是一個寄存器。

此外,你也會發現一個十六進制的數字常量:ox228$表明這個常量是一個絕對數值。

沒必要知道這些代碼正在做什么事,因為首先你首先需要學習寄存器和函數調用約定。

注:上述例子中,注意在寄存器和常量之前有一堆% $這樣的字符。這是反匯編程序如何格式化程序的。但是,有兩種主要的方式來展現匯編。第一個時Intel,第二個是AT&T匯編。

默認地,蘋果反匯編工具帶有AT&T匯編格式的程序,就如上面的例子一樣。盡管這是一個很好的工作格式,但誠然令人頭疼。

x86_64 vs ARM64

作為蘋果平臺的開發者,在學習匯編中你會遇到兩種主要的架構:x86_64和ARM64架構。x86_64架構一般使用的macOS的電腦上,除非你還在使用古老的Mac。

x86_64是64位的架構,意味著每個地址都持有了64個1或者0位。老款Mac使用的是32位架構,但蘋果在2010年就停止使用32位的了。在macOS下的程序都是64位兼容的,包括模擬器。也就是說,x86_64系統可以跑32位的應用程序。

如果你對現有硬件系統架構有任何疑問,可以通過以下shell命令獲取計算機的硬件架構:

uname -m

ARM64架構被使用在移動設備上比如你的iPhone,在這些設備中的能量消耗限制非常嚴格。

ARM架構強調能量保護,因此相對復雜匯編指令,它有消減過后的操作集來幫助其減少能量消耗。這對你來說是好事,因為這意味著在ARM架構中你要學習的指令更少。

以下是以上同一個函數的截圖,只不過這次是iPhone的ARM64匯編:

image

在許多它們的設備匯總,很多都加入到了64位的ARM處理器。Apple階段性地通過iOS版本升級將32位設備淘汰掉。例如,iPhone4s是32位的設備在iOS10就已經不支持升級了。iOS10系統支持的32位設備只有iPhone5了。

有趣的是,所有Apple Watch設備都是32位的。可能是因為32位的ARM CPU消耗更少的能量吧。這確實非常重要因為Watch的電池非常小。

考慮到最好是將重點集中到未來你需要的事情上,“Advanced Apple Debugging & Reverse Engineering”(英文書)將集中在64位的兩種架構中。此外,你將首先學習x86_64然后轉移到ARM64匯編,這樣就不會懵逼。嗯,不會太懵逼_

x86_64寄存器調用約定

在運行的程序內CPU使用一組寄存器操作數據。它們是存儲器,類似于計算機上的RAM。寄存器就在CPU上,且跟CPU使用它們的部分非常近。所以CPU上使用它們的組件訪問寄存器相當快。

大多數指令涉及到一個或多個操作符,比如將寄存器的內容寫入內存,讀取一塊內存到寄存器,或者在兩個寄存器上執行算術操作(加,減等)。

x64(從這里開始,x64就是x86_64的縮寫),有16個通用寄存器用于機器操作數據。

這些寄存器是RAX, RBX, RCX, RDX, RDI, RSI, RSP, RBP, R8, R15。這些名字現在沒什么意義,但待會你就知道他們每一個的重要性了。

當你在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
  • 第四個參數:RCX
  • 第五個參數: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函數一開始,以上的寄存器就會想上面展示的一樣包含合適的值。

但是,只要函數開場(函數開始的階段--準備棧和寄存器)完成執行,寄存器內的值很可能會變化。生成好的匯編碼可能重寫寄存器內的值,或是簡單的拋棄掉這些引用,因為這些代碼可能不需要了。

這意味著一旦你過了函數的開場階段(通過stepping over,stepping in或者stepping out),就不再能假設寄存器內的值是你期望監測的值了,除非你正好看到匯編代碼能知道它做了些什么。

這種調用約定嚴重影響你調試or斷點策略。如果你讓任何斷點自動化,將不得不在函數開始時打斷點為了觀察或者改變參數,而不需要真正地深入到匯編層面。

Objective-C和寄存器

寄存器使用特殊的調用約定。你也可以使用那些知識應用到其他的語言上。

當OC執行方法時,一個特殊的叫objc_msgsendC函數被調用。實際上有多個不同類型這樣的函數,但稍后會更多。這是消息轉發的核心。作為第一個參數,objc_msgsend持有了消息被發送的對象引用。接著第二個參數是selector,它僅僅就是一個char *指定了該對象的要調用的方法名。最后,objc_msgsend持有了一個可變參數,如果selector指定了其他參數。

讓我們來看看一個真實的iOS中的例子:

[UIApplication sharedApplication];

編譯器將編譯這段代碼并生成如下偽代碼:

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

第一個參數是UIApplication類,之后是sharedApplication函數名。一種非常簡單的方式計算函數的參數個數,就是數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類的一個實例,之后是selector,跟著是NSString類的實例參數。

使用objc_msgSend的知識,你可以在x64中使用寄存器來探索內容,你很快會完成它。

理論運用于實踐

可以下載 教程開始的工程

本段,你將會使用到本教程資源提供的名為Registers的工程。
用Xcode打開該工程并運行:

image

這是一個非常簡單的程序,只是展示了x64系統的一些寄存器內容。注意到程序并不能把每一時刻寄存器內容都展示出來,它僅僅展示了一個特殊函數調用過程中的寄存器值。這意味著你不會看到寄存器值太多的變化,因為它們很可能在這個抓取寄存器值函數調用時有著相同的值。

現在你已經大概了解這個mac系統應用程序的功能,那么在NSViewControllerviewDidLoad 方法上打上一個斷點吧。記得使用NS而不是UI開頭,因為你正在寫一個Cocoa應用程序。

image

編譯并運行。一但調試器停止,在LLDB控制臺輸入如下指令:

(lldb) register read

該命令會列出在程序執行暫停的時候主要的寄存器。但,這實在是太多信息了。你應該選擇性地打印出寄存器并將它們視為OC(Objective-C)對象。

如果你稍微回想一下, -[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代碼中打斷點,在LLDB的回溯內看不到objc_msgSend的影子。這是因為objc_msgSend方法簇執行了jmp,或者在匯編內的跳轉命令。意思就是說objc_msgSend扮演了中轉的角色,一但OC代碼開始執行,所有的關于objc_msgSend的棧中的回溯都將消失。這是一種叫做尾遞歸調用的優化。

試試打印下RSI,這很可能是方法的selector。輸入如下指令:

(lldb) po $rsi

不幸的是,你會看到的類似于這樣的垃圾輸出:

140735181830794

為什么嘞?

selector其實就是char *。也就是說,像其他C類型一樣,LLDB不知道怎么格式化這個數據。最后,你必須顯示地將這塊引用轉換成你希望的數據結構。

試試將它強轉成正確的類型:

(lldb) po (char *)$rsi 

你會看到:

"viewDidLoad"

必然,你也可以將它轉成Selector類型,并得到相同的結果:

(lldb) po (SEL)$rsi

現在呢,是時候看看帶有參數的OC方法了。因為你斷點停在了viewDidLoad方法,你可以假定NSView示例已經加載完畢。一個有趣的方法叫mouseUp:,它是NSView的父類NSResponder實現的。

在LLDB內,創建一個NSRespondermouseUp:斷點并繼續執行(resume)。如果你不知道怎么操作,以下命令可以使用:

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

現在,點擊應用窗口。并且保證你點擊的區域不在NSScrollView之內,因為NSScrollView會捕獲你的點擊,那么-[NSResponder mouseUp:]斷點就到不了了。

image

一旦你放開鼠標或者觸控板,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?ok,你可以查看[NSResponder mouseUp:]的開發文檔,或者簡單地使用OC方法獲取它的類型:

(lldb) po [$rdx class]

非常酷??,是不是~

有時候為了知道在內存中一個對象的指針,使用寄存器或者斷點是非常有用的。

例如,如果你想將窗口的顏色變成紅色,但是代碼中并沒有該窗口的引用,而且還不想重新編譯代碼?你可以簡單地在一個很容易捕獲的地方創建斷點,獲取寄存器的引用并隨意操縱該對象的實例。你就將窗口變為紅色啦~

注:盡管NSResponder實現了mouseDown:方法,但NSWindow重寫了它。你可以輸出所有實現了mouseDown:的類并猜想出它們之中哪些繼承自NSResponder來決定是否該方法被重寫了,而不用去看源碼。輸出所有實現了mouseDown:方法的OC類的命令是:image lookup -rn '\ mouseDown:'

第一步,移除所有之前在LLDB內打的斷點:

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

然后再LLDB中輸入如下命令:

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

這里設置了一個一次性的斷點。

點擊應用。點擊之后,斷點立即停住了。然后輸入如下命令:

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

一旦重新執行,NSWindow將變紅~

image

Swift與寄存器

在Swift內探索寄存器時,會遇到兩個使匯編調試在Swift中比OC更困難的障礙。

  1. 首先,在Swift調試上下文內寄存器不可用。意味著你不得不獲取到任何你想要的數據,并使用OC調試上下文打印出傳入Swift函數的寄存器。記住你可以使用expression -l objc -O命令,或者使用在書中第八章(“Persisting and Customizing Commands”)的cpo命令。幸運的是,register read命令依然是可以使用的。

  2. 其次,Swift相對于OC并不是動態的。事實上,有時候最好假設Swift像C語言一樣。如果知道了一個內存地址,你應該顯示地強轉為你想要的類型。不然Swift調試器沒有任何線索去解釋內存地址。

話雖如此,在Swift中也是使用的相同的寄存器調用約定。但是,有一個非常重要的不同點。當Swift調用一個函數時,它不需要調用objc_msgSend函數,除非你用dynamic對它做標記。也就是說Swift調用一個函數,之前說的用于存selectorRSI寄存器將包含函數的第二個參數。

理論說的差不多了,實踐一把。

回到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方法的第一行打個斷點,這樣調試器會在函數一開始的時候停住。這很重要,不然的話如果程序開始跑起來寄存器可能會有臟數據。

然后移除在-[NSViewController viewDidLoad]的斷點。
編譯&運行,等待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調用約定。RDI, RSI, RDX, RCX, R8,R9
持有了6個參數。

你可能看到其他的參數被存儲在了其他的寄存器內。雖然這是事實,但它只是代碼中的剩余部分,用于為其余參數設置堆棧。記住,第六個參數后的參數將進入堆棧。

RAX,用于返回的寄存器

等等--還有呢!到這里,你已經了解了函數中六個寄存器是如何調用的,但是返回值呢?

幸運的是,只有一個指定的寄存器用于返回值:RAX。回到executeLotsOfArguments函數并改變函數的返回值,像這樣:

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) register read rax 

你會看到類似:

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

bong~ 你的返回值!

返回值RAX的知識很重要,因為它會構建你將學到的調試腳本基礎。

改變寄存器值

為了鞏固寄存器的理解,你將會在已編譯的程序內來改變寄存器。

關閉Xcode和工程及模擬器,打開終端程序,輸入如下命令:

xcrun simctl list

你將看到長串的設備列表。搜索最新iOS版本安裝的模擬器。在這之下,找到iPhone7設備。它看起來是這樣的:

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

UDID就是你要找的。使用它并通過如下命令打開iOS模擬器(替換其中的UDID部分):

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

保證模擬器已經啟動而且在主屏幕上。你可以通過按下Command + Shift + H鍵回到主屏幕。一旦模擬器準備好了,回到終端窗口將LLDB綁定到SpringBoard程序上。

lldb -n SpringBoard

這樣會將LLDB綁定到正在模擬器上運行的SpringBoard實例上!SpringBoard就是在iOS上控制主屏幕的程序。

一旦綁定,輸入如下命令:

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

可以看到類似如下的輸出:

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

注意下剛剛創建的這個NSString實例,因為很快你會用到它。現在,給UILabelsetText:方法設置一個斷點:

(lldb) b -[UILabel setText:]

下一步,輸入如下:

(lldb) breakpoint command add 

LLDB會吐出一些輸出且進入多行編輯模式。這個命令讓你在剛剛打的斷點處添加多個額外要執行的命令。輸入如下,使用剛才的NSString地址替換下面的內存地址:

> po $rdx = 0x0000618000644080
> continue
> DONE

回去重新看下你剛剛做的。你在UILabelsetText:方法上添加了一個斷點。一旦遇到該方法,你就會用一個叫Yay! Debugging!NSString實例替換RDX---第三個參數。

使用continue命令讓調試器繼續執行:

(lldb) continue

看看SpringBoard模擬器程序什么發生了改變。從下往上掃帶出控制中心,觀察改變的地方:

image

試著彈出其他以模態彈出的地方,因為這樣很可能導致在新的UIViewController(包括它的子view)會懶加載,并觸發斷點。

image

盡管這可能看起來是一個非常酷的巧妙的把戲,但它卻展示了通過有限的匯編和寄存器的知識能夠在程序內產生你之前沒見過的大的變化的一種富有洞察力的觀察方式。

在調試的角度來看也是非常有用的,因為你可以快速形象地確認在SpringBoard程序內-[UILabel setText:]的執行位置,并通過運行斷點的條件來找到準確的設置UILabel文本的代碼行數。

繼續這個想法,任何沒有改變文本的UILabel也告訴了你一些東西。例如,UIButton的文本并沒有變成Yay! Debugging!。可能UILabelsetText:方法是在之前調用了?或者SpringBoard程序調用的是setAttributedText:方法?或者它們使用的是私有的方法而對于第三方開發者來說是不公開的?

就像你看到的,使用和操縱寄存器能夠讓你更深入地了解應用程序的功能_

何去何從

哇哦!這真的很長啊,不是么?你可以下載整個教程的工程

因此你學到了什么?

  • 定義調用約定的架構,指明了函數參數和返回值的位置;
  • OC中,RDI是對象的引用,RSI是Selector,RDX是第一個參數,等等。。。
  • 在Swift中,RDI是第一個參數,RSI是第二個參數,等等。。。這是在Swift方法沒有添加dynamic標識的時候。
  • RAX是用于存儲返回值的,無論在OC還是Swift中。
  • 保證你使用的是OC的上下文,當你在用$輸出寄存器的時候。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容