imx6ull中uboot源碼分析-C環(huán)境建立

概述

在基于imx6ull平臺的linux開發(fā)中柠贤,uboot的主要作用是為linux準(zhǔn)備好運行環(huán)境香浩,配置好硬件并將一些參數(shù)信息按照約定傳給內(nèi)核,然后跳轉(zhuǎn)到內(nèi)核運行臼勉。uboot本身是一個裸機程序主要功能是引導(dǎo)操作系統(tǒng)運行邻吭,功能其實并不復(fù)雜,只是開始要建立C運行環(huán)境宴霸,搬運代碼到不同地方執(zhí)行囱晴、兼容各種處理器架構(gòu)、存儲器瓢谢、網(wǎng)絡(luò)等功能代碼非郴矗晦澀。

imx系列處理器內(nèi)部都固化了一段程序恩闻,這個程序根據(jù)啟動引腳的配置艺糜,從不同存儲介質(zhì)約定的地址讀取程序的配置信息(IVT),通過讀取的信息可以找到用戶程序并加載到指定內(nèi)存運行;配置中還有一些處理器寄存器初始化數(shù)據(jù)破停,程序會將這些數(shù)據(jù)寫入寄存器完成硬件的基本初始化功能翅楼,其中最主要的是處理器時鐘和DDR控制器的配置數(shù)據(jù),只有時鐘和內(nèi)存處理器配置完成后CPU才具備基本的運行用戶程序運行的條件真慢。在DDR初始化之前所有運行的程序只能使用處理器內(nèi)部的SRAM毅臊,起使地址為0x00900000,大小為128K或256K黑界,依據(jù)具體器件而定管嬉。uboot也不列外,在編譯生成鏡像的時候會在uboot.bin前面加上這些配置信息生成最終的uboot.imx鏡像文件朗鸠,IVT寄存器配置信息在board/freescale/xxx/imximage.cfg中蚯撩,可以閱讀doc/imx/mkimage/imximage.txt查看具體的鏡像生成過程。

編譯

uboot的編譯時從make xxx_deconfig開始的烛占,這個命令執(zhí)行后會在uboot源碼的根目錄生成一個.config配置文件胎挎,xxx_deconfig和硬件電路板對應(yīng),不同板子可以創(chuàng)建自己的配置文件忆家,根據(jù)實際情況修改uboot的配置犹菇。

uboot運行

了解程序啟動過程必須先連接啟鏈接文件,找到處理器的中斷向量表所在的文件芽卿,進而從復(fù)位中斷入開始代碼運行之旅揭芍。鏈接文件和具體的處理器有關(guān),查看源碼根目錄makefile卸例,可以看到用戶可以指定鏈接文件称杨,如果沒指定依次在固定目錄查找。imx6ull使用的鏈接文件是arch/arm/cpu/u-boot.lds币厕。

# If board code explicitly specified LDSCRIPT or CONFIG_SYS_LDSCRIPT, use
# that (or fail if absent).  Otherwise, search for a linker script in a
# standard location.

ifndef LDSCRIPT
    #LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds.debug
    ifdef CONFIG_SYS_LDSCRIPT
        # need to strip off double quotes
        LDSCRIPT := $(srctree)/$(CONFIG_SYS_LDSCRIPT:"%"=%)
    endif
endif

# If there is no specified link script, we look in a number of places for it
#按優(yōu)先級在不同目錄查找連接文件
ifndef LDSCRIPT
    ifeq ($(wildcard $(LDSCRIPT)),)
        LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds
    endif
    ifeq ($(wildcard $(LDSCRIPT)),)
        LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.lds
    endif
    ifeq ($(wildcard $(LDSCRIPT)),)
        LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds
    endif
endif

鏈接文件中有如下一段:

SECTIONS
{
    . = ALIGN(4);
    .text :
    {
        *(.__image_copy_start)
        *(.vectors)
        CPUDIR/start.o (.text*)
    }
}

從這段中可以看出鏡像的開頭首先放了中斷向量表和一個名為start.o兩程序段列另,一般鏈接文件中具體指定名稱的段代表一個文件或函數(shù)。.vectors段定義在arch/arm/lib/vectors.S中旦装,這個文件定義了ARM架構(gòu)的中斷向量表页衙,從文件所處的目錄位置來看不同指令集的ARM中斷向量表相同。

arch/arm/lib/vectors.S:

    .macro ARM_VECTORS
#ifdef CONFIG_ARCH_K3
    ldr     pc, _reset
#else
    b   reset
#endif
#if !CONFIG_IS_ENABLED(SYS_NO_VECTOR_TABLE)
    ldr pc, _undefined_instruction
    ldr pc, _software_interrupt
    ldr pc, _prefetch_abort
    ldr pc, _data_abort
    ldr pc, _not_used
    ldr pc, _irq
    ldr pc, _fiq
#endif
    .endm

    .section ".vectors", "ax"   //定義.vectors段

#if defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK)
/*
 * Various SoCs need something special and SoC-specific up front in
 * order to boot, allow them to set that in their boot0.h file and then
 * use it here.
 *
 * To allow a boot0 hook to insert a 'special' sequence after the vector
 * table (e.g. for the socfpga), the presence of a boot0 hook supresses
 * the below vector table and assumes that the vector table is filled in
 * by the boot0 hook.  The requirements for a boot0 hook thus are:
 *   (1) defines '_start:' as appropriate
 *   (2) inserts the vector table using ARM_VECTORS as appropriate
 */
#include <asm/arch/boot0.h>
#else

/*
 *************************************************************************
 *
 * Exception vectors as described in ARM reference manuals
 *
 * Uses indirect branch to allow reaching handlers anywhere in memory.
 *
 *************************************************************************
 */

_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word   CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
    ARM_VECTORS
#endif /* !defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) */

arch/arm/lib/vectors.S---->arch/arm/cpu/armv7/start.S

