NVMain和Gem5环境搭建

前沿

这个是华科研究生课程计算机系统设计一个实验,做完整个实验大约花了两天多的时间吧,其中也是遇到了很多问题,想在博客中记录下来,日后若是能够帮助师弟师妹们或者其他的博友,便不负这篇博客的作用.

这是我整个结构的目录树,如果你在接下来阅读过程中,存在任何路径上的问题,请仔细阅读这四个目录的位置,这四个目录是同一级目录.

+-- nvmain
|
+-- gem5
|
+-- fs-image
|
+-- benchmark

NVMain环境搭建

下载NVMain

下载nvmain,并解压,将解压后目录的名字更改为nvmain

wget https://bitbucket.org/mrp5060/nvmain/get/9c0e87b164bc.zip -O nvmain.zip
unzip nvmain.zip
mv mrp5060-nvmain-9c0e87b164bc nvmain 

NVMain编译

  • 安装编译工具scons, scons是一个开放源码、以Python语言编码的自动化构建工具,可用来替代make编写复杂的makefile。并且scons是跨平台的,只要scons脚本写的好,可以在Linux和Windows下随意编译。
sudo apt-get install scons
  • 编译NVMain,其中编译的类型可以选择fast|debug|prof三种的任意一种,这里我们选用fast
scons --build-type=fast

如果在编译的过程中,报了如下的错误,无须紧张,这只是个小问题,是因为我们暂时还没有用到gem5,所以只需将这个报错的那一行代码注释即可.

scons: Reading SConscript files ...
ImportError: No module named gem5_scons:
  File "/home/greek/master/experiment/nvmain/SConstruct", line 253:
    SConscript(joinpath(root, 'SConscript'), variant_dir=build_dir)
  File "/usr/lib/scons/SCons/Script/SConscript.py", line 614:
    return method(*args, **kw)
  File "/usr/lib/scons/SCons/Script/SConscript.py", line 551:
    return _SConscript(self.fs, *files, **subst_kw)
  File "/usr/lib/scons/SCons/Script/SConscript.py", line 256:
    call_stack[-1].globals)
  File "/home/greek/master/experiment/nvmain/build/SConscript", line 36:
    from gem5_scons import Transform

具体的解决办法是将nvmain/build/SConscript中的第36行注释

# from gem5_scons import Transform # 这行注释
  • 解决错误之后,再重新编译
NVMain编译成功标识

NVMain测试

  • 指定配置文件,指定测试文件,周期, 命令的格式为: ./nvmain CONFIG_FILE TRACE_FILE [Cycles [PARAM=value]]

这里我们使用的测试命令如下

./nvmain.fast Config/PCM_ISSCC_2012_4GB.config Tests/Traces/hello_world.nvt 1000000
NVMain测试结果

Gem5环境搭建

下载Gem5

  • 使用常用工具git, 但是仓库在google中,如果不能翻墙,可能无法下载
git clone https://gem5.googlesource.com/public/gem5
  • [推荐使用] 使用工具hg. Mercurial是一款非常优秀的分布式版本控制系统(DCVS),具有高效率、跨平台、可扩展、使用简便且开源等优点,是目前最为流行的版本控制工具之一.
sudo apt-get install mercurial
hg clone http://repo.gem5.org/gem5

编译Gem5

  • 安装依赖库protobuf-compiler
sudo apt-get install protobuf-compiler
  • 编译Gem5,这里可以选择不同的架构,比如X86|ARM|ALPHA, 这里我们选择使用X86(我的电脑是X86结构的,一开始是怕出错,就编译了这个,后来感觉应该没啥影响))
scons build/X86/gem5.opt
Gem5编译成功

Gem5的SE测试

SE测试

  • 用来运行单个应用,一系列指令在MP/SMT上
  • 建模用户可见的ISA和常见的系统调用
  • 模拟系统调用,通过调用主机操作系统
  • 简单的地址翻译,没有调度
./build/X86/gem5.opt configs/example/se.py -c tests/test-progs/hello/bin/x86/linux/hello 
SE测试结果

Gem5的FS测试

FS测试

  • 能够启动完整的操作系统
  • 建模硬件设备
  • 中断,异常,故障处理函数

参考下载的网址: www.m5sim.org/Download

  1. 下载X86全系统对应的文件,一定要下载自己编译架构的对应文件
wget http://www.m5sim.org/dist/current/x86/x86-system.tar.bz2
  1. 解压文件,同时将解压后的文件夹放入同一目录fs-image
