IL2CPP 深入講解:代碼生成之旅

上次我們翻譯了由Unity開發(fā)人員JOSH PETERSON所寫的、IL2CPP深入講解系列的第一期下梢,現(xiàn)在第二期的中文版也新鮮出爐装诡,歡迎大家分享給身邊的程序員逻杖。

IL2CPP INTERNALS: A TOUR OF GENERATED CODE
作者:JOSH PETERSON
翻譯:Bowie

這是IL2CPP深入講解系列的第二篇博文。在這篇文章中澳厢,我們會對由il2cpp產(chǎn)生的C++代碼進行分析环础。我們會看到托管代碼中的類在C++中如何表示,對.NET虛擬機提供支持的C++代碼運行時檢查等功能剩拢。

后面例子會使用特定版本的Unity线得,隨著以后新版本的Unity發(fā)布,這些代碼可能會有所改變徐伐。不過這沒有關(guān)系贯钩,因為我們文中將要提到的概念是不會變的。

示例程序

我將用到Unity 5.0.1p1來創(chuàng)建示例程序办素。和第一篇博文一樣角雷,我創(chuàng)建了一個空的項目,添加一個文件性穿,加入如下內(nèi)容:
using UnityEngine; public class HelloWorld : MonoBehaviour { private class Important { public static int ClassIdentifier = 42; public int InstanceIdentifier; } void Start () { Debug.Log("Hello, IL2CPP!"); Debug.LogFormat("Static field: {0}", Important.ClassIdentifier); var importantData = new [] { new Important { InstanceIdentifier = 0 }, new Important { InstanceIdentifier = 1 } }; Debug.LogFormat("First value: {0}", importantData[0].InstanceIdentifier); Debug.LogFormat("Second value: {0}", importantData[1].InstanceIdentifier); try { throw new InvalidOperationException("Don't panic"); } catch (InvalidOperationException e) { Debug.Log(e.Message); } for (var i = 0; i < 3; ++i) { Debug.LogFormat("Loop iteration: {0}", i); } } }
把平臺切換到WebGL勺三,并且打開“Development Player”選項以便我們能得到相對可以閱讀的函數(shù),變量名稱需曾。我還將“Enable Exceptions”設(shè)置到“Full”以便打開異常捕捉吗坚。

生成代碼總覽

在WebGL項目生成之后祈远,產(chǎn)生的C++文件可以在項目的Temp\StagingArea\Data\il2cppOutput目錄下找到。一但Unity Editor關(guān)閉退出商源,這個臨時目錄就會被刪除车份。相反的,只要Editor還開著牡彻,這個目錄就會保持不變扫沼,方便我們對其檢視。

雖然這個示例項目很小讨便,只有一個C#代碼文件充甚,但是il2cpp還是產(chǎn)生了很多文件。我發(fā)現(xiàn)有4625個頭文件和89個C++文件霸褒。要處理這么多代碼文件伴找,我個人喜歡用Exuberant CTags 文本編輯工具。它可以快速的生成代碼文件標(biāo)簽废菱,讓瀏覽理解這些代碼變得更容易技矮。

一開始,你會發(fā)現(xiàn)這些生成的C++文件都不是來源于我們那個簡單的C#代碼殊轴,而是來源于諸如mscorlib.dll 這樣的C#標(biāo)準(zhǔn)庫衰倦。正如我們在第一篇文章中提到的,IL2CPP后臺使用的標(biāo)準(zhǔn)庫和Mono使用的庫是同一套旁理,沒有任何區(qū)別樊零。需要注意的是當(dāng)每次構(gòu)建項目的時候,il2cpp.exe都會把這些標(biāo)準(zhǔn)庫轉(zhuǎn)換一次孽文。貌似這沒啥必要驻襟,因為這些庫文件是不會改變的。

然而芋哭,在IL2CPP的后端處理中沉衣,通常會使用字節(jié)碼剝離(byte code stripping)技術(shù)來減少可執(zhí)行文件的尺寸。因此游戲代碼的一小點變化也會導(dǎo)致標(biāo)準(zhǔn)庫引用的改變减牺,并影響最終剝離代碼豌习。所以目前我們還是在每次生成項目的時候轉(zhuǎn)換所有的標(biāo)準(zhǔn)庫。我們也在研究是否有其他更好的方法可以加快項目生成的速度拔疚,但目前為止還沒有好的進展肥隆。

