Android系統之LK啟動流程分析(一)


1、前言

 LK是Little Kernel的縮寫,在Qualcomm平台的Android系統中普遍采用LK作為bootloader,它是一個開源項目,LK是整個系統的引導部分,所以不是獨立存在的,但是目前LK只支持arm和x86架構,LK顯著的特點是實現了一個簡單的線程機制(thread),並和Qualcomm的處理器深度定制和使用。

LK的代碼架構如下所示:

app         ---->   應用相關代碼
arch        ---->   處理器架構體系
dev         ---->   和設備相關代碼
include     ---->   相關頭文件
kernel      ---->   lk系統實現相關代碼
lib         ---->   相關庫
make        ---->   Makefile文件
platform    ---->   和平台相關驅動代碼
projects    ---->   Makefile文件
scripts     ---->   jtag腳本文件
target      ---->   和目標相關的驅動代碼

 

2、LK入口確定

 在Qualcomm平台上,編譯lk的命令為:

$ make aboot

編譯完成后,會生成文件emmc_appsboot.mbn的鏡像文件,對於mbn格式文件,為Qualcomm包含了特定運營商定制的一套efs、nv的集成包文件,大致格式類似於elf文件格式,要確定LK的入口,必須要先知道編譯LK的鏈接文件,相關的鏈接文件為:

bootable/bootloader/lk/arch/arm/system-onesegment.ld

鏈接文件內容如下所示:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)

ENTRY(_start)
SECTIONS
{
    . = %MEMBASE%;

    /* text/read-only data */
    .text.boot : { *(.text.boot) }
    .text :    { *(.text .text.* .glue_7* .gnu.linkonce.t.*) } =0x9090

    .interp : { *(.interp) }
    .hash : { *(.hash) }
    .dynsym : { *(.dynsym) }
    .dynstr : { *(.dynstr) }
    .rel.text : { *(.rel.text) *(.rel.gnu.linkonce.t*) }
    .rela.text : { *(.rela.text) *(.rela.gnu.linkonce.t*) }
    .rel.data : { *(.rel.data) *(.rel.gnu.linkonce.d*) }
    .rela.data : { *(.rela.data) *(.rela.gnu.linkonce.d*) }
    .rel.rodata : { *(.rel.rodata) *(.rel.gnu.linkonce.r*) }
    .rela.rodata : { *(.rela.rodata) *(.rela.gnu.linkonce.r*) }
    .rel.got : { *(.rel.got) }
    .rela.got : { *(.rela.got) }
    .rel.ctors : { *(.rel.ctors) }
    .rela.ctors : { *(.rela.ctors) }
    .rel.dtors : { *(.rel.dtors) }
    .rela.dtors : { *(.rela.dtors) }
    .rel.init : { *(.rel.init) }
    .rela.init : { *(.rela.init) }
    .rel.fini : { *(.rel.fini) }
    .rela.fini : { *(.rela.fini) }
    .rel.bss : { *(.rel.bss) }
    .rela.bss : { *(.rela.bss) }
    .rel.plt : { *(.rel.plt) }
    .rela.plt : { *(.rela.plt) }
    .init : { *(.init) } =0x9090
    .plt : { *(.plt) }

    .rodata : { 
        *(.rodata .rodata.* .gnu.linkonce.r.*)
        . = ALIGN(4);
        __commands_start = .;
        KEEP (*(.commands))
        __commands_end = .;
        . = ALIGN(4);
        __apps_start = .;
        KEEP (*(.apps))
        __apps_end = .;
        . = ALIGN(4); 
        __rodata_end = . ;        
    }

    /* writable data  */
    __data_start_rom = .;    /* in one segment binaries, the rom data address is on top of the ram data address */
    __data_start = .;
    .data : SUBALIGN(4) { *(.data .data.* .gnu.linkonce.d.*) }

    __ctor_list = .;
    .ctors : { *(.ctors) }
    __ctor_end = .;
    __dtor_list = .;
    .dtors : { *(.dtors) }
    __dtor_end = .;
    .got : { *(.got.plt) *(.got) }
    .dynamic : { *(.dynamic) }

    __data_end = .;

    /* unintialized data (in same segment as writable data) */
    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss .bss.*) }

    . = ALIGN(4); 
    _end = .;

    . = %MEMBASE% + %MEMSIZE%;
    _end_of_ram = .;

    /* Strip unnecessary stuff */
    /DISCARD/ : { *(.comment .note .eh_frame) }
}

