Android遠程調(diào)試的探索與實現(xiàn)

文章來源:美團點評技術(shù)團隊

作為移動開發(fā)者泼差,最頭疼的莫過于遇到產(chǎn)品上線以后出現(xiàn)了bug躯概,但是本地開發(fā)環(huán)境又無法復(fù)現(xiàn)的情況年栓。常見的調(diào)查線上棘手問題方式大概如下:

方法優(yōu)點缺點

聯(lián)系用戶安裝已添加測試日志的APK方便定位問題需要用戶積極配合流纹,如果日志添加不全面還需要反復(fù)重試

提前在一些關(guān)鍵路徑設(shè)置埋點糜烹,在用戶出現(xiàn)問題以后上報日志進而定位問題不需要用戶深度配合關(guān)鍵路徑不好預(yù)測

以上兩種方法在之前調(diào)查線上問題時都有使用,但因為二者都有明顯的缺點漱凝,所以效果不是特別理想疮蹦。

能否開發(fā)一種工具,既不需要用戶深度配合也不需要提前埋點就能方便茸炒、快速地定位線上問題愕乎?

作為程序員,查bug一般使用下面幾種方式:閱讀源碼壁公、記錄日志或調(diào)試程序感论。一般本地無法復(fù)現(xiàn)的問題通過閱讀源碼很難找到原因,而且大多數(shù)情況都和用戶本地環(huán)境有關(guān)紊册。記錄日志的缺點之前講過了比肄,同樣不予考慮,那能否像調(diào)試本地程序一樣調(diào)試已經(jīng)發(fā)布出去的程序呢囊陡?我們對此做了一些嘗試和探索芳绩。

調(diào)試原理

先看下調(diào)試原理,這里以Java為例(通過IDE調(diào)試Android程序也基于此原理)撞反。Java(Android)程序都是運行在Java(Dalvik\ART)虛擬機上的妥色,要調(diào)試Java程序,就需要向Java虛擬機請求當前程序運行狀態(tài)遏片,并對虛擬機發(fā)送一定的指令嘹害,設(shè)置一些回調(diào)等等。Java的調(diào)試體系吮便,就是虛擬機的一套用于調(diào)試的工具和接口笔呀。Java SE從1.2.2版本以后推出了JPDA框架(Java Platform Debugger Architecture,Java平臺調(diào)試體系結(jié)構(gòu))线衫。

JPDA框架

JPDA定義了一套獨立且完整的調(diào)試體系凿可,它由三個相對獨立的模塊組成惑折,分別為:

JVM TI:Java虛擬機工具接口(被調(diào)試者)授账。

JDWP:Java Debug Wire Protocol枯跑,Java調(diào)試協(xié)議(通道)。

JDI:Java Debug Interface白热,Java調(diào)試接口(調(diào)試者)敛助。

這三個模塊把調(diào)試過程分解成了三個自然的概念:

被調(diào)試者運行在我們想要調(diào)試的虛擬機上,它可以通過JVM TI這個標準接口監(jiān)控當前虛擬機的信息屋确。

調(diào)試者定義了用戶可以使用的調(diào)試接口纳击,用戶可以通過這些接口對被調(diào)試虛擬機發(fā)送調(diào)試命令,同時顯示調(diào)試結(jié)果攻臀。

在調(diào)試者和被調(diào)試者之間焕数,通過JDWP傳輸層傳輸消息。

整個過程如下:

Components? ? ? ? ? ? ? ? ? ? ? ? Debugger Interfaces

/? ? |--------------|

/? ? |? ? VM? ? ? |