托管代碼如何映射到C++代碼

在托管代碼中的每個類,il2cpp.exe都會相應(yīng)的生成一個有著C++定義的頭文件和另外一個進行函數(shù)聲明的頭文件稚失。舉個例子栋艳,讓我們看看UnityEngine.Vector3是如何被轉(zhuǎn)換的。這個類的頭文件名字叫UnityEngine_UnityEngine_Vector3.h墩虹。頭文件名的組成:一開始是程序集名稱(這里是UnityEngine)嘱巾,然后跟著命名空間(還是UnityEngine),最后是這個類型的名字(Vector3)诫钓。頭文件的內(nèi)容如下:

// UnityEngine.Vector3 struct Vector3_t78 { // System.Single UnityEngine.Vector3::x float ___x_1; // System.Single UnityEngine.Vector3::y float ___y_2; // System.Single UnityEngine.Vector3::z float ___z_3; };

il2cpp.exe對Vector3中三個成員都進行了轉(zhuǎn)換旬昭,并且適當(dāng)?shù)奶幚砹讼伦兞棵郑ㄔ诔蓡T變量前面添加下劃線)以避免和保留字沖突。

UnityEngine_UnityEngine_Vector3MethodDeclarations.h頭文件中則包含了Vector3這個類中所有相關(guān)的函數(shù)菌湃。比如我們熟悉的ToString函數(shù):

// System.String UnityEngine.Vector3::ToString() extern "C" String_t* Vector3_ToString_m2315 (Vector3_t78 * __this, MethodInfo* method) IL2CPP_METHOD_ATTR

請大家注意函數(shù)前面的注釋问拘,它能很好的反應(yīng)出這個函數(shù)在原本托管代碼中的名稱。我時常發(fā)現(xiàn)這些個注釋非常有用惧所,能讓我在C++代碼中快速定位我想要尋找的函數(shù)骤坐。

由il2cpp.exe生成的函數(shù)代碼有著以下一些有趣的特性:

所有的函數(shù)都不是成員函數(shù)。也就是說函數(shù)的第一個參數(shù)永遠都是“this”指針下愈。對于托管代碼中的靜態(tài)函數(shù)而言纽绍,IL2CPP會傳遞NULL作為第一個參數(shù)的值。這么做的好處是可以讓il2cpp.exe轉(zhuǎn)換代碼的邏輯更加簡單并且讓代理函數(shù)的處理變得更加容易势似。

所有的函數(shù)還有一個額外的MethodInfo*參數(shù)用來描述函數(shù)的元信息拌夏。這些元信息是虛函數(shù)調(diào)用的關(guān)鍵。Mono使用和特定平臺相關(guān)的方法來傳遞這些元信息履因。而IL2CPP出于可移植方面的考慮障簿,并沒有使用這些和平臺相關(guān)的特定代碼。所有的函數(shù)都被聲明成了extern “C”栅迄,這樣一來站故,在需要的時候我們就可以騙過C++編譯器讓其認(rèn)為所有這些函數(shù)都是一個類型。

托管函數(shù)中的類型會被加上“_t”的后綴毅舆,函數(shù)則是加上“_m”后綴西篓。最后我們加上一個唯一的數(shù)字來避免名字的重復(fù)。這些數(shù)字會隨著項目代碼的改變而改變朗兵,因此你不能把數(shù)字作為索引或者分析的參照污淋。

前兩個指針暗示著每個函數(shù)都至少有兩個參數(shù):“this”和“MethodInfo*”。這些額外的參數(shù)會加重整個調(diào)用的負(fù)擔(dān)么余掖?理論上是顯而易見會加重的寸爆,但是我們在實際的測試中還沒有發(fā)現(xiàn)這些參數(shù)對性能產(chǎn)生影響。

我們可以用Ctags工具跳轉(zhuǎn)到ToString函數(shù)的定義部分盐欺,位于Bulk_UnityEngine_0.cpp文件中赁豆。在這個函數(shù)中的代碼看上去和C#中Vector3::ToString()的代碼一點也不像。但是當(dāng)你用ILSpy 獲取到Vector3::ToString()內(nèi)部的代碼后冗美,你會發(fā)現(xiàn)C++代碼和C#的IL代碼是十分接近的魔种。

