Android Native库的加载及动态链接

Native库的装载过程

我们从一个简单的NDK Demo开始分析。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        final TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                tv.setText(stringFromJNI());
            }
        });
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    // Used to load the 'native-lib' library on application startup.
    // 动态库的装载及链接
    static {
        System.loadLibrary("native-lib");
    }
}

Android 链接器Linker之前的工作

下面从System.loadLibrary()开始分析。

    public static void loadLibrary(String libname) {
        这里VMStack.getCallingClassLoader()返回应用的类加载器
        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
    }

下面看loadLibrary0()

    synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        if (loader != null) {
            // findLibrary()返回库的全路径名
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                // It's not necessarily true that the ClassLoader used
                // System.mapLibraryName, but the default setup does, and it's
                // misleading to say we didn't find "libMyLibrary.so" when we
                // actually searched for "liblibMyLibrary.so.so".
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            // 装载动态库
            String error = doLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
        ......
    }

参数loader为Android的应用类加载器,它是PathClassLoader类型的对象,继承自BaseDexClassLoader对象,下面看BaseDexClassLoaderfindLibrary()方法。

    public String findLibrary(String name) {
        // 调用DexPathList的findLibrary方法
        return pathList.findLibrary(name);
    }

下面看DexPathListfindLibrary()方法

    public String findLibrary(String libraryName) {
        // 产生平台相关的库名称这里返回libxxx.so
        String fileName = System.mapLibraryName(libraryName);

        for (Element element : nativeLibraryPathElements) {
            // 查找动态库返回库的全路径名
            String path = element.findNativeLibrary(fileName);

            if (path != null) {
                return path;
            }
        }

        return null;
    }

回到loadLibrary0(),有了动态库的全路径名就可以装载库了,下面看doLoad()

    private String doLoad(String name, ClassLoader loader) {
        ......
        // 获取应用类加载器的Native库搜索路径
        String librarySearchPath = null;
        if (loader != null && loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            librarySearchPath = dexClassLoader.getLdLibraryPath();
        }
        // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
        // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
        // internal natives.
        synchronized (this) {
            return nativeLoad(name, loader, librarySearchPath);
        }
    }

nativeLoad()最终调用LoadNativeLibrary(),下面直接分析LoadNativeLibrary()

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                  const std::string& path,
                                  jobject class_loader,
                                  jstring library_path,
                                  std::string* error_msg) {
  ......
  SharedLibrary* library;
  Thread* self = Thread::Current();
  {
    // TODO: move the locking (and more of this logic) into Libraries.
    // 检查动态库是否已经装载过,如果已经装载过(类加载器也匹配)直接返回。
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);
  }
  ......
  // 没有装载过,装载链接动态库
  // 参数patch_str传递的是动态库的全路径,之所以还要传递搜索路径,是因为可能包含它的依赖库
  void* handle = android::OpenNativeLibrary(env,
                                            runtime_->GetTargetSdkVersion(),
                                            path_str,
                                            class_loader,
                                            library_path);
  ......
  // 查找动态库中的"JNI_OnLoad"函数
  sym = library->FindSymbol("JNI_OnLoad", nullptr);
  if (sym == nullptr) {
    VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
    was_successful = true;
  } else {
    // Call JNI_OnLoad.  We have to override the current class
    // loader, which will always be "null" since the stuff at the
    // top of the stack is around Runtime.loadLibrary().  (See
    // the comments in the JNI FindClass function.)
    ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
    self->SetClassLoaderOverride(class_loader);

    VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
    // 调用库的JNI_OnLoad函数注册JNI, 本文暂不讨论
    int version = (*jni_on_load)(this, nullptr);
    ......
  }
  ......
}

对于JNI注册,这里暂不讨论,下面看OpenNativeLibrary()的实现。