在文件vectors.S中我們看到定義了一個名為.vectors的段阴绢,這和鏈接文件中最開始放置的段名對應(yīng)店乐,因此程序從標(biāo)號_start開始運行,imx6ull中沒有定義CONFIG_SYS_DV_NOR_BOOT_CFG呻袭,因此第一行程序在ARM_VECTORS定義的宏中眨八,我看到這個宏正是定義了ARM的中斷向量表爹凹,表中的第一條指令復(fù)位后自動執(zhí)行吏廉,跳轉(zhuǎn)到reset函數(shù)中,函數(shù)實現(xiàn)在arch/arm/cpu/armv7/start.S文件中冰啃,這個文件和ARM的具體架構(gòu)有關(guān),我們下面一段一段分析這個文件中的代碼垦江。代碼分析是建立在定義了CONFIG_SPL_BUILD宏的基礎(chǔ)上操灿,也就是使用SPL俯画,如果不使用SPL則會省去relocate_code函數(shù)調(diào)用排抬,也就是不用搬運代碼了,會簡單一些连舍。不管是否使用SPL在復(fù)位發(fā)生時uboot一開始都運行在鏈接時指定的內(nèi)存地址中没陡,只是使用SPL時會搬運一次代碼到DDR頂部運行。

    .globl  reset
    .globl  save_boot_params_ret
    .type   save_boot_params_ret,%function
#ifdef CONFIG_ARMV7_LPAE
    .global switch_to_hypervisor_ret
#endif

reset:
    /* Allow the board to save important registers */
    b   save_boot_params   //跳轉(zhuǎn)到參數(shù)保存函數(shù)索赏,在本文件中盼玄,預(yù)留,沒有實際操作
#endif

代碼重定位

文件開頭就定義了全局標(biāo)號reset潜腻,中斷向量表中b reset指令正是跳轉(zhuǎn)到這里執(zhí)行埃儿,要強調(diào)一下這條指令時一條相對尋址指令,也就處理器是通過PC寄存器加上一個偏移值來獲取目標(biāo)地址然后跳轉(zhuǎn)過去的融涣,目標(biāo)地址和當(dāng)前PC的值是相關(guān)的蝌箍,只要他們相對位置固定這條指令總能正確執(zhí)行。這個很重要暴心,因為在編譯uboot的時候并不確定它會在內(nèi)存的哪個地址運行,如果采用絕對地址尋址杂拨,當(dāng)uboot實際運行地址和編譯時候指定的鏈接地址不同時程序會跳轉(zhuǎn)到一個錯誤地址专普。這種相對尋址的指令就可以解決這個問題,讓uboot可以運行在內(nèi)存任意地址(要想實現(xiàn)這點還有其它一些工作要做弹沽,下面會討論)檀夹。也許有人會想,我一個開始在給指定板子編譯uboot時根據(jù)硬件就可以知道內(nèi)存的地址信息策橘,直接按照這個地址來編譯鏡像炸渡,加載uboot時也加載在這個地址不就行了嗎。沒錯這樣確實可以保證程序邏輯上可以正確運行丽已,但是這樣有時是做不到的蚌堵。

  • 有的處理器上電后并不能自動初始化DDR控制器,一般內(nèi)部有一小段SRAM沛婴,上電后uboot的最前面的一段代碼會被拷貝到這個小內(nèi)存中運行吼畏,這段程序就是uboot的第一階段引導(dǎo)程序,它的主要功能之一就是初始化主內(nèi)存DDR嘁灯,主內(nèi)存初始化完成后會將所有uboot拷貝到DDR中運行泻蚊。這就帶來了問題uboot要保證在內(nèi)部SRAM和DDR中都能正確運行,但鏈接時只能指定一個運行地址(一般為DDR范圍內(nèi)的地址)丑婿。
  • uboot的主要作用是引導(dǎo)linux內(nèi)核運行性雄,內(nèi)存中會存在uboot和linux內(nèi)核兩個可執(zhí)行程序没卸,他們之間的地址存在沖突的可能性,因此uboot在設(shè)計的時候會自動自我拷貝到DDR的最高地址處運行把低地址留給linux內(nèi)核秒旋,也就是說即使在主內(nèi)存中uboot也有可能運行在不同地址约计。

基于上述兩點以及為了簡化uboot的編譯配置,uboot采用了位置無關(guān)的代碼編譯方式滩褥,即在編譯代碼時加入了-pie選項保證整個uboot的是位置無關(guān)的病蛉。

接著分析代碼,首先跳轉(zhuǎn)到save_boot_params處執(zhí)行,這在當(dāng)前本文件中定義瑰煎,預(yù)留的函數(shù)接口沒有實際操作然后跳回save_boot_params_ret處铺然。接下來的操作就比較有意思了,主要功能是第一次重定位代碼酒甸,這里說是第一次是因為后面的代碼在搬運uboot后還會再次重定位一次魄健。前文描述了編譯器在編譯uboot時使用了-pie選項這樣鏡像中會生成一個名為rel.dyn的段,這個段中包含了程序內(nèi)所有需要重定位的全局變量和函數(shù)的地址信息插勤。對于匯編程序可以精確的選擇相對尋址的指令引用相關(guān)函數(shù)和變量沽瘦,但是C語言程序要由編譯器翻譯成機器碼沒法精確要編譯器使用指定的匯編指令進行相對尋址,而且當(dāng)程序?qū)ぶ贩秶容^大時就不可避免的使用絕對尋址指令农尖,因此就設(shè)計了一套機制析恋,編譯器將所有需要絕對尋址的標(biāo)號(函數(shù)和全局變量)的內(nèi)存地址信息專門放到一個段中,運行代碼本身要根據(jù)實際運行內(nèi)存地址和編譯鏈接地址的差值去修正該段中導(dǎo)出的全局標(biāo)號的引用地址盛卡,當(dāng)實際運行內(nèi)存地址和編譯鏈接地址一致時不需要修正助隧。這個原理其實和操作系統(tǒng)常用的動態(tài)鏈接庫加載時根據(jù)got段引用外部標(biāo)號及導(dǎo)出內(nèi)部標(biāo)號異曲同工。原理是程序內(nèi)部在引用全局標(biāo)號時都不時直接以用滑沧,而是在將全局標(biāo)號的地址信息放到一個中轉(zhuǎn)表中并村,這個表每個條目記錄著全局標(biāo)號的實際內(nèi)存地址,當(dāng)引用全局標(biāo)號時首先從中轉(zhuǎn)表中拿到其內(nèi)存地址滓技,然后再通過這個內(nèi)存地址引用真正的全局標(biāo)號哩牍。而rel.dyn存在的意義就是方便修改中轉(zhuǎn)表中的內(nèi)容,達到重定位全局標(biāo)號的目的令漂。