tar vxfj x86-system.tar.bz2
mkdir fs-image
mv binaries fs-image
mv disks fs-image
  1. 下载alpha对应的全系统文件,里面包含linux-bigswap2.img,这个是运行的一个必须文件.下载之后,解压文件,同时将文件拷贝到fs-image/disks目录下,可以理解为专门存放镜像文件的目录.
wget http://www.m5sim.org/dist/current/m5_system_2.0b3.tar.bz2
tar vxfj m5_system_2.0b3.tar.bz2
cp m5_system_2.0b3/disks/linux-bigswap2.img fs-image/disks
  1. 运行FS模拟
 ./build/X86/gem5.opt configs/example/fs.py 

报了如下的错误,这里因为脚本代码中默认了镜像为x86root.img

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "build/X86/python/m5/main.py", line 453, in main
    exec(filecode, scope)
  File "configs/example/fs.py", line 340, in <module>
    test_sys = build_test_system(np)
  File "configs/example/fs.py", line 93, in build_test_system
    cmdline=cmdline)
  File "/home/greek/master/experiment/gem5/configs/common/FSConfig.py", line 614, in makeLinuxX86System
    makeX86System(mem_mode, numCPUs, mdesc, self, Ruby)
  File "/home/greek/master/experiment/gem5/configs/common/FSConfig.py", line 539, in makeX86System
    disk0.childImage(mdesc.disk())
  File "/home/greek/master/experiment/gem5/configs/common/Benchmarks.py", line 63, in disk
    return env.get('LINUX_IMAGE', disk('x86root.img'))
  File "/home/greek/master/experiment/gem5/configs/common/SysPaths.py", line 71, in __call__
    raise IOError("Can't find file '%s' on path." % filename)
IOError: Can't find file 'x86root.img' on path.

解决办法

这里有两种解决办法,推荐使用第二种:

  • 修改代码gem5/configs/common/Benchmarks.py中,第63行,修改为如下,其中linux-86.imgfs-image/disks目录下的镜像文件
return env.get('LINUX_IMAGE', disk('linux-x86.img'))
  • [推荐使用] 直接使用参数指定镜像文件,使用参数 --disk-image以及--kernel

命令使用如下格式:

./build/X86/gem5.opt configs/example/fs.py --disk-image linux-x86.img --kernel x86_64-vmlinux-2.6.22.9

指定M5_PATH路径,系统会自动从M5_PATH路径中disksbinaries两个目录寻找指定的镜像文件和对应的内核文件.

其中指定M5_PATH我提供了两种方式,推荐使用第二种(虽然我用的第一种)):

  • 修改.basrhc文件,在文件的末尾加入M5_PATH的路径
export M5_PATH=$M5_PATH:/home/hsc/fs-image
  • 直接在运行命令中动态添加环境变量
M5_PATH=/home/hsc/fs-image ./build/X86/gem5.opt configs/example/fs.py --disk-image linux-x86.img --kernel x86_64-vmlinux-2.6.22.9

linux-x86.imgfs-image/disks/目录下文件, x86_64-vmlinux-2.6.22.9fs-image/binaries/目录下文件,我看网上很多教程将x86_64-vmlinux-2.6.22.9改名为vmlinux,这步的话其实意义不是特别大,不过改了也方便,我这里暂不做修改.

  1. 连接模拟系统

当运行了FS测试之后,系统就启动了,可以通过远程连接的方式,连接上我们的系统.这里我们介绍两种方式进行连接:

  • 利用gem5系统自带的一个连接程序,在gem5/util/term/目录中,先进行make,生成可执行文件
cd util/term/
make

./m5term localhost 3456
  • [推荐使用] 直接利用telnet连接系统
telnet localhost 3456
FS系统运行状态
连接之后进入命令行状态
  1. 退出

这里我依旧使用两种方法,推荐使用第一种方法

  • 在连接的命令行中输入 m5 exit
  • 在连接的命令行中输入 ~.

NVMain和Gem5的集成环境

Gem5补丁

首先为了消除NVMainGem5版本之间的差异,所以首先需要给Gem5打上补丁.(别听到补丁就觉得畏惧,一开始我有这种感觉,其实后面发现真的没啥).

这里我提供两种方式,两种方式都很OK,如果第一种失败建议换成第二种.如果你的gem5不是用hg下载,请换第二种方式.

  1. 补丁文件已经在nvmain/patches/gem5目录中给出,在gem5目录中执行下列命令
hg qinit
hg qimport -f ../nvmain/patches/gem5/nvmain2-gem5-11688+
hg qpush

nvmain2-gem5-11688+这个是补丁文件的名字,在nvmain/patches/gem5中,以后可能是其他的版本,换成自己的版本补丁名字即可.

  1. 手动补丁,直接查看补丁的内容,手动修改文件,如下所示为补丁的内容
