Mach-O 可執(zhí)行文件

我們用 Xcode 構(gòu)建一個(gè)程序的過程中猫态,會(huì)把源文件 (.m.h) 文件轉(zhuǎn)換為一個(gè)可執(zhí)行文件。這個(gè)可執(zhí)行文件中包含的字節(jié)碼會(huì)將被 CPU (iOS 設(shè)備中的 ARM 處理器或 Mac 上的 Intel 處理器) 執(zhí)行。

本文將介紹一下上面的過程中編譯器都做了些什么寓盗,同時(shí)深入看看可執(zhí)行文件內(nèi)部是怎樣的傀蚌。實(shí)際上里面的東西要比我們第一眼看到的多得多。

這里我們把 Xcode 放一邊箩艺,將使用命令行工具 (command-line tools)。當(dāng)我們用 Xcode 構(gòu)建一個(gè)程序時(shí)静汤,Xcode 只是簡(jiǎn)單的調(diào)用了一系列的工具而已虫给。Florian 對(duì)工具調(diào)用是如何工作的做了更詳細(xì)的討論杰捂。本文我們就直接調(diào)用這些工具挨队,并看看它們都做了些什么盛垦。

真心希望本文能幫助你更好的理解 iOS 或 OS X 中的一個(gè)可執(zhí)行文件 (也叫做 Mach-O executable) 是如何執(zhí)行颊埃,以及怎樣組裝起來的。

xcrun

先來看一些基礎(chǔ)性的東西:這里會(huì)大量使用一個(gè)名為 xcrun 的命令行工具罗标。看起來可能會(huì)有點(diǎn)奇怪宙拉,不過它非常的出色。這個(gè)小工具用來調(diào)用別的一些工具澳化。原先缎谷,我們?cè)诮K端執(zhí)行如下命令:

% clang -v

現(xiàn)在我們用下面的命令代替:

% xcrun clang -v

在這里 xcrun 做的是定位到 clang,并執(zhí)行它希痴,附帶輸入 clang 后面的參數(shù)。

我們?yōu)槭裁匆@樣做呢嫩实?看起來沒有什么意義。不過 xcode 允許我們: (1) 使用多個(gè)版本的 Xcode晃洒,以及使用某個(gè)特定 Xcode 版本中的工具缘挑。(2) 針對(duì)某個(gè)特定的 SDK (software development kit) 使用不同的工具。如果你有 Xcode 4.5 和 Xcode 5惶翻,通過 xcode-selectxcrun 可以選擇使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具颅筋。在許多其它平臺(tái)中,這是不可能做到的先口。查閱 xcrunxcode-select 的主頁(yè)內(nèi)容可以了解到詳細(xì)內(nèi)容碉京。不用安裝 Command Line Tools,就能使用命令行中的開發(fā)者工具凡蜻。

不使用 IDE 的 Hello World

回到終端 (Terminal)设凹,創(chuàng)建一個(gè)包含一個(gè) C 文件的文件夾:

% mkdir ~/Desktop/objcio-command-line
% cd !$
% touch helloworld.c

接著使用你喜歡的文本編輯器來編輯這個(gè)文件 -- 例如 TextEdit.app:

% open -e helloworld.c

輸入如下代碼:

#include <stdio.h>
int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}

保存并返回到終端月匣,然后運(yùn)行如下命令:

% xcrun clang helloworld.c
% ./a.out

現(xiàn)在你能夠在終端上看到熟悉的 Hello World!锄开。這里我們編譯并運(yùn)行 C 程序头遭,全程沒有使用 IDE计维。深呼吸一下鲫惶,高興高興。

上面我們到底做了些什么呢赏淌?我們將 helloworld.c 編譯為一個(gè)名為 a.out 的 Mach-O 二進(jìn)制文件。注意缩擂,如果我們沒有指定名字,那么編譯器會(huì)默認(rèn)的將其指定為 a.out博脑。

這個(gè)二進(jìn)制文件是如何生成的呢?實(shí)際上有許多內(nèi)容需要觀察和理解疗杉。我們先看看編譯器吧烟具。

Hello World 和編譯器

時(shí)下 Xcode 中編譯器默認(rèn)選擇使用 clang(讀作 /kl??/)嗡午。關(guān)于編譯器,Chris 寫了更詳細(xì)的文章应媚。

簡(jiǎn)單的說,編譯器處理過程中丢胚,將 helloworld.c 當(dāng)做輸入文件,并生成一個(gè)可執(zhí)行文件 a.out峡蟋。這個(gè)過程有多個(gè)步驟/階段。我們需要做的就是正確的執(zhí)行它們蓬戚。

預(yù)處理
  • 符號(hào)化 (Tokenization)
  • 宏定義的展開
  • #include 的展開
語(yǔ)法和語(yǔ)義分析
  • 將符號(hào)化后的內(nèi)容轉(zhuǎn)化為一棵解析樹 (parse tree)
  • 解析樹做語(yǔ)義分析
  • 輸出一棵抽象語(yǔ)法樹(Abstract Syntax Tree* (AST))
生成代碼和優(yōu)化
  • 將 AST 轉(zhuǎn)換為更低級(jí)的中間碼 (LLVM IR)
  • 對(duì)生成的中間碼做優(yōu)化
  • 生成特定目標(biāo)代碼
  • 輸出匯編代碼
匯編器
  • 將匯編代碼轉(zhuǎn)換為目標(biāo)對(duì)象文件子漩。
鏈接器
  • 將多個(gè)目標(biāo)對(duì)象文件合并為一個(gè)可執(zhí)行文件 (或者一個(gè)動(dòng)態(tài)庫(kù))

所以總的流程以流程圖來表示的話如下圖所示


clang編譯流程

我們來看一個(gè)關(guān)于這些步驟的簡(jiǎn)單的例子。