地址重定位

如圖膝昆,程序中定義了全局變量A,當(dāng)我在代碼其它地方引用這個變量時一般編譯器會直接計算出A所在的地址直接操作內(nèi)存叠必,這樣有可能會通過絕對地址引用A外潜,當(dāng)鏡像運行地址和鏈接地址不一致時A的絕對地址已經(jīng)改變,再用原來的鏈接地址去引用A必然時錯誤的挠唆。圖中將地址A的地址放在了內(nèi)存0x81370000中处窥,假設(shè)A的鏈接地址是0x82150000,如果引用A時先從0x81370000地址處取得變量A的真實地址玄组,再用這個地址去引用變量A滔驾,同樣可以正確對A進項操作谒麦,當(dāng)重定位后整個鏡像的地址向下偏移了0x10000(從rel.dyn段所處的運行地址可以計算出),此時A的地址變?yōu)榱?x81360000哆致,如果還想正確引用A只要把0x81380000地址中A的指針改為0x81360000即可绕德,這個過程就實現(xiàn)了代碼的重定位。這里大家也許有個疑問摊阀,上述過程在獲取變量A指針的過程中使用了地址0x81370000這個也不是絕對地址嗎耻蛇?這個編譯器是這么處理的,編譯器會把被引用的全局標(biāo)號的地址放在引用它的代碼附近的內(nèi)存中胞此,這樣引用全局標(biāo)號的指令很容易通過相對地址引用到這個內(nèi)存進而獲取全局標(biāo)號的地址臣咖。之所以是這樣是因為相對地址引用的范圍是有限的,只能引用當(dāng)前PC前后一定范圍內(nèi)的地址漱牵。至于編譯器怎么實現(xiàn)將全局標(biāo)號的地址放在引用這個標(biāo)號的附近夺蛇,這個其實很好辦參考start.S中類似_rel_dyn_start_ofs的定義即可。

下面我們通過注釋分析代碼整個重定位流程:

save_boot_params_ret:
#ifdef CONFIG_POSITION_INDEPENDENT
    /*
     * Fix .rela.dyn relocations. This allows U-Boot to loaded to and
     * executed at a different address than it was linked at.
     */
pie_fixup:
    /* 獲取標(biāo)號reset的運行地址到r0 */
    adr r0, reset   /* r0 <- Runtime value of reset label */
    /* 獲取標(biāo)號reset的鏈接地址到r0 */
    ldr r1, =reset  /* r1 <- Linked value of reset label */
    /* 計算運行地址和link地址的偏移 */
    subs    r4, r0, r1  /* r4 <- Runtime-vs-link offset */

    /* 如果為0酣胀,說明link地址和運行地址一致刁赦,不需要重定位直接退出 */
    beq pie_fixup_done

    /* 
     * 下面幾行代碼的作用是計算運行時rel.dyn段在內(nèi)存中實際地址,只有獲取這個段的
     * 真實的起使地址才能依據(jù)其中的信息進行重定位闻镶。
     */
    //獲取pie_fixup標(biāo)號的運行地址
    adr r0, pie_fixup
    //_rel_dyn_start_ofs鏈接時rel.dyn段相對pie_fixup標(biāo)號的偏移
    ldr r1, _rel_dyn_start_ofs
    //計算rel.dyn運行時起始地址
    /*
     * rel.dyn(運行起始地址) = rel.dyn(鏈接起始地址)- pie_fixup(鏈接地址) + pie_fixup(運行地址)
     *
     * 個人認(rèn)為覺得已經(jīng)在r4中已經(jīng)計算了運行地址相對link地址的偏移甚脉,直接用rel.dyn鏈接地址加上這個偏移也可以計算rel.dyn的起使運行地址,而且這樣更直觀一些铆农。
    */
    add r2, r0, r1  /* r2 <- Runtime &__rel_dyn_start */
    ldr r1, _rel_dyn_end_ofs
    //計算rel.dyn運行結(jié)束地址
    add r3, r0, r1  /* r3 <- Runtime &__rel_dyn_end */

