linux驱动之uboot启动过程及参数传递

前言

一直以来都在学习和开发嵌入式linux,但对于一些常用的工具和机制却不甚了解,包括今天要说的uboot引导和启动linux内核,最近打算启动技术博客来学习和记录探索这些过程中所获得的知识。从事嵌入式linux开发的人应该都知道uboot,支持多种操作系统,多种硬件平台的uboot在嵌入式linux界可是大名鼎鼎,我们今天就来谈一谈uboot如何启动内核。这里我们不提供代码,仅给出相关的关键结构体,其余的请各位自行查看uboot代码

过程讲解

do_bootm

在uboot引导Linux启动时,使用的是bootm的命令。这个命令执行的函数就是do_bootm, 这个函数的地址在cmd/bootm.c中。
代码如下:

int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
    static int relocated = 0;

    if (!relocated) {
        int i;

        /* relocate names of sub-command table */
        for (i = 0; i < ARRAY_SIZE(cmd_bootm_sub); i++)
            cmd_bootm_sub[i].name += gd->reloc_off;

        relocated = 1;
    }
#endif

    /* determine if we have a sub command */
    argc--; argv++;
    if (argc > 0) {
        char *endp;

        simple_strtoul(argv[0], &endp, 16);
        /* endp pointing to NULL means that argv[0] was just a
         * valid number, pass it along to the normal bootm processing
         *
         * If endp is ':' or '#' assume a FIT identifier so pass
         * along for normal processing.
         *
         * Right now we assume the first arg should never be '-'
         */
        if ((*endp != 0) && (*endp != ':') && (*endp != '#'))
            return do_bootm_subcommand(cmdtp, flag, argc, argv);
    }

    return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |
        BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |
        BOOTM_STATE_LOADOS |
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
        BOOTM_STATE_RAMDISK |
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_MIPS)
        BOOTM_STATE_OS_CMDLINE |
#endif
        BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
        BOOTM_STATE_OS_GO, &images, 1);
}

参数

我们来说说这个函数的参数

cmd_tbl_t *cmdtp:目前笔者也不清楚它的来历,从命名方式中可以看出大约是命令表之类结构体

int flag:该参数笔者跟踪了一下其传入位置,目前并没有发现需要它的地方

int argc:不用说了,相信大家都知道这个就是bootm传入参数的个数

char * const argv[]:同上,这个就是传入的参数了

函数讲解

这里先略过前面的CONFIG_NEEDS_MANUAL_RELOC宏定义的部分,这里笔者也不甚了解

然后如果在命令有传入参数,则使用simple_strtoul对参数进行字符串到长整型数据类型的转换,这里解析的是传入的第一个参数,并将其赋值给endp,其实该参数就是在存储介质中内核的地址,但这个变量似乎并没有传入到函数里面去,仅用作判断。

执行函数do_bootm_subcommand,这个函数中执行了do_bootm_states,uboot分阶段启动,每一个阶段称之为subcommand

do_bootm_states执行的就是不同阶段的subcommand

在这里我们可以见到,如果没有传入do_bootm参数,也就是参数argc为0,那么do_bootm_statesstate参数将会是一大堆的标志宏,这些标志宏就是uboot启动时需要的阶段,每个阶段都有一个宏来表示

do_bootm_states

我们现在假设没有给bootm命令传入参数,那么我们现在进入do_bootm_states函数了
代码如下,有点长,节选部分出来

