Android系統啓動流程 -- bootloader

 

摘要:本文講解Android系統在啓動過程中的關鍵動作,擯棄特定平臺之間的差異,討論共性的部分,至於啓動更加詳細的過程,需要結合代碼分析,這裏給出流程框架,旨在讓大家對開機過程更明瞭。

 

關鍵詞:U-boot、Linux、Android

 

目錄

       第一部分:Bootloader啓動

一、Bootloader的定義和種類

二、Arm特定平臺的Bootloader

三、U-boot啓動流程分析

 

       第二部分:Linux啓動

一、zImage是怎樣煉成的?

二、linux的c啓動階段

 

       第三部分:Android啓動

一、init進程

二、init啓動的各種服務

              三、android啓動圖示

             

       對於Android整個啓動過程來說,基本可以劃分成三個階段:Bootloader引導、Linux kernel啓動、Android啓動。下面分別對每個階段一一展開討論。

 

第一部分:Bootloader啓動

一、             Bootloader的定義和種類

簡單地說,BootLoader是在操作系統運行之前運行的一段程序,它可以將系統的軟硬件

環境帶到一個合適狀態,爲運行操作系統做好準備。這樣描述是比較抽象的,但是它的任務確實不多,終極目標就是把OS拉起來運行。

 

在嵌入式系統世界裏存在各種各樣的Bootloader,種類劃分也有多種方式。除了按照處

理器體系結構不同劃分以外,還有功能複雜程度的不同。

先區分一下Bootloader和Monitor[l1] : 嚴格來說,Bootloader只是引導OS運行起來的代

碼;而Monitor另外還提供了很多的命令行接口,可以進行調試、讀寫內存、燒寫Flash、配置環境變量等。在開發過程中Monitor提供了很好地調試功能,不過在開發結束之後,可以完全將其設置成一個Bootloader。所以習慣上將其叫做Bootloader。

 

Bootloader

Monitor?

描述

X86

ARM

PowerPC

U-boot

通用引導程序

RedBoot[l2] 

基於eCos的引導程序

BLOB[l3] 

(StrongARM構架)LART(主板)等硬件平臺的引導程序

LILO

Linux磁盤引導程序

GRUB

GNU的LILO替代程序

Loadlin

從DOS引導Linux

Vivi

韓國mizi 公司開發的bootloader

       更多bootloader還有:ROLO、Etherboot、ARMboot 、LinuxBIOS等。

      

       對於每種體系結構,都有一系列開放源碼Bootloader可以選用:

       X86:X86的工作站和服務器上一般使用LILO和GRUB。

       ARM:最早有爲ARM720處理器開發板所做的固件,又有了armboot,StrongARM平

臺的blob,還有S3C2410處理器開發板上的vivi等。現在armboot已經併入了U-Boot,所以U-Boot也支持ARM/XSCALE平臺。U-Boot已經成爲ARM平臺事實上的標準Bootloader。

       PowerPC:最早使用於ppcboot,不過現在大多數直接使用U-boot。

       MIPS:最早都是MIPS開發商自己寫的bootloader,不過現在U-boot也支持MIPS架構。

       M68K:Redboot能夠支持m68k系列的系統。

      

二、             Arm特定平臺的bootloader

到目前爲止,我們公司已經做過多個Arm平臺的android方案,包括:marvell(pxa935)、

informax(im9815)、mediatek(mt6516/6517)、broadcom(bcm2157)。由於不同處理器芯片廠商對arm core的封裝差異比較大,所以不同的arm處理器,對於上電引導都是由特定處理器芯片廠商自己開發的程序,這個上電引導程序通常比較簡單,會初始化硬件,提供下載模式等,然後纔會加載通常的bootloader。

下面是幾個arm平臺的bootloader方案:

marvell(pxa935) :                bootROM + OBM [l4] + BLOB

informax(im9815) :             bootROM + barbox + U-boot

mediatek(mt6516/6517) :     bootROM + pre-loader[l5]  + U-boot

broadcom(bcm2157) :          bootROM + boot1/boot2 + U-boot

 

爲了明確U-boot之前的兩個loader的作用,下面以broadcom平臺爲例,看下在上電之