從鏈接文件中,可以確定LK啟動入口為_start函數,該函數的定義在匯編文件:

bootable/bootloader/lk/arch/arm/ctr0.S

該文件的部分代碼如下:

.section ".text.boot"
.globl _start
_start:
    b    reset
    b    arm_undefined
    b    arm_syscall
    b    arm_prefetch_abort
    b    arm_data_abort
    b    arm_reserved
    b    arm_irq
    b    arm_fiq

reset:
    ....
    ....
    ....
    bl    kmain    /* 跳到kmain函數執行 */
    b     .
    ....

_start函數的主要功能是設置中斷向量表、初始化bss段、初始化與處理器架構的相關寄存器、搭建C運行環境等,然后開始運行bl kmain代碼,跳轉到kmain函數處運行,進入的C語言的世界。

 

3、kmain函數分析

 在_start函數的最后,將會調用kmain函數,接下來,對kmain函數的流程進行分析,該函數的定義在文件:

bootable/bootloader/lk/kernel/main.c

函數的定義如下所示:

void kmain(void)
{
    // get us into some sort of thread context
    thread_init_early();    /* thread系統早期初始化 */

    // early arch stuff
    arch_early_init();    /* arch架構相關早期初始化,使能mmu等 */

    // do any super early platform initialization
    platform_early_init();    /* msm平台的早期初始化(board、時鍾和中斷控制器初始化等) */

    // do any super early target initialization
    target_early_init();    /* target早期初始化(主要是debug串口的初始化) */

    dprintf(INFO, "welcome to lk\n\n");
    bs_set_timestamp(BS_BL_START);

    // deal with any static constructors
    dprintf(SPEW, "calling constructors\n");
    call_constructors();

    // bring up the kernel heap
    dprintf(SPEW, "initializing heap\n");
    heap_init();    /* kernel heap初始化 */

    __stack_chk_guard_setup();

    // initialize the threading system
    dprintf(SPEW, "initializing threads\n");
    thread_init();    /* thread系統初始化 */

    // initialize the dpc system
    dprintf(SPEW, "initializing dpc\n");
    dpc_init();    /* dpc系統相關初始化 */

    // initialize kernel timers
    dprintf(SPEW, "initializing timers\n");
    timer_init();    /* kernel timer初始化 */

#if (!ENABLE_NANDWRITE)
    // create a thread to complete system initialization
    dprintf(SPEW, "creating bootstrap completion thread\n"); /* 創建bootstrap2線程完成system初始化 */
    thread_resume(thread_create("bootstrap2", &bootstrap2, NULL, DEFAULT_PRIORITY, DEFAULT_STACK_SIZE));

    // enable interrupts
    exit_critical_section();    /* 使能中斷 */

    // become the idle thread
    thread_become_idle();    /* 將當前線程設置為idle狀態 */
#else
    bootstrap_nandwrite();
#endif
}

對於kmain函數實現的主要功能,在代碼中已經注釋得很清楚了,函數調用后,首先是對早期的thread線程系統進行初始化,接下來則是調用arch_early_init()函數,對CPU處理器架構相關的早期初始化,例如關閉cache,使能mmu等功能,然后開始調用與平台早期初始化的相關函數,對早期需要使用的外設進行初始化,例如中斷控制器、debug串口等外設,接下來,則是調用函數搭建出一個完整的thread線程系統,並對lk中的定時器進行初始化,調用thread_create()函數創建出"bootstrap2"線程,並調用thread_resume()函數,讓該線程在系統中工作,最后,則是設置kmain線程為idle狀態。