void* OpenNativeLibrary(JNIEnv* env,
                        int32_t target_sdk_version,
                        const char* path,
                        jobject class_loader,
                        jstring library_path) {
#if defined(__ANDROID__)
  UNUSED(target_sdk_version);
  if (class_loader == nullptr) {
    return dlopen(path, RTLD_NOW);
  }

  std::lock_guard<std::mutex> guard(g_namespaces_mutex);
  // 找到类加载器映射的命名空间(Android应用类加载器创建时创建)
  // 关于命名空间的动态链接请参考http://jackwish.net/namespace-based-dynamic-linking-chn.html
  android_namespace_t* ns = g_namespaces->FindNamespaceByClassLoader(env, class_loader);
  .......
  android_dlextinfo extinfo;
  // 在一个不同的命名空间中装载
  extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE;
  extinfo.library_namespace = ns;
  // RILD_NOW表示重定位在dlopen返回前完成,不会延迟到第一次执行(RTLD_LAZY)
  return android_dlopen_ext(path, RTLD_NOW, &extinfo);
  ......
}

下面看android_dlopen_ext()的实现

void* android_dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo) {
  // __builtin_return_address是编译器的内建函数,__builtin_return_address(0)表示当前函数的返回地址
  void* caller_addr = __builtin_return_address(0);
  return dlopen_ext(filename, flags, extinfo, caller_addr);
}

接下来就Android链接器linker的工作了。

Android 链接器Linker的装载工作

下面从do_dlopen()开始分析。

void* do_dlopen(const char* name, int flags, const android_dlextinfo* extinfo,
                  void* caller_addr) {
  // caller_addr在libnativeloader.so中
  // 查找地址所在的动态库(采用遍历查找,可以优化查找)
  soinfo* const caller = find_containing_library(caller_addr);
  // ns为调用库所在命名空间
  android_namespace_t* ns = get_caller_namespace(caller);
  ......
  if (extinfo != nullptr) {
    ......
    // extinfo->flags为ANDROID_DLEXT_USE_NAMESPACE
    if ((extinfo->flags & ANDROID_DLEXT_USE_NAMESPACE) != 0) {
      if (extinfo->library_namespace == nullptr) {
        DL_ERR("ANDROID_DLEXT_USE_NAMESPACE is set but extinfo->library_namespace is null");
        return nullptr;
      }
      // 命名空间使用应用自身类加载器-命名空间
      ns = extinfo->library_namespace;
    }
  }
  ......
  // 在命名空间ns中装载库
  soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
  ......
}

find_library()当参数translated_name不为空时,直接调用find_libraries(),这是装载链接的关键函数,下面看它的实现。