預(yù)處理

編譯過程中,編譯器首先要做的事情就是對(duì)文件做處理重父。預(yù)處理結(jié)束之后,如果我們停止編譯過程丹允,那么我們可以讓編譯器顯示出預(yù)處理的一些內(nèi)容:

% xcrun clang -E helloworld.c

喔喔。 上面的命令輸出的內(nèi)容有 413 行批狐。我們用編輯器打開這些內(nèi)容,看看到底發(fā)生了什么:

% xcrun clang -E helloworld.c | open -f

在頂部可以看到的許多行語(yǔ)句都是以 # 開頭 (讀作 hash)食零。這些被稱為 行標(biāo)記 的語(yǔ)句告訴我們后面跟著的內(nèi)容來自哪里迁霎。如果再回頭看看 helloworld.c 文件频伤,會(huì)發(fā)現(xiàn)第一行是:

#include <stdio.h>

我們都用過 #includeimport憋肖。它們所做的事情是告訴預(yù)處理器將文件 stdio.h 中的內(nèi)容插入到 #include 語(yǔ)句所在的位置膊升。這是一個(gè)遞歸的過程:stdio.h 可能會(huì)包含其它的文件。

由于這樣的遞歸插入過程很多债查,所以我們需要確保記住相關(guān)行號(hào)信息。為了確保無誤,預(yù)處理器在發(fā)生變更的地方插入以 # 開頭的 行標(biāo)記缸榄。跟在 # 后面的數(shù)字是在源文件中的行號(hào)甚带,而最后的數(shù)字是在新文件中的行號(hào)〖淹罚回到剛才打開的文件鹰贵,緊跟著的是系統(tǒng)頭文件,或者是被看做為封裝了 extern "C" 代碼塊的文件畜晰。

如果滾動(dòng)到文件末尾砾莱,可以看到我們的 helloworld.c 代碼:

# 2 "helloworld.c" 2
int main(int argc, char *argv[])
{
 printf("Hello World!\n");
 return 0;
}

在 Xcode 中,可以通過這樣的方式查看任意文件的預(yù)處理結(jié)果:Product -> Perform Action -> Preprocess块蚌。注意辆毡,編輯器加載預(yù)處理后的文件需要花費(fèi)一些時(shí)間 -- 接近 100,000 行代碼主慰。

編譯

下一步:分析和代碼生成鲤妥。我們可以用下面的命令讓 clang 輸出匯編代碼:

% xcrun clang -S -o - helloworld.c | open -f

我們來看看輸出的結(jié)果。首先會(huì)看到有一些以點(diǎn) . 開頭的行刁憋。這些就是匯編指令。其它的則是實(shí)際的 x86_64 匯編代碼惫皱。最后是一些標(biāo)記 (label)扫皱,與 C 語(yǔ)言中的類似。

我們先看看前三行:

    .section    __TEXT,__text,regular,pure_instructions
    .globl  _main
    .align  4, 0x90

這三行是匯編指令拣宏,不是匯編代碼罩引。.section 指令指定接下來會(huì)執(zhí)行哪一個(gè)段领炫。

第二行的 .globl 指令說明 _main 是一個(gè)外部符號(hào)砚哗。這就是我們的 main() 函數(shù)涯竟。這個(gè)函數(shù)對(duì)于二進(jìn)制文件外部來說是可見的,因?yàn)橄到y(tǒng)要調(diào)用它來運(yùn)行可執(zhí)行文件。

.align 指令指出了后面代碼的對(duì)齊方式。在我們的代碼中,后面的代碼會(huì)按照 16(2^4) 字節(jié)對(duì)齊,如果需要的話倘感,用 0x90 補(bǔ)齊镜廉。

接下來是 main 函數(shù)的頭部:

_main:                                  ## @main
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp2:
    .cfi_def_cfa_offset 16
Ltmp3:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp4:
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp

上面的代碼中有一些與 C 標(biāo)記工作機(jī)制一樣的一些標(biāo)記违诗。它們是某些特定部分的匯編代碼的符號(hào)鏈接趁怔。首先是 _main 函數(shù)真正開始的地址丁稀。這個(gè)符號(hào)會(huì)被 export。二進(jìn)制文件會(huì)有這個(gè)位置的一個(gè)引用。

.cfi_startproc 指令通常用于函數(shù)的開始處洒扎。CFI 是調(diào)用幀信息 (Call Frame Information) 的縮寫。這個(gè)調(diào)用 以松散的方式對(duì)應(yīng)著一個(gè)函數(shù)颤绕。當(dāng)開發(fā)者使用 debugger 和 step instep out 時(shí),實(shí)際上是 stepping in/out 一個(gè)調(diào)用幀。在 C 代碼中忧吟,函數(shù)有自己的調(diào)用幀厕倍,當(dāng)然,別的一些東西也會(huì)有類似的調(diào)用幀。.cfi_startproc 指令給了函數(shù)一個(gè) .eh_frame 入口,這個(gè)入口包含了一些調(diào)用棧的信息(拋出異常時(shí)也是用其來展開調(diào)用幀堆棧的)翁都。這個(gè)指令也會(huì)發(fā)送一些和具體平臺(tái)相關(guān)的指令給 CFI碍论。它與后面的 .cfi_endproc 相匹配,以此標(biāo)記出 main() 函數(shù)結(jié)束的地方柄慰。