pie_fix_loop:
    //獲取rel.dyn段地址中的內(nèi)容宦焦,參考上圖執(zhí)行后r0中的值是0x81370000
    ldr r0, [r2]    /* r0 <- Link location */’
    //獲取rel.dyn段地址接下來4個字節(jié)中的內(nèi)容,參考上圖執(zhí)行后r1中的值是0x17即23
    ldr r1, [r2, #4]    /* r1 <- fixup */
    //如果r1等于23則執(zhí)行重定位
    cmp r1, #23     /* relative fixup? */
    bne pie_skip_reloc

    /* relative fix: increase location by offset */
    //將r0中的地址加上偏移地址獲得實際運行時的有效地址顿涣,執(zhí)行后r0值為0x81380000
    add r0, r4
    //獲取0x81380000中的內(nèi)容,值為0x82150000
    ldr r1, [r0]
    //0x82150000 + 0x10000, r0中的內(nèi)容加上偏移后存回0x81380000
    add r1, r4
    str r1, [r0]
    //更新運行時rel.dyn段所在地址0x82010000酝豪,中的內(nèi)容涛碑,使其指向運行時變量A地址所在的內(nèi)存
    str r0, [r2]
    add r2, #8
pie_skip_reloc:
    //判斷是否所有表項都修改完成,沒完成則循環(huán)操作
    cmp r2, r3
    blo pie_fix_loop
pie_fixup_done:
=================================================
_rel_dyn_start_ofs:
    .word   __rel_dyn_start - pie_fixup
_rel_dyn_end_ofs:
    .word   __rel_dyn_end - pie_fixup

上面描述了重定位的原理和過程孵淘,不過start.S中的這次重定位rel.dyn一般是直接跳過的蒲障,因為從后面的一些代碼來看在board_init_f函數(shù)調(diào)用返回后再次搬運代碼之前,uboot中很多地方必須要求鏈接地址和運行地址一致瘫证,比如沒有對board_init_f中調(diào)用的函數(shù)列表中的指針加上偏移值做修正揉阎。

關(guān)閉中斷和切換到SVC模式

在引導(dǎo)過程中不需要開啟中斷以此關(guān)閉中斷,同時進入SCV模式背捌,這樣可以時uboot運行在更高優(yōu)先級可以更好的控制CPU毙籽,同時linux系統(tǒng)也是運行在這個模式下。

#ifdef CONFIG_ARMV7_LPAE
/*
 * check for Hypervisor support
 */
    mrc p15, 0, r0, c0, c1, 1       @ read ID_PFR1
    and r0, r0, #CPUID_ARM_VIRT_MASK    @ mask virtualization bits
    cmp r0, #(1 << CPUID_ARM_VIRT_SHIFT)
    beq switch_to_hypervisor
switch_to_hypervisor_ret:
#endif
    /*
     * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
     * except if in HYP mode already
     */
    mrs r0, cpsr
    and r1, r0, #0x1f       @ mask mode bits
    teq r1, #0x1a       @ test for HYP mode
    bicne   r0, r0, #0x1f       @ clear all mode bits
    orrne   r0, r0, #0x13       @ set SVC mode
    orr r0, r0, #0xc0       @ disable FIQ and IRQ
    msr cpsr,r0

重定位中斷向量表關(guān)閉緩存和MMU

重定位中斷向量表毡庆,中斷向量指向arch/arm/lib/vectors.S中的中斷向量表坑赡。ARM結(jié)構(gòu)中的p15協(xié)處理器有一個VBAR寄存器烙如,中斷時處理器根據(jù)這個寄存器中的地址尋找中斷向量表。不過這里設(shè)置的中斷向量表基地址明顯是有問題毅否,把_start的鏈接地址寫入到了VBAR中亚铁,這明顯是錯誤的,應(yīng)該把_start的運行地址寫入VBAR螟加。不過這個沒關(guān)系徘溢,因為這里已經(jīng)關(guān)閉了中斷,而且后面在調(diào)用_main時還要重新設(shè)置捆探。

#if !CONFIG_IS_ENABLED(SYS_NO_VECTOR_TABLE)
/*
 * Setup vector:
 */
    /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
    mrc p15, 0, r0, c1, c0, 0   @ Read CP15 SCTLR Register
    bic r0, #CR_V       @ V = 0
    mcr p15, 0, r0, c1, c0, 0   @ Write CP15 SCTLR Register

#ifdef CONFIG_HAS_VBAR
    /* Set vector address in CP15 VBAR register */
    ldr r0, =_start
    mcr p15, 0, r0, c12, c0, 0  @Set VBAR
#endif
#endif

cpu_init_cp15中主是關(guān)閉緩存和MMU,關(guān)閉MMU是邏輯程序和操作系統(tǒng)最主要的區(qū)別然爆,因此我們認(rèn)為uboot是一個復(fù)雜一些的裸機程序。

#if !CONFIG_IS_ENABLED(SKIP_LOWLEVEL_INIT)
#ifdef CONFIG_CPU_V7A
    bl  cpu_init_cp15
#endif
#if !CONFIG_IS_ENABLED(SKIP_LOWLEVEL_INIT_ONLY)
    bl  cpu_init_crit
#endif
#endif

    bl   _main

cpu_init_crit調(diào)用了lowlevel_init徐许,這個函數(shù)在文件arch/arm/cpu/armv7/lowlevel_init.S中

ENTRY(cpu_init_crit)
    /*
     * Jump to board specific initialization...
     * The Mask ROM will have already initialized
     * basic memory. Go here to bump up clock rate and handle
     * wake up conditions.
     */
    b   lowlevel_init       @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)

arch/arm/cpu/armv7/start.S---->arch/arm/cpu/armv7/lowlevel_init.S

lowlevel_init函數(shù)主要將棧指向內(nèi)部IRMA中施蜜,imx6ull中定義了CONFIG_SPL_BUILD宏,其次將r9寄存器指向gdata數(shù)據(jù)結(jié)構(gòu)雌隅,gdata中保存了很多全局變量翻默。不過由于使用了SPL這些設(shè)置都意義不大,后面board_init_f還要重新設(shè)置恰起。

.pushsection .text.s_init, "ax"
WEAK(s_init)
    bx  lr
ENDPROC(s_init)
.popsection

.pushsection .text.lowlevel_init, "ax"
WEAK(lowlevel_init)
    /*
     * Setup a temporary stack. Global data is not available yet.
     */
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    ldr sp, =CONFIG_SPL_STACK
#else
    ldr sp, =CONFIG_SYS_INIT_SP_ADDR
#endif
    bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_DM
    mov r9, #0
#else
    /*
     * Set up global data for boards that still need it. This will be
     * removed soon.
     */
#ifdef CONFIG_SPL_BUILD
    ldr r9, =gdata
#else
    sub sp, sp, #GD_SIZE
    bic sp, sp, #7
    mov r9, sp