static bool find_libraries(android_namespace_t* ns,
                           soinfo* start_with,
                           const char* const library_names[],
                           size_t library_names_count, soinfo* soinfos[],
                           std::vector<soinfo*>* ld_preloads,
                           size_t ld_preloads_count, int rtld_flags,
                           const android_dlextinfo* extinfo,
                           bool add_as_children) {
  // ns为应用类加载器-命名空间
  // 这里start_with为libnativeloader.so的soinfo
  // library_names为需要装载的动态库,不包含依赖库
  // library_names_count需要装载的动态库的数量,这里为1
  // ld_preloads为nullptr
  // add_as_children为false
  ......
  // 为需要装载的动态库创建LoadTask添加到load_tasks
  // LoadTask用于管理动态库的装载
  for (size_t i = 0; i < library_names_count; ++i) {
    const char* name = library_names[i];
    load_tasks.push_back(LoadTask::create(name, start_with, &readers_map));
  }
  // Construct global_group.
  // 收集命名空间ns中设置了DF_1_GLOBAL(RTLD_GLOBAL:共享库中的符号可被后续装载的库重定位)标志的动态库
  soinfo_list_t global_group = make_global_group(ns);
  ......
  // Step 1: expand the list of load_tasks to include
  // all DT_NEEDED libraries (do not load them just yet)
  // load_tasks以广度优先遍历的顺序存储动态库依赖树
  // 例如依赖树:  1
  //           /  \
  //          2    3
  //         /      \
  //        4        5
  // load_tasks: 1->2->3->4->5
  for (size_t i = 0; i<load_tasks.size(); ++i) {
    LoadTask* task = load_tasks[i];
    soinfo* needed_by = task->get_needed_by();

    bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
    task->set_extinfo(is_dt_needed ? nullptr : extinfo);
    task->set_dt_needed(is_dt_needed);
    // 收集动态库的信息以及它的依赖库
    if(!find_library_internal(ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) {
      return false;
    }

    soinfo* si = task->get_soinfo();

    if (is_dt_needed) {
      // si添加到needed_by的依赖中
      needed_by->add_child(si);
    }

    if (si->is_linked()) {
      // 已经链接过的库增加引用计数
      si->increment_ref_count();
    }
    ......
    if (soinfos_count < library_names_count) {
      soinfos[soinfos_count++] = si;
    }
  }

  // Step 2: Load libraries in random order (see b/24047022)
  LoadTaskList load_list;
  // 需要装载的库放到load_list中
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    auto pred = [&](const LoadTask* t) {
      return t->get_soinfo() == si;
    };

    if (!si->is_linked() &&
        std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end() ) {
      load_list.push_back(task);
    }
  }
  // 随机化load_list中库的顺序
  shuffle(&load_list);

  for (auto&& task : load_list) {
    // 装载动态库
    if (!task->load()) {
      return false;
    }
  }
  // Step 3: pre-link all DT_NEEDED libraries in breadth first order.
  // 预链接load_tasks中没有链接过的库,见下文
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    if (!si->is_linked() && !si->prelink_image()) {
      return false;
    }
  }

  // Step 4: Add LD_PRELOADed libraries to the global group for
  // future runs. There is no need to explicitly add them to
  // the global group for this run because they are going to
  // appear in the local group in the correct order.
  if (ld_preloads != nullptr) {
    for (auto&& si : *ld_preloads) {
      si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);
    }
  }


  // Step 5: link libraries.
  // 链接过程,见下文
  soinfo_list_t local_group;
  // 广度优先遍历添加动态库依赖图到local_group中
  walk_dependencies_tree(
      (start_with != nullptr && add_as_children) ? &start_with : soinfos,
      (start_with != nullptr && add_as_children) ? 1 : soinfos_count,
      [&] (soinfo* si) {
    local_group.push_back(si);
    return true;
  });

  // We need to increment ref_count in case
  // the root of the local group was not linked.
  bool was_local_group_root_linked = local_group.front()->is_linked();

  bool linked = local_group.visit([&](soinfo* si) {
    if (!si->is_linked()) {
      if (!si->link_image(global_group, local_group, extinfo)) {
        return false;
      }
    }

    return true;
  });

  if (linked) {
    // 设置链接标志
    local_group.for_each([](soinfo* si) {
      if (!si->is_linked()) {
        si->set_linked();
      }
    });

    failure_guard.disable();
  }
  ......
}

find_libraries()中动态库的装载可以分为两部分

  • 收集动态库的信息及其依赖库
  • 装载动态库及依赖库

收集动态库的信息及依赖库

下面从find_library_internal()开始分析。

static bool find_library_internal(android_namespace_t* ns,
                                  LoadTask* task,
                                  ZipArchiveCache* zip_archive_cache,
                                  LoadTaskList* load_tasks,
                                  int rtld_flags) {
  soinfo* candidate;

  // 在应用类加载器-命名空间中查找动态库是否已经装载过
  if (find_loaded_library_by_soname(ns, task->get_name(), &candidate)) {
    task->set_soinfo(candidate);
    return true;
  }

  // 在默认命名空间中查找动态库是否已经装载过
  if (ns != &g_default_namespace) {
    // check public namespace
    candidate = g_public_namespace.find_if([&](soinfo* si) {
      return strcmp(task->get_name(), si->get_soname()) == 0;
    });
    ......
  }
  ......
  // 装载库
  if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags)) {
    return true;
  } else {
    // In case we were unable to load the library but there
    // is a candidate loaded under the same soname but different
    // sdk level - return it anyways.
    if (candidate != nullptr) {
      task->set_soinfo(candidate);
      return true;
    }
  }

  return false;
}

下面分析load_library()

static bool load_library(android_namespace_t* ns,
                         LoadTask* task,
                         ZipArchiveCache* zip_archive_cache,
                         LoadTaskList* load_tasks,
                         int rtld_flags) {
  ......
  // 打开库文件返回文件描述符
  int fd = open_library(ns, zip_archive_cache, name, needed_by, &file_offset, &realpath);
  if (fd == -1) {
    DL_ERR("library \"%s\" not found", name);
    return false;
  }

  task->set_fd(fd, true);
  task->set_file_offset(file_offset);
  // 装载库
  return load_library(ns, task, load_tasks, rtld_flags, realpath);
}

下面看另一个load_library()的实现

static bool load_library(android_namespace_t* ns,
                         LoadTask* task,
                         LoadTaskList* load_tasks,
                         int rtld_flags,
                         const std::string& realpath) {
  off64_t file_offset = task->get_file_offset();
  const char* name = task->get_name();
  const android_dlextinfo* extinfo = task->get_extinfo();
  ......
  // 为动态库创建soinfo,用于记录动态链接信息等
  soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
  if (si == nullptr) {
    return false;
  }

  task->set_soinfo(si);

  // Read the ELF header and some of the segments.
  // 读取ELF文件头以及一些段信息
  if (!task->read(realpath.c_str(), file_stat.st_size)) {
    soinfo_free(si);
    task->set_soinfo(nullptr);
    return false;
  }
  ......
  // 查找依赖库,创建LoadTask添加到load_tasks
  for_each_dt_needed(task->get_elf_reader(), [&](const char* name) {
    load_tasks->push_back(LoadTask::create(name, si, task->get_readers_map()));
  });

  return true;
}

下面分析ELF文件头以及段信息的读取过程,也就是LoadTask的read(),它直接调用ElfReader的Read()方法。

bool ElfReader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {
  CHECK(!did_read_);
  CHECK(!did_load_);
  name_ = name;
  fd_ = fd;
  file_offset_ = file_offset;
  file_size_ = file_size;

  if (ReadElfHeader() &&
      VerifyElfHeader() &&
      ReadProgramHeaders() &&
      ReadSectionHeaders() &&
      ReadDynamicSection()) {
    did_read_ = true;
  }

  return did_read_;
}
  • ReadElfHeader() : 读取ELF文件头信息
  • VerifyElfHeader() : 校验ELF(文件类型等)
  • ReadProgramHeaders() : 根据ELF文件头信息获取程序头表
  • ReadSectionHeaders() : 根据ELF文件头信息获取段头表
  • ReadDynamicSection() : 获取Dynamic Section的信息

装载动态库

动态库的装载在LoadTask的load()中实现。

  bool load() {
    ElfReader& elf_reader = get_elf_reader();
    // 映射动态库的可加载Segment到进程的虚拟地址空间中
    if (!elf_reader.Load(extinfo_)) {
      return false;
    }
    // 保存装载信息
    // 动态库装载的起始地址
    si_->base = elf_reader.load_start();
    // 可装载的Segment大小之和
    si_->size = elf_reader.load_size();
    si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
    // 动态库装载的期望起始地址,通常si_->load_bias = si_->base
    si_->load_bias = elf_reader.load_bias();
    // 动态库程序头表项数
    si_->phnum = elf_reader.phdr_count();
    // 动态库程序头表的地址
    si_->phdr = elf_reader.loaded_phdr();

    return true;
  }

在实际的地址计算中,使用si_->load_bias,不使用si_->base。

下面看ElfReader的Load()方法

bool ElfReader::Load(const android_dlextinfo* extinfo) {
  CHECK(did_read_);
  CHECK(!did_load_);
  if (ReserveAddressSpace(extinfo) &&
      LoadSegments() &&
      FindPhdr()) {
    did_load_ = true;
  }

  return did_load_;
}
  • ReserveAddressSpace(): 保留虚拟地址空间为动态库(装载地址随机化)
  • LoadSegments() : 装载ELF文件中可装载的Segments
  • FindPhdr() : 确保程序头表包含在一个可加载的Segment中

动态库的装载已经完成,下面看链接过程。

Native库动态链接的过程

预链接

下面看prelink_image()

bool soinfo::prelink_image() {
  /* Extract dynamic section */
  ElfW(Word) dynamic_flags = 0;
  // 根据程序头表的地址计算dynamic section的地址
  phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);
  ......
  uint32_t needed_count = 0;
  // 解析dynamic section获取动态链接信息
  for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
    DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",
          d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
    switch (d->d_tag) {
      ......
      case DT_STRTAB:
        // 动态字符串表的地址
        strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);
        break;

      case DT_STRSZ:
        strtab_size_ = d->d_un.d_val;
        break;

      case DT_SYMTAB:
        // 动态符号表的地址
        symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);
        break;
      ......
      case DT_JMPREL:
        // 需重定位的函数表(.rela.plt)的地址
#if defined(USE_RELA)
        plt_rela_ = reinterpret_cast<ElfW(Rela)*>(load_bias + d->d_un.d_ptr);
#else
        plt_rel_ = reinterpret_cast<ElfW(Rel)*>(load_bias + d->d_un.d_ptr);
#endif
        break;
      ......
      case DT_RELA:
        // 需重定位的数据表(.rela.dyn)的地址
        rela_ = reinterpret_cast<ElfW(Rela)*>(load_bias + d->d_un.d_ptr);
        break;
      ......
      case DT_NEEDED:
        // 依赖的动态库
        ++needed_count;
        break;
    }
  }
  ......
  // Sanity checks.
  // 检查动态链接信息
  if (relocating_linker && needed_count != 0) {
    DL_ERR("linker cannot have DT_NEEDED dependencies on other libraries");
    return false;
  }
  if (nbucket_ == 0 && gnu_nbucket_ == 0) {
    DL_ERR("empty/missing DT_HASH/DT_GNU_HASH in \"%s\" "
        "(new hash type from the future?)", get_realpath());
    return false;
  }
  if (strtab_ == 0) {
    DL_ERR("empty/missing DT_STRTAB in \"%s\"", get_realpath());
    return false;
  }
  if (symtab_ == 0) {
    DL_ERR("empty/missing DT_SYMTAB in \"%s\"", get_realpath());
    return false;
  }
  ......
}

链接

链接主要完成符号重定位工作,下面从link_image()开始分析

bool soinfo::link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
                        const android_dlextinfo* extinfo) {
  ......
#if defined(USE_RELA)
  // rela_为重定位数据表的地址
  if (rela_ != nullptr) {
    DEBUG("[ relocating %s ]", get_realpath());
    // 数据引用重定位
    if (!relocate(version_tracker,
            plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
      return false;
    }
  }
  // plt_rela_为重定位函数表的地址
  if (plt_rela_ != nullptr) {
    DEBUG("[ relocating %s plt ]", get_realpath());
    // 函数引用重定位
    if (!relocate(version_tracker,
            plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
      return false;
    }
  }
#else
  ......
}

下面以函数引用重定位为例分析relocate()方法

template<typename ElfRelIteratorT>
bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
                      const soinfo_list_t& global_group, const soinfo_list_t& local_group) {
  for (size_t idx = 0; rel_iterator.has_next(); ++idx) {
    const auto rel = rel_iterator.next();
    if (rel == nullptr) {
      return false;
    }

    // rel->r_info的低32位
    ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
    // rel->r_info的高32位
    ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);

    // 重定位地址的存储位置
    ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rel->r_offset + load_bias);
    ElfW(Addr) sym_addr = 0;
    const char* sym_name = nullptr;
    ElfW(Addr) addend = get_addend(rel, reloc);
    ......
    if (sym != 0) {
      // sym为动态符号表项的索引
      // symtab_[sym].st_name为符号在动态字符串表的索引
      // sysm_name为需重定位的符号名
      sym_name = get_string(symtab_[sym].st_name);
      const version_info* vi = nullptr;

      if (!lookup_version_info(version_tracker, sym, sym_name, &vi)) {
        return false;
      }
      // 查找符号返回符号表项的地址
      if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) {
        return false;
      }

      if (s == nullptr) {
        ......
      } else {
        ......
        // 根据符号表项计算符号地址
        sym_addr = lsi->resolve_symbol_address(s);
        ......
      }
      ......
    }
    switch (type) {
      // ELF64中R_GENERIC_JUMP_SLOT = R_AARCH64_JUMP_SLOT
      case R_GENERIC_JUMP_SLOT:
        count_relocation(kRelocAbsolute);
        MARK(rel->r_offset);
        TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
                   reinterpret_cast<void*>(reloc),
                   reinterpret_cast<void*>(sym_addr + addend), sym_name);
        // 符号地址更新到reloc(GOT表)中
        *reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
        break;
        ......
    }
  }
  return true;
}

参考

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

推荐阅读更多精彩内容