http://blog.csdn.net/skyflying2012/article/details/35787971
最近閱讀代碼學習了uboot boot kernel的過程以及uboot如何傳參給kernel,記錄下來,與大家共享:
U-boot版本:2014.4
Kernel版本:3.4.55
一 uboot 如何啟動 kernel
1 do_bootm
uboot下使用bootm命令啟動內核鏡像文件uImage,uImage是在zImage頭添加了64字節的鏡像信息供uboot解析使用,具體這64字節頭的內容,我們在分析bootm命令的時候就會一一說到,那直接來看bootm命令。
在common/cmd_bootm.c中
數組boot_os是bootm最后階段啟動kernel時調用的函數數組,CONFIG_NEEDS_MANUAL_RELOC中的代碼含義是將boot_os函數都進行偏移(uboot啟動中會將整個code拷貝到靠近sdram頂端的位置執行),
但是boot_os函數在uboot relocate時已經都拷貝了,所以感覺沒必要在進行relocate。這個宏因此沒有定義,直接走下面。
新版uboot對於boot kernel實現了一個類似狀態機的機制,將整個過程分成很多個階段,uboot將每個階段稱為subcommand,
核心函數是do_bootm_states,需要執行哪個階段,就在do_bootm_states最后一個參數添加那個宏定義,如: BOOTM_STATE_START
do_bootm_subcommand是按照bootm參數來指定運行某一個階段,也就是某一個subcommand
對於正常的uImage,bootm加tftp的load地址就可以。
2 do_bootm_states
這樣會走到最后函數do_bootm_states,那就來看看核心函數do_bootm_states
參數中需要注意bootm_headers_t *images,這個參數用來存儲由image頭64字節獲取到的的基本信息。由do_bootm傳來的該參數是images,是一個全局的靜態變量。
首先將states存儲在images的state中,因為states中有BOOTM_STATE_START,調用bootm_start.
3 第一階段:bootm_start
獲取verify,bootstage_mark_name標志當前狀態為bootm start(bootstage_mark_name可以用於無串口調試,在其中實現LED控制)。
boot_start_lmb暫時還沒弄明白,以后再搞清楚。
最后修改images.state為bootm start。
bootm_start主要工作是清空images,標志當前狀態為bootm start。
4 第二階段:bootm_find_os
由bootm_start返回后,do_bootm傳了BOOTM_STATE_FINDOS,所以進入函數bootm_find_os
調用boot_get_kernel,函數較長,首先是獲取image的load地址,如果bootm有參數,就是img_addr,之后如下:
首先標志當前狀態,然后調用genimg_get_image,該函數會檢查當前的img_addr是否在sdram中,如果是在flash中,則拷貝到sdram中CONFIG_SYS_LOAD_ADDR處,修改img_addr為該地址。
這里說明我們的image可以在flash中用bootm直接起
map_sysmem為空函數,buf即為img_addr。
首先來說明一下image header的格式,在代碼中由image_header_t代表,如下:
genimg_get_format檢查img header的頭4個字節,代表image的類型,有2種,legacy和FIT,這里使用的legacy,頭4個字節為0x27051956。
image_get_kernel則會來計算header的crc是否正確,然后獲取image的type,根據type來獲取os的len和data起始地址。
最后將hdr的數據拷貝到images的legacy_hdr_os_copy,防止kernel image在解壓是覆蓋掉hdr數據,保存hdr指針到legacy_hdr_os中,置位legacy_hdr_valid。
從boot_get_kernel中返回到bootm_find_os,繼續往下:
根據hdr獲取os的type,comp,os,end,load addr。
獲取os的start。
到這里bootm_find_os就結束了,主要工作是根據image的hdr來做crc,獲取一些基本的os信息到images結構體中。
回到do_bootm_states中接下來調用bootm_find_other,
5 第三階段:bootm_find_other
該函數大體看一下,對於legacy類型的image,獲取查詢是否有ramdisk,此處我們沒有用單獨的ramdisk,ramdisk是直接編譯到kernel image中的。
回到do_bootm_states中接下來會調用bootm_load_os。
6 第四階段:bootm_load_os
static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end, int boot_progress) { image_info_t os = images->os; uint8_t comp = os.comp; 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; __maybe_unused uint unc_len = CONFIG_SYS_BOOTM_LEN; int no_overlap = 0; void *load_buf, *image_buf; #if defined(CONFIG_LZMA) || defined(CONFIG_LZO) int ret; #endif /* defined(CONFIG_LZMA) || defined(CONFIG_LZO) */ const char *type_name = genimg_get_type_name(os.type); load_buf = map_sysmem(load, unc_len); image_buf = map_sysmem(image_start, image_len); switch (comp) { case IH_COMP_NONE: if (load == blob_start || load == image_start) { printf(" XIP %s ... ", type_name); no_overlap = 1; } else { printf(" Loading %s ... ", type_name); memmove_wd(load_buf, image_buf, image_len, CHUNKSZ); } *load_end = load + image_len; break; #ifdef CONFIG_GZIP case IH_COMP_GZIP: printf(" Uncompressing %s ... ", type_name); if (gunzip(load_buf, unc_len, image_buf, &image_len) != 0) { puts("GUNZIP: uncompress, out-of-mem or overwrite " "error - must RESET board to recover\n"); if (boot_progress) bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE); return BOOTM_ERR_RESET; } *load_end = load + image_len; break; #endif /* CONFIG_GZIP */
load_buf是之前find_os是根據hdr獲取的load addr,image_buf是find_os獲取的image的開始地址(去掉64字節頭)。
之后則是根據hdr的comp類型來解壓拷貝image到load addr上。
這里就需要注意,kernel選項的壓縮格式必須在uboot下打開相應的解壓縮支持,或者就不進行壓縮
這里還有一點,load addr與image add是否可以重疊,看代碼感覺是可以重疊的,還需要實際測試一下。
回到do_bootm_states,接下來根據os從boot_os數組中獲取到了相應的os boot func,這里是linux,則是do_bootm_linux。后面代碼如下:
這時do_bootm最后的代碼,如果正常,boot kernel之后就不應該回來了。states中定義了BOOTM_STATE_OS_PREP(對於mips處理器會使用BOOTM_STATE_OS_CMDLINE),調用do_bootm_linux,如下:
do_bootm_linux實現跟do_bootm類似,也是根據flag分階段運行subcommand,這里會調到boot_prep_linux。
7 第五階段:boot_prep_linux
該函數作用是為啟動后的kernel准備參數,這個函數我們在第三部分uboot如何傳參給kernel再仔細分析一下
boot_prep_linux完成返回到do_bootm_states后接下來就是最后一步了。執行boot_selected_os調用do_bootm_linux,flag為BOOTM_STATE_OS_GO,則調用boot_jump_linux
8 第六階段:boot_jump_linux
boot_jump_linux主體函數如上
獲取gd->bd->bi_arch_number為machid,如果有env則用env的machid,kernel_entry為之前由hdr獲取的ep,也就是內核的入口地址。
fake為0,直接調用kernel_entry,參數1為0,參數2為machid,參數3為bi_boot_params。
這之后就進入了kernel的執行流程啟動,就不會再回到uboot
這整個boot過程中bootm_images_t一直作為對image信息的全局存儲結構。
三 uboot如何傳參給kernel
uboot下的傳參機制就直接來分析boot_prep_linux函數就可以了,如下:
static void boot_prep_linux(bootm_headers_t *images) { char *commandline = getenv("bootargs"); if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len) { #ifdef CONFIG_OF_LIBFDT debug("using: FDT\n"); if (image_setup_linux(images)) { printf("FDT creation failed! hanging..."); hang(); } #endif } else if (BOOTM_ENABLE_TAGS) { debug("using: ATAGS\n"); setup_start_tag(gd->bd); if (BOOTM_ENABLE_SERIAL_TAG) setup_serial_tag(¶ms); if (BOOTM_ENABLE_CMDLINE_TAG) setup_commandline_tag(gd->bd, commandline); if (BOOTM_ENABLE_REVISION_TAG) setup_revision_tag(¶ms); if (BOOTM_ENABLE_MEMORY_TAGS) setup_memory_tags(gd->bd); if (BOOTM_ENABLE_INITRD_TAG) { if (images->rd_start && images->rd_end) { setup_initrd_tag(gd->bd, images->rd_start,images->rd_end); } } setup_board_tags(¶ms); setup_end_tag(gd->bd); } else { printf("FDT and ATAGS support not compiled in - hanging\n"); hang(); } do_nonsec_virt_switch(); }
首先獲取出環境變量bootargs,這就是要傳遞給kernel的參數。
在配置文件中定義了CONFIG_CMDLINE_TAG以及CONFIG_SETUP_MEMORY_TAGS,根據arch/arm/include/asm/bootm.h,則會定義BOOTM_ENABLE_TAGS,首先調用setup_start_tag,如下:
params是一個全局靜態變量用來存儲要傳給kernel的參數,這里bd->bi_boot_params的值賦給params,因此bi_boot_params需要進行初始化,從而將params放在一個合理的內存區域。
這里params為struct tag的結構,如下:
tag包括hdr和各種類型的tag_*,hdr來標志當前的tag是哪種類型的tag。
setup_start_tag是初始化了第一個tag,是tag_core類型的tag。最后調用tag_next跳到第一個tag末尾,為下一個tag做准備。
回到boot_prep_linux,接下來調用setup_commandline_tag,如下:
該函數設置第二個tag的hdr.tag為ATAG_CMDLINE,然后拷貝cmdline到tags的cmdline結構體中,跳到下一個tag。
回到boot_prep_linux,調用setup_memory_tag,如下:
static void setup_memory_tags(bd_t *bd) { int i; for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) { params->hdr.tag = ATAG_MEM; params->hdr.size = tag_size (tag_mem32); params->u.mem.start = bd->bi_dram[i].start; params->u.mem.size = bd->bi_dram[i].size; params = tag_next (params); } }
過程類似,將第三個tag設為ATAG_MEM,將mem的start,size保存在此處,如果有多片ram(CONFIG_NR_DRAM_BANKS > 1),則將下一個tag保存下一片ram的信息,依次類推。
回到boot_prep_linux中,調用setup_board_tags,這個函數是__weak屬性,我們可以在自己的板級文件中去實現來保存跟板子相關的參數,如果沒有實現,則是空函數。
最后調用setup_end_tags,如下:
最后將最末尾的tag設置為ATAG_NONE,標志tag結束。
這樣整個參數的准備就結束了,最后在調用boot_jump_linux時會將tags的首地址也就是bi_boot_params傳給kernel,供kernel來解析這些tag,kernel如何解析看第四部分kenrel如何找到並解析參數
總結一下,uboot將參數以tag數組的形式布局在內存的某一個地址,每個tag代表一種類型的參數,首尾tag標志開始和結束,首地址傳給kernel供其解析。
四 kernel如何找到並解析參數
uboot在調用boot_jump_linux時最后kernel_entry(0, machid, r2);
按照二進制規范eabi,machid存在寄存器r1,r2即tag的首地址存在寄存器r2.
查看kernel的入口函數,在arch/arm/kernel/head.S,中可以看到如下一段匯編:
可以看出kernel剛啟動會調用__vet_atags來處理uboot傳來的參數,如下:
主要是對tag進行了一個簡單的校驗,查看tag頭4個字節(tag_core的size)和第二個4字節(tag_core的type)。
之后對參數的真正分析處理是在start_kernel的setup_arch中,在arch/arm/kernel/setup.c中,如下:
關鍵函數是setup_machine_tags,如下:
首先回去獲取tags的首地址,如果收個tag是ATAG_CORE類型,則會調用save_atags拷貝一份tags,最后調用parse_tags來分析這個tag list,如下:
遍歷tags list,找到在tagstable中匹配的處理函數(hdr.tag一致),來處理響應的tag。
這個tagtable的處理函數是在調用__tagtable來注冊的,如下:
看這個對cmdline類型的tag的處理,就是將tag中的cmdline拷貝到default_command_line中。還有其他如mem類型的參數也會注冊這個處理函數,來匹配處理響應的tag。這里就先以cmdline的tag為例。
這樣遍歷並處理完tags list之后回到setup_machine_tags,將from(即default_command_line)中的cmdline拷貝到boot_command_line,
最后返回到setup_arch中,
將boot_command_line拷貝到start_kernel給setup_arch的cmdline_p中,這里中間拷貝的boot_command_line是給parse_early_param來做一個早期的參數分析的。
到這里kernel就完全接收並分析完成了uboot傳過來的args。
簡單的講,uboot利用函數指針及傳參規范,它將
l R0: 0x0
l R1: 機器號
l R2: 參數地址
三個參數傳遞給內核。
其中,R2寄存器傳遞的是一個指針,這個指針指向一個TAG區域。
UBOOT和Linux內核之間正是通過這個擴展了的TAG區域來進行復雜參數的傳遞,如 command line,文件系統信息等等,用戶也可以擴展這個TAG來進行更多參數的傳遞。TAG區域的首地址,正是R2的值。