#endif
#endif
    /*
     * Save the old lr(passed in ip) and the current lr to stack
     */
    push    {ip, lr}

    /*
     * Call the very early init function. This should do only the
     * absolute bare minimum to get started. It should not:
     *
     * - set up DRAM
     * - use global_data
     * - clear BSS
     * - try to start a console
     *
     * For boards with SPL this should be empty since SPL can do all of
     * this init in the SPL board_init_f() function which is called
     * immediately after this.
     */
    bl  s_init
    pop {ip, pc}
ENDPROC(lowlevel_init)
.popsection

cpu_init_crit是個空函數(shù)修械,接著調(diào)用_main函數(shù),其定義在arch/arm/lib/crt0.S文件中。

arch/arm/cpu/armv7/start.S---->arch/arm/lib/crt0.S

_main主要完成C語言環(huán)境的初始化工作检盼,為調(diào)用c語言編寫的board_init_f函數(shù)組做準(zhǔn)備肯污,這個函數(shù)會真正初始化SPL階段的棧和gd全局?jǐn)?shù)據(jù)結(jié)構(gòu)。棧的基地址被設(shè)置成了CONFIG_TPL_STACK并做了8字節(jié)對齊處理吨枉,CONFIG_TPL_STACK在include/configs文件夾下板子對應(yīng)的配置文件中定義蹦渣。這里先將CONFIG_TPL_STACK臨時作為堆棧的起始地址是因為后面會調(diào)用C函數(shù)了,必須有一個可用的堆棧貌亭。堆棧的設(shè)置并沒有一步到位后面調(diào)用C函數(shù)后還會依據(jù)返回值重新設(shè)置第一階段使用的最終堆棧柬唯。

SPL階段內(nèi)存布局.png

上圖展示了最終處理器內(nèi)部SRAM的布局情況,從CONFIG_TPL_STACK地址開始向上依次為heap圃庭、gddata锄奢、棧,這些工作是由board_init_f_alloc_reserve剧腻、board_init_f_init_reserve兩個C函數(shù)完成的拘央,他們定義在/common/init/board_init.c文件中,board_init_f_alloc_reserve函數(shù)負(fù)責(zé)為heap和gddata預(yù)留內(nèi)存空間书在,board_init_f_init_reserve負(fù)責(zé)從已經(jīng)預(yù)留的內(nèi)空間中為heap和gddata做些初始化工作灰伟,此后r9寄存器固定指向gd結(jié)構(gòu)首地址。堆棧環(huán)境準(zhǔn)備好了儒旬,接下來就是盡早調(diào)用串口初始化函數(shù)debug_uart_init,去實現(xiàn)再在文件include/debug_uart.h中袱箱,這是一個宏遏乔,最終調(diào)用board_debug_uart_init()、 _debug_uart_init()兩個函數(shù)发笔,imx6ull中board_debug_uart_init是個空函數(shù)盟萨,_debug_uart_init定義在drivers/serial/serial_mxc.c中,主要就是初始化串口相關(guān)寄存器了讨,此后串口就可以正常打印log了捻激。接下來清零bss段,這個通過宏CLEAR_BSS引用前计。至此SPL階段的C語言環(huán)境完全準(zhǔn)備就緒可以調(diào)用board_init_f完善gd數(shù)據(jù)結(jié)構(gòu)及其余的基礎(chǔ)初始化工作胞谭。

.macro CLEAR_BSS
    ldr r0, =__bss_start    /* this is auto-relocated! */

#ifdef CONFIG_USE_ARCH_MEMSET
    ldr r3, =__bss_end      /* this is auto-relocated! */
    mov r1, #0x00000000     /* prepare zero to clear BSS */

    subs    r2, r3, r0      /* r2 = memset len */
    bl  memset
#else
    ldr r1, =__bss_end      /* this is auto-relocated! */
    mov r2, #0x00000000     /* prepare zero to clear BSS */

clbss_l:cmp r0, r1          /* while not at end of BSS */
    strlo   r2, [r0]        /* clear 32-bit BSS word */
    addlo   r0, r0, #4      /* move to next */
    blo clbss_l
#endif
.endm

/*
 * entry point of crt0 sequence
 */

ENTRY(_main)

/* Call arch_very_early_init before initializing C runtime environment. */
#if CONFIG_IS_ENABLED(ARCH_VERY_EARLY_INIT)
    bl  arch_very_early_init
#endif

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */

#if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)
    ldr r0, =(CONFIG_TPL_STACK)
#elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    ldr r0, =(CONFIG_SPL_STACK)
#else
    ldr r0, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
    bic r0, r0, #7  /* 8-byte alignment for ABI compliance */
    mov sp, r0
    bl  board_init_f_alloc_reserve
    mov sp, r0
    /* set up gd here, outside any C code */
    mov r9, r0
    bl  board_init_f_init_reserve

#if defined(CONFIG_DEBUG_UART) && CONFIG_IS_ENABLED(SERIAL)
    bl  debug_uart_init
#endif

#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_EARLY_BSS)
    CLEAR_BSS
#endif

    mov r0, #0
    bl  board_init_f

