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;
}

參數

cmdtp、flag、argc、argv[]這幾個參數相信不用我講大家也都知道他們是什么了

函數講解

最先看到的是清空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_data,os_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,在這里有一個全局變量params,bd->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_tag、setup_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閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容