接著是另外一個(gè) label ## BB#0:鳍悠。然后,終于坐搔,看到第一句匯編代碼:pushq %rbp藏研。從這里開始事情開始變得有趣。在 OS X上概行,我們會(huì)有 X86_64 的代碼蠢挡,對(duì)于這種架構(gòu),有一個(gè)東西叫做 ABI ( 應(yīng)用二進(jìn)制接口 application binary interface)凳忙,ABI 指定了函數(shù)調(diào)用是如何在匯編代碼層面上工作的业踏。在函數(shù)調(diào)用期間,ABI 會(huì)讓 rbp 寄存器 (基礎(chǔ)指針寄存器 base pointer register) 被保護(hù)起來消略。當(dāng)函數(shù)調(diào)用返回時(shí)堡称,確保 rbp 寄存器的值跟之前一樣,這是屬于 main 函數(shù)的職責(zé)艺演。pushq %rbprbp 的值 push 到棧中却紧,以便我們以后將其 pop 出來。

接下來是兩個(gè) CFI 指令:.cfi_def_cfa_offset 16.cfi_offset %rbp, -16胎撤。這將會(huì)輸出一些關(guān)于生成調(diào)用堆棧展開和調(diào)試的信息晓殊。我們改變了堆棧和基礎(chǔ)指針,而這兩個(gè)指令可以告訴編譯器它們都在哪兒伤提,或者更確切的巫俺,它們可以確保之后調(diào)試器要使用這些信息時(shí),能找到對(duì)應(yīng)的東西肿男。

接下來介汹,movq %rsp, %rbp 將把局部變量放置到棧上。subq $32, %rsp 將棧指針移動(dòng) 32 個(gè)字節(jié)舶沛,也就是函數(shù)會(huì)調(diào)用的位置嘹承。我們先將老的棧指針存儲(chǔ)到 rbp 中,然后將此作為我們局部變量的基址如庭,接著我們更新堆棧指針到我們將會(huì)使用的位置叹卷。

之后,我們調(diào)用了 printf()

leaq    L_.str(%rip), %rax
movl    $0, -4(%rbp)
movl    %edi, -8(%rbp)
movq    %rsi, -16(%rbp)
movq    %rax, %rdi
movb    $0, %al
callq   _printf

首先,leaq 會(huì)將 L_.str 的指針加載到 rax 寄存器中骤竹。留意 L_.str 標(biāo)記在后面的匯編代碼中是如何定義的帝牡。它就是 C 字符串"Hello World!\n"edirsi 寄存器保存了函數(shù)的第一個(gè)和第二個(gè)參數(shù)蒙揣。由于我們會(huì)調(diào)用別的函數(shù)靶溜,所以首先需要將它們的當(dāng)前值保存起來。這就是為什么我們使用剛剛存儲(chǔ)的 rbp 偏移32個(gè)字節(jié)的原因鸣奔。第一個(gè) 32 字節(jié)的值是 0墨技,之后的 32 字節(jié)的值是 edi 寄存器的值 (存儲(chǔ)了 argc)。然后是 64 字節(jié) 的值:rsi 寄存器的值 (存儲(chǔ)了 argv)挎狸。我們?cè)诤竺娌]有使用這些值扣汪,但是編譯器在沒有經(jīng)過優(yōu)化處理的時(shí)候,它們還是會(huì)被存下來锨匆。

現(xiàn)在我們把第一個(gè)函數(shù) printf() 的參數(shù) rax 設(shè)置給第一個(gè)函數(shù)參數(shù)寄存器 edi 中崭别。printf() 是一個(gè)可變參數(shù)的函數(shù)。ABI 調(diào)用約定指定恐锣,將會(huì)把使用來存儲(chǔ)參數(shù)的寄存器數(shù)量存儲(chǔ)在寄存器 al 中茅主。在這里是 0。最后 callq 調(diào)用了 printf() 函數(shù)土榴。

    movl    $0, %ecx
    movl    %eax, -20(%rbp)         ## 4-byte Spill
    movl    %ecx, %eax

上面的代碼將 ecx 寄存器設(shè)置為 0诀姚,并把 eax 寄存器的值保存至棧中,然后將 ect 中的 0 拷貝至 eax 中玷禽。ABI 規(guī)定 eax 將用來保存一個(gè)函數(shù)的返回值赫段,或者此處 main() 函數(shù)的返回值 0:

    addq    $32, %rsp
    popq    %rbp
    ret
    .cfi_endproc

函數(shù)執(zhí)行完成后,將恢復(fù)堆棧指針 —— 利用上面的指令 subq $32, %rsp 把堆棧指針 rsp 上移 32 字節(jié)矢赁。最后糯笙,把之前存儲(chǔ)至 rbp 中的值從棧中彈出來,然后調(diào)用 ret 返回調(diào)用者撩银, ret 會(huì)讀取出棧的返回地址给涕。 .cfi_endproc 平衡了 .cfi_startproc 指令。

接下來是輸出字符串 "Hello World!\n":

    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz   "Hello World!\n"

同樣额获,.section 指令指出下面將要進(jìn)入的段够庙。L_.str 標(biāo)記運(yùn)行在實(shí)際的代碼中獲取到字符串的一個(gè)指針。.asciz 指令告訴編譯器輸出一個(gè)以 ‘\0’ (null) 結(jié)尾的字符串抄邀。

__TEXT __cstring 開啟了一個(gè)新的段首启。這個(gè)段中包含了 C 字符串:

L_.str:                                 ## @.str
    .asciz     "Hello World!\n"

上面兩行代碼創(chuàng)建了一個(gè) null 結(jié)尾的字符串。注意 L_.str 是如何命名撤摸,之后會(huì)通過它來訪問字符串。

最后的 .subsections_via_symbols 指令是靜態(tài)鏈接編輯器使用的。

更過關(guān)于匯編指令的資料可以在 蘋果的 OS X Assembler Reference 中看到准夷。AMD 64 網(wǎng)站有關(guān)于 ABI for x86 的文檔钥飞。另外還有 Gentle Introduction to x86-64 Assembly