#if ! defined(CONFIG_SPL_BUILD)

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
 */

    ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
    bic r0, r0, #7  /* 8-byte alignment for ABI compliance */
    mov sp, r0
    ldr r9, [r9, #GD_NEW_GD]        /* r9 <- gd->new_gd */

    adr lr, here
#if defined(CONFIG_POSITION_INDEPENDENT)
    adr r0, _main
    ldr r1, _start_ofs
    add r0, r1
    ldr r1, =CONFIG_SYS_TEXT_BASE
    sub r1, r0
    add lr, r1
#endif
    ldr r0, [r9, #GD_RELOC_OFF]     /* r0 = gd->reloc_off */
    add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
    orr lr, #1              /* As required by Thumb-only */
#endif
    ldr r0, [r9, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
    b   relocate_code
here:
/*
 * now relocate vectors
 */

    bl  relocate_vectors

/* Set up final (full) environment */

    bl  c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)

#if !defined(CONFIG_SPL_BUILD) || !defined(CONFIG_SPL_EARLY_BSS)
    CLEAR_BSS
#endif

# ifdef CONFIG_SPL_BUILD
    /* Use a DRAM stack for the rest of SPL, if requested */
    bl  spl_relocate_stack_gd
    cmp r0, #0
    movne   sp, r0
    movne   r9, r0
# endif

#if ! defined(CONFIG_SPL_BUILD)
    bl coloured_LED_init
    bl red_led_on
#endif
    /* call board_init_r(gd_t *id, ulong dest_addr) */
    mov     r0, r9                  /* gd_t */
    ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
    /* call board_init_r */
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
    ldr lr, =board_init_r   /* this is auto-relocated! */
    bx  lr
#else
    ldr pc, =board_init_r   /* this is auto-relocated! */
#endif
    /* we should not return here. */
#endif

ENDPROC(_main)

_start_ofs:
    .word   _start - _main

arch/arm/cpu/armv7/start.S---->arch/arm/lib/crt0.S---->common/board_f.c

static const init_fnc_t init_sequence_f[] = {
    setup_mon_len,
#ifdef CONFIG_OF_CONTROL
    fdtdec_setup,
#endif
#ifdef CONFIG_TRACE_EARLY
    trace_early_init,
#endif
    initf_malloc,
    log_init,
    initf_bootstage,    /* uses its own timer, so does not need DM */
    event_init,
#ifdef CONFIG_BLOBLIST
    bloblist_init,
#endif
    setup_spl_handoff,
#if defined(CONFIG_CONSOLE_RECORD_INIT_F)
    console_record_init,
#endif
#if defined(CONFIG_HAVE_FSP)
    arch_fsp_init,
#endif
    arch_cpu_init,      /* basic arch cpu dependent setup */
    mach_cpu_init,      /* SoC/machine dependent CPU setup */
    initf_dm,
#if defined(CONFIG_BOARD_EARLY_INIT_F)
    board_early_init_f,
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_SYS_FSL_CLK) || defined(CONFIG_M68K)
    /* get CPU and bus clocks according to the environment variable */
    get_clocks,     /* get CPU and bus clocks (etc.) */
#endif
#if !defined(CONFIG_M68K)
    timer_init,     /* initialize timer */
#endif
#if defined(CONFIG_BOARD_POSTCLK_INIT)
    board_postclk_init,
#endif
    env_init,       /* initialize environment */
    init_baud_rate,     /* initialze baudrate settings */
    serial_init,        /* serial communications setup */
    console_init_f,     /* stage 1 init of console */
    display_options,    /* say that we are here */
    display_text_info,  /* show debugging info if required */
    checkcpu,
#if defined(CONFIG_SYSRESET)
    print_resetinfo,
#endif
#if defined(CONFIG_DISPLAY_CPUINFO)
    print_cpuinfo,      /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DTB_RESELECT)
    embedded_dtb_select,
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
    show_board_info,
#endif
    INIT_FUNC_WATCHDOG_INIT
    misc_init_f,
    INIT_FUNC_WATCHDOG_RESET
#if CONFIG_IS_ENABLED(SYS_I2C_LEGACY)
    init_func_i2c,
#endif
#if defined(CONFIG_VID) && !defined(CONFIG_SPL)
    init_func_vid,
#endif
    announce_dram_init,
    dram_init,      /* configure available RAM banks */
#ifdef CONFIG_POST
    post_init_f,
#endif
    INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_SYS_DRAM_TEST)
    testdram,
#endif /* CONFIG_SYS_DRAM_TEST */
    INIT_FUNC_WATCHDOG_RESET

#ifdef CONFIG_POST
    init_post,
#endif
    INIT_FUNC_WATCHDOG_RESET
    /*
     * Now that we have DRAM mapped and working, we can
     * relocate the code and continue running from DRAM.
     *
     * Reserve memory at end of RAM for (top down in that order):
     *  - area that won't get touched by U-Boot and Linux (optional)
     *  - kernel log buffer
     *  - protected RAM
     *  - LCD framebuffer
     *  - monitor code
     *  - board info struct
     */
    setup_dest_addr,
#ifdef CONFIG_OF_BOARD_FIXUP
    fix_fdt,
#endif
#ifdef CONFIG_PRAM
    reserve_pram,
#endif
    reserve_round_4k,
    arch_reserve_mmu,
    reserve_video,
    reserve_trace,
    reserve_uboot,
    reserve_malloc,
    reserve_board,
    reserve_global_data,
    reserve_fdt,
    reserve_bootstage,
    reserve_bloblist,
    reserve_arch,
    reserve_stacks,
    dram_init_banksize,
    show_dram_config,
    INIT_FUNC_WATCHDOG_RESET
    setup_bdinfo,
    display_new_sp,
    INIT_FUNC_WATCHDOG_RESET
    reloc_fdt,
    reloc_bootstage,
    reloc_bloblist,
    setup_reloc,
#if defined(CONFIG_X86) || defined(CONFIG_ARC)
    copy_uboot_to_ram,
    do_elf_reloc_fixups,
#endif
    clear_bss,
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
        !CONFIG_IS_ENABLED(X86_64)
    jump_to_copy,
#endif
    NULL,
};

void board_init_f(ulong boot_flags)
{
    gd->flags = boot_flags;
    gd->have_console = 0;

    if (initcall_run_list(init_sequence_f))
        hang();

#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
        !defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
        !defined(CONFIG_ARC)
    /* NOTREACHED - jump_to_copy() does not return */
    hang();
#endif
}