後到U-boot的流程,如圖1.2.1:

                                          圖1.2.1 broadcom平臺上電流程

 

 

三、             U-boot啓動流程分析

最常用的bootloader還是U-boot,可以引導多種操作系統,支持多種架構的CPU。它支持的操作系統有:Linux、NetBSD、VxWorks、QNX、RTEMS、ARTOS、LynxOS等,支持的CPU架構有:ARM、PowerPC、MISP、X86、NIOS、Xscale等。

 

手機系統不像其他的嵌入式系統,它還需要在啓動的過程中關心CP的啓動,這個時候就涉及到CP的image和喚醒時刻,而一般的嵌入式系統的uboot只負責引導OS內核。所以這裏我們也暫不關心CP的啓動,而主要關心AP側。

從上面第二小節中可以看出,bootloader通常都包含有處理器廠商開發的上電引導程序,不過也不是所有的處理都是這樣,比如三星的S3C24X0系列,它的bootROM直接跳到U-boot中執行,首先由bootROM將U-boot的前4KB拷貝到處理器ISRAM,接着在U-boot的前4KB中必須保證要完成的兩項主要工作:初始化DDR,nand和nand控制器,接着將U-boot剩餘的code拷貝到SDRAM中,然後跳到SDRAM的對應地址上去繼續跑U-boot。

所以U-boot[l6] 的啓動過程,大致上可以分成兩個階段:第一階段,彙編代碼;第二階段,c代碼。

      

3.1 第一階段

       U-boot的第一條指令從cpu/arm920t/start.S文件開始,第一階段主要做了如下事情:

      

       1. 設置CPU進入SVC模式(系統管理模式),cpsr[4:0]=0xd3。

       2. 關中斷,INTMSK=0xFFFFFFFF, INTSUBMSK=0x3FF。

       3. 關看門狗,WTCON=0x0。

4. 調用s3c2410_cache_flush_all函數,使TLBS,I、D Cache,WB中數據失效。

5. 時鐘設置CLKDIVN=0x3 , FCLK:HCLK:PCLK = 1:2:4。

6. 讀取mp15的c1寄存器,將最高兩位改成11,表示選擇了異步時鐘模型。

7. 檢查系統的復位狀態,以確定是不是從睡眠喚醒。

8.  ldr r0,_TEXT_BASE

    adr r1,_start

    cmp r0,r1

    blne cpu_init_crit

    根據這幾條語句來判斷系統是從nand啓動的還是直接將程序下載到SDRAM中運行

的,這裏涉及到運行時域[l7] 和位置無關代碼的概念,ldr r0,_TEXT_BASE的作用是將board/nextdvr2410/config.mk文件中定義的TEXT_BASE值(0x33f80000)裝載到r0中,adr r1,_start該指令是條僞指令,在編譯的時候會被轉換成ADD或SUB指令根據當前pc值計算出_start標號的地址,這樣的話就可以知道當前程序在什麼地址運行(位置無關代碼:做成程序的所有指令都是相對尋址的指令,包括跳轉指令等,這樣代碼就可以不在鏈接所指定的地址上運行)。在上電之後,系統從nand啓動,這裏得到r0和r1值是不一樣的,r0=0x33f80000,而r1=0x00000000。所以接下來會執行cpu_init_crit函數。

 

9. cpu_init_crit函數,主要完成了兩個工作:首先使ICache and Dcache,TLBs中早期內容失效,再設置p15 control register c1,關閉MMU,Dcache,但是打開了Icache和Fault checking,(要求mmu和Dcache是必須要關閉的,而Icache可以打開可以關閉);其次調用/board/nextdvr2410/memsetup.S文件中的memsetup函數來建立對SDRAM的訪問時序。

 

10. Relocate函數,加載nand flash中的uboot到SDRAM中,代碼會加載到0x33f80000開始的地址,空間大小是512。

1). ndf2ram函數

a.  設置NFCONF,使能2410的nand 控制器,初始化ECC,disable chip等

b.  enable chip,復位chip,讀nand狀態,判斷是否busy,空閒的話再次disable chip;