重申一下衫嵌,通過下面的選擇操作读宙,我們可以用 Xcode 查看任意文件的匯編輸出結(jié)果:Product -> Perform Action -> Assemble.

匯編器

匯編器將可讀的匯編代碼轉(zhuǎn)換為機(jī)器代碼。它會(huì)創(chuàng)建一個(gè)目標(biāo)對(duì)象文件楔绞,一般簡(jiǎn)稱為 對(duì)象文件结闸。這些文件以 .o 結(jié)尾。如果用 Xcode 構(gòu)建應(yīng)用程序酒朵,可以在工程的 derived data 目錄中桦锄,Objects-normal 文件夾下找到這些文件。

鏈接器

稍后我們會(huì)對(duì)鏈接器做更詳細(xì)的介紹蔫耽。這里簡(jiǎn)單介紹一下:鏈接器解決了目標(biāo)文件和庫(kù)之間的鏈接结耀。什么意思呢?還記得下面的語(yǔ)句嗎:

callq   _printf

printf()libc 庫(kù)中的一個(gè)函數(shù)匙铡。無論怎樣图甜,最后的可執(zhí)行文件需要能需要知道 printf() 在內(nèi)存中的具體位置:例如,_printf 的地址符號(hào)是什么鳖眼。鏈接器會(huì)讀取所有的目標(biāo)文件 (此處只有一個(gè)) 和庫(kù) (此處是 libc)黑毅,并解決所有未知符號(hào) (此處是 _printf) 的問題。然后將它們編碼進(jìn)最后的可執(zhí)行文件中 (可以在 libc 中找到符號(hào) _printf)钦讳,接著鏈接器會(huì)輸出可以運(yùn)行的執(zhí)行文件:a.out矿瘦。

Section

就像我們上面提到的一樣,這里有些東西叫做 section蜂厅。一個(gè)可執(zhí)行文件包含多個(gè)段匪凡,也就是多個(gè) section【蛟常可執(zhí)行文件不同的部分將加載進(jìn)不同的 section病游,并且每個(gè) section 會(huì)轉(zhuǎn)換進(jìn)某個(gè) segment 里。這個(gè)概念對(duì)于所有的可執(zhí)行文件都是成立的稠通。

我們來看看 a.out 二進(jìn)制中的 section衬衬。我們可以使用 size 工具來觀察:

% xcrun size -x -l -m a.out 
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
    Section __text: 0x37 (addr 0x100000f30 offset 3888)
    Section __stubs: 0x6 (addr 0x100000f68 offset 3944)
    Section __stub_helper: 0x1a (addr 0x100000f70 offset 3952)
    Section __cstring: 0xe (addr 0x100000f8a offset 3978)
    Section __unwind_info: 0x48 (addr 0x100000f98 offset 3992)
    Section __eh_frame: 0x18 (addr 0x100000fe0 offset 4064)
    total 0xc5
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
    Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
    Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
    total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

如上代碼所示,我們的 a.out 文件有 4 個(gè) segment改橘。有些 segment 中有多個(gè) section滋尉。

當(dāng)運(yùn)行一個(gè)可執(zhí)行文件時(shí),虛擬內(nèi)存 (VM - virtual memory) 系統(tǒng)將 segment 映射到進(jìn)程的地址空間上飞主。映射完全不同于我們一般的認(rèn)識(shí)狮惜,如果你對(duì)虛擬內(nèi)存系統(tǒng)不熟悉高诺,可以簡(jiǎn)單的想象虛擬內(nèi)存系統(tǒng)將整個(gè)可執(zhí)行文件加載進(jìn)內(nèi)存 -- 雖然在實(shí)際上不是這樣的。VM 使用了一些技巧來避免全部加載碾篡。

當(dāng)虛擬內(nèi)存系統(tǒng)進(jìn)行映射時(shí)虱而,segment 和 section 會(huì)以不同的參數(shù)和權(quán)限被映射。

上面的代碼中开泽,__TEXT segment 包含了被執(zhí)行的代碼牡拇。它被以只讀和可執(zhí)行的方式映射。進(jìn)程被允許執(zhí)行這些代碼穆律,但是不能修改惠呼。這些代碼也不能對(duì)自己做出修改,因此這些被映射的頁(yè)從來不會(huì)被改變峦耘。

__DATA segment 以可讀寫和不可執(zhí)行的方式映射剔蹋。它包含了將會(huì)被更改的數(shù)據(jù)。

第一個(gè) segment 是 __PAGEZERO贡歧。它的大小為 4GB滩租。這 4GB 并不是文件的真實(shí)大小,但是規(guī)定了進(jìn)程地址空間的前 4GB 被映射為 不可執(zhí)行利朵、不可寫和不可讀律想。這就是為什么當(dāng)讀寫一個(gè) NULL 指針或更小的值時(shí)會(huì)得到一個(gè) EXC_BAD_ACCESS 錯(cuò)誤。這是操作系統(tǒng)在嘗試防止引起系統(tǒng)崩潰绍弟。

在 segment中技即,一般都會(huì)有多個(gè) section。它們包含了可執(zhí)行文件的不同部分樟遣。在 __TEXT segment 中而叼,__text section 包含了編譯所得到的機(jī)器碼。__stubs__stub_helper 是給動(dòng)態(tài)鏈接器 (dyld) 使用的豹悬。通過這兩個(gè) section葵陵,在動(dòng)態(tài)鏈接代碼中,可以允許延遲鏈接瞻佛。__const (在我們的代碼中沒有) 是常量脱篙,不可變的,就像 __cstring (包含了可執(zhí)行文件中的字符串常量 -- 在源碼中被雙引號(hào)包含的字符串) 常量一樣伤柄。