為什么il2cpp.exe不針對每一個類中的函數(shù)生成單獨的一個cpp文件呢传于?看看Bulk_UnityEngine_0.cpp痘昌,你會發(fā)現(xiàn)它有驚人的20,481行颖榜!之所以這么做的原因是我們發(fā)現(xiàn)C++編譯器在處理大量的文件時會有問題狈究。編譯四千多個.cpp文件所用的時間遠比編譯相同的代碼量,但是集中在80個.cpp文件中所用的時間要長得多安拟。因此il2cpp.exe將所有類的函數(shù)定義放到一個組里并為這個組生成C++文件蛤吓。

現(xiàn)在讓我們看看函數(shù)聲明頭文件的第一行:

#include "codegen/il2cpp-codegen.h"

il2cpp-codegen.h文件中包含了用來調(diào)用運行時庫libil2cpp的代碼。我們在稍后會談?wù)務(wù){(diào)用運行時庫的一些方法糠赦。

函數(shù)預(yù)處理代碼段(Method prologues )

讓我們再仔細(xì)的看下Vector3::ToString()函數(shù)的定義会傲,你會發(fā)現(xiàn)函數(shù)中有一段特有的代碼,這段代碼是il2cpp.exe模板產(chǎn)生的拙泽,會插入到任何函數(shù)的最前面淌山。

StackTraceSentry _stackTraceSentry(&Vector3_ToString_m2315_MethodInfo); static bool Vector3_ToString_m2315_init; if (!Vector3_ToString_m2315_init) { ObjectU5BU5D_t4_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&ObjectU5BU5D_t4_0_0_0); Vector3_ToString_m2315_init = true; }

代碼的第一行是一個局部變量StackTraceSentry。這個變量是用來跟蹤托管代碼的堆棧調(diào)用的顾瞻。有了這個變量泼疑,IL2CPP就能在Environment.StackTrace調(diào)用中正確的打印出堆棧信息。是否產(chǎn)生這行代碼是可選的荷荤,當(dāng)你在il2cpp.exe命令行中加入--enable-stacktrace開關(guān)(因為我在WebGL選項中設(shè)置了“Enable Exceptions”為“Full”)王浴,就會生成這行代碼。我們發(fā)現(xiàn)對于簡單的小函數(shù)來說梅猿,這行代碼的加入對代碼的執(zhí)行性能是有影響的氓辣。所以對于iOS或者其他有內(nèi)置棧信息的平臺來說,我們不會加入這行代碼(而使用平臺內(nèi)置的棧信息)袱蚓。但是對于WebGL來說钞啸,由于是在瀏覽器中執(zhí)行,所以沒有系統(tǒng)內(nèi)置的棧信息可供調(diào)用喇潘。只能由il2cpp.exe加入以便托管代碼的異常機制能正常運作体斩。

代碼序的第二部分是數(shù)組或者和類型相關(guān)的元信息的延遲加載。ObjectU5BU5D_t4實際代表的是System.Object[]颖低。這部分代碼永遠只執(zhí)行一次絮吵,如果這個類型的元信息已經(jīng)加載過了,就直接跳過這段代碼忱屑,啥也不做蹬敲。所以這段代碼不會帶來性能下降。

那么這段代碼是線程安全的嘛莺戒?如果兩個線程都同時進行Vector3::ToString() 調(diào)用會發(fā)生什么伴嗡?實際上,這不會有任何問題从铲,因為libil2cpp運行時中的類型初始化函數(shù)是線程安全的瘪校。不管初始化函數(shù)被多少個線程同時調(diào)用,實際的執(zhí)行是同一時間只能有一個線程的函數(shù)在執(zhí)行。其他線程的函數(shù)都會被掛起直到當(dāng)前的函數(shù)處理完成阱扬。所以總的來說泣懊,代碼是線程安全的。

運行時檢查