c.  爲調用c函數準備堆棧空間,這裏的堆棧是放在uboot代碼在SDRAM空間的最後位置armboot_end開始的128KB地址處(包含3 words for abort-stack,實際的SP位置是128*1024-12B處)。

d.  調用c函數copy_uboot_to_ram():nandll_reset() 設置NFCONF(新增設置了時間參數,其餘設置和前面一樣),復位nand flash;nandll_read_blocks(),傳遞了3個參數給它,0x33f80000,0x0, 9*NAND_BLOCK_SIZE.這裏在讀的過程中檢查每個塊的壞塊標誌,如果是壞塊,則跳過不讀。詳情不敘,請看uboot的註釋。該部分的c代碼在cpu/arm920t/Nand_cp.c文件中

e.  ok_nand_read函數:讀取SDRAM的前4k內容和SRAM的4K內容進行比較,只要出現不一樣的地方就會進入死循環狀態,目的就是爲了確保轉移代碼的正確性。

f.  跳回到調用ndf2ram函數處繼續執行

2). ldr pc, _start_armboot

    _start_armboot: .word start_armboot

    這裏將會進入第二階段的c代碼部分:start_armboot()函數,/lib_arm/board.c。

 

3.2 第二階段

    第二階段從文件/lib_arm/board.c的start_armboot()函數開始。

   

1.     定義一個struct global_data結構體指針gd,struct global_data結構體對象

gd_data,定義一個struct bd_info結構體對象bd_data,定義一個指向函數的二級指針init_fnc_ptr,定義的全局結構體對象都是放在堆棧中的,gd是放在寄存器中的。

2.     gd=&gd_data,gd->bd = &bd_data,並且全部空間清0。

3.     init_fnc_ptr = init_sequence(一個初始化函數指針數組)。將會在接下來的for循環中提取出每一個函數來依次執行完。

init_fnc_t *init_sequence[] = {

    cpu_init,       /* 基本的處理器相關配置 -- cpu/arm920t/cpu.c */

    board_init,        

/* 基本的板級相關配置 -- board/nextdvr2410/nextdvr2410.c */

    interrupt_init,/* 初始化中斷處理 -- cpu/arm920t/interrupt.c */

    env_init,       /* 初始化環境變量 -- common/env_flash.c */

    init_baudrate,  /* 初始化波特率設置 -- lib_arm/board.c */

    serial_init,    /* 串口通訊設置 -- cpu/arm920t/serial.c */

    console_init_f,/* 控制檯初始化階段1 -- common/console.c */

    display_banner,/* 打印u-boot信息 -- lib_arm/board.c */

    dram_init,  /* 配置可用的RAM -- board/nextdvr2410/nextdvr2410.c */

    display_dram_config,/* 顯示RAM的配置大小 -- lib_arm/board.c */

#if defined(CONFIG_VCMA9)

         checkboard,     /* display board info */

#endif

    NULL,

};

cpu_init:根據需要設定IRQ,FIR堆棧。如果使用中斷的話,中斷堆棧就接在後面。

 

board_init:設置LOCKTIME,配置MPLL,UPLL,配置IO ports,設置gd->bd->bi_arch_number(553),gd->bd->bi_boot_params = 0x30000100設置boot參數地址,使能Icache和Dcache。

 

interrupt_init:使用timer 4來作爲系統clock, 即時鐘滴答, 10ms一次,到點就產生一箇中斷,但由於此時中斷還沒打開所以這個中斷不會響應。

 

env_init:該函數主要做關於環境變量的工作,這個環境變量可以不用存放在nor或者nand flash上,直接在內存中生成(default_environment)。不過對於那些掉電需要保存的參數來說,保存在flash上無疑是最可靠的方式。有的uboot還支持冗餘存儲,也就是存兩份做備份。

        在env初始化的時候,是通過env_init—>nandll_read_blocks將位於nand第9