__DATA segment 中包含了可讀寫數(shù)據(jù)绊困。在我們的程序中只有 __nl_symbol_ptr__la_symbol_ptr,它們分別是 non-lazylazy 符號(hào)指針适刀。延遲符號(hào)指針用于可執(zhí)行文件中調(diào)用未定義的函數(shù)秤朗,例如不包含在可執(zhí)行文件中的函數(shù),它們將會(huì)延遲加載笔喉。而針對(duì)非延遲符號(hào)指針取视,當(dāng)可執(zhí)行文件被加載同時(shí)硝皂,也會(huì)被加載。

_DATA segment 中的其它常見 section 包括 __const贫途,在這里面會(huì)包含一些需要重定向的常量數(shù)據(jù)吧彪。例如 char * const p = "foo"; -- p 指針指向的數(shù)據(jù)是可變的。__bss section 沒有被初始化的靜態(tài)變量丢早,例如 static int a; -- ANSI C 標(biāo)準(zhǔn)規(guī)定靜態(tài)變量必須設(shè)置為 0。并且在運(yùn)行時(shí)靜態(tài)變量的值是可以修改的秧倾。__common section 包含未初始化的外部全局變量怨酝,跟 static 變量類似。例如在函數(shù)外面定義的 int a;那先。最后农猬,__dyld 是一個(gè) section 占位符,被用于動(dòng)態(tài)鏈接器售淡。

蘋果的 OS X Assembler Reference 文檔有更多關(guān)于 section 類型的介紹斤葱。

Section 中的內(nèi)容

下面,我們用 otool(1) 來觀察一個(gè) section 中的內(nèi)容:

% xcrun otool -s __TEXT __text a.out 
a.out:
(__TEXT,__text) section
0000000100000f30 55 48 89 e5 48 83 ec 20 48 8d 05 4b 00 00 00 c7 
0000000100000f40 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7 
0000000100000f50 b0 00 e8 11 00 00 00 b9 00 00 00 00 89 45 ec 89 
0000000100000f60 c8 48 83 c4 20 5d c3 

上面是我們 app 中的代碼揖闸。由于 -s __TEXT __text 很常見揍堕,otool 對(duì)其設(shè)置了一個(gè)縮寫 -t 。我們還可以通過添加 -v 來查看反匯編代碼:

% xcrun otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000f30    pushq   %rbp
0000000100000f31    movq    %rsp, %rbp
0000000100000f34    subq    $0x20, %rsp
0000000100000f38    leaq    0x4b(%rip), %rax
0000000100000f3f    movl    $0x0, 0xfffffffffffffffc(%rbp)
0000000100000f46    movl    %edi, 0xfffffffffffffff8(%rbp)
0000000100000f49    movq    %rsi, 0xfffffffffffffff0(%rbp)
0000000100000f4d    movq    %rax, %rdi
0000000100000f50    movb    $0x0, %al
0000000100000f52    callq   0x100000f68
0000000100000f57    movl    $0x0, %ecx
0000000100000f5c    movl    %eax, 0xffffffffffffffec(%rbp)
0000000100000f5f    movl    %ecx, %eax
0000000100000f61    addq    $0x20, %rsp
0000000100000f65    popq    %rbp
0000000100000f66    ret

上面的內(nèi)容是一樣的汤纸,只不過以反匯編形式顯示出來衩茸。你應(yīng)該感覺很熟悉,這就是我們?cè)谇懊婢幾g時(shí)候的代碼贮泞。唯一的不同就是楞慈,在這里我們沒有任何的匯編指令在里面。這是純粹的二進(jìn)制執(zhí)行文件啃擦。

同樣的方法囊蓝,我們可以查看別的 section:

% xcrun otool -v -s __TEXT __cstring a.out
a.out:
Contents of (__TEXT,__cstring) section
0x0000000100000f8a  Hello World!\n

或:

% xcrun otool -v -s __TEXT __eh_frame a.out 
a.out:
Contents of (__TEXT,__eh_frame) section
0000000100000fe0    14 00 00 00 00 00 00 00 01 7a 52 00 01 78 10 01 
0000000100000ff0    10 0c 07 08 90 01 00 00 

性能上需要注意的事項(xiàng)

從側(cè)面來講,__DATA__TEXT segment對(duì)性能會(huì)有所影響令蛉。如果你有一個(gè)很大的二進(jìn)制文件聚霜,你可能得去看看蘋果的文檔:關(guān)于代碼大小性能指南。將數(shù)據(jù)移至 __TEXT 是個(gè)不錯(cuò)的選擇言询,因?yàn)檫@些頁(yè)從來不會(huì)被改變俯萎。

任意的片段

使用鏈接符號(hào) -sectcreate 我們可以給可執(zhí)行文件以 section 的方式添加任意的數(shù)據(jù)。這就是如何將一個(gè) Info.plist 文件添加到一個(gè)獨(dú)立的可執(zhí)行文件中的方法运杭。Info.plist 文件中的數(shù)據(jù)需要放入到 __TEXT segment 里面的一個(gè) __info_plist section 中夫啊。可以將 -sectcreate segname sectname file 傳遞給鏈接器(通過將下面的內(nèi)容傳遞給 clang):

-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist

同樣辆憔,-sectalign 規(guī)定了對(duì)其方式撇眯。如果你添加的是一個(gè)全新的 segment报嵌,那么需要通過 -segprot 來規(guī)定 segment 的保護(hù)方式 (讀/寫/可執(zhí)行)。這些所有內(nèi)容在鏈接器的幫助文檔中都有熊榛,例如 ld(1)锚国。

我們可以利用定義在 /usr/include/mach-o/getsect.h 中的函數(shù) getsectdata() 得到 section,例如 getsectdata() 可以得到指向 section 數(shù)據(jù)的一個(gè)指針玄坦,并返回相關(guān) section 的長(zhǎng)度血筑。

Mach-O