函數(shù)的下個部分創(chuàng)建了一個object數(shù)組麻惶,將Vector3的x存在局部變量中嗅定,然后將這個變量裝箱并加入到數(shù)組的零號位置中。下面是生成的C++代碼:

// Create a new single-dimension, zero-based object array ObjectU5BU5D_t4* L_0 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 3)); // Store the Vector3::x field in a local float L_1 = (__this->___x_1); float L_2 = L_1; // Box the float instance, since it is a value type. Object_t * L_3 = Box(InitializedTypeInfo(&Single_t264_il2cpp_TypeInfo), &L_2); // Here are three important runtime checks NullCheck(L_0); IL2CPP_ARRAY_BOUNDS_CHECK(L_0, 0); ArrayElementTypeCheck (L_0, L_3); // Store the boxed value in the array at index 0 *((Object_t **)(Object_t **)SZArrayLdElema(L_0, 0)) = (Object_t *)L_3;

在IL代碼中沒有出現(xiàn)的三個運行時檢查是由il2cpp.exe加入的用踩。

如果數(shù)組為空,NullCheck代碼會拋出NullReferenceException異常忙迁。

如果數(shù)組的索引不正確脐彩,IL2CPP_ARRAY_BOUNDS_CHECK代碼會拋出IndexOutOfRangeException異常。

如果加入數(shù)組的類型和數(shù)組類型不符合姊扔,ArrayElementTypeCheck代碼會拋出ArrayTypeMismatchException異常惠奸。

這三個檢查本來都是由.NET虛擬機完成的,在Mono實現(xiàn)中恰梢,不會插入這些個代碼而是使用平臺相關(guān)的信號機制來進行檢查佛南。對于IL2CPP,我們希望做到和平臺無關(guān)的可移植性并且還要支持像WebGL這樣的平臺嵌言,所以不能使用Mono的機制嗅回,而是顯示的插入檢查代碼。

這些檢查會引起性能的下降么摧茴?在大多數(shù)情況下绵载,我們并沒有看到由此帶來的性能損失,并且好處是我們提供了.NET虛擬機需要的安全保護機制苛白。在某些特定的場合娃豹,比如在大量的循環(huán)中,我們確實看到了性能的下降购裙。目前我們正在尋找方法在il2cpp.exe生成代碼的時候減少這些運行時檢查懂版,各位有興趣的可以繼續(xù)關(guān)注。

靜態(tài)變量

我們已經(jīng)了解了實例變量(Vector3)如何運作躏率,現(xiàn)在讓我們來看看托管代碼中的靜態(tài)變量是如何轉(zhuǎn)換成C++代碼并使用的躯畴。讓我們找到HelloWorld_Start_m3函數(shù),這個函數(shù)應(yīng)該在Bulk_Assembly-CSharp_0.cpp文件中薇芝。從這個函數(shù)我們找到一個叫Important_t1的類型(這個類型應(yīng)該是在U2DCSharp_HelloWorld_Important.h頭文件里)