board_init_f函數(shù)定義在文件common/board_f.c中,代碼很簡單就是順序調(diào)用init_sequence_f列表中函數(shù)男杈,參數(shù)boot_flags為0丈屹。

  • setup_mon_len 將uboot從開始到bss段結(jié)束的長度信息設(shè)置到gd->mon_len
  • fdtdec_setup 將設(shè)備樹起始地址設(shè)置到gd->fdt_blob中,設(shè)備樹放置的位置有多種方式伶棒,可以緊跟著uboot的__bss_end放置旺垒,也可以放在uboot中的__dtb_dt_spl_begin段中,用戶也可以通過配置自己指定肤无。這個函數(shù)還要校驗設(shè)備樹的有效性先蒋。
  • initf_malloc 初始化gd中malloc使用的heap相關(guān)信息,基址除外宛渐,基址已經(jīng)在board_init_f_init_reserve中設(shè)置
  • initf_bootstage 記錄引導(dǎo)階段
  • console_record_init:控制臺輸入輸出緩沖區(qū)初始化
  • arch_cpu_init:cpu相關(guān)初始化和具體器件有關(guān)竞漾,imx6ull這個函數(shù)定義在arch/arm/mach-imx/mx6/soc.c中
  • initf_dm:設(shè)備模型初始化,這個比較復(fù)雜本文不做討論
  • board_early_init_f:板級初始化窥翩,imx6ull定義在board/freescale/mx6ullevk/mx6ullevk.c中业岁,與板子相關(guān)
  • env_init:環(huán)境變量輸出化,uboot環(huán)境環(huán)境變量可以存儲在很多介質(zhì)中寇蚊,每種類型的介質(zhì)會提供一個struct env_driver類型的驅(qū)動結(jié)構(gòu)來從這些介質(zhì)訪問保存的環(huán)境變量笔时,這里就遍歷驅(qū)動列表調(diào)用其初始化函數(shù),如果沒有唯一指定用哪個介質(zhì)中的環(huán)境變量幔荒,還會設(shè)置uboot鏡像中名為default_environment的環(huán)境變量,這個環(huán)境變量很多條目是在include/configs板級別頭文件中定義的梳玫,所有使用的環(huán)境變量最終會用一個hash列表管理爹梁。
  • display_options:到這里就在控制臺打印出uboot的基本信息了
  • dram_init:獲取DDR大小到gd->ram_size中,imx6ull是通過讀取DDR控制器中配置的信息獲取大小的
  • setup_dest_addr:初始化ddr相關(guān)信息提澎,這些在后面代碼重定位時會用到姚垃,知道ddr這些信息后就可以重新為堆、棧以及其它數(shù)據(jù)結(jié)構(gòu)在ddr中分配內(nèi)存盼忌,分配完還要把相關(guān)數(shù)據(jù)拷貝到新內(nèi)存中积糯,后面一系列函數(shù)都做這個工作掂墓。
  • setup_reloc:計算uboot重定位的偏移地址,后面會根據(jù)這個偏移把uboot鏡像拷貝到DDR中看成,同時拷貝老gd內(nèi)容到新gd中君编。

至此board_init_f函數(shù)完成,可以看到這個函數(shù)主要時完成了gd數(shù)據(jù)結(jié)構(gòu)的初始化川慌、ddr內(nèi)存分配和一些數(shù)據(jù)結(jié)構(gòu)的搬運調(diào)整工作吃嘿,uboot將被搬運到DDR的接近top的地址處。沒有設(shè)計到太多硬件的初始化梦重。

arch/arm/lib/crt0.S