在 OS X 和 iOS 中可執(zhí)行文件的格式為 Mach-O

% file a.out 
a.out: Mach-O 64-bit executable x86_64

對(duì)于 GUI 程序也是一樣的:

% file /Applications/Preview.app/Contents/MacOS/Preview 
/Applications/Preview.app/Contents/MacOS/Preview: Mach-O 64-bit executable x86_64

關(guān)于 Mach-O 文件格式 蘋果有詳細(xì)的介紹。

我們可以使用 otool(1) 來觀察可執(zhí)行文件的頭部 -- 規(guī)定了這個(gè)文件是什么煎楣,以及文件是如何被加載的豺总。通過 -h 可以打印出頭信息:

% otool -v -h a.out           a.out:
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL LIB64     EXECUTE    16       1296   NOUNDEFS DYLDLINK TWOLEVEL PIE

cputypecpusubtype 規(guī)定了這個(gè)可執(zhí)行文件能夠運(yùn)行在哪些目標(biāo)架構(gòu)上。ncmdssizeofcmds 是加載命令择懂,可以通過 -l 來查看這兩個(gè)加載命令:

% otool -v -l a.out | open -f
a.out:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
...

加載命令規(guī)定了文件的邏輯結(jié)構(gòu)和文件在虛擬內(nèi)存中的布局喻喳。otool 打印出的大多數(shù)信息都是源自這里的加載命令±铮看一下 Load command 1 部分表伦,可以找到 initprot r-x,它規(guī)定了之前提到的保護(hù)方式:只讀和可執(zhí)行慷丽。

對(duì)于每一個(gè) segment蹦哼,以及segment 中的每個(gè) section,加載命令規(guī)定了它們?cè)趦?nèi)存中結(jié)束的位置盈魁,以及保護(hù)模式等翔怎。例如,下面是 __TEXT __text section 的輸出內(nèi)容:

Section
  sectname __text
   segname __TEXT
      addr 0x0000000100000f30
      size 0x0000000000000037
    offset 3888
     align 2^4 (16)
    reloff 0
    nreloc 0
      type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
 reserved1 0
 reserved2 0

上面的代碼將在 0x100000f30 處結(jié)束杨耙。它在文件中的偏移量為 3888赤套。如果看一下之前 xcrun otool -v -t a.out 輸出的反匯編代碼,可以發(fā)現(xiàn)代碼實(shí)際位置在 0x100000f30珊膜。

我們同樣看看在可執(zhí)行文件中佛南,動(dòng)態(tài)鏈接庫(kù)是如何使用的:

% otool -v -L a.out
a.out:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)
    time stamp 2 Thu Jan  1 01:00:02 1970

上面就是我們可執(zhí)行文件將要找到 _printf 符號(hào)的地方丈牢。

一個(gè)更復(fù)雜的例子

我們來看看有三個(gè)文件的復(fù)雜例子:

Foo.h:

#import <Foundation/Foundation.h>

@interface Foo : NSObject

- (void)run;

@end

Foo.m:

#import "Foo.h"

@implementation Foo

- (void)run
{
    NSLog(@"%@", NSFullUserName());
}

@end

helloworld.m:

#import "Foo.h"

int main(int argc, char *argv[])
{
    @autoreleasepool {
        Foo *foo = [[Foo alloc] init];
        [foo run];
        return 0;
    }
}

編譯多個(gè)文件

在上面的示例中喷市,有多個(gè)源文件骡尽。所以我們需要讓 clang 對(duì)輸入每個(gè)文件生成對(duì)應(yīng)的目標(biāo)文件:

% xcrun clang -c Foo.m
% xcrun clang -c helloworld.m

我們從來不編譯頭文件。頭文件的作用就是在被編譯的實(shí)現(xiàn)文件中對(duì)代碼做簡(jiǎn)單的共享竹祷。Foo.mhelloworld.m 都是通過 #import 語(yǔ)句將 Foo.h 文件中的內(nèi)容添加到實(shí)現(xiàn)文件中的谈跛。

最終得到了兩個(gè)目標(biāo)文件:

% file helloworld.o Foo.o
helloworld.o: Mach-O 64-bit object x86_64
Foo.o:        Mach-O 64-bit object x86_64

為了生成一個(gè)可執(zhí)行文件,我們需要將這兩個(gè)目標(biāo)文件和 Foundation framework 鏈接起來:

xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation

現(xiàn)在可以運(yùn)行我們的程序了:

% ./a.out 
2013-11-03 18:03:03.386 a.out[8302:303] Daniel Eggert

符號(hào)表和鏈接

我們這個(gè)簡(jiǎn)單的程序是將兩個(gè)目標(biāo)文件合并到一起的塑陵。Foo.o 目標(biāo)文件包含了 Foo 類的實(shí)現(xiàn)感憾,而 helloworld.o 目標(biāo)文件包含了 main() 函數(shù),以及調(diào)用/使用 Foo 類令花。

另外阻桅,這兩個(gè)目標(biāo)對(duì)象都使用了 Foundation framework凉倚。helloworld.o 目標(biāo)文件使用了它的 autorelease pool,并間接的使用了 libobjc.dylib 中的 Objective-C 運(yùn)行時(shí)嫂沉。它需要運(yùn)行時(shí)函數(shù)來進(jìn)行消息的調(diào)用稽寒。Foo.o 目標(biāo)文件也有類似的原理。

所有的這些東西都被形象的稱之為符號(hào)趟章。我們可以把符號(hào)看成是一些在運(yùn)行時(shí)將會(huì)變成指針的東西杏糙。雖然實(shí)際上并不是這樣的。