塊上的環境變量(16K)全部讀入到0x33ef0000這個起始地址中來,在接下來將堆空間分配好之後,在函數env_relocate中,通過在堆中獲得一塊區域來存放環境變量,env_ptr指向這塊區域,接下來所謂的重新獲得環境變量無非就是將原來0x33ef0000開始的16K數據拷貝到env_ptr所指的區域中去。這裏分第一次uboot啓動(泛指只要在第一次運行saveenv指令之前所啓動的uboot過程)和保存過環境變量的情況,但實質是一樣的,所不同的是,第一次uboot啓動,nand第9塊區域中的數據肯定不是什麼環境變量,所以這是的crc校驗肯定出錯,所以這時系統使用了默認的環境變量,但是隻要這個默認的環境變量沒有寫到nand中(運行saveenv)的話,uboot的每次啓動都被認爲是第一次啓動。而保存過環境變量之後的話,在執行env_init的時候,就是從nand中讀出了實際存在的環境變量參數,至於修不修改環境變量,保不保存,都沒有上面的那種情況出現了。

 

    init_baudrate:第一次啓動uboot的時候,採用nextdvr2410nand.h中定義的115200默認波特率,後面的啓動如果說在參數裏設置了新的波特率的話就會用新的波特率來初始化。

   

    display_banner:打印uboot的一些信息,版本信息:NC-Boot 1.5 日期-時間 ,coed範圍,bss開始地址,IRQ、FIR堆棧地址。

 

    dram_init: gd->bd->bi_dram[0].start = PHYS_SDRAM_1;

gd->bd->bi_dram[0].size  = PHYS_SDRAM_1_SIZE;設置板級數據中

的SDRAM開始地址和大小

    display_dram_config:打印SDRAM的配置信息,如下:

                    …

RAM Configuration:

Bank#0: 30000000 64 MB

                    Checkboard: NULL

 

4.     配置可用的flash空間,並且打印出相關信息,flash_init()和display_flash_config()。

 

5.     mem_malloc_init()函數,分配堆空間

    CFG_MALLOC_LEN = 16K(CFG_ENV_SIZE)+128K

    mem_malloc_start = _armboot_start(0x33f80000)- CFG_MALLOC_LEN

    mem_malloc_end = _armboot_start(0x33f80000)

 

6.     env_relocate該函數的作用是將0x33ef0000開始16K的環境參數拷貝到堆空間中去。

 

7.    gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr")通過這中方式獲得環境變量列表中的ipaddr參數(開發板ip),獲得環境變量中的MAC地址,設置到gd->bd->bi_enetaddr[reg]中。

8.     devices_init函數,創建了devlist,但是隻有一個串口設備註冊在內。

9.     console_init_r函數:控制檯完全初始化,此後可以使用函數serial_getc和serial_putc或者putc和getc來輸出log。

 

10.  使能中斷,如果有網卡設備,設置網卡MAC和IP地址。

 

11.  main_loop ();定義於common/main.c。到此所有的初始化工作已經完成,main_loop在標準輸入設備中接受命令,然後分析,查找和執行。

去掉所有無關緊要的宏和代碼,main_loop()函數如下:

void main_loop()