struct Important_t1 : public Object_t { // System.Int32 HelloWorld/Important::InstanceIdentifier int32_t ___InstanceIdentifier_1; }; struct Important_t1_StaticFields { // System.Int32 HelloWorld/Important::ClassIdentifier int32_t ___ClassIdentifier_0; };

大伙兒可能注意到了私股,il2cpp.exe將生成的C++代碼分成了兩個結(jié)構(gòu),一個結(jié)構(gòu)負(fù)責(zé)普通的成員變量恩掷,另一個結(jié)構(gòu)負(fù)責(zé)靜態(tài)成員倡鲸。因為靜態(tài)成員是所有實例共享的數(shù)據(jù),因此在運行的時候黄娘,Important_t1_StaticFields只有一份峭状。所有的Important_t1實例都共享這個數(shù)據(jù)克滴。在生成的代碼中,通過下面的代碼來獲取靜態(tài)數(shù)據(jù):

int32_t L_1 = (((Important_t1_StaticFields*)InitializedTypeInfo(&Important_t1_il2cpp_TypeInfo)->static_fields)->___ClassIdentifier_0);

在Important_t1的元信息結(jié)構(gòu)中有一個指向Important_t1_StaticFields結(jié)構(gòu)的指針(static_fields)优床,然后通過類型轉(zhuǎn)換再取出需要的值(___ClassIdentifier_0)

異常

在托管代碼中的異常會被il2cpp.exe轉(zhuǎn)換成C++的異常劝赔。我們再一次的選擇了這個策略還是出于可移植性的考慮:去掉和平臺相關(guān)的方案。當(dāng)il2cpp.exe需要轉(zhuǎn)換生成一個托管的異常的時候胆敞,它會調(diào)用il2cpp_codegen_raise_exception函數(shù)着帽。

在我們的例子中,生成的C++異常處理代碼如下:

try { // begin try (depth: 1) InvalidOperationException_t7 * L_17 = (InvalidOperationException_t7 *)il2cpp_codegen_object_new (InitializedTypeInfo(&InvalidOperationException_t7_il2cpp_TypeInfo)); InvalidOperationException__ctor_m8(L_17, (String_t*) &_stringLiteral5, /*hidden argument*/&InvalidOperationException__ctor_m8_MethodInfo); il2cpp_codegen_raise_exception(L_17); // IL_0092: leave IL_00a8 goto IL_00a8; } // end try (depth: 1) catch(Il2CppExceptionWrapper& e) { __exception_local = (Exception_t8 *)e.ex; if(il2cpp_codegen_class_is_assignable_from (&InvalidOperationException_t7_il2cpp_TypeInfo, e.ex->object.klass)) goto IL_0097; throw e; } IL_0097: { // begin catch(System.InvalidOperationException) V_1 = ((InvalidOperationException_t7 *)__exception_local); NullCheck(V_1); String_t* L_18 = (String_t*)VirtFuncInvoker0< String_t* >::Invoke(&Exception_get_Message_m9_MethodInfo, V_1); Debug_Log_m6(NULL /*static, unused*/, L_18, /*hidden argument*/&Debug_Log_m6_MethodInfo); // IL_00a3: leave IL_00a8 goto IL_00a8; } // end catch (depth: 1)

所有的托管異常都被封裝進了il2CppExceptionWrapper的C++類型移层。當(dāng)C++代碼捕獲了這種異常之后仍翰,會試圖將包解開獲得托管異常(Exception_t8)。就這個例子而言观话,我們期待的是一個InvalidOperationException異常予借,所以當(dāng)我們發(fā)現(xiàn)拋出的異常不是這個類型的時候,代碼會創(chuàng)建一個C++異常的拷貝并重新拋出频蛔。反之如果異常正是我們所關(guān)注的灵迫,代碼就會跳到異常處理的那段。

Goto是個什么鬼晦溪?跳轉(zhuǎn)語句F僦唷?三圆!

這段代碼有一個有意思的地方:大伙兒發(fā)現(xiàn)了labels標(biāo)簽和goto語句沒有利凑?這些不太使用的東西居然出現(xiàn)在了結(jié)構(gòu)化的代碼中(譯注:主流觀點都不建議使用labels和goto語句,因為這會破壞程序的結(jié)構(gòu)化導(dǎo)致各種bug的產(chǎn)生)嫌术。為什么會這樣哀澈?因為IL!IL是沒有諸如for度气,while循環(huán)和if/then判斷結(jié)構(gòu)化概念的低等級的語言割按。因為il2cpp.exe需要處理IL代碼,因此也會出現(xiàn)goto語句磷籍。

還是看例子适荣,讓我們看看HelloWorld_Start_m3函數(shù)中的循環(huán)是個啥樣子的:

IL_00a8: { V_2 = 0; goto IL_00cc; } IL_00af: { ObjectU5BU5D_t4* L_19 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 1)); int32_t L_20 = V_2; Object_t * L_21 = Box(InitializedTypeInfo(&Int32_t5_il2cpp_TypeInfo), &L_20); NullCheck(L_19); IL2CPP_ARRAY_BOUNDS_CHECK(L_19, 0); ArrayElementTypeCheck (L_19, L_21); *((Object_t **)(Object_t **)SZArrayLdElema(L_19, 0)) = (Object_t *)L_21; Debug_LogFormat_m7(NULL /*static, unused*/, (String_t*) &_stringLiteral6, L_19, /*hidden argument*/&Debug_LogFormat_m7_MethodInfo); V_2 = ((int32_t)(V_2+1)); } IL_00cc: { if ((((int32_t)V_2) < ((int32_t)3))) { goto IL_00af; } }