int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
            int states, bootm_headers_t *images, int boot_progress)
{
    boot_os_fn *boot_fn;
    ulong iflag = 0;
    int ret = 0, need_boot_fn;

    images->state |= states;

    /*
     * Work through the states and see how far we get. We stop on
     * any error.
     */
    if (states & BOOTM_STATE_START)
        ret = bootm_start(cmdtp, flag, argc, argv);

    if (!ret && (states & BOOTM_STATE_FINDOS))
        ret = bootm_find_os(cmdtp, flag, argc, argv);

    if (!ret && (states & BOOTM_STATE_FINDOTHER))
        ret = bootm_find_other(cmdtp, flag, argc, argv);

    /* Load the OS */
    if (!ret && (states & BOOTM_STATE_LOADOS)) {
        ulong load_end;

        iflag = bootm_disable_interrupts();
        ret = bootm_load_os(images, &load_end, 0);
        if (ret == 0)
            lmb_reserve(&images->lmb, images->os.load,
                    (load_end - images->os.load));
        else if (ret && ret != BOOTM_ERR_OVERLAP)
            goto err;
        else if (ret == BOOTM_ERR_OVERLAP)
            ret = 0;
    }

       ........

    /* From now on, we need the OS boot function */
    if (ret)
        return ret;
    boot_fn = bootm_os_get_boot_func(images->os.os);
    need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
            BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
            BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);
    if (boot_fn == NULL && need_boot_fn) {
        if (iflag)
            enable_interrupts();
        printf("ERROR: booting os '%s' (%d) is not supported\n",
               genimg_get_os_name(images->os.os), images->os.os);
        bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
        return 1;
    }


    /* Call various other states that are not generally used */
    if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
        ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);
    if (!ret && (states & BOOTM_STATE_OS_BD_T))
        ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);
    if (!ret && (states & BOOTM_STATE_OS_PREP)) {
#if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY)
        if (images->os.os == IH_OS_LINUX)
            fixup_silent_linux();
#endif
        ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
    }

#ifdef CONFIG_TRACE
    /* Pretend to run the OS, then run a user command */
    if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
        char *cmd_list = getenv("fakegocmd");

        ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,
                images, boot_fn);
        if (!ret && cmd_list)
            ret = run_command_list(cmd_list, -1, flag);
    }
#endif

    /* Check for unsupported subcommand. */
    if (ret) {
        puts("subcommand not supported\n");
        return ret;
    }

    /* Now run the OS! We hope this doesn't return */
    if (!ret && (states & BOOTM_STATE_OS_GO))
        ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
                images, boot_fn);

    /* Deal with any fallout */

}

参数

cmd_tbl_t *cmdtp:同上

int flag:同上

int argc:同上

char * const argv[]:同上

int states:这个参数就是那一大堆的标志宏

bootm_headers_t *images:这个数据结果就重要了,他传入的是一个全局的结构体,这个结构体用于保存从存储介质中读到的linux内核头部信息,同时这个全局结构体的也被命名为images

int boot_progress:似乎无作用

函数讲解

那么我们最先看到的是images的成员被赋值为states

往下看是会将参数states跟宏BOOTM_STATE_START进行与操作,如果通过则执行

bootm_start,那么这里就可以知道上面所说的每个阶段都有一个宏来表示

接下来就会进入bootm_start这个函数了

1、这里我们先设置一个断点,直接跳到下面看bootm_start,看完我们再回来

好,我们看完bootm_start后,接下来往下面,接着执行bootm_find_os,同样,我们先跳到后面去查看它的函数讲解,等下再回来

2、执行完bootm_find_os,接着执行bootm_find_other,这里不细讲,主要是查询是否有ramdisk

3、然后关闭中断,执行bootm_load_os,我们继续跳到后面去看这个函数

4、跳过ramdisk的代码,我们直接查看bootm_os_get_boot_func,这个函数很简单,直接查看boot_os变量,直接获取我们使用的操作系统的启动函数,uboot为每个不同的操作系统都编写了不同的启动函数。将其返回并赋值给变量boot_fn

终于要接近尾声了,继续跳过一些可选代码。我们直接看boot_selected_os,这函数里面就执行do_bootm_linux跳转到我们的内核去运行了,如无意外,到了这里一般情况下就不返回了。

这里我们使用的是linux,所以它的启动函数是do_bootm_linux

这里会根据不同的阶段去执行boot_fn,需要执行的阶段有以下这些

BOOTM_STATE_OS_CMDLINE

BOOTM_STATE_OS_BD_T

BOOTM_STATE_OS_PREP

BOOTM_STATE_OS_GO

5、在这里,我们讲解的是ARM架构,在这种结构中前2个阶段是不用的,我们跳到文章后面查看do_bootm_linuxBOOTM_STATE_OS_PREPBOOTM_STATE_OS_GO实现吧

bootm_start

代码如下:

static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc,
               char * const argv[])
{
    memset((void *)&images, 0, sizeof(images));
    images.verify = getenv_yesno("verify");

    boot_start_lmb(&images);

    bootstage_mark_name(BOOTSTAGE_ID_BOOTM_START, "bootm_start");
    images.state = BOOTM_STATE_START;

    return 0;
}

参数

cmdtpflagargcargv[]这几个参数相信不用我讲大家也都知道他们是什么了

函数讲解

最先看到的是清空images结构体,然后获取uboot的环境变量verify,并复制给images的成员

然后执行boot_start_lmb,这个函数看起来想是初始化镜像结构体的lmb成员,
然后获取环境变量中的某些变量并复制到images->lmb中,具体其作用目前暂不明白

最后执行bootstage_mark_name,大致就是记录启动阶段的名字和记录此时的一些数据

好,我们回去刚刚的do_bootm_states

bootm_find_os

代码如下:

static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc,
             char * const argv[])
{
    const void *os_hdr;
    bool ep_found = false;
    int ret;

    /* get kernel image header, start address and length */
    os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,
            &images, &images.os.image_start, &images.os.image_len);
    if (images.os.image_len == 0) {
        puts("ERROR: can't get kernel image!\n");
        return 1;
    }

    /* get image parameters */
    switch (genimg_get_format(os_hdr)) {
                images.os.type = image_get_type(os_hdr);
        images.os.comp = image_get_comp(os_hdr);
        images.os.os = image_get_os(os_hdr);

        images.os.end = image_get_image_end(os_hdr);
        images.os.load = image_get_load(os_hdr);
        images.os.arch = image_get_arch(os_hdr);
        }
        ......
    if (images.os.type == IH_TYPE_KERNEL_NOLOAD) {
        images.os.load = images.os.image_start;
        images.ep += images.os.load;
    }

    images.os.start = map_to_sysmem(os_hdr);
}

参数

bootm_start

函数讲解

这里需要说一下函数,该函数主要参数有imagesos_dataos_len

boot_get_kernel函数先执行genimg_get_kernel_addr_fit来获取内核镜像在存储介质中的位置,如果没有传入命令参数,则默认使用全局变量load_addr并返回,该变量由每个硬件平台自己定义宏并赋值,可能这里是移植需要做的工作之一。另外它似乎有扫描多个内核镜像并找到启动镜像的功能,但这里暂且不表。

获取到内核的存储地址后,使用genimg_get_image读取内核到内存中,这个函数将内核头部的64字节信息和内核全部读到指定地址CONFIG_SYS_LOAD_ADDR,然后返回内核所在的内存地址。

genimg_get_image获取头部信息指针后,然后根据头部指针来获取内核的大小和内核目前所在的内存地址os_lenos_data,这2个指针指向的其实就是在imges结构体中的成员。到了这里,boot_get_kernel执行完毕,我们返回内核头部信息指针

接着从内核头部信息指针中获取内核的格式,格式有传统格式,FIT格式和安卓格式等,这里我们使用传统格式来讲解。我们回到bootm_find_os

根据返回的头部信息指针,我们去获取到内核想信息并复制给images.os的各个成员,包括内核类型type,内核压缩方式comp,内核是什么操作系统os,内核要装载到内存的哪个位置load,内核是什么体系架构arch,为以后的工作做准备,这里要说明一下,现在内核所在的内存地址是uboot所指定,而内核启动的内存地址不一定在这里,是在laod成员所执行的地址,后面需要把整个镜像拷贝到这里。

最后将images.os.load赋值给images.ep,其实就是内核的启动地址了

下面内核头部信息结构体,到了这里,我们就可以返回到do_bootm_states

typedef struct image_header {
    __be32      ih_magic;   /* Image Header Magic Number    */ 
    __be32      ih_hcrc;    /* Image Header CRC Checksum    */
    __be32      ih_time;    /* Image Creation Timestamp */
    __be32      ih_size;    /* Image Data Size      */
    __be32      ih_load;    /* Data  Load  Address      */
    __be32      ih_ep;      /* Entry Point Address      */
    __be32      ih_dcrc;    /* Image Data CRC Checksum  */
    uint8_t     ih_os;      /* Operating System     */
    uint8_t     ih_arch;    /* CPU architecture     */
    uint8_t     ih_type;    /* Image Type           */
    uint8_t     ih_comp;    /* Compression Type     */
    uint8_t     ih_name[IH_NMLEN];  /* Image Name       */
} image_header_t;

