原來Uboot是這樣啟動(dòng)的!
同大多數(shù)的Bootloader一樣,uboot的啟動(dòng)過程也分為BL1、BL2兩個(gè)階段,分別對(duì)應(yīng)著SPL和Uboot。
-
SPL(BL1階段):負(fù)責(zé)開發(fā)板的基礎(chǔ)配置和設(shè)備初始化,并且搬運(yùn)Uboot到內(nèi)存中,由匯編代碼和少量的C語言實(shí)現(xiàn)
-
Uboot(BL2階段):主要負(fù)責(zé)初始化外部設(shè)備,引導(dǎo)Kernel啟動(dòng),由純C語言實(shí)現(xiàn)。
我們這篇文章,主要介紹Uboot(BL2階段)的啟動(dòng)流程,BL1階段啟動(dòng)流程的詳細(xì)分析,可以見我的后續(xù)文章。想要深入了解的,可以好好研究下!
一程序執(zhí)行流程圖
我們先總體來看一下Uboot的執(zhí)行步驟,這里以EMMC作為啟動(dòng)介質(zhì),進(jìn)行分析!
無論是哪種啟動(dòng)介質(zhì),基本流程都相似,我們這就往下看!
打開圖片,結(jié)合文檔、圖片、代碼進(jìn)行理解!
二u-boot.lds——Uboot的入口函數(shù)
u-boot.lds:是uboot工程的鏈接腳本文件,對(duì)于工程的編譯和鏈接有非常重要的作用,決定了uboot的組裝,并且u-boot.lds鏈接文件中的ENTRY(_start)指定了uboot程序的入口地址。
如果不知道u-boot.lds放到在哪里,可以通過find -name u-boot.lds查找,根目錄要進(jìn)入到uboot的源碼的位置哦!
如果查找結(jié)果有很多,結(jié)合自己的板子信息,確定自己使用的u-boot.lds。
當(dāng)然,準(zhǔn)確的方法是查看Makefile文件,分析出來u-boot.lds所生成的位置。
在u-boot.lds的文件中,可以看到.text段,存放的就是執(zhí)行的文本段。截取部分代碼段如下:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000; @起始地址 . = ALIGN(4); @四字節(jié)對(duì)齊 .text : { *(.__image_copy_start) @映像文件復(fù)制起始地址 *(.vectors) @異常向量表 arch/arm/cpu/armv7/start.o (.text*) @啟動(dòng)函數(shù) } ...... }
-
ENTRY(_start):程序的入口函數(shù),_start在arch/arm/lib/vectors.S中定義.globl _start
-
SECTIONS定義了段,包括text文本段、data數(shù)據(jù)段、bss段等。
-
__image_copy_start在System.map和u-boot.map中均有定義
-
arch/arm/cpu/armv7/start.o對(duì)應(yīng)文件arch/arm/cpu/armv7/start.S,該文件中定義了main函數(shù)的入口。
Tip:上面只進(jìn)行大概分析,有匯編經(jīng)驗(yàn)的朋友,可以詳細(xì)進(jìn)行分析!
三board_init_f——板級(jí)前置初始化
跟隨上文的程序執(zhí)行流程圖,我們看board_init_f這個(gè)函數(shù)。其位于common/board_f.c。
void board_init_f(ulong boot_flags) { gd->flags = boot_flags; gd->have_console = 0; if (initcall_run_list(init_sequence_f)) hang(); } static const init_fnc_t init_sequence_f[] = { setup_mon_len, ... log_init, arch_cpu_init, /* basic arch cpu dependent setup */ env_init, /* initialize environment */ ... reloc_fdt, reloc_bootstage, reloc_bloblist, setup_reloc, ... }
board_init_f(),其最核心的內(nèi)容就是調(diào)用了init_sequence_f初始化序列,進(jìn)行了一系列初始化的工作。
主要包括:串口、定時(shí)器、設(shè)備樹、DM驅(qū)動(dòng)模型等,另外還包括global_data結(jié)構(gòu)體相關(guān)對(duì)象的變量。
詳細(xì)分析,可以看文末的參考文章[1]
我們需要注意的一點(diǎn)就是,在初始化隊(duì)列末尾,執(zhí)行了幾個(gè)reloc_xxx的函數(shù),這幾個(gè)函數(shù)實(shí)現(xiàn)了Uboot的重定向功能。
四relocate_code重定向
4.1為什么需要重定向呢?重定向技術(shù),可以說也算是Uboot的一個(gè)重點(diǎn)了,也就是將uboot自身鏡像拷貝到ddr上的另外一個(gè)位置的動(dòng)作。
一般需要重定向的條件如下:
-
uboot存儲(chǔ)在只讀存儲(chǔ)器上,比如ROM、Nor flash,需要將代碼拷貝到DDR上,才能完整運(yùn)行Uboot。
-
為Kernel騰空間,Kernel一般會(huì)放在DDR的地段地址上,所以要把Uboot重定向到頂端地址,避免沖突。
Uboot的重定向有如下幾個(gè)步驟:
-
對(duì)relocate進(jìn)行空間劃分
-
計(jì)算uboot代碼空間到relocate的位置的偏移
-
relocate舊的global_data到新的global_data空間上
-
relocateUboot
-
修改relocate后的全局變量的label
-
relocate中斷向量表
運(yùn)行大致流程:
arch/arm/lib/crt0.S文件內(nèi),主要實(shí)現(xiàn)了:
ENTRY(_main) bl board_init_f @@ 在board_init_f里面實(shí)現(xiàn)了 @@ (1)對(duì)relocate進(jìn)行空間規(guī)劃 @@ (2)計(jì)算uboot代碼空間到relocation的位置的偏移 @@ (3)relocate舊的global_data到新的global_data的空間上 ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */ bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ ldr r9, [r9, #GD_BD] /* r9 = gd->bd */ sub r9, r9, #GD_SIZE /* new GD is below bd */ @@ 把新的global_data地址放在r9寄存器中 adr lr, here ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */ add lr, lr, r0 @@ 計(jì)算返回地址在新的uboot空間中的地址。b調(diào)用函數(shù)返回之后,就跳到了新的uboot代碼空間中。 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ @@ 把uboot的新的地址空間放到r0寄存器中,作為relocate_code的參數(shù) b relocate_code @@ 跳轉(zhuǎn)到relocate_code中,在這里面實(shí)現(xiàn)了 @@ (1)relocate舊的uboot代碼空間到新的空間上去 @@ (2)修改relocate之后全局變量的label @@ 注意,由于上述已經(jīng)把lr寄存器重定義到uboot新的代碼空間中了,所以返回之后,就已經(jīng)跳到了新的代碼空間了!?。。。?! bl relocate_vectors @@ relocate中斷向量表
-
setup_reloc——重定向地址查看(仿真有關(guān))
在這里我們說明一下board_init_f里面的setup_reloc初始化函數(shù)
static int setup_reloc(void) { if (gd->flags & GD_FLG_SKIP_RELOC) { debug("Skipping relocation due to flag\n"); return 0; } #ifdef CONFIG_SYS_TEXT_BASE #ifdef ARM gd->reloc_off = gd->relocaddr - (unsigned long)__image_copy_start; #elif defined(CONFIG_M68K) /* * On all ColdFire arch cpu, monitor code starts always * just after the default vector table location, so at 0x400 */ gd->reloc_off = gd->relocaddr - (CONFIG_SYS_TEXT_BASE + 0x400); #elif !defined(CONFIG_SANDBOX) gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE; #endif #endif memcpy(gd->new_gd, (char *)gd, sizeof(gd_t)); debug("Relocation Offset is: %08lx\n", gd->reloc_off); if (is_debug_open()) { printf("Relocating to %08lx, new gd at %08lx, sp at %08lx\n", gd->relocaddr, (ulong)map_to_sysmem(gd->new_gd), gd->start_addr_sp); } return 0; }
由于,Uboot進(jìn)行了重定向,所以按照常規(guī)的地址仿真的話,我們可能訪問到錯(cuò)誤的內(nèi)存空間,通過setup_reloc的Relocating to %08lx打印,我們可以得到重定向后的地址,方便我們仿真。
Uboot的重定向也有相當(dāng)大的一部分知識(shí)點(diǎn),上面也僅僅是簡(jiǎn)單介紹了relocate的基本步驟和流程,后續(xù)看大家需要,如果大家想了解,我再補(bǔ)上這一部分。
總之,Uboot重定向之后,把Uboot整體搬運(yùn)到了高端內(nèi)存區(qū),為Kernel的加載提供空間,避免內(nèi)存踐踏。
五board_init_r——板級(jí)后置初始化
我們接著跟著流程圖往下看,重定向之后,Uboot運(yùn)行于新的地址空間,接著我們執(zhí)行board_init_r,主要作為Uboot運(yùn)行的最后初始化步驟。
board_init_r這個(gè)函數(shù),同樣位于common/board_f.c,主要用于初始化各類外設(shè)信息
void board_init_r(gd_t *new_gd, ulong dest_addr) { if (initcall_run_list(init_sequence_r)) hang(); /* NOTREACHED - run_main_loop() does not return */ hang(); } static init_fnc_t init_sequence_r[] = { initr_reloc, initr_reloc_global_data, board_init, /* Setup chipselects */ initr_dm, initr_mmc, ... run_main_loop }
與board_init_f相同,同樣有一個(gè)init_sequence_r初始化列表,包括:initr_dmDM模型初始化,initr_mmcMMC驅(qū)動(dòng)初始化,等等。
最終,uboot就運(yùn)行到了run_main_loop,進(jìn)而執(zhí)行main_loop這個(gè)函數(shù)。
六main_loop——Uboot主循環(huán)
該函數(shù)為Uboot的最終執(zhí)行函數(shù),無論是加載kernel還是uboot的命令行體系,均由此實(shí)現(xiàn)。
void main_loop(void) { const char *s; bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop"); if (IS_ENABLED(CONFIG_VERSION_VARIABLE)) env_set("ver", version_string); /* set version variable */ cli_init(); if (IS_ENABLED(CONFIG_USE_PREBOOT)) run_preboot_environment_command(); if (IS_ENABLED(CONFIG_UPDATE_TFTP)) update_tftp(0UL, NULL, NULL); s = bootdelay_process(); if (cli_process_fdt(&s)) cli_secure_boot_cmd(s); autoboot_command(s); cli_loop(); panic("No CLI available"); }
env_set:設(shè)置環(huán)境變量,兩個(gè)參數(shù)分別為name和value
cli_init:用于初始化hash shell的一些變量
run_preboot_environment_command:執(zhí)行預(yù)定義的環(huán)境變量的命令
bootdelay_process:加載延時(shí)處理,一般用于Uboot啟動(dòng)后,有幾秒的倒計(jì)時(shí),用于進(jìn)入命令行模式。
cli_loop:命令行模式,主要作用于Uboot的命令行交互。
記得對(duì)照文章開始的執(zhí)行流程圖哦!
詳細(xì)解釋標(biāo)注于代碼中......
const char *bootdelay_process(void) { char *s; int bootdelay; bootcount_inc(); s = env_get("bootdelay"); //先判斷是否有bootdelay環(huán)境變量,如果沒有,就使用menuconfig中配置的CONFIG_BOOTDELAY時(shí)間 bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY; if (IS_ENABLED(CONFIG_OF_CONTROL)) //是否使用設(shè)備樹進(jìn)行配置 bootdelay = fdtdec_get_config_int(gd->fdt_blob, "bootdelay", bootdelay); debug("### main_loop entered: bootdelay=%d\n\n", bootdelay); if (IS_ENABLED(CONFIG_AUTOBOOT_MENU_SHOW)) bootdelay = menu_show(bootdelay); bootretry_init_cmd_timeout(); #ifdef CONFIG_POST if (gd->flags & GD_FLG_POSTFAIL) { s = env_get("failbootcmd"); } else #endif /* CONFIG_POST */ if (bootcount_error()) s = env_get("altbootcmd"); else s = env_get("bootcmd"); //獲取bootcmd環(huán)境變量,用于后續(xù)的命令執(zhí)行 if (IS_ENABLED(CONFIG_OF_CONTROL)) process_fdt_options(gd->fdt_blob); stored_bootdelay = bootdelay; return s; }6.2autoboot_command
詳細(xì)解釋標(biāo)注于代碼中......
void autoboot_command(const char *s) { debug("### main_loop: bootcmd=\"%s\"\n", s ? s : ""); if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) { bool lock; int prev; lock = IS_ENABLED(CONFIG_AUTOBOOT_KEYED) && !IS_ENABLED(CONFIG_AUTOBOOT_KEYED_CTRLC); if (lock) prev = disable_ctrlc(1); /* disable Ctrl-C checking */ run_command_list(s, -1, 0); if (lock) disable_ctrlc(prev); /* restore Ctrl-C checking */ } if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY) && menukey == AUTOBOOT_MENUKEY) { s = env_get("menucmd"); if (s) run_command_list(s, -1, 0); } }
我們看一下判斷條件stored_bootdelay != -1 && s && !abortboot(stored_bootdelay
-
stored_bootdelay:為環(huán)境變量的值,或者menuconfig設(shè)置的值
-
s:為環(huán)境變量bootcmd的值,為后續(xù)運(yùn)行的指令
-
abortboot(stored_bootdelay):主要用于判斷是否有按鍵按下。如果按下,則不執(zhí)行bootcmd命令,進(jìn)入cli_loop命令行模式;如果不按下,則執(zhí)行bootcmd命令,跳轉(zhuǎn)到加載Linux啟動(dòng)。
void cli_loop(void) { bootstage_mark(BOOTSTAGE_ID_ENTER_CLI_LOOP); #ifdef CONFIG_HUSH_PARSER parse_file_outer(); /* This point is never reached */ for (;;); //死循環(huán) #elif defined(CONFIG_CMDLINE) cli_simple_loop(); #else printf("## U-Boot command line is disabled. Please enable CONFIG_CMDLINE\n"); #endif /*CONFIG_HUSH_PARSER*/ }
如上代碼,程序只執(zhí)行parse_file_outer來處理用戶的輸入、輸出信息。
好啦,基本到這里,我們已經(jīng)對(duì)Uboot的啟動(dòng)流程了然于胸了吧!
當(dāng)然,更深層次的不建議去深入了解,有時(shí)間可以慢慢去研究。
大家有疑問,可以評(píng)論區(qū)交流......