在這里變量V_2是循環(huán)的索引,從0開始院领,在循環(huán)代碼的最后進行累加弛矛。

V_2 = ((int32_t)(V_2+1));

循環(huán)的結(jié)束檢查代碼:

if ((((int32_t)V_2) < ((int32_t)3)))

只要V_2小于3,goto語句就會跳轉(zhuǎn)到IL_00af標(biāo)簽處比然,也就是循環(huán)的一開始繼續(xù)執(zhí)行丈氓。你可能會想:嗯。。il2cpp.exe一定在偷懶万俗,直接使用了IL的代碼而不是使用抽象的語法分析樹湾笛。如果你是這么想的,那么恭喜你猜對了闰歪。嚎研。。 你可能還會注意到在上面的這段運行時檢查的代碼中库倘,有下面的情況:

float L_1 = (__this->___x_1); float L_2 = L_1;

很顯然临扮, 變量L_2不是必須的,大多數(shù)的C++編譯器會將其優(yōu)化掉教翩。對于我們來說杆勇,我們在想辦法不去生成這行代碼(譯注:因為il2cpp.exe是從IL進行代碼的轉(zhuǎn)換,沒有使用高級的語法分析迂曲,所以會產(chǎn)生多余的代碼)。我們也在研究使用高級的抽象語法樹(
Abstract Syntax Tree寥袭,縮寫:AST)以便更好的理解IL代碼從而產(chǎn)生更好的C++代碼(譯注:可能以后就會去除goto跳轉(zhuǎn)語句了)

總結(jié)

通過一個簡單的項目路捧,我們初窺了IL2CPP如何將托管代碼轉(zhuǎn)換成C++代碼。如果你沒有生成測試項目传黄,我強烈建議你做一遍并進行一些研究杰扫。在你做這件事的同時,請記住膘掰,在后續(xù)Unity的版本中章姓,生成的C++代碼可能會和本文有所不同。這是正常的识埋,因為我們在不斷的改進和優(yōu)化IL2CPP凡伊。

通過將IL代碼轉(zhuǎn)換成C++,我們能夠獲得在可移植和性能上的一個很好的平衡窒舟。我們能擁有高效開發(fā)的托管代碼的同時系忙,還能獲得高質(zhì)量的C++代碼。

在接下來的文章中惠豺,我們將探索更多的C++代碼银还,包括函數(shù)調(diào)用,函數(shù)對原生庫的封裝和共享等洁墙。下篇文章我們將會圍繞iOS 64-bit和Xcode展開蛹疯。

歡迎關(guān)注IndieACE微信公眾號,查看更多好內(nèi)容热监。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捺弦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌羹呵,老刑警劉巖骂际,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異冈欢,居然都是意外死亡歉铝,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門凑耻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來太示,“玉大人,你說我怎么就攤上這事香浩±噻停” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵邻吭,是天一觀的道長餐弱。 經(jīng)常有香客問我,道長囱晴,這世上最難降的妖魔是什么膏蚓? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮畸写,結(jié)果婚禮上驮瞧,老公的妹妹穿的比我還像新娘。我一直安慰自己枯芬,他們只是感情好论笔,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著千所,像睡著了一般狂魔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淫痰,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天毅臊,我揣著相機與錄音,去河邊找鬼黑界。 笑死管嬉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的朗鸠。 我是一名探鬼主播蚯撩,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烛占!你這毒婦竟也來了胎挎?” 一聲冷哼從身側(cè)響起沟启,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎犹菇,沒想到半個月后德迹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡揭芍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年胳搞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片称杨。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡肌毅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姑原,到底是詐尸還是另有隱情悬而,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布锭汛,位于F島的核電站笨奠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏唤殴。R本人自食惡果不足惜般婆,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望眨八。 院中可真熱鬧腺兴,春花似錦左电、人聲如沸廉侧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽段誊。三九已至,卻和暖如春栈拖,著一層夾襖步出監(jiān)牢的瞬間连舍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工涩哟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留索赏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓贴彼,卻偏偏與公主長得像潜腻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子器仗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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