bootm_load_os

主要参数

bootm_headers_t *images:就在这一行字的上边
代码如下:

static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end,
             int boot_progress)
{
    image_info_t os = images->os;
    ulong load = os.load;
    ulong blob_start = os.start;
    ulong blob_end = os.end;
    ulong image_start = os.image_start;
    ulong image_len = os.image_len;
    bool no_overlap;
    void *load_buf, *image_buf;
    int err;

    load_buf = map_sysmem(load, 0);
    image_buf = map_sysmem(os.image_start, image_len);
    err = bootm_decomp_image(os.comp, load, os.image_start, os.type,
                 load_buf, image_buf, image_len,
                 CONFIG_SYS_BOOTM_LEN, load_end);
    if (err) {
        bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE);
        return err;
    }
    flush_cache(load, ALIGN(*load_end - load, ARCH_DMA_MINALIGN));

    debug("   kernel loaded at 0x%08lx, end = 0x%08lx\n", load, *load_end);
    bootstage_mark(BOOTSTAGE_ID_KERNEL_LOADED);

    no_overlap = (os.comp == IH_COMP_NONE && load == image_start);

    if (!no_overlap && (load < blob_end) && (*load_end > blob_start)) {
        debug("images.os.start = 0x%lX, images.os.end = 0x%lx\n",
              blob_start, blob_end);
        debug("images.os.load = 0x%lx, load_end = 0x%lx\n", load,
              *load_end);

        /* Check what type of image this is. */
        if (images->legacy_hdr_valid) {
            if (image_get_type(&images->legacy_hdr_os_copy)
                    == IH_TYPE_MULTI)
                puts("WARNING: legacy format multi component image overwritten\n");
            return BOOTM_ERR_OVERLAP;
        } else {
            puts("ERROR: new format image overwritten - must RESET the board to recover\n");
            bootstage_error(BOOTSTAGE_ID_OVERWRITTEN);
            return BOOTM_ERR_RESET;
        }
    }

    return 0;
}

函数讲解

首先调用bootm_decomp_image,来解压内核。查看其传入参数,我们知道都是从上一步中获取得到的各种数据,包括装载地址,解压类型等等。os.image_start是内核未解压时所在的地址,load_buf是内核的启动位置也就是解压后内核所在的地址了。

我们继续返回到do_bootm_states

do_bootm_linux

不同的硬件平台有不同的实现,我们这里查看的ARM架构的实现代码
代码如下

int do_bootm_linux(int flag, int argc, char * const argv[],
           bootm_headers_t *images)
{
    /* No need for those on ARM */
    if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
        return -1;

    if (flag & BOOTM_STATE_OS_PREP) {
        boot_prep_linux(images);
        return 0;
    }

    if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
        boot_jump_linux(images, flag);
        return 0;
    }

    boot_prep_linux(images);
    boot_jump_linux(images, flag);
    return 0;
}

boot_prep_linux

首先先执行BOOTM_STATE_OS_PREP的代码,这里调用的是boot_prep_linux,这个函数跟内核传递参数有关系,uboot向内核传递参数就是在这里做的准备

这里先调用char *commandline = getenv("bootargs");从uboot的环境变量中获取到我们传入的启动参数,并
使用指针指向了这串字符串,该字符串在后面会用到

再调用setup_start_tag设置启动要用到的 tag,在这里有一个全局变量paramsbd->bi_boot_params的值赋给它,params的结构如下

struct tag {
    struct tag_header hdr;
    union {
        struct tag_core     core;
        struct tag_mem32    mem;
        struct tag_videotext    videotext;
        struct tag_ramdisk  ramdisk;
        struct tag_initrd   initrd;
        struct tag_serialnr serialnr;
        struct tag_revision revision;
        struct tag_videolfb videolfb;
        struct tag_cmdline  cmdline;

        /*
         * Acorn specific
         */
        struct tag_acorn    acorn;
 