對kmain函數調用流程整理如下:

thread_init_early();    /* thread早期初始化 */
arch_early_init();    /* arch架構早期初始化 */
platform_early_init();    /* msm平台的早期初始化(board、時鍾和中斷控制器初始化等) */
target_early_init();    /* target早期初始化(主要是debug串口的初始化) */
bs_set_timestamp(BS_BL_START);
call_constructors();
heap_init();    /* kernel heap初始化 */
__stack_chk_guard_setup();
thread_init();    /* thread線程系統初始化 */
dpc_init();    /* dpc系統初始 */
timer_init();    /* kernel timer初始化 */
thread_create(); /* 創建bootstrap2線程 */
thread_resume();    /* 運行bootstrap2線程 */
exit_critical_section();    /* 使能中斷 */
thread_become_idle();    /* 將當前線程設置為idle狀態 */

使用thread_create()函數創建出"bootstrap2"線程后,並使用thread_resume()啟動該線程后,接下來將會運行bootstrap2()函數,該函數可以看成是lk啟動的第二階段,它將會繼續完成外設的初始化和啟動。

 

4、bootstrap2線程分析

 在kmain函數的最后階段,在thread線程系統搭建完成后,將會運行下面的代碼創建出bootstrap2線程:

thread_resume(thread_create("bootstrap2", &bootstrap2, NULL, DEFAULT_PRIORITY, DEFAULT_STACK_SIZE));

此時,將會跳轉到bootstrap2函數繼續運行,完成整個lk系統啟動,bootstarp2函數的定義在文件:

bootable/bootloader/lk/kernel/main.c

該函數的定義,如下所示:

/* lk啟動的第二階段(bootstrap2) */
static int bootstrap2(void *arg)
{
    dprintf(SPEW, "top of bootstrap2()\n");

    arch_init(); /* arch處理器架構第二階段初始化 */

    // XXX put this somewhere else
#if WITH_LIB_BIO
    bio_init();
#endif
#if WITH_LIB_FS
    fs_init();
#endif

    // initialize the rest of the platform
    dprintf(SPEW, "initializing platform\n");
    platform_init(); /* platform第二階段初始化(msm8909只是簡單輸出debug信息) */

    // initialize the target
    dprintf(SPEW, "initializing target\n");
    target_init();    /* target第二階段初始化,按鍵、分區表等 */

    dprintf(SPEW, "calling apps_init()\n");
    apps_init();    /* 創建多個app線程並運行,aboot_init將加載Linux內核 */

    return 0;
}

在代碼中,比較重要的是target_init()函數和apps_init()函數,target_init()函數將針對不同的硬件平台進行一些外設初始化,例如,按鍵、emmc分區等,apps_init()函數則是將整個lk系統要啟動的app全部進行啟動運行,本質是使用thread_create()函數和thread_resume()函數,創建多個線程並在lk系統中調度線程,比較重要的是aboot_init線程,它將會啟動Linux內核。

 

5、apps_init函數分析

 apps_init()函數的主要功能是將lk系統中的app線程進行創建和調度,其中比較重要的aboot_init線程,它用於啟動Linux內核,apps_init函數的定義在文件:

bootable/bootloader/lk/app/app.c

該函數的定義如下所示:

extern const struct app_descriptor __apps_start;
extern const struct app_descriptor __apps_end;

/* one time setup */
void apps_init(void)
{
    const struct app_descriptor *app;

    /* call all the init routines */
    for (app = &__apps_start; app != &__apps_end; app++) {    /* 遍歷所有apps */
        if (app->init)    /* 判斷app_descriptor結構的init函數是否存在 */
            app->init(app);    /* 如果存在,則調用init函數 */
    }

    /* start any that want to start on boot */
    for (app = &__apps_start; app != &__apps_end; app++) {
        if (app->entry && (app->flags & APP_FLAG_DONT_START_ON_BOOT) == 0) {
            start_app(app);    /* 啟動所有要在lk階段啟動的app */
        }
    }
}