{

    static char lastcommand[CFG_CBSIZE] = { 0, };

    int len;

    int rc = 1;

    int flag;

    char *s;

int bootdelay;

 

    s = getenv ("bootdelay");   //自動啓動內核等待延時

bootdelay =

s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;

    

    s = getenv ("bootcmd");  //取得環境中設置的啓動命令行

   

    if (bootdelay >= 0 && s && !abortboot (bootdelay)){

     run_command (s, 0);

//執行啓動命令行,smdk2410.h中沒有定義CONFIG_BOOTCOMMAND,所以沒有命令執行。

    }

   

    for (;;) {

    len = readline(CFG_PROMPT);

//讀取鍵入的命令行到console_buffer

        

         flag = 0;       /* assume no special flags for now */

         if (len > 0)

             strcpy (lastcommand, console_buffer);

//拷貝命令行到lastcommand.

         else if (len == 0)

             flag |= CMD_FLAG_REPEAT;

             if (len == -1)

             puts ("\n");

         else

             rc = run_command (lastcommand, flag); //執行這個命令行。

  

     if (rc <= 0) {

      /* invalid command or not repeatable, forget it */

      lastcommand[0] = 0;

    }

}

12.  在上面的main_loop函數中,通常在開發完成的階段都會設置一個bootcmd的環境

變量,然後將延時bootdelay設置成0,這樣當u-boot跑到這裏的時候就不會因爲用戶按下了任意鍵就進入了命令行模式,可以直接運行bootcmd的命令來直接加載kernel的Image然後移交控制權。如果進入了命令行模式,我們也可以手動輸入命令來啓動系統,輸入的命令也是基本和bootcmd一樣。

 

不過值得一提的是,從這裏開始到引導內核的函數do_bootimg_linux()之前,不同

廠商之間做的都和原始的U-boot代碼差別挺大,不過萬變不離其宗,都是加載各種各樣的Image到SDRAM中,不過關於CP部分的Image有的廠商是在這裏加載,有的是kernel起來後來有kernel來加載,不過都需要加載的Image就是linux kernel的Image。爲了方便,只討論加載kernel Image的情況。

   

    在繼續往下之前,有必要提一下幾種不同格式linux kernel編譯之後所產生的鏡像文件,包括其各種頭和ramdisk的混合,容易讓人迷糊。

    ramdisk是linux內核啓動過程中需要使用的一種臨時文件系統,它要麼單獨編譯成ramdisk.img(也有叫initrd或者initramfs),要麼編譯進內核。

    Linux編譯之後最終會產生zImage文件,不過呢,爲了迎合U-boot的要求,所以也有專門爲U-boot的引導做一個uImage,這個只是加了一個U-boot中定義的一個head而已,用於U-boot中檢查,當然前面的ramdisk.img也是需要加這個頭的,頭裏面有這個Image的魔數,大小,類型等信息。現在的android中的u-boot也有要求加頭的,他對U-boot進行了改進和優化,做成了自己的一套檢查機制,所以現在android編譯出來linux部分的Image的名字叫boot.img。

    這個boot.img是zImage和ramdisk.img合成之後的,而且還加了專門的頭,這個head和U-boot原始的不一樣,具體的源碼路徑可以參考:system/core/mkbootimg/。

/*

** +-----------------+

** | boot header     | 1 page

** +-----------------+

** | kernel          | n pages 

** +-----------------+

** | ramdisk         | m pages 

** +-----------------+

** | second stage    | o pages

** +-----------------+

**

** n = (kernel_size + page_size - 1) / page_size

** m = (ramdisk_size + page_size - 1) / page_size

** o = (second_size + page_size - 1) / page_size

*/

Android就沒有在ramdisk和zImage上單獨重複加頭了,不過近期做的mtk的平臺,他們有點怪,除了上面的額外信息之外,還在這二者上單獨加了標誌字符串,ROOTFS和KERNEL。

   

    瞭解了上面這些內容之後,對於從nand上加載uImage或者boot.img,都需要經過分離head進行檢查,ok之後纔會真正地將數據導入SDRAM。另外別忘了的是,如果ramdisk.img是單獨的,那麼在加載linux kernel的鏡像的時候也需要將其加載進SDRAM,如果是編譯到內核了,那就不用了。

   

    通常我們的uboot起來之後,我們會運行下面的命令之一來啓動內核

tftp 0x30800000 uImage;bootm (地址可選)

或者

nand read 0x30800000 0x40000 0x200000 ; bootm

   

    例如informax的平臺u-boot的bootcmd是:

    #define BOOTCMD

"mcu_clk 260;a7vector_SDRAM;dsp_clk 130;nand read 0x46000000 0x200000 0x400000;boot_from_flash boot"

很明顯,原始U-boot中沒有boot_from_flash命令,是經過他們改造過的。不過功能基本一樣。所以還是以bootm來引導uImage爲例來討論。

 

        bootm命令位於cmd_bootm.c文件中:

        U_BOOT_CMD(

            bootm,  CFG_MAXARGS,    1,  do_bootm,

            "bootm   - boot application image from memory\n",

            "[addr [arg ...]]\n    - boot application image stored in memory\n"

            "        passing arguments 'arg ...'; when booting a Linux kernel,\n"

            "        'arg' can be the address of an initrd image\n"

);

在將nand上0x40000開始的2MB數據拷貝到SDRAM的0x30800000之後,就開始執行bootm命令,其所做的工作大致如下:

12.1如果bootm命令沒有帶地址參數,將會採用默認地址0x30800000,帶地址則保存下這個參數地址。

12.2 從SDRAM的0x30800000開始拷貝64字節到一個dead結構體中進行crc32校驗,校驗ok之後將會調用調用函數print_image_hdr()打印出如下信息:

Image Name:   Linux-2.6.8-rc2-nc-v1

Created:      2010-05-04   4:14:19 UTC

Image Type:   ARM Linux Kernel Image (uncompressed)

Data Size:    1054712 Bytes =  1 MB

Load Address: 30008000

Entry Point:  30008000

 

12.3 跳過64字節的head,開始校驗kernel的Image數據,校驗碼ok之後會打印:Verifying Checksum ... OK

12.4覈對cpu類型

   

12.5 檢查Image的類型

   

12.6 禁止中斷,檢查內核的壓縮類型,這裏不是指的image和zImage的區別,而是有沒有在這基礎上進行ZIP或ZIP2的壓縮。通常這裏是沒有這樣的壓縮的。所以接下來將0x30800000+64B開始的zImage數據搬運到ih_load(0x30008000)處,這個數據就是kernel的Image數據。

 

12.7 根據head中OS的類型,如果是linux,head中類型值就是IH_OS_LINUX,所以接下來會執行u-boot到kernel的過渡程序。

do_bootm_linux (cmdtp, flag, argc, argv, addr, len_ptr, verify);

 

12.8定義thekernel函數指針,獲取bootargs參數給commandline指針。

12.9 theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep),將內核的入口地址賦給thekernel函數指針。