#if ! defined(CONFIG_SPL_BUILD)

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
 */

    ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
    bic r0, r0, #7  /* 8-byte alignment for ABI compliance */
    mov sp, r0
    ldr r9, [r9, #GD_NEW_GD]        /* r9 <- gd->new_gd */

    adr lr, here   //獲取here當(dāng)前運行地址
#if defined(CONFIG_POSITION_INDEPENDENT)
    /* 計算當(dāng)前運行地址和鏈接地址的偏移 */
    adr r0, _main
    ldr r1, _start_ofs
    add r0, r1
    ldr r1, =CONFIG_SYS_TEXT_BASE
    sub r1, r0
    add lr, r1
#endif
    /* 獲取gd->reloc_off值 */
    ldr r0, [r9, #GD_RELOC_OFF]     /* r0 = gd->reloc_off */
    //計算最終的返回地址
    add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
    orr lr, #1              /* As required by Thumb-only */
#endif
    ldr r0, [r9, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
    b   relocate_code
here:
/*
 * now relocate vectors
 */

    bl  relocate_vectors

/* Set up final (full) environment */

    bl  c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)

#if !defined(CONFIG_SPL_BUILD) || !defined(CONFIG_SPL_EARLY_BSS)
    CLEAR_BSS
#endif

# ifdef CONFIG_SPL_BUILD
    /* Use a DRAM stack for the rest of SPL, if requested */
    bl  spl_relocate_stack_gd
    cmp r0, #0
    movne   sp, r0
    movne   r9, r0
# endif

#if ! defined(CONFIG_SPL_BUILD)
    bl coloured_LED_init
    bl red_led_on
#endif
    /* call board_init_r(gd_t *id, ulong dest_addr) */
    mov     r0, r9                  /* gd_t */
    ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
    /* call board_init_r */
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
    ldr lr, =board_init_r   /* this is auto-relocated! */
    bx  lr
#else
    ldr pc, =board_init_r   /* this is auto-relocated! */
#endif
    /* we should not return here. */
#endif

ENDPROC(_main)

回到crt0.S中,開始的時候r9保存了gd老地址的指針兑燥,接下來做下面幾個工作

  • 重新初始化棧基地址寄存器琴拧,新地址在ddr中降瞳,由reserve_stacks函數(shù)分配
  • 將r9指向ddr中g(shù)d的新地址,之前在setup_reloc中將老gd拷貝到了新的gd
  • 調(diào)用relocate_code函數(shù)將uboot鏡像搬運到ddr中蚓胸,目標(biāo)地址由reserve_uboot計算挣饥,在調(diào)用這個函數(shù)之前還有一件重要任務(wù)是計算函數(shù)的返回地址lr,因為從relocate_code返回時赢织,ddr中新地址的uboot鏡像已經(jīng)就緒要跳到新地址運行而且要返回到新地址的here處繼續(xù)向下執(zhí)行亮靴,并不能重新從uboot開始處執(zhí)行。這里計算lr比較復(fù)雜于置,因為涉及到三個地址之間的關(guān)系鏈接地址茧吊、當(dāng)前運行地址、目標(biāo)運行地址(一般這里鏈接地址八毯、當(dāng)前運行地址一致)搓侄,gd->reloc_off是目標(biāo)運行地址與鏈接地址的差值,因此返回地址計算公式為:

lr = gd->reloc_off-當(dāng)前運行地址和鏈接地址的偏移 + here當(dāng)前運行地址

lr = 目標(biāo)運行地址和當(dāng)前運行地址的偏移 + here當(dāng)前運行地址

gd->reloc_off-當(dāng)前運行地址和鏈接地址的偏移就等于目標(biāo)運行地址和當(dāng)前運行地址的偏移话速。

  • relocate_code:這段代碼和“代碼重定位”一節(jié)中描述的功能完全一致讶踪,再次修正rel.dyn段是因為uboot鏡像又被搬運到新地址了。
  • relocate_vectors:重新設(shè)置中斷向量表基地址泊交,前文設(shè)置過一次但是是不正確的乳讥,搬運過代碼再次設(shè)置。
  • 前面已經(jīng)清除過bss段廓俭,這里不用再清除
  • spl_relocate_stack_gd: 這里又重新計算棧云石、堆、gd全局變量的地址如果deconfig文件中配置了CONFIG_SPL_STACK_R研乒,這是因為不同配置的uboot可能需要更大的棧和堆汹忠,所以設(shè)置到ddr中,如果沒有配置CONFIG_SPL_STACK_R則棧是在sram中,由board_init_f_alloc_reserve分配宽菜,但是gd和堆其實已經(jīng)在ddr中谣膳,地址是由函數(shù)reserve_global_data、reserve_malloc分配铅乡,gd基地址后續(xù)在crt0.S文件中設(shè)置到r9寄存器中继谚。配置CONFIG_SPL_STACK_R后gd會再次從ddr一個地址搬運到另外一個地址。個人覺得uboot對棧和gd的處理實在過于復(fù)雜隆判,本來可以至多兩次設(shè)置的事情犬庇,硬是搞成了3次,而且沒看到對不同處理器的兼容性由任何幫助侨嘀。
  • board_init_r:這個函數(shù)也是完成一組函數(shù)的調(diào)用臭挽,最終根據(jù)環(huán)境變量的配置和終端的操作引導(dǎo)操作系統(tǒng)或啟動hush(shell),至此uboot的c語言環(huán)境完全建立。

arch/arm/cpu/armv7/start.S---->arch/arm/lib/crt0.S---->common/board_r.c

void board_init_r(gd_t *new_gd, ulong dest_addr)
{
    /*
     * Set up the new global data pointer. So far only x86 does this
     * here.
     * TODO(sjg@chromium.org): Consider doing this for all archs, or
     * dropping the new_gd parameter.
     */
    if (CONFIG_IS_ENABLED(X86_64) && !IS_ENABLED(CONFIG_EFI_APP))
        arch_setup_gd(new_gd);

#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
    gd = new_gd;
#endif
    gd->flags &= ~GD_FLG_LOG_READY;

    if (IS_ENABLED(CONFIG_NEEDS_MANUAL_RELOC)) {
        for (int i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
            MANUAL_RELOC(init_sequence_r[i]);
    }

    if (initcall_run_list(init_sequence_r))
        hang();

    /* NOTREACHED - run_main_loop() does not return */
    hang();
}
  • 首先要重定位init_sequence_r中的函數(shù)指針咬腕,因為init_sequence_r數(shù)組中的函數(shù)地址還是鏈接時候的地址欢峰,直接調(diào)用肯定出錯所以要加上偏移地址,uboot中這種修正很多涨共,不過這里又一個疑問init_sequence_f中并沒有做這個操作纽帖,唯一的解釋是最后一次搬移代碼之前鏈接地址和運行地址必須一致。
  • 順序調(diào)用init_sequence_r中的函數(shù)

這些函數(shù)本文就不具體分析了举反,放在另一片文章分析懊直,函數(shù)最終調(diào)用run_main_loop進入主循環(huán),自動執(zhí)行環(huán)境變量bootcmd中的命令引導(dǎo)系統(tǒng)火鼻,如果在規(guī)定的時間內(nèi)控制臺輸入回車則中斷引導(dǎo)進入shell界面(hush)室囊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市魁索,隨后出現(xiàn)的幾起案子融撞,更是在濱河造成了極大的恐慌,老刑警劉巖粗蔚,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尝偎,死亡現(xiàn)場離奇詭異,居然都是意外死亡鹏控,警方通過查閱死者的電腦和手機致扯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來当辐,“玉大人抖僵,你說我怎么就攤上這事∑俟梗” “怎么了裆针?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長寺晌。 經(jīng)常有香客問我世吨,道長,這世上最難降的妖魔是什么呻征? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任耘婚,我火速辦了婚禮,結(jié)果婚禮上陆赋,老公的妹妹穿的比我還像新娘沐祷。我一直安慰自己,他們只是感情好攒岛,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布赖临。 她就那樣靜靜地躺著,像睡著了一般灾锯。 火紅的嫁衣襯著肌膚如雪兢榨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天顺饮,我揣著相機與錄音吵聪,去河邊找鬼。 笑死兼雄,一個胖子當(dāng)著我的面吹牛吟逝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赦肋,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼块攒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了金砍?” 一聲冷哼從身側(cè)響起局蚀,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恕稠,沒想到半個月后琅绅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡鹅巍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年千扶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骆捧。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡澎羞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出敛苇,到底是詐尸還是另有隱情妆绞,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站括饶,受9級特大地震影響株茶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜图焰,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一启盛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧技羔,春花似錦僵闯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拙绊,卻和暖如春牺弹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背时呀。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工张漂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谨娜。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓航攒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親趴梢。 傳聞我的和親對象是個殘疾皇子漠畜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容