diff --git a/configs/common/Options.py b/configs/common/Options.py
--- a/configs/common/Options.py
+++ b/configs/common/Options.py
@@ -63,6 +63,12 @@
 # being used, and consequently no CPUs, but rather various types of
 # testers and traffic generators.
 def addNoISAOptions(parser):
+    # Check for extra nvmain configuration override options
+    for arg in sys.argv:
+        if arg[:9] == "--nvmain-":
+            parser.add_option(arg, type="string", default="NULL",
+                       help="Set NVMain configuration value for a parameter")
+
     parser.add_option("-n", "--num-cpus", type="int", default=1)
     parser.add_option("--sys-voltage", action="store", type="string",
                       default='1.0V',

只需在gem5/configs/common/Options.py的第82行之后,加入如下三行

for arg in sys.argv:
    if arg[:9] == "--nvmain-":
        parser.add_option(arg, type="string", default="NULL", help="Set NVMain configuration value for a parameter")
补丁之后

NVMain和Gem5混合编译

  • 编译命令
scons EXTRAS=../nvmain build/X86/gem5.opt
  • 测试,其中混合编译成功的一个重要标志是可以使用--mem-type=NVMainMemory参数来指定主存为NVM
M5_PATH=/home/hsc/fs-image ./build/X86/gem5.opt configs/example/se.py -c tests/test-progs/hello/bin/x86/linux/hello --caches --l2cache --mem-type=NVMainMemory --nvmain-config=../nvmain/Config/PCM_ISSCC_2012_4GB.config
混合测试结果

PARSEC 测试

我参考的资料是: pfzuo.github.io/2016/06/06/Configure-and-run-parsec-2.1-benchmark-in-GEM5/

这一层我的目录结构为

+-- fs-image/
|      +-- disks/
|      |     +-- linux-bigswap2.img
|      |     +-- x86root-parsec.img
|      +-- binaries/
|      |     +-- x86_64-vmlinux-2.6.28.4-smp
|      |     +-- tsb_osfpal
+-- benchmark
|      +--  TR-09-32-parsec-2.1-alpha-files  
  1. 下载kernel文件,复制到fs-image/binaries目录下
wget http://www.cs.utexas.edu/~parsec_m5/x86_64-vmlinux-2.6.28.4-smp
  1. 下载PARSEC对应的PAL code文件, 复制到fs-image/binaries目录下
wget http://www.cs.utexas.edu/~parsec_m5/tsb_osfpal
  1. 下载PARSEC-2.1 Disk Image并解压到fs-image/disks目录中
wget http://www.cs.utexas.edu/~parsec_m5/x86root-parsec.img.bz2

bzip2 -d x86root-parsec.img.bz2
  1. 下载PARSEC script生成包,并解压到benchmark目录下
wget http://www.cs.utexas.edu/~parsec_m5/TR-09-32-parsec-2.1-alpha-files.tar.gz
tar zxvf TR-09-32-parsec-2.1-alpha-files.tar.gz
  1. 生成script
 ./writescripts.pl <benchmark> <nthreads>

其中benchmark包含如下

 blackscholes
 bodytrack
 canneal
 dedup
 facesim
 ferret
 fluidanimate
 freqmine
 streamcluster
 swaptions
 vips
 x264
 rtview

我生成的脚本是blackscholes,利用gem5进行全系统测试

./writescripts.pl blackscholes 4
  1. 运行生成的脚本
M5_PATH=/home/hsc/fs-image ./build/X86/gem5.opt ./configs/example/fs.py -n 2 --script=../benchmark/TR-09-32-parsec-2.1-alpha-files/blackscholes_4c_test.rcS --disk-image x86root-parsec.img --kernel x86_64-vmlinux-2.6.28.4-smp
  1. 连接测试
telnet localhost 3456
PARSEC测试结果

FPC压缩方案

在修改NVMain源代码实现FPC压缩方案时,我走了不少的弯路.一开始看了实验推荐的一篇关于FPC的论文,但是确实看的特别懵,感觉不是很清楚作者压缩的思路.究其原因,作者没有在论文中给出一个具体的实例,导致理解起来有些困难.在此,我还得特别感谢坐我旁边的魏博士,是他推荐了一篇更新的关于FPC的论文,并且论文中给出了详细的实例,便于理解.并给我梳理NVMain的一个整体框架,使我能够快速找到修改的位置.

参考论文,推荐重点看第二篇

修改位置

打了箭头的位置,即为修改的位置,其中FPCompress为自己创建的文件夹,并新建的类,及其实现.

+-- nvmain/
|     +-- DataEncoders/
|     |        +-- FlipNWrite/
|     |        |       +--  FlipNWrite.h
|     |        |       +--  FlipNWrite.cpp
|     |        |       +--  SConscript
|     |        +-- FPCompress/                  <-  
|     |        |       +--  FPCompress.h        <-
|     |        |       +--  FPCompress.cpp      <-
|     |        |       +--  SConsript           <-
|     |        +-- DataEncoderFactory.cpp       <- 

其实整体的思路可以理解为,要写入的数据,其中默认有一种编码方式,即不进行任何操作,而FlipNWrite为另一种编码方式,我们只需要模仿FlipNWrite实现自己的编码方案,在编码方式中实现自己的FPC压缩,就可以相当于拦截写入的数据,并替换成自己的压缩后的数据,成功写入.

/* FPCompress.h
 * Author: hsc
 * date: 2019-10-9
*/

#ifndef __NVMAIN_FPCOMPRESS_H__
#define __NVMAIN_FPCOMPRESS_H__

#include "src/DataEncoder.h"

namespace NVM {

class FPCompress : public DataEncoder {
  public:
    FPCompress();
    ~FPCompress();

    void SetConfig( Config *config, bool createChildren = true );

    ncycle_t Read( NVMainRequest *request );
    ncycle_t Write( NVMainRequest *request );

    void RegisterStats( );
  
  private:
    uint64_t p0; /* pattern zero */
    uint64_t p1;
    uint64_t p2;
    uint64_t p3;
    uint64_t p4;
    uint64_t p5;
    uint64_t p6;
    uint64_t p7; /* pattern seven */

    void set_data( uint8_t *address, uint64_t index, uint64_t data);
};

/* zero run */
static inline bool pattern_zero(int64_t x){
  return x == 0;
}

/* 8bit sign extended */
static inline bool pattern_one(int64_t x){
  return (x >> 7) == -1 || (x >> 7) == 0;
}

/* 16 bit sign extended */
static inline bool pattern_two(int64_t x){
  return (x >> 15) == -1 || (x >> 15) == 0;
}

/* halfword sign extended */
static inline bool pattern_three(int64_t x){
  return (x >> 31) == -1 || (x >> 31) == 0;
}

/* halfword padded with a zero halfword */
static inline bool pattern_four(int64_t x){
  return (x << 32) == 0;
}

/* two half words, each a byte sign-extended */
static inline bool pattern_five(int64_t x){
  int32_t high = static_cast<int32_t>(x >> 32);
  int32_t low  = static_cast<int32_t>(x & 0xffffffff);

  bool high_valid = (high >> 15) == -1 || (high >> 15) == 0;
  bool low_valid  = (low >> 15) == -1 || (low >> 15) == 0;

  return high_valid && low_valid;
}

/* word consisting of repeated bytes */
static inline bool pattern_six(int64_t x){
  int64_t total = 0;
  
  
  
int64_t pattern_byte = x & 0xffff;
  
  total |= pattern_byte;
  total <<= 16;

  total |= pattern_byte;
  total <<= 16;

  total |= pattern_byte;
  total <<= 16;

  total |= pattern_byte;
  total <<= 16;

  return total == x;
}

};

#endif

其中,我觉得比较重要的是实现Write, RegisterStats这两个函数,一个用来实现压缩方案,一个用来打印各种数据模式的统计量.在各种模式的判别中,一开始我是读的uint64_t无符号类型的,发现这样判断实现的不是很优雅,后来换成了带符号数据int64_t.

/* FPCompress.cpp
 * Author : hsc
 * date: 2019-10-9
*/

#include "DataEncoders/FPCompress/FPCompress.h"

using namespace NVM;

FPCompress::FPCompress()
{
    /* clear statistics */
    p0 = 0;
    p1 = 0;
    p2 = 0;
    p3 = 0;
    p4 = 0;
    p5 = 0;
    p6 = 0;
    p7 = 0;
}

FPCompress::~FPCompress()
{
    /*
     * nothing to do here
     */
}

void FPCompress::SetConfig( Config *config, bool /*createChildren*/ )
{
    Params *params = new Params( );
    params->SetParams( config );
    SetParams( params );
}

void FPCompress::RegisterStats()
{
    AddStat(p0);
    AddStat(p1);
    AddStat(p2);
    AddStat(p3);
    AddStat(p4);
    AddStat(p5);
    AddStat(p6);
    AddStat(p7);
}

ncycle_t FPCompress::Read( NVMainRequest *request )
{
   ncycle_t rv = 0;

    // TODO: Add some energy here

    return rv; 
}

ncycle_t FPCompress::Write( NVMainRequest *request ){
    NVMDataBlock& newData = request->data;
    NVMDataBlock& oldData = request->oldData;

    int64_t *ptr = (int64_t*)(newData.rawData);
    
    uint64_t offset = 0, size =  0, encode_data = 0;

    while(size < newData.GetSize()){
        int64_t dword = *ptr;

        if(pattern_zero(dword)){
            encode_data = 0;
            set_data(oldData.rawData, offset,  encode_data);
            p0++; /* zero statistics */
            offset += 3;
        }else if(pattern_one(dword)){
            encode_data = dword & 0xff;
            encode_data |= (0x1 << 8);
            set_data(oldData.rawData, offset,  encode_data);
            p1++;
            offset += 11;
        }else if(pattern_two(dword)){
            encode_data = dword & 0xffff;
            encode_data |= (0x2 << 16);
            set_data(oldData.rawData, offset,  encode_data);
            p2++;
            offset += 19;
        }else if(pattern_three(dword)){
            encode_data = dword & 0xffffffff;
            encode_data |= ((uint64_t)0x3 << 32);
            set_data(oldData.rawData, offset,  encode_data);
            p3++;
            offset += 35;
        }else if(pattern_four(dword)){
            encode_data = dword >> 32;
            encode_data |= ((uint64_t)0x4 << 32);
            set_data(oldData.rawData, offset,  encode_data);
            p4++;
            offset += 35;
        }else if(pattern_five(dword)){
            encode_data = (dword & 0xffff);
            encode_data |= ((dword >> 32) & 0xffff) << 16;
            encode_data |= ((uint64_t)0x5 << 32);
            set_data(oldData.rawData, offset,  encode_data);
            p5++;
            offset += 35;
        }else if(pattern_six(dword)){
            encode_data = dword && 0xffff;
            encode_data |= (0x6 << 16);
            set_data(oldData.rawData, offset,  encode_data);
            p6++;
            offset += 19;
        }else {
            encode_data = dword;
            set_data(oldData.rawData, offset, (encode_data >> 32) & 0xffffffff);
            set_data(oldData.rawData, offset + 32, (encode_data & 0xffffffff));
            p7++;
            offset += 64;
        }

        /* pointer to next dword */
        ptr++;

        size += 8;
    }
}

void FPCompress::set_data( uint8_t *address, uint64_t offset, uint64_t data)
{
    uint64_t *ptr = (uint64_t*)(address + (offset / 8));
    uint64_t new_data = *ptr;
    uint64_t len = offset - (offset / 8) * 8;
    new_data = (new_data << (64 - len)) >> (64 - len);
    new_data |= (data << len);
    *ptr = new_data;
}

思路

  • request中包含的是uint8_t类型的指针地址,将其强制转换为int64_t类型的指针,即可读取64bit数据
  • 读出数据,判断是8种数据模式中的哪一种数据模式,然后构造压缩后的数据
  • 因为压缩的数据,特别不规整,有3bit, 11bit, 19bit, 35bit不等.比如我当前要写3bit,这3bit可能还跨了不同的字节,因此导致情况特别复杂.
  • 我的方法是计算写入位置最近的以字节为单位对齐的偏移地址, 从该偏移地址中读取64bit数据,这样的话,这64bit中最多包含7位之前写入的数据.我们通过将之前写入的数据,和压缩后的数据进行合并成一个新的64bit数据,然后重新写入.
数据写入

DataEncoderFactory.cpp中加入自己的编码方式

// DataEncoderFactory.cpp
DataEncoder *DataEncoderFactory::CreateDataEncoder( std::string encoderName )
{
    DataEncoder *encoder = NULL;

    if( encoderName == "default" ) encoder = new DataEncoder( );
    else if( encoderName == "FlipNWrite" ) encoder = new FlipNWrite( );
    else if( encoderName == "FPCompress") encoder = new FPCompress( );

    return encoder;
}

为了使用我们的编码方式,我们需要在配置文件中指定编码方式,这里我选用的配置文件为nvmain/Config/PCM_MLC_example.config,在文件的末尾,加入如下内容:

DataEncoder FPCompress

编译

scons --build-type=fast

测试

利用我们刚刚修改的配置文件nvmain/Config/PCM_MLC_example.config进行测试

./nvmain.fast Config/PCM_MLC_example.config Tests/Traces/hello_world.nvt 1000000

数据写入统计

我是若木,倘若大家有更好的实现方式,记得与我联系,也可参观一下我的博客.

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

推荐阅读更多精彩内容