• <tbody id="wslfv"><pre id="wslfv"></pre></tbody>
    <span id="wslfv"></span>
    <tbody id="wslfv"><pre id="wslfv"></pre></tbody>
    <th id="wslfv"><track id="wslfv"><rt id="wslfv"></rt></track></th>

    <li id="wslfv"><acronym id="wslfv"></acronym></li>
    更多課程 選擇中心

    嵌入式培訓
    達內IT學院

    400-111-8989

    手寫一個Simple Bootloader,囊括嵌入式Bootloader的核心和精華

    • 發布:嵌入式培訓
    • 來源:嵌入式教程
    • 時間:2018-01-26 16:59

    手寫一個Simple Bootloader,囊括嵌入式Bootloader的核心和精華

    CPU上電后會從IO空間的某地址取第一條指令。但此時:PLL沒有啟動,CPU工作頻率為外部輸入晶振頻率,非常低;CPU工作模式、中斷設置等不確定;存儲空間的各個BANK(包括內存)都沒有驅動,內存不能使用。在這種情況下必須在第一條指令處做一些初始化工作,這段初始化程序與操作系統獨立分開,稱之為bootloader。

    實際上,很少有必要自己寫一個Bootloader,因為U-Boot已經強大到能夠滿足各種需要。但是強大必然復雜,一個初學者想要分析U-Boot的源代碼,還是有些難度的。出于學習的目的,我寫了這個史上最簡單的啟動加載器,它只包含最基本的功能,卻囊括了一個嵌入式Bootloader應該有的核心和精華。我把這個啟動加載器命名為S-Boot, 是Simple Bootloader的縮寫,亦可進一步簡稱為SB。

    使用的實驗環境為OK2440開發板,板上處理器為S3C2440A,有64M內存,Nand存儲器為K9F1208,64M。網口芯片為CS8900A。我們要實現的功能是:從串口下載Linux內核映像到RAM;從網口下載Linux內核映像到RAM;從RAM啟動內核掛載NFS根文件系統。

    1. 第一階段的匯編代碼:start.S

    一個嵌入式Bootloader最初始部分的代碼幾乎必須是用匯編語言寫成的,因為開發板剛上電后沒有準備好C程序運行環境,比如堆棧指針SP沒有指到正確的位置。匯編代碼應該完成最原始的硬件設備初始化,并準備好C運行環境,這樣后面的功能就可以用C語言來寫了。

    對我們的S-Boot來說,上電后的起始運行代碼是 start/start.S。

    .

    text

    .global _start

    _start:

    b Reset @ 0x00: 發生復位異常時從地址零處開始運行

    b HandleUndef @ 0x04: 未定義指令中止模式的向量地址

    b HandleSWI @ 0x08: 管理模式的向量地址,通過SWI指令進入此模式

    b HandlePrefetchAbort @ 0x0C: 指令預取終止導致的異常的向量地址

    b HandleDataAbort @ 0x10: 數據訪問終止導致的異常的向量地址

    b HandleNotUsed @ 0x14: 保留

    b HandleIRQ @ 0x18: 中斷模式的向量地址

    b HandleFIQ @ 0x1C: 快中斷模式的向量地址

    這里,匯編指示符.text表明以下內容屬于代碼段,.global _start指明_start是全局可訪問的符號(Give the symbol external linkage)。按照ARM920T的規定,從地址0x00到0x1C放置異常向量表,向量表每個條目占四個字節,正好可以放置一條跳轉指令,跳轉到相應異常的服務程序中去。在S-Boot中沒有使用中斷,所以除Reset異常外,其它異常的服務程序都可簡單地寫個死循環。Reset異常是系統上電后自動觸發的,所以我們的代碼都寫在Reset的服務程序里面。

    實際上,異常向量表不一定非要位于地址0x00處,CP15協處理器中的c1寄存器的第13位用來控制異常向量表的起始地址。該位為0時,異常向量表位于低地址0x00處;該位為1時,異常向量表位于高地址 0xFFFF0000處。我們沒有必要改變這個位的值,使用默認的低地址就行了。

    Reset:

    mrs r0,cpsr @set cpu to SVC32 mode

    bic r0,r0,#0x1F

    orr r0,r0,#0xD3

    msr cpsr,r0 @cpsr=11x10011, IRQ/FIQ disabled

    代碼最初始的任務是設置CPU工作在SVC32模式,關閉所有中斷,禁用看門狗。實際上,即使不設置工作模式,CPU在復位之后將自動工作在管理模式。在整個S-Boot運行期間,我們沒有使用中斷,也沒有改變CPU工作模式,它將一直工作在SVC32模式。

    MMU、ICache、DCache的打開和關閉都是由CP15協處理器的c1寄存器控制的。實際上在復位之后這三者都是自動關閉的,所以省略了關閉它們的代碼。

    S3C2440A的PSR寄存器(Program Status Reguster)中每個Bit位的含義如圖1所示。Bit4~Bit0為模式位,用來設置CPU工作模式,現在只要知道 M[4:0] = 10011 表示SVC32模式就行了。Bit5為狀態位,T=0表示工作在ARM狀態,T=1表示工作在Thumb狀態,默認為0,不需要改變。Bit6為快速中斷禁止位,F=1為禁止快速中斷,F=0為使能快速中斷。Bit7為中斷禁止位,I=1為禁止中斷,F=0為使能中斷。其它Bit位暫時可以不必理會。

    mrs 和msr是在PSR寄存器和其它寄存器間傳遞數據的指令。如:mrs r0,cpsr 把cpsr的值傳送到r0中, msr cpsr,r0 把r0的值傳送到cpsr中。bic是位清零(Bit Clear)指令,bic r0,r0,#0x1F 意思是把r0的Bit[4:0]位清零(由0x1F指示),然后把結果寫入r0中。 orr是按位求或指令,orr r0,r0,#0xD3 表示把r0的 Bit7,Bit6,Bit4,Bit1,Bit0 置為1,其它位保持不變。

    執行完上述操作后,cpsr中的 I=1, F=1, T保持不變(默認為0),M[4:0]=10011,意思是禁止IRQ,禁止FIQ,工作在ARM狀態,工作在SVC32模式。

    ldr r0, =0x53000000

    mov r1, #0x0

    str r1, [r0] @disable watch dog

    禁用看門狗更簡單,因為WTCON寄存器的地址為0x53000000,直接向該寄存器寫0即可。

    到目前為止,CPU工作在外接晶振12MHz頻率之下。使用以下代碼設置PLL,提升工作頻率。

    ldr r0, =0x4C000014 @CLKDIVN register

    mov r1, #0x05 @FCLK:HCLK:PCLK = 1:4:8

    str r1, [r0]

    mrc p15,0,r0,c1,c0,0 @if HDIVN Not 0, must asynchronous bus mode

    orr r0,r0,#0xC0000000 @see S3C2440A manual P7-9

    mcr p15,0,r0,c1,c0,0

    ldr r0, =0x4C000004 @MPLLCON register

    ldr r1, =0x0005C011 @((92<<12)|(1<<4)|(1))

    str r1, [r0] @FCLK is 400 MHz !

    最后的結果是,FCLK=400MHz,HCLK=100MHz,PCLK=50MHz。

    @ SDRAM Init

    mov r1, #0x48000000 @MEM_CTL_BASE

    adrl r2, mem_cfg_val

    add r3, r1, #52

    1:

    ldr r4, [r2], #4 @ 讀取設置值,并讓r2加4

    str r4, [r1], #4 @ 將此值寫入寄存器,并讓r1加4

    cmp r1, r3 @ 判斷是否設置完所有13個寄存器

    bne 1b @ 若沒有寫成,繼續

    設置存儲控制器。

    ldr sp, =0x32FFF000 @設置堆棧

    bl nand_init @初始化NAND Flash

    @nand_read_ll函數需要3個參數:

    ldr r0, =0x33000000 @1. 目標地址=0x30000000,這是SDRAM的起始地址

    mov r1, #0 @2. 源地址 =0,S-Boot代碼都存在NAND地址0開始處

    mov r2, #102400 @3. 復制長度=102400(bytes)

    bl nand_read @調用C函數nand_read

    ldr lr, =halt_loop @設置返回地址

    ldr pc, =main @b指令和bl指令只能前后跳轉32M的范圍,故使用向pc賦值的方法進行跳轉

    halt_loop:

    b halt_loop

    這里把所有的代碼從Nand拷貝到RAM中,然后跳轉到main函數去執行。此后程序便在RAM中運行了。但是到目前為止,前面的程序都是在SteppingStone里運行的。所謂SteppingStone,是指在S3C2440A的內部的4KB的RAM緩存,它總是映射到地址0x00處。硬件加電后會自動將Nand Flash中的前4KB的數據拷貝到Stepping Stone中,然后從地址0x00處開始運行。

    如果代碼足夠小(小于4KB)的話,那只在SteppingStone中運行,加載Linux內核到內存即可。但通常代碼肯定會大于4KB。所以Bootloader一般分為兩部分,Stage1的代碼在SteppingStone中運行,它會把Stage2的代碼拷貝到RAM中,并跳轉到RAM中執行;Stage2的代碼在RAM中執行,它可以完成加載內核及其它任何復雜的功能。因為Stage2的起始位置不好確定,為了方便,我們把所有的代碼都拷貝到RAM中了。

    C 函數nand_read有三個參數,第一個參數為目的地起始地址,第二個參數為源起始地址,第三個參數為要復制的數據長度,以字節為單位。根據ATPCS 函數調用規則,三個參數分別用寄存器r0,r1,r2來傳遞。我們在內存的0x33000000處存放Bootloader,復制長度根據編譯生成的S- Boot.bin映像文件大小,向上取512字節的整數倍。

    這里先來規劃一下內存空間的分配。RAM的地址范圍是從0x30000000到0x34000000共64MByte。把S-Boot和Kernel放在高地址處,S-Boot從 0x33000000開始,預留8MByte的空間,內核從0x33800000開始,可供使用的空間也是8MByte。因棧空間是向下生長的,我們在 S-Boot下面預留4096Byte的空閑區域,然后向下為棧空間,故棧指針SP初始化為 0x32FFF000。其實留不留空閑區域是無所謂的,這里只是為了把二者更明顯地區分開。我們只設置SVC模式下的SP,不使用CPU的其它工作模式,所以也沒必要設置其它模式下的棧指針。另外,程序中不使用動態內存分配,故而也不必分配堆空間。

    2. nand讀操作

    在編譯連接時,我們把上述 start.S 代碼放在生成的二進制映像文件的最開始位置,因而也被燒寫到 Nand Flash 的最起始位置,因而會被自動拷貝到 SteppingStone 里運行。start.S 要完成的任務之一,是把S-Boot的所有代碼從Nand Flash拷貝到內存中,這里需要對NAND的讀操作,因此對NAND的初始化和讀操作要在第一階段寫好。

    以開發板上使用的K9F1208為例,每個頁(page)為512Byte數據和16Byte校驗,每個塊(Block)為32個頁,即16KByte數據和512Byte校驗。

    Nand Flash只用8根線與CPU的DATA0-7連接,位寬為8位,不管是數據、地址或控制字都通過這8根線傳遞,如果讀寫數據的話每次只能傳輸一個字節數據。Nand Flash的操作通過NFCONF、NFCMD、NFADDR、NFDATA、NFSTAT和NFECC六個寄存器來完成。在S3C2440A數據手冊第218頁可以看到讀寫Nand Flash的操作時序:1. 通過NFCONF寄存器配置Nand Flash;2.寫Nand Flash命令到NFCMD寄存器;3.寫Nand Flash地址到 NFADDR寄存器;4. 在讀寫數據時,通過NFSTAT寄存器獲得Nand Flash的狀態信息。應該在讀操作前或寫操作后檢查R/nB信號(Ready/Busy信號)。

    初始化NAND Flash:S3C2440的NFCONF寄存器用來設置時序參數TACLS、TWRPH0、TWRPH1,設置數據位寬;還有一些只讀位。TACLS、 TWRPH0、TWRPH1這三個參數控制的是Nand Flash信號線CLE/ALE與寫控制信號nWE的時序關系。

    注意,寄存器值轉換成實際的時鐘周期值時,TACLS不需加1,而TWRPH0和TWRPH1需要加1。比如NFCONF寄存器中設置 TACLS=1,TWRPH0=3,TWRPH1=0,意思是時序圖中 TACLS=1個HCLK時鐘,TWRPH0=4個HCLK時鐘,TWRPH1=1個HCLK時鐘。

    void nand_init(void)

    {

    //時間參數設為:TACLS=0 TWRPH0=3 TWRPH1=0

    NFCONF = 0x300;

    /* 使能NAND Flash控制器, 初始化ECC, 禁止片選 */

    NFCONT = (1<<4)|(1<<1)|(1<<0);

    /* 復位NAND Flash */

    NFCONT &= ~(1<<1); //發出片選信號

    NFCMMD = 0xFF; //復位命令

    s3c2440_wait_idle();//循環查詢NFSTAT位0,直到它等于1

    NFCONT |= 0x2; //取消片選信號

    }

    讀操作:讀操作也是以頁(512Byte)為單位進行的。在初始上電時,器件進入缺省的“讀方式1模式”。在這一模式下,頁讀操作通過將0x00寫入指令寄存器,接著寫入3個地址(1個列地址和2個行地址)來啟動。一旦頁讀指令被器件鎖存,下面的頁讀操作就不需要再重復寫入頁讀指令了。寫入頁讀指令和地址后,處理器可以通過對信號線R//B的分析來判斷頁讀操作是否完成。如果信號為低電平,表示器件正忙;如果信號為高電平,表示器件內部操作完成,要讀取的數據被送入了數據寄存器。外部控制器可以再以50ns為周期的連續/RE脈沖信號的控制下,從IO口依次讀出數據。連續頁讀操作中,輸出的數據是從指定的列地址開始,直到該頁最后一個列地址的數據為止。

    for(i=start_addr; i < (start_addr + size);)

    {

    NFCMMD = 0; //發出READ0命令

    s3c2440_write_addr(i); //Write Address

    s3c2440_wait_idle(); //循環查詢NFSTAT位0,直到它等于1

    for(j=0; j < NAND_SECTOR_SIZE; j++, i++)

    {

    *buf = (unsigned char)NFDATA;

    buf++;

    }

    }

    缺點:沒有使用ECC校驗和糾錯;沒有使用壞塊檢查;

    3. main 函數

    串口初始化,以便能夠向用戶輸出一些信息;網口初始化,以便能夠從主機下載內核映像;輸出一些菜單,以便用戶選擇執行所需要的功能。比如,用戶可以選擇從串口或網口下載內核映像到RAM中某個地址,然后運行這個內核。關于下載內核映像的實現,在后文會詳細介紹。這里只看當內核映像已經存在于RAM中時,怎樣才能把這個內核啟動起來。

    4. 啟動參數的傳遞

    啟動Linux內核之前需要設置好一些必要的啟動參數,這些參數以TAG列表的形式傳遞給內核。所謂TAG列表,就是多個TAG在內存空間中按順序排列。每個TAG,其實都是一個結構體,每個結構體中又包含了一個頭部結構體和一個內容結構體稱。頭部結構體指明了本TAG的類型、占用空間大小;所謂TAG的類型,就是一個宏定義,用一個確定的整數來識別該標記。內容結構體包含了該TAG的具體內容。

    下面以具體的例子做說明。

    在atag.h中就有:

    #define ATAG_CORE 0x54410001

    #define ATAG_MEM 0x54410002

    #define ATAG_CMDLINE 0x54410009

    #define ATAG_NONE 0x00000000

    這些都是TAG的類型,注意這些整數跟地址沒有關系,只是一個用來識別標記類型的符號而已。

    每個Tag都用結構體表示,包含TagHeader 頭結構體以及隨后的參數值數據結構。如 ATAG_CORE:

    struct Atag {

    struct TagHeader stHdr;

    struct TagCore stCore;

    };

    其中包含兩個結構體。第一個結構體TagHeader含兩個整型變量,用以表示本結構體的長度、標記類型;nSzie賦值為頭部TagHeader和數據TagCore的大小之和,注意是以字(即4字節)為單位;ulTag 就賦值為先前定義的宏ATAG_CORE。第二個結構體就是實際的數據了。

    struct TagHeader {

    UINT32 nSize;

    UINT32 ulTag;

    };

    struct TagCore {

    UINT32 ulFlags;

    UINT32 nPageSize;

    UINT32 ulRootDev;

    };

    由于每個Tag都由一個TagHeader加一個數據部分組成,因此通常的做法是使用Struct和Union相結合來定義:

    struct Atag {

    struct TagHeader stHdr;

    union {

    struct TagCore stCore;

    struct TagMem32 stMem;

    struct TagVideoText stVideoText;

    struct TagRamDisk stRamDisk;

    struct TagInitrd stInitRd;

    struct TagSerialnr stSerialNr;

    struct TagRevision stRevision;

    struct TagVideolfb stVideoLfb;

    struct TagCmdline stCmdLine;

    };

    };

    其中涉及到的所有數據結構均可在 Linux 內核源碼的include/asm/setup.h 頭文件找到,我們把這些定義放在Bootloader的頭文件atag.h中。

    啟動參數標記列表以標記 ATAG_CORE 開始,以標記 ATAG_NONE 結束。每個標記由標識被傳遞參數的 tag_header 結構以及隨后的參數值數據結構來組成。數據結構 tag 和 tag_header 定義在 Linux 內核源碼的include/asm/setup.h 頭文件中,在我們的S-Boot中對應的頭文件為 atag.h。

    在嵌入式 Linux 系統中,通常需要由 Boot Loader 設置的常見啟動參數有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。

    向內核傳遞參數的方法,先在內存中某個起始地址開始,連續存放多個Tag, 組成Tag列表。列表中的每個Tag包括頭部TagHeader和數據結構體。按規定,第一個Tag必須是ATAG_CORE, 最末一個Tag必須是ATAG_NONE,而且中間必須包含至少一個ATAG_MEM。 注意的是末尾的ATAG_NONE只包括頭部,沒有數據內容。如圖所示。

    在編程時先定義好起始地址,然后用一個指針,每設置完畢一個Tag的內容就向后移動相應的長度,然后設置下一個Tag內容,以保證各個Tag的連續存放。

    下面具體說明幾個關鍵Tag的數據區域內容的設置。struct TagCore結構體已經在前面列出,它包含三個整型變量,ulFlags一般設為零,nPageSize表示分頁內存管理中每一頁的大小,一般為4096字節,ulRootDev是系統啟動的設備號,設為零即可,因為通常在后面的命令行參數Cmdline中覆蓋這個設置。Struct TagMem用來描述系統的物理內存地址空間,定義如下:

    struct atag_mem {

    UINT32 nSize; /* size of the area */

    UINT32 ulStart; /* physical start address */

    };

    其中nSzie表示內存的總大小,ulStart為內存的起始物理地址,二者結合告訴內核系統可用的物理內存空間是哪些。Struct TagCmdline結構體的定義就更簡單了,只是一個字符數組,初始長度為1,如下所示:

    struct TagCmdline {

    char cCmdLine[1]; /* this is the minimum size */

    };

    實際上命令行參數不可能只有一個字節,我們通常使用strcpy函數把命令行參數拷貝到cCmdLine地址處,在結尾附加一個字符串結束符’/0’,然后用strlen函數獲得cCmdLine數組的實際長度(包括字符串結束符)。常見的命令行參數如:root=/dev/mtdblock2 init=/linuxrc console=ttySAC0,115200 mem=65536。我們知道的是,Bootloader以標記列表的形式向內核傳遞的參數,大概有10種不同類型的Tag,而命令行參數只是其中的一種。其它需要設置的Tag包括ATAG_RAMDISK、ATAG_INITRD等,此處不再詳細介紹。

    在我們的S-Boot中設置了ATAG_CORE,ATAG_MEM,ATAG_CMDLINE,ATAG_NONE 四項。其中CmdLine 使用的是:

    const char *CmdLine = "root=/dev/nfs fsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfs ip=192.168.1.252:192.168.1.249:192.168.1.1:255.255.255.0:hwlee.net:eth0:off console=ttySAC0,115200 init=/linuxrc mem=65536K console=tty1 fbcon=rotate:2";

    這里root=/dev/nfs表示使用NFS做根文件系統,注意并不真的存在/dev/nfs這個設備,它只是一個符號而已,告訴內核使用NFS而不是使用真正的設備做根文件系統。

    nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>]

    nfsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfs是NFS服務器地址及要掛載的目錄。

    ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>

    ip=192.168.1.252:192.168.1.249:192.168.1.1:255.255.255.0:hwlee.net:eth0:off

    只說明一下autoconf,這一個選項指明開發板使用的自動配置IP地址的方法,有時開發板可以設置成通過DHCP或者BOOTP等協議從服務器獲取IP地址。off 或 none 表示不使用自動配置,使用指定的靜態IP地址信息。

    console=ttySAC0,115200 串口控制臺

    console=tty1 fbcon=rotate:2 液晶屏Framebuffer控制臺,如果內核支持,可以在LCD屏幕上顯示Linux內核啟動過程,起點結束后在LCD屏幕上進入Shell控制臺供用戶操作。fbcon=rotate:2表示控制臺旋轉180度,若為1表示旋轉90度,3旋轉270度,0不旋轉。

    4. boot kernel zImage

    zImage 二進制文件包含兩部分內容,起始部分是解壓縮程序,后面是壓縮的內核。解壓縮程序是最先運行的,內核中文件是:arch/arm/boot /compressed/head.S,它負責把壓縮的內核解壓到0x30008000處。因此zImage可以下載到RAM任意位置處,由解壓縮程序負責搬移到正確的運行地址。

    所以 Bootloader啟動Linux內核的方法就是直接跳轉到內核的第一條指令處,也就是跳轉到內存中存放內核映像的開始地址,內核映像具有自解壓功能,會把自己釋放到正確的運行地址。Tag列表怎樣傳給內核呢?使用的方法是把Tag列表的起始地址傳給內核。首先,定義一個指向函數的指針:

    typedef void (*LINUX_KERNEL_ENTRY)(int, int, UINT32);

    LINUX_KERNEL_ENTRY pfExecKernel;

    這樣pfExecKernel就是一個函數指針,函數具有三個整型變量。然后,讓pfExecKernel指向內核映像的起始地址處,這里使用強制類型轉換把地址轉換成函數指針類型:

    pfExecKernel = (LINUX_KERNEL_ENTRY)pKernelStartAddr;

    最后,以三個參數調用pfExecKernel函數:

    pfExecKernel(0, MACH_ID, ATAG_BASE);

    其中第一個參數默認為零,可以不必理會。第二個參數是機器ID號,不同的CPU有不同的號碼與之對應,可以在內核源代碼的linux/arch/arm/tools/mach-types 文件中查到,S3C2440 對應的MACH_ID 為362。第三個參數ATAG_BASE就是上文講到的Tag列表的首地址。

    這個函數調用的作用其實就是設置 r0=0,r1=機器ID,r2=TAG首地址,然后跳到arch/arm/boot/compressed/head.S文件中的第一條指令處。

    既然可以把TAG首地址傳遞給內核,那么TAG LIST就可以放在RAM中的任何位置了,只要不與其它有用內容沖突即可。但是事實卻并不是想象的這樣。實驗發現,第三個參數傳遞進去的TAG首地址似乎沒有起到作用,因為啟動時總是找不到正確的啟動參數。后來發現內核有個默認的TAG首地址0x30000100,它總是到0x30000100去尋找啟動參數,而不理會我們傳進來的第三個參數。所以,S-Boot中把TAG首地址就設置為0x30000100。

    5. 小結

    綜上所述,包含最基本功能的S-Boot運行流程已經很清楚了。下圖對此作了一個總結。

    預約申請免費試聽課

    填寫下面表單即可預約申請免費試聽!怕錢不夠?可就業掙錢后再付學費! 怕學不會?助教全程陪讀,隨時解惑!擔心就業?一地學習,可全國推薦就業!

    上一篇:嵌入式開發工程師進階學習路線解析
    下一篇:初學者角度談談嵌入式開發與學習的一些問題

    裸編程怎么做?裸編程具體做法

    嵌入式編程中你必須知道的小知識

    嵌入式C語言編程小知識總結

    有哪些工具可以讓嵌入式開發事半功倍?

    • 掃碼領取資料

      回復關鍵字:視頻資料

      免費領取 達內課程視頻學習資料

    • 視頻學習QQ群

      添加QQ群:1143617948

      免費領取達內課程視頻學習資料

    Copyright ? 2021 Tedu.cn All Rights Reserved 京ICP備08000853號-56 京公網安備 11010802029508號 達內時代科技集團有限公司 版權所有

    選擇城市和中心
    黑龍江省

    吉林省

    河北省

    湖南省

    貴州省

    云南省

    廣西省

    海南省

    欧美一级高清片,一级欧美免费大片视频,欧美三级在线电影免费 百度 好搜 搜狗
    <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>