link与Symbol

Mach-O

Mach-O(Mach Object)是macOSiOSiPadOS存储程序和库的文件格式。对应系统通过应用二进制接口(application binary interface,缩写为 ABI)来运行该格式的文件。
Mach-O格式用来替代BSD系统的a.out格式。Mach-O文件格式保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式。

可执行文件的调用过程:

  1. 调用fork函数,创建一个process(进程);
  2. 调用execve或其衍生函数,在该进程上加载,执行Mach-O文件。
    当调用execve(程序加载器) 时,内核实际上在执行以下操作:
    i.将文件加载到内存;
    ii.开始分析Mach-O中的mach_header,以确认它是有效的Mach-O文件。

Mach-o File Format

一个Mach-o文件有两部分组成:headerdata

image.png

一个简单的main函数例子:

int main(int argc, const char * argv[]) {
    return 0;
}

编译好后查看下__TEXT 段:
objdump --macho -d ${MACHO_PATH}
main函数对应的机器码如下,后面的汇编是给我们阅读的,机器并不需要,可以理解为注释。

image.png

'int main() { }' compiled for x86_64-apple-macosx with clang
0x55,                         // offset 0 -- pushq %rbp
0x48, 0x89, 0xe5,  // offset 1 -- movq %rsp, %rbp
0x31, 0xc0,             // offset 4 -- xorl %eax, %eax
0x5d,                       // offset 6 -- popq %rbp
0xc3                        // offset 7 -- retq

这里的机器码是固定的,也就是所谓的ABI稳定。

Big endian or little endian

iOSmacOS开发都是小端模式。

image.png

当读取一个地址的时候

  • 小端:从右往左读
  • 大端:从左往右读

header

代表了文件的映射,描述了文件的内容以及文件所有内容所在的位置。header内的section描述了对应的二进制信息。
header包含三种类型:

  • Mach header
  • segment
  • sections

Mach header

⚠️:Mach header属于header的一部分,它包含了整个文件的信息和segment信息。

image.png

直接新建一个工程看下Mach header

objdump --macho -private-header ${MACHO_PATH}
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL  0x00     EXECUTE    19       1944   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

也可以通过otool查看:

otool -h ${MACHO_PATH}
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777223          3  0x00           2    20       1960 0x00218085

查看mach header

  1. objdump --macho -private-header ${MACHO_PATH}
  2. otool -h ${MACHO_PATH}

相对来说objdump更容易阅读,otool更接近原始数据。

data

紧跟header之后,由多个二进制组成,one by one。

Load Commands

二进制文件加载进内存要执行的一些指令。
这里的指令主要在负责我们 APP 对应进程的创建和基本设置(分配虚拟内存,创建主线程,处理代码签名/加密的工作),然后对动态链接库(.dylib 系统库和我们自己创建的动态库)进行库加载和符号解析的工作。

直接在.app目录下dumpobjdump --macho --private-headers)一下可执行文件:

➜  TestMutableConfig.app objdump --macho --private-headers TestMutableConfig

可以看到load command信息如下:

image.png

都知道程序主入口是main函数,看下对应的main信息:

objdump --macho --private-headers TestMutableConfig | grep 'LC_MAIN' -A 3

       cmd LC_MAIN
   cmdsize 24
  entryoff 7856
 stacksize 0

dyld通过LC_MAIN去找程序的主入口。当然可以自己指定不为main
可以理解为Mach-O就是:文件配置+二进制代码

Mach-O 是可读可写的

上面通过objdump输出的信息有点多,如果只想看自己关注的信息,可以精简下写一个自定义读取程序machoInfo
原理是根据Mach-O格式读取数据
1:直接终端命令查看:

image.png

2:直接main函数传参进去:
image.png

image.png

machoInfo地址:machoInfo

目标文件生成过程:


image.png
  • 链接器(llvm-ld)并没有被执行。
  • 目标文件不会包含Unix程序在被装载和执行时所必须包含的信息。
    修改main.m文件:
int global_uninit_value;

// 外部符号
int global_init_value = 10;

double default_x __attribute__((visibility("hidden"))) ;

static int static_init_value = 9;

static int static_uninit_value;

int main(int argc, const char * argv[]) {
    static_uninit_value = 10;
    NSLog(@"%d", static_init_value);

    return 0;
}

对应的代码段:

(__TEXT,__text) section
_main:
100003f50:  55  pushq   %rbp
100003f51:  48 89 e5    movq    %rsp, %rbp
100003f54:  48 83 ec 10 subq    $16, %rsp
100003f58:  48 8d 05 a9 00 00 00    leaq    169(%rip), %rax ## Objc cfstring ref: @"%d"
100003f5f:  c7 45 fc 00 00 00 00    movl    $0, -4(%rbp)
100003f66:  89 7d f8    movl    %edi, -8(%rbp)
100003f69:  48 89 75 f0 movq    %rsi, -16(%rbp)
100003f6d:  c7 05 a1 40 00 00 0a 00 00 00   movl    $10, _static_init_value(%rip)
100003f77:  8b 35 97 40 00 00   movl    _static_init_value(%rip), %esi
100003f7d:  48 89 c7    movq    %rax, %rdi
100003f80:  b0 00   movb    $0, %al
100003f82:  e8 09 00 00 00  callq   0x100003f90 ## symbol stub for: _NSLog
100003f87:  31 c0   xorl    %eax, %eax
100003f89:  48 83 c4 10 addq    $16, %rsp
100003f8d:  5d  popq    %rbp
100003f8e:  c3  retq

在生成.o的过程中:

  1. 能变成汇编的代码尽量变成汇编。
  2. 符号归类 -> 重定位符号表(放的是.m/.o文件用到的API)。也就是保存了当前文件用到的符号。
  3. .o -> 链接 -> 合并到一张表 -> exec(可执行文件)

查看重定位符号表
objdump --macho -reloc test.o(目标文件)

image.png

链接

链接的本质就是把多个目标文件组合成一个文件
将多个.o文件合并成一个可执行文件,在链接的过程中可以进行操作。
可以通过man ld查看都有哪些功能,比如:why-livewhy-load等。

Symbol

  • Symbol Table: 用来保存符号。
  • String Table: 用来保存符号的名称。
  • Indirect Symbol Table: 间接符号表。保存使用的外部符号,更准确一点就是使用外部动态库的符号。是Symbol Table的子集。

符号的种类

为了方便操作,直接在Xcode Run script中添加命令编译后直接将结果输出到终端:
Run Script

#输出 "HotpotCat" 重定位到 终端 /dev/ttys003
echo "HotpotCat" > /dev/ttys003

其中/dev/ttys003为终端,可以在终端中输入tty查看获取。

image.png

Run Script也可以读取XCConfig中的内容。可以配合起来完成。所以我们可以分为3步:配置读取符号脚本,XCConfig传递参数,Run sctipt 执行脚本

1.xcode_run_cmd.sh

定义一个xcode_run_cmd脚本,将符号输出到终端,这个脚本需要2个参数,参数从XCConfig配置。

#!/bin/sh

RunCommand() {
  #判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
  #[[是 bash 程序语言的关键字。用于判断
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    #作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
      if [[ -n "$TTY" ]]; then
          echo "♦ $@" 1>$TTY
      else
          echo "♦ $*"
      fi
      echo "------------------------------------------------------------------------------" 1>$TTY
  fi
  #与$*相同。但是使用时加引号,并在引号中返回每个参数。"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数
  if [[ -n "$TTY" ]]; then
        eval "$@" &>$TTY
  else
      "$@"
  fi
  #显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
  return $?
}