12.10將傳遞給內核的參數放在0x30000100處,以tag的方式存放,主要放置了memery和cmdline的參數。

12.11關中斷,關閉IDCache,同時使ID Cache數據失效。

12.12再次獲取bi_arch_number參數爲553。

12.13 theKernel (0, bd->bi_arch_number, bd->bi_boot_params)進入內核,第一個參數必須爲0,第二個參數爲機器類型553,第三個參數爲傳遞給內核參數的其實地址0x30000100。

 

總結下,U-Boot調用內核之前,下面的條件必須滿足:

a.  R0=0,R1爲機器類型ID,參考linux/arch/arm/tools/mach-types,R2爲啓動參數tag列表在RAM中的基地址。

b.  CPU的工作模式必須爲SVC模式,必須禁止中斷(IRQS和FIRS)。

c.  數據cache和MMU必須關閉,指令cache可以打開也可以關閉。

 

這裏移交控制權之後,u-boot的使命就算是完成了。說起來U-boot命運挺悲慘的,因爲它重要而卻最不受內核待見。接下來內核的啓動更加複雜。

 

 


 [l1]這個詞經常在老外的相關博客或者bootloader代碼中出現。

 [l2]Redboot支持的處理器構架有ARM,MIPS,MN10300,PowerPC,v850,x86等。

 [l3]Blob提供兩種工作模式:啓動加載模式和下載模式,會在進入加載模式之前等待數秒,如果用戶有任意鍵按下,那麼blob將進入下載模式,否則繼續啓動引導linux。

 [l4]OEM Boot Module(OBM),bootROM執行完cpu特定初始化後,拷貝部分OBM到ISRAM內將控制權轉給OBM,OBM的前面部分主要完成DDR和EMPI的初始化,之後將自己整個拷貝進DDR,然後轉到DDR來執行,接着拷貝AP和CP的Image到DDR,此後OBM復位CP,CP和AP開始同時啓動,AP側OBM執行完後將控制權交給BLOB。

 [l5]在ISRAM內執行,barbox也是。

 [l6]注:下面的分析基於s3c2410,arm920t的arm core爲例。

 [l7]Image的運行空間

 

參考網址:

1.        http://blog.csdn.net/cuijianzhongswust/article/details/6612624

2.        http://blog.csdn.net/sustzombie/article/details/6659622

3.        http://www.docin.com/p-191202348.html

4.        http://blog.csdn.net/maxleng/article/details/5508372

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章