每個(gè)函數(shù)尤揣、全局變量和類等都是通過符號(hào)的形式來定義和使用的搔啊。當(dāng)我們將目標(biāo)文件鏈接為一個(gè)可執(zhí)行文件時(shí),鏈接器 (ld(1)) 在目標(biāo)文件和動(dòng)態(tài)庫(kù)之間對(duì)符號(hào)做了解析處理北戏。

可執(zhí)行文件和目標(biāo)文件有一個(gè)符號(hào)表,這個(gè)符號(hào)表規(guī)定了它們的符號(hào)漫蛔。如果我們用 nm(1) 工具觀察一下 helloworld.o 目標(biāo)文件嗜愈,可以看到如下內(nèi)容:

% xcrun nm -nm helloworld.o
                 (undefined) external _OBJC_CLASS_$_Foo
0000000000000000 (__TEXT,__text) external _main
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
                 (undefined) external _objc_msgSend
                 (undefined) external _objc_msgSend_fixup
0000000000000088 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_
000000000000008e (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_1
0000000000000093 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_2
00000000000000a0 (__DATA,__objc_msgrefs) weak private external l_objc_msgSend_fixup_alloc
00000000000000e8 (__TEXT,__eh_frame) non-external EH_frame0
0000000000000100 (__TEXT,__eh_frame) external _main.eh

上面就是那個(gè)目標(biāo)文件的所有符號(hào)。_OBJC_CLASS_$_FooFoo Objective-C 類的符號(hào)莽龟。該符號(hào)是 undefined, external 蠕嫁。External 的意思是指對(duì)于這個(gè)目標(biāo)文件該類并不是私有的,相反毯盈,non-external 的符號(hào)則表示對(duì)于目標(biāo)文件是私有的剃毒。我們的 helloworld.o 目標(biāo)文件引用了類 Foo,不過這并沒有實(shí)現(xiàn)它搂赋。因此符號(hào)表中將其標(biāo)示為 undefined赘阀。

接下來是 _main 符號(hào),它是表示 main() 函數(shù)脑奠,同樣為 external基公,這是因?yàn)樵摵瘮?shù)需要被調(diào)用,所以應(yīng)該為可見的宋欺。由于在 helloworld.o 文件中實(shí)現(xiàn)了 這個(gè) main 函數(shù)轰豆。這個(gè)函數(shù)地址位于 0處,并且需要轉(zhuǎn)入到__TEXT,__text section齿诞。接著是 4 個(gè) Objective-C 運(yùn)行時(shí)函數(shù)酸休。它們同樣是 undefined的,需要鏈接器進(jìn)行符號(hào)解析祷杈。

如果我們轉(zhuǎn)而觀察 Foo.o 目標(biāo)文件斑司,可以看到如下輸出:

% xcrun nm -nm Foo.o
0000000000000000 (__TEXT,__text) non-external -[Foo run]
                 (undefined) external _NSFullUserName
                 (undefined) external _NSLog
                 (undefined) external _OBJC_CLASS_$_NSObject
                 (undefined) external _OBJC_METACLASS_$_NSObject
                 (undefined) external ___CFConstantStringClassReference
                 (undefined) external __objc_empty_cache
                 (undefined) external __objc_empty_vtable
000000000000002f (__TEXT,__cstring) non-external l_.str
0000000000000060 (__TEXT,__objc_classname) non-external L_OBJC_CLASS_NAME_
0000000000000068 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000b0 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000d0 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000118 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000140 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
0000000000000168 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_
000000000000016c (__TEXT,__objc_methtype) non-external L_OBJC_METH_VAR_TYPE_
00000000000001a8 (__TEXT,__eh_frame) non-external EH_frame0
00000000000001c0 (__TEXT,__eh_frame) non-external -[Foo run].eh

第五行至最后一行顯示了 _OBJC_CLASS_$_Foo 已經(jīng)定義了,并且對(duì)于 Foo.o 是一個(gè)外部符號(hào) -- ·Foo.o· 包含了這個(gè)類的實(shí)現(xiàn)吠式。

Foo.o 同樣有 undefined 的符號(hào)陡厘。首先是使用了符號(hào) NSFullUserName()抽米,NSLog()NSObject

當(dāng)我們將這兩個(gè)目標(biāo)文件和 Foundation framework (是一個(gè)動(dòng)態(tài)庫(kù)) 進(jìn)行鏈接處理時(shí)糙置,鏈接器會(huì)嘗試解析所有的 undefined 符號(hào)云茸。它可以解析 _OBJC_CLASS_$_Foo。另外谤饭,它將使用 Foundation framework标捺。

當(dāng)鏈接器通過動(dòng)態(tài)庫(kù) (此處是 Foundation framework) 解析成功一個(gè)符號(hào)時(shí),它會(huì)在最終的鏈接圖中記錄這個(gè)符號(hào)是通過動(dòng)態(tài)庫(kù)進(jìn)行解析的揉抵。鏈接器會(huì)記錄輸出文件是依賴于哪個(gè)動(dòng)態(tài)鏈接庫(kù)亡容,并連同其路徑一起進(jìn)行記錄。在我們的例子中冤今,_NSFullUserName闺兢,_NSLog_OBJC_CLASS_$_NSObject戏罢,_objc_autoreleasePoolPop 等符號(hào)都是遵循這個(gè)過程屋谭。

我們可以看一下最終可執(zhí)行文件 a.out 的符號(hào)表,并注意觀察鏈接器是如何解析所有符號(hào)的:

% xcrun nm -nm a.out 
                 (undefined) external _NSFullUserName (from Foundation)
                 (undefined) external _NSLog (from Foundation)
                 (undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)
                 (undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external __objc_empty_cache (from libobjc)
                 (undefined) external __objc_empty_vtable (from libobjc)
                 (undefined) external _objc_autoreleasePoolPop (from libobjc)
                 (undefined) external _objc_autoreleasePoolPush (from libobjc)
                 (undefined) external _objc_msgSend (from libobjc)
                 (undefined) external _objc_msgSend_fixup (from libobjc)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e50 (__TEXT,__text) external _main
0000000100000ed0 (__TEXT,__text) non-external -[Foo run]
0000000100001128 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001150 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

可以看到所有的 Foundation 和 Objective-C 運(yùn)行時(shí)符號(hào)依舊是 undefined龟糕,不過現(xiàn)在的符號(hào)表中已經(jīng)多了如何解析它們的信息桐磁,例如在哪個(gè)動(dòng)態(tài)庫(kù)中可以找到對(duì)應(yīng)的符號(hào)。

可執(zhí)行文件同樣知道去哪里找到所需庫(kù):

% xcrun otool -L a.out
a.out:
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1056.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 855.11.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

在運(yùn)行時(shí)讲岁,動(dòng)態(tài)鏈接器 dyld(1) 可以解析這些 undefined 符號(hào)我擂,dyld 將會(huì)確定好 _NSFullUserName 等符號(hào),并指向它們?cè)?Foundation 中的實(shí)現(xiàn)等缓艳。

我們可以針對(duì) Foundation 運(yùn)行 nm(1)校摩,并檢查這些符號(hào)的定義情況:

% xcrun nm -nm `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName
0000000000007f3e (__TEXT,__text) external _NSFullUserName 

動(dòng)態(tài)鏈接編輯器

有一些環(huán)境變量對(duì)于 dyld 的輸出信息非常有用。首先郎任,如果設(shè)置了 DYLD_PRINT_LIBRARIES秧耗,那么 dyld將會(huì)打印出什么庫(kù)被加載了:

% (export DYLD_PRINT_LIBRARIES=; ./a.out )
dyld: loaded: /Users/deggert/Desktop/command_line/./a.out
dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
dyld: loaded: /usr/lib/libobjc.A.dylib
dyld: loaded: /usr/lib/libauto.dylib
[...]

上面將會(huì)顯示出在加載 Foundation 時(shí),同時(shí)會(huì)加載的 70 個(gè)動(dòng)態(tài)庫(kù)舶治。這是由于 Foundation 依賴于另外一些動(dòng)態(tài)庫(kù)分井。運(yùn)行下面的命令:

% xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation

可以看到 Foundation 使用了 15 個(gè)動(dòng)態(tài)庫(kù)。

dyld 的共享緩存

當(dāng)你構(gòu)建一個(gè)真正的程序時(shí)霉猛,將會(huì)鏈接各種各樣的庫(kù)尺锚。它們又會(huì)依賴其他一些 framework 和 動(dòng)態(tài)庫(kù)。需要加載的動(dòng)態(tài)庫(kù)會(huì)非常多惜浅。而對(duì)于相互依賴的符號(hào)就更多了瘫辩。可能將會(huì)有上千個(gè)符號(hào)需要解析處理,這將花費(fèi)很長(zhǎng)的時(shí)間:一般是好幾秒鐘伐厌。

為了縮短這個(gè)處理過程所花費(fèi)時(shí)間承绸,在 OS X 和 iOS 上的動(dòng)態(tài)鏈接器使用了共享緩存,共享緩存存于 /var/db/dyld/挣轨。對(duì)于每一種架構(gòu)军熏,操作系統(tǒng)都有一個(gè)單獨(dú)的文件,文件中包含了絕大多數(shù)的動(dòng)態(tài)庫(kù)卷扮,這些庫(kù)都已經(jīng)鏈接為一個(gè)文件荡澎,并且已經(jīng)處理好了它們之間的符號(hào)關(guān)系。當(dāng)加載一個(gè) Mach-O 文件 (一個(gè)可執(zhí)行文件或者一個(gè)庫(kù)) 時(shí)晤锹,動(dòng)態(tài)鏈接器首先會(huì)檢查 共享緩存 看看是否存在其中摩幔,如果存在,那么就直接從共享緩存中拿出來使用鞭铆。每一個(gè)進(jìn)程都把這個(gè)共享緩存映射到了自己的地址空間中或衡。這個(gè)方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動(dòng)時(shí)間。


原文: Mach-O Executables

譯文 objc.io 第6期 Mach-O 可執(zhí)行文件

精細(xì)校對(duì) @BeyondVincent

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末车遂,一起剝皮案震驚了整個(gè)濱河市薇宠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌艰额,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椒涯,死亡現(xiàn)場(chǎng)離奇詭異柄沮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)废岂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門祖搓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人湖苞,你說我怎么就攤上這事拯欧。” “怎么了财骨?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵镐作,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我隆箩,道長(zhǎng)该贾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任捌臊,我火速辦了婚禮杨蛋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己逞力,他們只是感情好曙寡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寇荧,像睡著了一般举庶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砚亭,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天灯变,我揣著相機(jī)與錄音,去河邊找鬼捅膘。 笑死添祸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寻仗。 我是一名探鬼主播刃泌,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼署尤!你這毒婦竟也來了耙替?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤曹体,失蹤者是張志新(化名)和其女友劉穎俗扇,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體箕别,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铜幽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了串稀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片除抛。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖母截,靈堂內(nèi)的尸體忽然破棺而出到忽,到底是詐尸還是另有隱情,我是刑警寧澤清寇,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布喘漏,位于F島的核電站,受9級(jí)特大地震影響颗管,放射性物質(zhì)發(fā)生泄漏陷遮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一垦江、第九天 我趴在偏房一處隱蔽的房頂上張望帽馋。 院中可真熱鬧搅方,春花似錦、人聲如沸绽族。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吧慢。三九已至涛漂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間检诗,已是汗流浹背匈仗。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逢慌,地道東北人悠轩。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像攻泼,于是被迫代替她去往敵國(guó)和親火架。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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