        /*
         * DC21285 specific
         */
        struct tag_memclk   memclk;
    } u;

他本质是一个tag结构。该结构包括hdr和各种类型的tag_*

hdr来标志当前的tag是哪种类型。
setup_start_tag是初始化了第一个tag,类型为tag_core

最后调用tag_next跳到第一个tag末尾,为下一个tag赋值做准备。

接下来调用setup_serial_tag,代码如下,功能笔者觉得是设置控制台串口号。其中get_board_serial是各个硬件平台的实现,其功能大概是获取环境变量中的串口号,将该串口作为控制台输出。

static void setup_serial_tag(struct tag **tmp)
{
    struct tag *params = *tmp;
    struct tag_serialnr serialnr;

    get_board_serial(&serialnr);
    params->hdr.tag = ATAG_SERIAL;
    params->hdr.size = tag_size (tag_serialnr);
    params->u.serialnr.low = serialnr.low;
    params->u.serialnr.high= serialnr.high;
    params = tag_next (params);
    *tmp = params;
}

接着,再调用setup_commandline_tag,代码如下,可以看出,这里调用了strcpy来赋值字符串,赋值的字符串正是上面提到的,函数开头使用getenv获取的启动参数字符串

static void setup_commandline_tag(bd_t *bd, char *commandline)
{
    char *p;

    if (!commandline)
        return;

    /* eat leading white space */
    for (p = commandline; *p == ' '; p++);

    /* skip non-existent command lines so the kernel will still
     * use its default command line.
     */
    if (*p == '\0')
        return;

    params->hdr.tag = ATAG_CMDLINE;
    params->hdr.size =
        (sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;

    strcpy (params->u.cmdline.cmdline, p);

    params = tag_next (params);
}

接着下面还调用了setup_revision_tagsetup_memory_tags,同理都是设置不同的tag而已。这里比较特殊的是setup_memory_tags,如果有多片内存ram,会循环为每一片的ram设置一个tag

继续调用setup_board_tags,这个是板级实现,如果没有实现则跳过

最后将最末尾的tag设置为ATAG_NONE,标志tag结束。

由此可知我们的启动参数params是一片连续的内存,这片内存有很多个tag,我们通过调用不同的程序来设置这些tag

这样整个参数的准备就结束了,最后在调用boot_jump_linux时会将tags的首地址也就是bi_boot_params传给内核,让内核解析这些tag。当然我想也有朋友不懂内核传递的参数都是字符串,设置这些tag跟传递的参数有什么关系呢,笔者也不明白,等到后面我们再来讲解。

总结一下,uboot将参数以tag数组的形式布局在内存的某一个地址,每个tag代表一种类型的参数,首尾tag标志开始和结束,首地址传给kernel供其解析。

我们回到do_bootm_linux,其实这行到这里是跳出do_bootm_linux回到我们的do_bootm_states

boot_jump_linux

kernel_entry变量是个函数指针,我们会讲images->ep赋值给它作为跳转到内核执行的入口。寄存器r2会赋值为gd->bd->bi_boot_params,就是我们之前所有是tag启动参数

然后传入其余相关参数并执行kernel_entry启动内核

到了这里,uboot引导内核的启动过程讲解完毕

后记

总结来说,uboot启动内核就是读取内核,加载内核,解析内核头部,解压内核,装载内核到执行启动地址,准备启动参数,启动内核这几个阶段。
写完该片,笔者对uboot的理解深了一层,当然该文所讲的只是uboot引导的主要部分,还有很多细节我们跳过了(但大体上不影响)。以前笔者仅仅只是使用uboot,并没有对它进行一个系统的理解,今天算是对uboot的有了一些更深的理解。当然了,这些都是理论层面,还需要各位去根据uboot的代码进行实践。实践出真知,要了解uboot后的内核启动,我们还需要对操作系统和编译原理有一定的了解,关于我们后面有机会再继续聊吧

本文参考:
uboot向kernel的传参机制——bootm与tags https://blog.csdn.net/skyflying2012/article/details/35787971
什么是FIT uImage? https://blog.csdn.net/rikeyone/article/details/86594196

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

推荐阅读更多精彩内容