debuggee ----(? ? ? |--------------|? <------- JVM TI - Java VM Tool Interface

\? ? |? back-end? |

\? ? |--------------|

/? ? ? ? ? |

comm channel -(? ? ? ? ? ? |? <--------------- JDWP - Java Debug Wire Protocol

\? ? ? ? ? |

|--------------|

| front-end? ? |

|--------------|? <------- JDI - Java Debug Interface

|? ? ? UI? ? ? |

|--------------|

下面重點介紹一下JDWP協(xié)議刨啸。

JDWP協(xié)議

JDWP協(xié)議是用于調(diào)試器與目標虛擬機之間進行調(diào)試交互的通信協(xié)議堡赔,它的通信會話主要包含兩類數(shù)據(jù)包:

Command Packet:命令包。調(diào)試器發(fā)送給虛擬機Command设联,用于獲取程序狀態(tài)或控制程序執(zhí)行善已;虛擬機發(fā)送Command給調(diào)試器,用于通知事件觸發(fā)消息离例。

Reply Packet:回復(fù)包换团,虛擬機發(fā)送給調(diào)試者回復(fù)命令的請求或者執(zhí)行結(jié)果。

JDWP的數(shù)據(jù)包主要包含包頭和數(shù)據(jù)兩部分宫蛆,包頭字段含義如下:

數(shù)據(jù)包部分JDWP協(xié)議按照功能分為18組命令(以Java 7為例)艘包,包含了虛擬機、引用類型洒扎、對象辑甜、線程、方法袍冷、堆棧磷醋、事件等不同類型的操作命令。

Dalvik虛擬機/ART虛擬機對JDWP協(xié)議的支持并不完整胡诗,但是大部分關(guān)鍵命令都是支持的邓线,具體信息可以參考Dalvik-JDWPART-JDWP中所支持的消息。

Android調(diào)試原理

Android調(diào)試模型可以看作JPDA框架的具體實現(xiàn)煌恢。其中變化比較大的一個是JVM TI適配了Android設(shè)備特有的Dalvik虛擬機/ART虛擬機骇陈,另一個是JDWP的實現(xiàn)支持ADB和Socket兩種通信方式(ADB全稱為Android Debug Bridge,是Android系統(tǒng)的一個很重要的調(diào)試工具)瑰抵。整體的調(diào)試模型如下:

____________________________________? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? |ADBServer(host)|? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | Debugger <---> LocalSocket <----> RemoteSocket? |? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ||? ? ? |? ? ? ? ? ? |___________________________||_______|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ||? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Transport ||(TCPforemulator - USBfordevice)||? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ||? ? ? ? ? ? ___________________________||_______? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ||? ? ? |? ? ? ? ? ? |ADBD(device)||? ? ? |? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ||? ? ? |Android-VM? |? ? ? ? ? ? ? ? ? ? ? ? ? ||? ? ? |JDWP-thread <====> LocalSocket <-> RemoteSocket? |? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? |____________________________________|

運行在PC上的ADB Server和運行在Android設(shè)備上的ADBD守護進程之間通過USB或者無線網(wǎng)絡(luò)建立連接你雌,分別負責Debugger和Android設(shè)備的虛擬機進行通信。一旦連接建立起來,Debugger和Android VM通過“橋梁”進行數(shù)據(jù)的交換婿崭,ADB Server和ADBD對它們來說是透明的拨拓。

遠程調(diào)試

綜上,要實現(xiàn)遠程調(diào)試氓栈,關(guān)鍵需要實現(xiàn)兩部分功能:

能夠自定義JDWP通道渣磷。

能模擬ADB和ADBD實現(xiàn)消息的轉(zhuǎn)發(fā)。

先看下如何實現(xiàn)自定義JDWP通道授瘦。

JDWP啟動過程

我們看下Android 5.0系統(tǒng)在啟動一個應(yīng)用時是如何啟動JDWP Thread的醋界。

點擊圖片查看大圖

通過上圖可以看到,Android在創(chuàng)建虛擬機的同時會創(chuàng)建一個JDWP-Thread提完,JDWP默認有ADB和Socket兩種通信方式形纺。要實現(xiàn)遠程調(diào)試,ADB這種方式肯定不適用徒欣,所以能否實現(xiàn)一個自定義的Socket通道來實現(xiàn)JDWP的消息轉(zhuǎn)發(fā)成了問題的關(guān)鍵挡篓。

Hack-Native-JDWP

通過閱讀JDWP啟動源碼(Android-API-21)發(fā)現(xiàn),要想讓JDWP通過自定義的Socket通道進行通信帚称,需要滿足兩個條件:

能夠修改全局變量gJdwpOptions的值官研,使其配置為Socket模式,并指明對應(yīng)的端口號闯睹。

使用新的gJdwpOptions參數(shù)重新啟動JDWP-Thread戏羽。

在Android中,JDWP相關(guān)代碼分別被編譯成libart.so(Art)和libdvm.so(Dalvik)楼吃。修改或調(diào)用其他so庫中的代碼需要用到動態(tài)加載始花,使用動態(tài)加載,應(yīng)用程序需要先指定要加載的庫孩锡,然后將該庫作為一個可執(zhí)行程序來使用(即調(diào)用其中的函數(shù))酷宵。動態(tài)加載API 就是為了動態(tài)加載而存在的,它允許共享庫對用戶空間程序可用躬窜。下面表格展示了這個完整的 API:

函數(shù)描述

dlopen使對象文件可被程序訪問

dlsym獲取執(zhí)行了dlopen函數(shù)的對象文件中的符號的地址

dlerror返回上一次出現(xiàn)錯誤的字符串

dlclose關(guān)閉目標文件

在介紹如何調(diào)用動態(tài)加載功能之前浇垦,先介紹一下C/C++編譯器在編譯目標文件時所進行的名字修飾(符號化)。

符號化

上文提到要想自定義JDWP-Thread荣挨,首先需要修改gJdwpOptions的值男韧,該值是在debugger.cc中通過Dbg::ParseJdwpOptions方法來設(shè)置的,所以只要用新的配置重新調(diào)用一次ParseJdwpOptions即可默垄。

如何找到Dbg::ParseJdwpOptions這個函數(shù)地址呢此虑?為了保證每個函數(shù)、變量名都有唯一的標識口锭,編譯器在將源代碼編譯成目標文件時會對變量名或函數(shù)名進行名字修飾朦前。

先看一個例子,下面的C++程序中兩個f()的定義:

intf(void){return1; }intf(int){return0; }voidg(void){inti = f(), j = f(0); }

這些是不同的函數(shù),除了函數(shù)名相同以外沒有任何關(guān)系韭寸。如果不做任何改變直接把它們當成C代碼这溅,結(jié)果將導(dǎo)致一個錯誤:C語言不允許兩個函數(shù)同名。所以棒仍,C++編譯器將會把它們的類型信息編碼成符號名,結(jié)果類似下面的代碼:

int__f_v (void) {return1; }int__f_i (int)? {return0; }void__g_v (void) {inti = __f_v(), j = __f_i(0); }

可以通過nm命令查看so文件中的符號信息臭胜。

nm -D libart.so | grep ParseJdwpOptions

001778d0 T _ZN3art3Dbg16ParseJdwpOptionsERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE

這樣就得到了ParseJdwpOptions函數(shù)在動態(tài)鏈接庫文件中符號化以后的函數(shù)名莫其。

找到符號化了的函數(shù)名后,就可以通過調(diào)用動態(tài)鏈接庫中的函數(shù)重新啟動JDWP-Thread耸三。部分代碼如下(以下代碼只針對Android-API-21和Android-API-22版本有效):

void*handler = dlopen("/system/lib/libart.so", RTLD_NOW);if(handler ==NULL){? ? ? ? LOGD(LOG_TAG,env->NewStringUTF(dlerror()));? ? }//對于debuggable false的配置乱陡,重新設(shè)置為可調(diào)試void(*allowJdwp)(bool);? ? allowJdwp = (void(*)(bool)) dlsym(handler,"_ZN3art3Dbg14SetJdwpAllowedEb");? ? allowJdwp(true);void(*pfun)();//關(guān)閉之前啟動的jdwp-threadpfun = (void(*)()) dlsym(handler,"_ZN3art3Dbg8StopJdwpEv");? ? pfun();//重新配置gJdwpOptionsbool(*parseJdwpOptions)(conststd::string&);? ? parseJdwpOptions = (bool(*)(conststd::string&)) dlsym(handler,"_ZN3art3Dbg16ParseJdwpOptionsERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE");std::stringoptions ="transport=dt_socket,address=8000,server=y,suspend=n";? ? parseJdwpOptions(options);//重新startJdwppfun = (void(*)()) dlsym(handler,"_ZN3art3Dbg9StartJdwpEv");? ? pfun();

以上代碼關(guān)閉了之前可能存在的JDWP-Thread,同時開啟一個本地的Socket通道來進行通信仪壮,這樣就能通過本地的Socket通道來進行JDWP消息的傳遞憨颠。

突破7.0動態(tài)鏈接的限制

通過上面代碼可知,實現(xiàn)自定義的JDWP通道主要是采用動態(tài)調(diào)用libart.so/libdvm.so中的函數(shù)實現(xiàn)积锅。但從 Android 7.0 開始爽彤,系統(tǒng)將阻止應(yīng)用動態(tài)鏈接非公開 NDK庫,詳情請參考《Android 7.0行為變更》缚陷,強制調(diào)用會產(chǎn)生如下Crash:

java.lang.UnsatisfiedLinkError: dlopen failed: library"/system/lib/libart.so"needed or dlopened by"/system/lib/libnativeloader.so"is not accessibleforthe namespace"classloader-namespace"

如何繞過這個限制來動態(tài)調(diào)用libart.so中的方法适篙?既然直接調(diào)用dlopen會失敗,那是不是可以模擬dlopen和dlsym的實現(xiàn)來繞過這個限制箫爷?

dlopen和dlsym分別返回動態(tài)鏈接庫在內(nèi)存中的句柄和某個符號的地址嚷节,所以只要能找到dlopen返回的句柄并通過句柄找到dlsym符號對應(yīng)的地址,就相當于實現(xiàn)了這兩個函數(shù)的功能虎锚。libart.so會在程序啟動之后就被加載到內(nèi)存中硫痰,可以在/proc/self/maps找到當前進程中l(wèi)ibart.so在內(nèi)存中映射的地址:

vbox86p:/ # cat /proc/1665/maps | grep libart.so

e2d50000-e3473000 r-xp 00000000 08:06 1087? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /system/lib/libart.so

e3474000-e347c000 r--p 00723000 08:06 1087? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /system/lib/libart.so

e347c000-e347e000 rw-p 0072b000 08:06 1087? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /system/lib/libart.so

這里libart.so被分成了三個連續(xù)子空間,從e2d50000開始窜护。

如何才能在內(nèi)存中找到想要打開的函數(shù)地址效斑?我們先看下ELF文件結(jié)構(gòu)

要實現(xiàn)dlsym,首先要保證查找的符號在動態(tài)符號表中能找到柱徙,在ELF文件中鳍悠,SHT_DYNSYM對應(yīng)的Section定義了當前文件中的動態(tài)符號;SHT_STRTAB定義了動態(tài)庫中所有字符串坐搔;SHT_PROGBITS則定義了動態(tài)庫中定義的信息藏研。如何找到這些Section:

通過內(nèi)存映射的方式把libart.so映射到內(nèi)存中;

按照ELF文件結(jié)構(gòu)解析映射到內(nèi)存中的libart.so概行;

解析SHT_DYNSYM蠢挡,并把當前section復(fù)制到內(nèi)存中;

解析SHT_STRTAB,并把當前section復(fù)制到內(nèi)存中(后面需要根據(jù)SHT_STRTAB來找到特定的符號)业踏;

解析SHT_PROGBITS禽炬,得到當前內(nèi)存映射的偏移地址,這里要注意:不同進程中相同動態(tài)庫的同一個函數(shù)的偏移地址是一樣的勤家。

以上邏輯的部分代碼片段如下:

fd = open(libpath, O_RDONLY);? ? size = lseek(fd,0, SEEK_END);if(size <=0) fatal("lseek() failed for %s", libpath);? ? elf = (Elf_Ehdr *) mmap(0, size, PROT_READ, MAP_SHARED, fd,0);? ? close(fd);? ? fd = -1;if(elf == MAP_FAILED) fatal("mmap() failed for %s", libpath);? ? ctx = (structctx *)calloc(1,sizeof(structctx));if(!ctx) fatal("no memory for %s", libpath);//通過/proc/self/proc 找到的libart.so的起始地址ctx->load_addr = (void*) load_addr;? ? shoff = ((char*) elf) + elf->e_shoff;for(k =0; k < elf->e_shnum; k++)? {? ? ? ? shoff = (char*)shoff + elf->e_shentsize;? ? ? ? Elf_Shdr *sh = (Elf_Shdr *) shoff;? ? ? ? log_dbg("%s: k=%d shdr=%p type=%x", __func__, k, sh, sh->sh_type);switch(sh->sh_type) {caseSHT_DYNSYM:if(ctx->dynsym) fatal("%s: duplicate DYNSYM sections", libpath);/* .dynsym */ctx->dynsym =malloc(sh->sh_size);if(!ctx->dynsym) fatal("%s: no memory for .dynsym", libpath);memcpy(ctx->dynsym, ((char*) elf) + sh->sh_offset, sh->sh_size);//ctx->nsyms 動態(tài)符號表的個數(shù)ctx->nsyms = (sh->sh_size/sizeof(Elf_Sym)) ;break;caseSHT_STRTAB:if(ctx->dynstr)break;/* .dynstr is guaranteed to be the first STRTAB */ctx->dynstr =malloc(sh->sh_size);if(!ctx->dynstr) fatal("%s: no memory for .dynstr", libpath);memcpy(ctx->dynstr, ((char*) elf) + sh->sh_offset, sh->sh_size);break;//當前段內(nèi)容為program defined information:程序定義區(qū)caseSHT_PROGBITS:if(!ctx->dynstr || !ctx->dynsym)break;//得到偏移地址ctx->bias = (off_t) sh->sh_addr - (off_t) sh->sh_offset;break;? ? ? ? }? ? }//關(guān)閉內(nèi)存映射munmap(elf, size);

接下來就可以根據(jù)要找的符號名在SHT_DYNSYM中對應(yīng)的位置得到具體的函數(shù)指針腹尖,部分代碼如下:

void*fake_dlsym(void*handle,constchar*name){intk;structctx *ctx = (structctx *) handle;? ? Elf_Sym *sym = (Elf_Sym *) ctx->dynsym;char*strings = (char*) ctx->dynstr;for(k =0; k < ctx->nsyms; k++, sym++)if(strcmp(strings + sym->st_name, name) ==0) {//動態(tài)庫的基地址 + 當前符號section地址 - 偏移地址return(char*)ctx->load_addr + sym->st_value - ctx->bias;? ? ? ? }return0;}

通過以上模擬dlopen和dlsym的邏輯,我們成功繞過了系統(tǒng)將阻止應(yīng)用動態(tài)鏈接非公開 NDK庫的限制伐脖。

消息轉(zhuǎn)發(fā)

完成上面邏輯以后就可以通過本地Socket在虛擬機和用戶進程之間傳遞JDWP消息热幔。但是要實現(xiàn)遠程調(diào)試,還需要遠程下發(fā)虛擬機的調(diào)試指令并回傳執(zhí)行結(jié)果讼庇。我們通過App原有Push通道加上線上消息轉(zhuǎn)發(fā)服務(wù)绎巨,實現(xiàn)了整個調(diào)試工具的消息轉(zhuǎn)發(fā)功能:

Proguard對調(diào)試的影響

正常發(fā)布到市場的項目都會通過Proguad進行混淆,不同力度的混淆配置會生成不同的字節(jié)碼文件蠕啄。對調(diào)試功能影響比較大的配置有兩個:

LineNumberTable

LocalVariableTable

如果Proguard中沒有對這兩個屬性進行Keep场勤,那經(jīng)過Proguard處理的方法字節(jié)碼中會缺失這兩個模塊,對調(diào)試的影響分別是無法在方法的某一行設(shè)置斷點和無法獲取當前本地變量的值(但能獲取到方法參數(shù)變量和類成員變量)歼跟。一般為了在應(yīng)用發(fā)生崩潰時能獲取到調(diào)用棧中每個函數(shù)對應(yīng)的行號和媳,需要保留LineNumberTable,同時為了減少包體積會放棄LocalVariableTable哈街。在沒有LocalVariableTable的情況下窗价,可以通過調(diào)用Execute命令得到一些運行時結(jié)果間接得獲取到本地變量。

JDI的實現(xiàn)

整個消息交互流程跑通以后叹卷,接下來要做的就是根據(jù)JDI規(guī)范作進一步的封裝撼港。為了方便快速調(diào)試,目前調(diào)試工具的前端實現(xiàn)主要參考了LLDB的調(diào)試流程骤竹,通過設(shè)置命令的方式進行調(diào)試帝牡,整體樣式如下圖所示:

點擊圖片查看大圖

總結(jié)

本文從調(diào)查線上問題的常見手段入手,介紹了到店餐飲移動團隊在實現(xiàn)遠程調(diào)試過程中的嘗試和探索蒙揣。通過遠程調(diào)試可以方便快捷地獲取用戶當前App運行時的狀態(tài)靶溜,助力開發(fā)者快速定位線上問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懒震,一起剝皮案震驚了整個濱河市罩息,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌个扰,老刑警劉巖瓷炮,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異递宅,居然都是意外死亡娘香,警方通過查閱死者的電腦和手機苍狰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烘绽,“玉大人淋昭,你說我怎么就攤上這事“步樱” “怎么了翔忽?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盏檐。 經(jīng)常有香客問我歇式,道長,這世上最難降的妖魔是什么糯笙? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮撩银,結(jié)果婚禮上给涕,老公的妹妹穿的比我還像新娘。我一直安慰自己额获,他們只是感情好够庙,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抄邀,像睡著了一般耘眨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上境肾,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天剔难,我揣著相機與錄音,去河邊找鬼奥喻。 笑死偶宫,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的环鲤。 我是一名探鬼主播纯趋,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冷离!你這毒婦竟也來了吵冒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤西剥,失蹤者是張志新(化名)和其女友劉穎痹栖,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞭空,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡结耀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年留夜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片图甜。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡碍粥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黑毅,到底是詐尸還是另有隱情嚼摩,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布矿瘦,位于F島的核電站枕面,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏缚去。R本人自食惡果不足惜潮秘,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望易结。 院中可真熱鬧枕荞,春花似錦、人聲如沸搞动。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹦肿。三九已至矗烛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間箩溃,已是汗流浹背瞭吃。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留涣旨,地道東北人虱而。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像开泽,于是被迫代替她去往敵國和親牡拇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容