EchoError() {
    #在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
    # >  默认为标准输出重定向,与 1> 相同
    # 2>&1  意思是把 标准错误输出 重定向到 标准输出.
    # &>file  意思是把标准输出 和 标准错误输出 都重定向到文件file中
    # 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
    if [[ -n "$TTY" ]]; then
        echo "$@" 1>&2>$TTY
    else
        echo "$@" 1>&2
    fi
    
}

RunCMDToTTY() {
    # CMD = 运行到命令
    # TTY = 终端
    if [[ ! -e "$TTY" ]]; then
        EchoError "=========================================="
        EchoError "ERROR: Not Config tty to output."
        exit -1
    fi
    if [[ -n "$CMD" ]]; then
        RunCommand $CMD
    else
        EchoError "=========================================="
        EchoError "ERROR:Failed to run CMD. THE CMD must not null"
    fi
}


RunCMDToTTY

2.XCConfig配置:

由于脚本中有3个参数,可以直接在XCConfig中配置,脚本中就可以读取到了。因为脚本是在Run strip执行的,并且和工程在同一目录。

// BUILD_DIR build路径 *为可执行文件名称,这里为了适配写为*通配符
MACHO_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/*
//# CMD = 运行的命令
//# CMD_FLAG = 运行的命令参数
//# TTY = 终端
CMD = nm
// -p: 不排序
// -a: 显示所有符号,包含调试符号
CMD_FLAG = -pa ${MACHO_PATH}
TTY = /dev/ttys000

BUILD_DIR:

image.png

由于不需要调试符号,可以脱去(strip)调试符号
我们知道build setting中有个strip设置:
Deployment PostprocessingStrip Style:
image.png

image.png

可以看到Strip是在Run Script之后执行的。所以在build setting中设置行不通。
那么直接man ld查看下有没有可用的命令:
image.png

可以看到有个S比较符合。XCConfig修改完后配置如下:

// BUILD_DIR build路径 *为可执行文件名称,这里为了适配写为*通配符
MACHO_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/*
//# CMD = 运行的命令
//# CMD_FLAG = 运行的命令参数
//# TTY = 终端
CMD = nm
// -p: 不排序
// -a: 显示所有符号,包含调试符号
CMD_FLAG = -pa ${MACHO_PATH}
TTY = /dev/ttys000
//-Xlinker 虽然执行的ld,但是命令传给连接器
OTHER_LDFLAGS=-Xlinker -S

3.Run script执行脚本

# SRCROOT 当前代码路径
/bin/sh "$SRCROOT/xcode_run_cmd.sh"

至此,环境已经配置好了。

接下来配置一些符号看下:

//全局变量
int global_uninit_value;

// 外部符号
int global_init_value = 10;

double default_x __attribute__((visibility("hidden"))) ;
//静态变量
static int static_init_value = 9;
static int static_uninit_value;

int main(int argc, const char * argv[]) {
    static_uninit_value = 10;
    NSLog(@"%d", static_init_value);

    return 0;
}

objdump --macho --syms ${MACHO_PATH}查看下:

image.png

可以看到有好多Debug符号,在生成文件的时候会生成DWARF调试文件,放在.o专门的DWARF段,当连接的时候会放入符号表中。也就是说调试符号在目标文件中是放在DWARF段中的,当链接后放在符号表中。去掉这些干扰信息,有两种方式:

  1. xcconfig中配置OTHER_LDFLAGS=$(inherited) -Xlinker -S
  2. xcode中配置。
    image.png
  • 从上面的分析可以看出,不论初始化的还是未初始化的全局变量都是全局符号。
  • 静态变量都变成了本地符号。

全局和本地本质上就是可见性。
全局变量和静态变量区别
当将一个全局变量定义为静态变量后就变成了本地变量,从全局可见变成了文件可见。 (静态变量 -> 本地变量)。

按功能划分:

Type 说明 备注
f File 文件
F Function 方法
O Data 数据
d Debug 调试符号
ABS Absolute
COM Common
UND ? 未定义符号

符号可见性

符号可见性在clang下有两种hiddendefault(默认)。protectedclang下是没有的。

  • default:用它定义的符号将被导出。
  • hidden:用它定义的符号将不被导出。

__attribute__可以把编译器支持的参数传递给编译器。我们经常遇到就是__attribute__((deprecated))
visibility控制文件导出符号,限制符号可见性。

int hidden_y __attribute__((visibility("hidden"))) = 99;
double default_y __attribute__((visibility("default"))) = 100;

在上面例子中default_x通过hidden变成了l本地符号,放在__DATA__common未初始化区。
所以可以通过:

  1. static
  2. __attribute__((visibility("hidden")))

控制符号可见性。

two_levelnamespace & flat_namespace

二级命名空间与一级命名空间。链接器默认采用二级命名空间,也就是除了会记录符号 名称,还会记录符号属于哪个Mach-O的,比如会记录下来_NSLog来自Foundation
所以如果我们在自己主工程中有一个函数,另外一个framework中也有同名函数:

void global_object() {
    NSLog(@"global_object");
}

在调用过程中由于存在二级命名空间并不会产生冲突。如果修改为一级命名空间就会冲突。

导入导出符号

export symbol:导出符号意味着,告诉别的模块,我有一个这样的符号,你可以将 其导入(Import)。
导出导入是相对的。比如NSLog对于Foundation来说是导出符号,对于调用方而言是导入符号。导出符号一定是全局符号。
查看导出符号:
objdump --macho --exports-trie ${MACHO_PATH}

Exports trie:
0x100000000  __mh_execute_header
0x100003F50  _main
0x100008010  _global_init_value
0x100008020  _global_uninit_value

可以看到就是上面例子中的全局符号。全局变量默认会被导出。
对于动态库而言是在运行的过程中被加载,在编译连接阶段只要提供符号就可以了。这里就涉及到间接符号表(保存着可执行文件使用的其它动态库的符号)了。
查看间接符号表:
objdump --macho --indirect-symbols ${MACHO_PATH}

image.png

  • 全局符号可以变成导出符号。

由于符号影响macho的大小,而间接符号不能删除。也就意味着使用到的动态库的全局符号不能删除。所以在strip动态库的时候只能脱不是全局符号的符号。
间接符号->动态库的导出符号->全局符号->strip只能拖不是全局符号的符号
定义一个OC类:

@interface OCObject ()
- (void)testOCObject;
@end

@implementation OCObject
- (void)testOCObject {
    NSLog(@"testOCObject");
}
@end

查看一下导出符号:

Exports trie:
0x100000000  __mh_execute_header
0x100003F20  _main
0x1000080B8  _OBJC_METACLASS_$_OCObject
0x1000080E0  _OBJC_CLASS_$_OCObject
0x100008110  _global_init_value
0x100008120  _global_uninit_value

可以看到OC的类,默认就是导出符号。所以对于OC的动态库要控制体积,对于不想暴露的符号可以借助链接器设置-unexported_symbol 后面跟不想导出的符号:
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_OCObject

image.png

可以看到已经没有了_OBJC_CLASS_$_OCObject导出符号了。这个时候就可以strip掉了。对于外界也不可见了。

weak symbol

Weak Reference Symbol: 表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。
Weak defintion Symbol: 表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。

定义WeakSymbol.h:

// 弱定义
void weak_function(void)  __attribute__((weak));
//弱引用
void weak_import_function(void) __attribute__((weak_import));

实现WeakSymbol.m:

void weak_function(void) {
    NSLog(@"weak_function");
}

void weak_import_function(void) {
    NSLog(@"weak_import_function");
}

image.png

如果在main文件中再实现一个weak_function

void weak_function(void) {
    NSLog(@"main weak_function");
}

这个时候正常情况下应该报符号冲突,弱定义后并不会冲突。


image.png
  • 弱定义符号并不影响符号的全局和导出。
  • 声明成弱定义的符号找到一个符号后,其它的会被忽略。

若把一个弱定义的全局符号隐藏掉,则会变成弱定义的本地符号。

void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));
image.png

弱引用
如果没有实现则不会报错,调用的地方做判断即可。

    if (weak_import_function) {
        weak_import_function();
    }

在编译的时候需要告诉编译器不要管-U,这个符号是动态实现的。

-U symbol_name
Specified that it is ok for symbol_name to have no definition. With -two_levelnamespace,the resulting symbol will be marked dynamic_lookup which means dyld will search all loaded images.

OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function
这个时候即使weak_import_function不实现,编译和运行也都不会报错。
在这里如果我们将动态库全部声明为弱引用,则就不会报找不到符号的错误了。

重新导出符号

比如NSLog对于我们来说是导入符号,对于可执行文件是一个未定义符号(存在间接符号表中)。

image.png

这里的NSLog只能在当前文件中使用,如果想给其它文件使用就需要重新导出。重新导出后就放在导出符号表中,外部就可以使用了。可以通过给一个符号起别名(只能给间接符号表中的符号起别名,起别名后会自动把间接符号表中的符号变成导出符号)的方式重新导出符号。

OTHER_LDFLAGS=$(inherited) -Xlinker -alias  -Xlinker _NSLog -Xlinker HOTPOT_NSLog

image.png

通过易于阅读的nm方式输出下:

nm -m ${MACHO_PATH} | grep 'HOTPOT_NSLog'

image.png

可以看到是间接的外部的_NSLog别名的符号。
接着看下导出符号表的情况

objdump --macho --exports-trie ${MACHO_PATH}

image.png

可以看到HOTPOT_NSLog是一个re-export的导出符号。所以我们已通过将自己库依赖的动态库重新导出让动态库可见。一般用在自己的库依赖其它动态库,让其它动态库可见(可以是库也可以是符号)。

-reexported_symbols_list file
The specified filename contains a list of symbol names that are implemented in a dependent dylib and should be re-exported through the dylib being created.
-unexported_symbol symbol
The specified symbol is added to the list of global symbols names that will not remain as global symbols in the output file. This option can be used multiple times. For short lists, this can be more convenient than creating a file and using -unexported_symbols_list.
reexported_symbols_list后面跟文件,可以将需要重新导出的符号写入到文件中。也可以通过unexported_symbol隐藏起来。

swift symbol

internal enum SwiftEnumSymbol {
    case maxHeap
    case minHeap

    internal func testSwiftEnumSymbol<T: Comparable>(type: T.Type) -> (T, T) -> Bool {
        switch self {
        case .maxHeap:
            return (>)
        case .minHeap:
            return (<)
        }
    }
}

struct SwiftStructSymbol {
    func testSwiftStructSymbol(o: Int) {

    }
}

public protocol SwiftProtocolSymbol: class {
    func testSwiftProtocolSymbol()
}

public class SwiftClassSymbol {

    func testSwiftClassSymbol() {

    }
}

查看全部符号

objdump --macho --syms ${MACHO_PATH}

可以看到添加swift文件后多了很多符号:

image.png

只查看SwiftClassSymbol的符号:

objdump --macho --syms ${MACHO_PATH} | grep 'SwiftClassSymbol'
image.png

私有化SwiftClassSymbol

private class SwiftClassSymbol {

    func testSwiftClassSymbol() {

    }
}
image.png

可以看到都变成了local。所以swift是编译型语言,编译过程中就已经确定了符号的类型。

strip

  • 动态库不能脱全局符号。
  • App导出符号一般不提供给别人使用,对于间接符号(也就是导入符号)不能脱, 其它的都可以脱(本地+全局+弱定义都可以脱,只留下间接符号表中的符号)。
  • 静态库是.o文件的合集 + 重定位符号表。所以重定位符号不能脱,唯一能脱的就是调试符号。

strip style

  • Debugging Symbols (.o 静态库 / 可执行文件 动态库)
  • All Symbols(App)
  • Non-Global Symbols(动态库)

只符号表来说App使用静态库比使用动态库文件大小要小。由于静态库最终会合并到macho,对于App来说会脱去所有符号,只留下间接符号。单纯只说静态库和动态库,那么静态库相对较小。引入App后静态库小。(仅仅从符号的角度考虑)。
对于动态库可以考虑从导出符号优化体积,对于OC默认就是导出符号。

strip命令:

-x: non_global
无参数: 代表全部符号
-S: 调试符号

image.png

dead code strip

在构建完成之后如果是 C、C++ 等静态的语言的代码、一些常量定义,如果发现没有被使用到将会被标记为 Dead code。开启 DEAD_CODE_STRIP = YES 这些 Dead code 将不会被打包到安装包中。在 LinkMap 这些符号也会被标记为 <<dead>>。

dead code strip(死代码剥离)是一个连接器参数,可以扫描哪些代码没有用到并且删掉的不是导出符号(本地符号)。

image.png

-dead_strip
Remove functions and data that are unreachable by the entry point or exported symbols.
可以通过终端man ld然后输入/ + 关键字搜索命令,n往下查找,N往上查找。

打开code dead strip前:

image.png

打开code dead strip后可以查看下符号表:
image.png

可以看到没有使用的weak_hidden_function方法被脱去了。

strip原理

.o/静态库

image.png

.o/静态库符号放在__DWARF段,在脱符号的时候:
1.遍历LoadCommands找到Segname__DWARFLoadCommand然后移除下面所有的Section;
2.再移除符号表中的Symbol;
3.将重新改写后的macho文件重新写入;

动态库/可执行文件

调试符号

image.png

遍历符号表判断符号表中的符号n_type是否包含N_STAB(0xe0),是调试符号就删除。

All Symbols

image.png

遍历间接符号表中所有的符号,只要不是间接符号表中的符号都可以删除。

Non-Global Symbols

image.png

遍历符号表,通过n_type != N_EXT判断。

dead code stripstrip主要是操作符号。

包大小优化方向

  • -O1 -Oz 生成目标文件
  • dead code strip 死代码剥离 链接的时候
  • strip 剥离符号 修改mach-o

LLVM调试

对于想要调试的命令直接添加scheme,然后在Compile Sources中查看文件:

image.png

2.找到对应文件在main函数中打断点:

image.png

3.添加-pa并将要调试的可执行程序拖进去

image.png

4.run without building

image.png

strip调试

1.在编译好的LLVM源码工程中在TARGETS中搜索strip都时候,只有一个llvm-strip

image.png

可以看到是一个脚本:
image.png

这个脚本将llvm-objcopy可执行文件链接成了llvm-strip可执行文件。类似快捷方式。
2.添加llvm-strip脚本对应的scheme编译。
3.编译完成后在源码对应的llvm-project/build/Debug/bin目录下可以看到llvm-strip对应的快捷方式。
image.png

4.在targets中复制一个可执行文件命名为strip
image.png

image.png

由于llvm-objcopy的代码中作乐判断,如果名称为llvm-objcopy句当作llvm-objcopy运行,如果是strip就当作strip运行。
strip
image.png

Show In Finder这个时候是找不到对应的文件的。
5.优化
复制一份llvm-strip
image.png

改名为strip
image.png

这个时候再重复4中的Show In Finder,就自动关联到strip了。
6.添加strip scheme并且在llvm-objcopy.cpp文件的main函数打上断点,然后run without building就可以调试了
image.png

  • **br read -f 文件路径**可以将一个文件的断点信息导入。导入后默认没有启用。
  • br list strip将所有断点放入strip组中。
  • br enable strip启用组里面的所有断点。

Xcode默认断点式根据绝对路径来断的,不通用。通过lldb终端下的断点式真正的符号断点,可以通用。
我们当然可以将自己的断点写入到文件中:
br write -f 文件写入文件。

7.拖入可执行文件就可以开始调试了


image.png

demo地址

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

推荐阅读更多精彩内容