從代碼中知道,apps_init函數使用了兩個for循環,調用了位於__apps_start與__apps_end之間的函數,對於__apps_start和__apps_end需要去相應的ld鏈接文件中去尋找,在上面提到的system-onesegment.ld文件中有:

 __apps_start = .;
KEEP (*(.apps))
 __apps_end = .;
. = ALIGN(4); 

可以知道是,調用了所有放在*.apps段中的函數了,在下面的文件中有和*.apps段的相關宏:

bootable/bootloader/lk/include/app.h

宏APP_START和struct app_descriptor結構體定義如下:

/* each app needs to define one of these to define its startup conditions */
struct app_descriptor {
    const char *name;
    app_init  init;
    app_entry entry;
    unsigned int flags;
};

#define APP_START(appname) struct app_descriptor _app_##appname __SECTION(".apps") = { .name = #appname,
#define APP_END };

因此,可以知道,每個app都有一個app_descriptor結構體進行描述,這些結構體的定義都在.apps段中,接下來,繼續搜索使用APP_START宏添加的結構體和函數有什么:

在文件:

bootable/bootloader/lk/app/aboot/aboot.c

使用了APP_START宏的定義,如下:

APP_START(aboot)
    .init = aboot_init,
APP_END

這就是aboot這個app的定義,aboot_init函數就是要啟動的線程,該線程用來啟動Linux內核,非常重要,其它的app定義類似,就不全都講解了。

 

6、aboot_init函數分析

 對於aboot_init()函數的定義在文件:

bootable/bootloader/lk/app/aboot/aboot.c

函數的內容如下所示:

void aboot_init(const struct app_descriptor *app)
{
    unsigned reboot_mode = 0;
    bool boot_into_fastboot = false;

    /* Setup page size information for nv storage */
    if (target_is_emmc_boot())    /* 判斷目標板是否是emmc啟動 */
    {
        page_size = mmc_page_size(); /* 讀取對應存儲介質的page和block大小*/
        page_mask = page_size - 1;
        mmc_blocksize = mmc_get_device_blocksize();
        mmc_blocksize_mask = mmc_blocksize - 1;
    }
    else
    {
        page_size = flash_page_size();
        page_mask = page_size - 1;
    }

    ASSERT((MEMBASE + MEMSIZE) > MEMBASE);

    read_device_info(&device);    /* 讀取設備的信息 */
    read_allow_oem_unlock(&device);    /* oem解鎖 */

    /* Display splash screen if enabled */    /* 初始化LCD接口並顯示log */
#if DISPLAY_SPLASH_SCREEN
    dprintf(INFO, "Display Init: Start\n");
    target_display_init(device.display_panel);
    dprintf(INFO, "Display Init: Done\n");
#endif

    target_serialno((unsigned char *) sn_buf);
    dprintf(SPEW,"serial number: %s\n", sn_buf);
    memset(display_panel_buf, '\0', MAX_PANEL_BUF_SIZE);

    /*
     * Check power off reason if user force reset,
     * if yes phone will do normal boot.
     */
    if (is_user_force_reset())
        goto normal_boot;

    /* Check if we should do something other than booting up */
    if (keys_get_state(KEY_VOLUMEUP) && keys_get_state(KEY_VOLUMEDOWN)) /* 根據按鍵進入到不同的啟動模式 */
    {
        dprintf(ALWAYS,"dload mode key sequence detected\n");
        if (set_download_mode(EMERGENCY_DLOAD))
        {
            dprintf(CRITICAL, "dload mode not supported by target\n");
        }
        else
        {
            reboot_device(DLOAD);
            dprintf(CRITICAL,"Failed to reboot into dload mode\n");
        }
        boot_into_fastboot = true;
    }
    if (!boot_into_fastboot)
    {
        if (keys_get_state(KEY_HOME) || keys_get_state(KEY_BACK))
            boot_into_recovery = 1;
        if (!boot_into_recovery &&
            (keys_get_state(KEY_BACK) || keys_get_state(KEY_VOLUMEDOWN)))
            boot_into_fastboot = true;
    }
    #if NO_KEYPAD_DRIVER
    if (fastboot_trigger())
        boot_into_fastboot = true;
    #endif

#if USE_PON_REBOOT_REG
    reboot_mode = check_hard_reboot_mode();
#else
    reboot_mode = check_reboot_mode();
#endif
    if (reboot_mode == RECOVERY_MODE)
    {
        boot_into_recovery = 1;
    }
    else if(reboot_mode == FASTBOOT_MODE)
    {
        boot_into_fastboot = true;
    }
    else if(reboot_mode == ALARM_BOOT)
    {
        boot_reason_alarm = true;
        }
#if VERIFIED_BOOT
#if !VBOOT_MOTA
        else if(reboot_mode == DM_VERITY_ENFORCING) {
        device.verity_mode = 1;
        write_device_info(&device);
    }
#if ENABLE_VB_ATTEST
    else if (reboot_mode == DM_VERITY_EIO)
#else
    else if (reboot_mode == DM_VERITY_LOGGING)
#endif
    {
        device.verity_mode = 0;
        write_device_info(&device);
    } else if(reboot_mode == DM_VERITY_KEYSCLEAR) {
        if(send_delete_keys_to_tz())
            ASSERT(0);
    }
#endif
#endif

normal_boot:
    if (!boot_into_fastboot)
    {
        if (target_is_emmc_boot())
        {
            if(emmc_recovery_init())
                dprintf(ALWAYS,"error in emmc_recovery_init\n");
            if(target_use_signed_kernel())
            {
                if((device.is_unlocked) || (device.is_tampered))
                {
                #ifdef TZ_TAMPER_FUSE
                    set_tamper_fuse_cmd();
                #endif
                #if USE_PCOM_SECBOOT
                    set_tamper_flag(device.is_tampered);
                #endif
                }
            }
            boot_linux_from_mmc();    /* 從emmc讀取linux內核鏡像並啟動 */
        }
        else
        {
            recovery_init();
    #if USE_PCOM_SECBOOT
        if((device.is_unlocked) || (device.is_tampered))
            set_tamper_flag(device.is_tampered);
    #endif
            boot_linux_from_flash();
        }
        dprintf(CRITICAL, "ERROR: Could not do normal boot. Reverting "
            "to fastboot mode.\n");
    }

    /* We are here means regular boot did not happen. Start fastboot. */

    /* register aboot specific fastboot commands */
    aboot_fastboot_register_commands();

    /* dump partition table for debug info */
    partition_dump();

    /* initialize and start fastboot */
    fastboot_init(target_get_scratch_address(), target_get_max_flash_size());
#if FBCON_DISPLAY_MSG
    display_fastboot_menu();
#endif
}

aboot_init()函數被調用后,首先是判斷目標板是從emmc還是nand flash啟動,判斷完存儲介質后,讀取相應的頁面和塊大小,讀取設備的信息,然后調用target_display_init()函數將LCD接口進行初始化,並在屏幕上顯示出log圖片,接下來,就是判斷啟動模式,對於emmc存儲介質,則會調用boot_linux_from_mmc()函數,從emmc介質中讀取Linux內核鏡像,並啟動Linux系統,aboot_init()函數最主要的功能就是要啟動Linux內核,在這,只是簡單闡述啟動流程,需要了解更詳細的內容,可以深入源碼分析。

 

7、小結

 本篇文章簡單介紹了Android系統中LK啟動流程,LK是一個輕量級的線程系統,是一個Bootloader,其最主要的目的就是將Linux內核鏡像從emmc或nand flash中加載入RAM中,然后將Linux內核系統啟動起來。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM