uboot啟動流程


1. 鏈接腳本u-boot.lds

指定鏈接的首地址在哪里,哪一行代碼是第一行。所以需要先編譯。
image
打開該源碼,可知u-boot的入口地址是_start;
可以搜索_start. 在文件 arch/arm/lib/vectors.S 中有定義:
image
image
如代碼中定義的,這里面包含復位和中斷向量表的起始地址;
繼續回到u-boot.lds:
image
.text是代碼段,中間有個__image_copy_start,可以通過全局搜索在
image
可以看到,是0x87800000,.vectors也存在這里,它是中斷向量表,和裸機中斷時一樣的;
繼續回到u-boot.lds:
image
對應的map為(復位和初始化CPU,就類似裸機開發必須要存放的兩個,剩下的代碼段就可以隨意存放了):
image
繼續回到u-boot.lds:
image
鏡像拷貝結束,它和image_copy_start是成對的,因為需要重定位,所以要拷貝copy,搜索一下它的位置:
image
由圖中可以看到它的鏡像結束地址,不同的編譯可能最后的低嗎段結束不一樣,這樣就得到代碼的大小了,差不多300多kb
繼續回到u-boot.lds:
image
rel_dyn_start在map中的地址為:
image
(end就不找了),差不多30多k
__end段
image
再找找bss段
image
image
自此,我們每個段的地址都找到了,不同編譯產生的地址是不一樣的,除了開始的87800000

2. u-boot啟動流程

剛才說到了,入口地址是_start,所以匯編從這里分析:
image

2.1 reset函數

上一張圖是vector.S主要定義了一些中斷向量表,我們找rest函數,是在start.s
image
由圖中可以看到,reset是跳轉函數,不用往下搜索了,跳着跳着還是回到下面的函數里了:
image
注:HYP模式是超級監視者模式,比超級管理員模式第一點,主要用來做一些虛擬化擴展
繼續往下看:
image
如果沒有定義這兩個變量,就執行下面語句,#if后面都是注釋的顏色,那肯定沒有定義
image
這個寄存器如圖所示:
image
綜上所述:就是設置偏移為0,重定位向量表,再把中斷向量表遷移到87800000
image
接下來有兩個函數,一個看名字就知道是初始化cp15,比如關閉MMU啥的,這些都比較固定,就不分析了,
一個是cpu_init_crit,最后一個是main
cpu_init_crit是跳到lowlevel_init

2.2 lowlevel_init函數

函數 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定義
image
image
GENERATED_GBL_DATA_SIZE 256
image
image
image
總結:這個函數就是設置sp指針和R9寄存器,保存棧指針
接下來我們來看s_init

2.3 s_init函數

我們知道 lowlevel_init 函數后面會調用 s_init 函數,s_init 函數定義在文件arch/arm/cpu/armv7/mx6/soc.c
image
由圖可知,相當於空函數

2.4 __main函數

image
上面分析可知,該進入main函數了_main 函數定義在文件 arch/arm/lib/crt0.S 中
image
image
我們再看board_init_f_alloc_reserve這個函數,此函數定義在文件common/init/board_init.c 中
image
image
image
image
gd_t 是個結構體,在 include/asm-generic/global_data.h 里面有定義,gd_t里面還有一個bd_t也是非常重要的函數
我們再回到89行crt0.S,board_init_f_init_reserve函數,
image
此時和上面對起來了
image
我們再回到crt0.S,
image
這里借助bd計算出新的gd位置,然后將其保存在r9里面,bd可以直接獲得,而gd緊挨着bd,所以減去gd大小就是gd的新地址
這里面有個問題,就是bd的大小可以直接獲得
image
接下來可以重定位代碼
在整個ddr中,從87800000開始,是uboot的代碼,要將這部分代碼移植到高地址,也就是最下面,這樣把其他的代碼考到87800000上就還是會運行,因為uboot去高地址了
here函數中:
重定位中斷向量表;
調用函數 c_runtime_cpu_setup,此函數定義在文件 arch/arm/cpu/armv7/start.S
image
關閉ICache和DCache
image
這段代碼清除bss段
image
image
由圖可知,board_init_r函數要傳遞兩個參數r0,r1

2.5 board_init_f函數

  • 初始化一系列外設,比如串口、定時器,或者打印一些消息等。
  • 初始化 gd 的各個成員變量,uboot 會將自己重定位到 DRAM 最后面的地址區域,也就是將自己拷貝到 DRAM 最后面的內存區域中。這么做的目的是給 Linux 騰出空間,防止 Linux kernel 覆蓋掉 uboot,將 DRAM 前面的區域完整的空出來。在拷貝之前肯定要給 uboot 各部分分配好內存位置和大小,比如 gd 應該存放到哪個位置,malloc 內存池應該存放到哪個位置等等。這些信息都保存在 gd 的成員變量中,因此要對 gd 的這些成員變量做初始化。最終形成一個完整的內存“分配圖”,在后面重定位 uboot 的時候就會用到這個內存“分配圖”。
    image
    再看一下init_sequence_f
    整理一下
/*****************去掉條件編譯語句后的 init_sequence_f***************/
static init_fnc_t init_sequence_f[] = { 
	setup_mon_len,  //__bss_end-_start是整個代碼的長度, 鏡像大小,大約600-700k,也是要拷貝的的代碼大小
	initf_malloc, // 里面設置了malloc大小0x400
	initf_console_record, //因為uboot沒有定義,返回0
	arch_cpu_init, //初始化架構有關的工作,CPU級別
	initf_dm,  // 驅動初始化
	arch_cpu_init_dm,  //該函數未實現
	mark_bootstage, /* need timer, go after init dm */ 
	board_early_init_f,  //板子一些早期初始化配置,初始化串口IO等
	timer_init, /* initialize timer */ 
	board_postclk_init,  //設置vddsoc電壓
	get_clocks, //獲取時鍾,對於I.MX獲取SD卡外設時鍾
	env_init, /* initialize environment */ 
	init_baud_rate, /* initialze baudrate settings */ 
	serial_init, /* serial communications setup */ 
	console_init_f, /*設置 gd->have_console 為 1,打開控制台,初始化之前是把環境放在緩沖區里,這個為1時正好打印 */ 
	display_options, /* 通過串口輸出一些信息 */ 
	// 需要開啟debug,在configs/mx6ull_evk里面define
	display_text_info, /* ,打印一些文本信息,如果開啟 UBOOT 的 DEBUG 功能的話就
會輸出 text_base、bss_start、bss_end */ 
	print_cpuinfo, /* CPU信息 */ 
	show_board_info,  // 打印板子信息
	INIT_FUNC_WATCHDOG_INIT //初始化看門狗
	INIT_FUNC_WATCHDOG_RESET //復位看門狗
	init_func_i2c,  //初始化i2c
	announce_dram_init,  //輸出字符串“DRAM”
	/* TODO: unify all these dram functions? */
	dram_init, /* 並不是初始化DRAM只是設置這個值為512M ,是通過讀取SD卡的配置信息初始化dram*/ 
	post_init_f, //用來完成測試 
	INIT_FUNC_WATCHDOG_RESET
	testdram, //測試ddr,空函數
	INIT_FUNC_WATCHDOG_RESET
	INIT_FUNC_WATCHDOG_RESET
	/*
	* Now that we have DRAM mapped and working, we can
	* relocate the code and continue running from DRAM.
	*
	* Reserve memory at end of RAM for (top down in that order):
	* - area that won't get touched by U-Boot and Linux (optional)
	* - kernel log buffer
	* - protected RAM
	* - LCD framebuffer
	* - monitor code
	* - board info struct
	*/
	setup_dest_addr, //設置gd->ram_size,gd->ram_top,gd->relocaddr這三個的值
	//gd->ram_size = 0X20000000 //ram 大小為 0X20000000=512MB
	//gd->ram_top = 0XA0000000//ram最高地址為 0X80000000+0X20000000=0XA0000000
	//gd->relocaddr = 0XA0000000 //重定位后最高地址為 0XA0000000,拷貝的地址 
	reserve_round_4k, //gd->relocaddr四字節對齊,本來就4k對齊,所以不需要了
	reserve_mmu, //留出mmu區域(內存映射單元),0x4000,所以要減去
	//gd->arch.tlb_size= 0X4000 //MMU 的 TLB 表大小
	//gd->arch.tlb_addr=0X9FFF0000 //MMU 的 TLB 表起始地址,64KB 對齊以后
	//gd->relocaddr=0X9FFF0000 //relocaddr 地址
	reserve_trace, //留出調試跟蹤的內存,沒有用到
	reserve_uboot, //reserve_uboot, 留出重定位后的 uboot 所占用的內存區域,uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空間以后還要對 gd->relocaddr 做 4K 字節對齊,並且重新設置 gd->start_addr_sp
	//gd->mon_len = 0XA8EF4
	//gd->start_addr_sp = 0X9FF47000
	//gd->relocaddr = 0X9FF47000, 從這開始定死了,把8780000的代碼拷貝到這里,拷貝大小為0xA8EF4,此時定義的是relocaddr,接下來變化的就是start_addr_sp
	
	reserve_malloc, //留出 malloc 區域,調整 gd->start_addr_sp 位置,malloc 區域由宏TOTAL_MALLOC_LEN 定義
	//TOTAL_MALLOC_LEN=0X1002000
	//gd->start_addr_sp=0X9EF45000 //0X9FF47000-16MB-8KB=0X9EF45000
	reserve_board, //留出板子 bd 所占的內存區,bd 是結構體 bd_t,bd_t 大小為80 字節
	//gd->start_addr_sp=0X9EF44FB0
	//gd->bd=0X9EF44FB0
	setup_machine, //setup_machine,設置機器 ID,linux 啟動的時候會和這個機器 ID 匹配,如果匹配的話 linux 就會啟動正常。但是!!I.MX6ULL 不用這種方式了,這是以前老版本的 uboot 和linux 使用的,新版本使用設備樹了,因此此函數無效。
	reserve_global_data,// 保留出 gd_t 的內存區域,gd_t 結構體大小為 248B
	//gd->start_addr_sp=0X9EF44EB8 //0X9EF44FB0-248=0X9EF44EB8
	//gd->new_gd=0X9EF44EB8
	reserve_fdt, //留出設備樹相關的內存區域,I.MX6ULL 的 uboot 沒有用到
	reserve_arch, //空函數
	reserve_stacks,//留出棧空間,先對 gd->start_addr_sp 減去 16,然后做 16 字節對其。如果使能 IRQ 的話還要留出 IRQ 相應的內存,在本 uboot 中並沒有使用到 IRQ
	//gd->start_addr_sp=0X9EF44E90
	setup_dram_config, //函數設置 dram 信息,就是設置 gd->bd->bi_dram[0].start 和gd->bd->bi_dram[0].size,后面會傳遞給 linux 內核,告訴 linux DRAM 的起始地址和大小
	show_dram_config, //顯示dram信息
	display_new_sp, //顯示新的 sp 位置,也就是 gd->start_addr_sp,不過要定義宏 DEBUG
	INIT_FUNC_WATCHDOG_RESET//
	reloc_fdt, //函數用於重定位 fdt,沒有用到
	setup_reloc, //設置 gd 的其他一些成員變量,供后面重定位的時候使用,並且將以前的 gd(R9) 拷貝到 gd->new_gd 處。需要使能 DEBUG 才能看到相應的信息輸出,這里是在new——gd里有,r9此時還是內部ram里面的地址呢
	NULL,
};

最后的內存分布圖:
image

2.6 relocate_code函數

目前分析的uboot代碼還沒有拷貝到gd->relocaddr中,這個函數是拷貝用的
調用這個函數之前,參數r0=0x9ff47000
image
此函數定義在文件 arch/arm/lib/relocate.S 中
image
image
這里面有一個問題:
我原來的函數的地址(沒拷貝之前)在那,有另一個函數會調用它,但是你把它拷貝走了再調用,就沒了,全局變量引用也會出問題。uboot對於這個處理方法,就是位置無關方法,借助.rel.dyn段
image
image
image
image
如上圖可知:
borad_init 調用 rel_test 函數,用到了 bl 指令,而 bl 指令時位置無關指令,bl 指令是相對尋址的(pc+offset),因此 uboot 中函數調用是與絕對位置無關的。
再來看一下函數 rel_test 對於全局變量 rel_a 的調用,第 2 行設置 r3 的值為 pc+12 地址處的值,因為ARM流水線的原因,pc寄存器的值為當前地址+8,因此pc = 0X87804184 + 8 = 0X8780418C, r3=0X8780418C+12=0X87804198,第 7 行就是0X87804198 這個地址,0X87804198 處的值為0X8785DA50。根據第 17 行可知,0X8785DA50 正是變量 rel_a 的地址,最終 r3=0X8785DA50。
image
並沒有直接讀取變量的地址,而是借助了另一個地址,間接讀取,+8+12偏移地址,也就是說這個過程也沒有用到絕對地址;
繼續往下:
image
這個段的作用是,給需要偏移的加上label,判斷是label之后就要偏移了
image
image

2.7 relocate_vectors函數

中斷向量表偏移
image
第 29 行,如果定義了 CONFIG_CPU_V7M 的話就執行第 30~36 行的代碼,這是 Cortex-M內核單片機執行的語句,因此對於 I.MX6ULL 來說是無效的。
第 38 行,如果定義了 CONFIG_HAS_VBAR 的話就執行此語句,這個是向量表偏移,CortexA7是支持向量表偏移的。而且,在.config 里面定義了 CONFIG_HAS_VBAR,因此會執行這個分支。
第 43 行,r0=gd->relocaddr,也就是重定位后 uboot 的首地址,向量表肯定是從這個地址開始存放的。
第 44 行,將 r0 的值寫入到 CP15 的 VBAR 寄存器中,也就是將新的向量表首地址寫入到寄存器 VBAR 中,設置向量表偏移。

2.8 board_init_r函數

第 32.2.5 小節講解了 board_init_f 函數,在此函數里面會調用一系列的函數來初始化一些外設和 gd 的成員變量。但是 board_init_f 並沒有初始化所有的外設,還需要做一些后續工作,這些后續工作就是由函數 board_init_r 來完成的,board_init_r 函數定義在文件common/board_r.c
image

init_fnc_t init_sequence_r[] = {
	initr_trace, //如果定義了宏 CONFIG_TRACE 的話就會調用函數 trace_init,初始化和調試跟蹤有關的內容
	initr_reloc, // 標記重定位完成
	initr_caches, // 初始化cache,並使能cache
	initr_reloc_global_data, // 初始化重定位后的gd相關成員變量
	initr_barrier, // I.MX沒用到
	initr_malloc, // 初始化malloc區域
	initr_console_record, // 初始化控制台的,也沒用到
	bootstage_relocate, // 啟動狀態重定位
	initr_bootstage, // 初始化啟動狀態
	board_init, /* Setup chipselects */ //板級初始化,包括 74XX 芯片,I2C、FEC、USB 和 QSPI 等。這里執行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函數
	stdio_init_tables, //stdio相關初始化
	initr_serial, //初始化串口
	initr_announce, // 與調試有關,通知已在ram運行
	INIT_FUNC_WATCHDOG_RESET// 看門狗
	INIT_FUNC_WATCHDOG_RESET//
	INIT_FUNC_WATCHDOG_RESET//
	power_init_board, // 初始化電源,沒有用到
	initr_flash, // flash初始化,如果有宏定義了才要初始化,沒有定義就不需要了
	INIT_FUNC_WATCHDOG_RESET//
	initr_nand, // 初始化nand
	initr_mmc, // 初始化emmc
	initr_env, // 初始化環境
	INIT_FUNC_WATCHDOG_RESET//
	initr_secondary_cpu, // 初始化其他cpu,單核不需要
	INIT_FUNC_WATCHDOG_RESET// 
	stdio_add_devices, // 輸入輸出設備初始化,如lcd
	initr_jumptable, // 初始化跳表
	console_init_r, /* fully init console as a device *///控 制 台 初 始 化 , 初 始 化 完 成 以 后 此 函 數 會 調 用stdio_print_current_devices 函數來打印出當前的控制台設備
	INIT_FUNC_WATCHDOG_RESET //
	interrupt_init, // 初始化中斷
	initr_enable_interrupts, // 使能中斷
	initr_ethaddr, // 初始化網絡MAC
	board_late_init, //board_late_init 函數,板子后續初始化,此函數定義在文件 mx6ull_alientek_emmc.c中,如果環境變量存儲在 EMMC 或者 SD 卡中的話此函數會調用 board_late_mmc_env_init 函數初始化 EMMC/SD
	INIT_FUNC_WATCHDOG_RESET //
	INIT_FUNC_WATCHDOG_RESET//
	INIT_FUNC_WATCHDOG_RESET//
	initr_net, // 初始化網絡設備
	INIT_FUNC_WATCHDOG_RESET//
	run_main_loop, //  主循環,處理命令
};
	

2.9 run_main_loop函數

uboot 啟動以后會進入 3 秒倒計時,如果在 3 秒倒計時結束之前按下按下回車鍵,那么就會進入 uboot 的命令模式,如果倒計時結束以后都沒有按下回車鍵,那么就會自動啟動 Linux 內核 ,這個功能就是由run_main_loop函數來完成的。run_main_loop函數定義在文件common/board_r.c 中
image
image
image

2.10 cli_loop函數

image
image
image
image
image
image
image
image
image

2.12 cmd_process函數

在學習cmd_process 之前先看一下uboot中命令是如何定義的。uboot使用宏U_BOOT_CMD來定義命令,宏 U_BOOT_CMD 定義在文件 include/command.h 中
image
image
image
image
image
image
image
image
當我們在 uboot 的命令行中輸入“dhcp”這個命令的時候,最終執行的是 do_dhcp 這個函數。總結一下,uboot 中使用 U_BOOT_CMD 來定義一個命令,最終的目的就是為了定義一個cmd_tbl_t 類型的變量,並初始化這個變量的各個成員。uboot 中的每個命令都存在.u_boot_list段中,每個命令都有一個名為 do_xxx(xxx 為具體的命令名)的函數,這個do_xxx 函數就是具體的命令處理函數。
image
image

3. u-boot啟動linux內核函數過程

3.1 images全局變量

image

typedef struct bootm_headers {
	/*
	* Legacy os image header, if it is a multi component image
	* then boot_get_ramdisk() and get_fdt() will attempt to get
	* data from second and third component accordingly.
	*/
	image_header_t *legacy_hdr_os; /* image header pointer */
	image_header_t legacy_hdr_os_copy; /* header copy */
	ulong legacy_hdr_valid;
	......
	#ifndef USE_HOSTCC
	image_info_t os; /* OS 鏡像信息 */
	ulong ep; /* OS 入口點 */

	ulong rd_start, rd_end; /* ramdisk 開始和結束位置 */

	char *ft_addr; /* 設備樹地址 */
	ulong ft_len; /* 設備樹長度 */

	ulong initrd_start; /* initrd 開始位置 */ 
	ulong initrd_end; /* initrd 結束位置 */
	ulong cmdline_start; /* cmdline 開始位置 */
	ulong cmdline_end; /* cmdline 結束位置 */
	bd_t *kbd;
	#endif

	int verify; /* getenv("verify")[0] != 'n' */

	#define BOOTM_STATE_START (0x00000001)
	#define BOOTM_STATE_FINDOS (0x00000002)
	#define BOOTM_STATE_FINDOTHER (0x00000004)
	#define BOOTM_STATE_LOADOS (0x00000008)
	#define BOOTM_STATE_RAMDISK (0x00000010)
	#define BOOTM_STATE_FDT (0x00000020)
	#define BOOTM_STATE_OS_CMDLINE (0x00000040)
	#define BOOTM_STATE_OS_BD_T (0x00000080)
	#define BOOTM_STATE_OS_PREP (0x00000100)
	#define BOOTM_STATE_OS_FAKE_GO (0x00000200)/*'Almost' run the OS*/
	#define BOOTM_STATE_OS_GO (0x00000400)
	int state;

	#ifdef CONFIG_LMB
	struct lmb lmb; /* 內存管理相關,不深入研究 */
	#endif
} bootm_headers_t;

image

3.2 do_bootz函數

bootz 命令的執行函數為 do_bootz,在文件 cmd/bootm.c
image

3.3 bootz_start函數

image
image
先看看bootz_setup:
image
bootm_find_images:
image

3.4 do_bootm_states函數

do_bootz最后調用的就是函數do_bootm_states,而且在bootz_start中也調用了do_bootm_states函數,看來do_bootm_states函數還是個香餑餑。此函數定義在文件common/bootm.c 中
image
image
image
image
image
image
image
image

3.5 bootm_os_get_boot_func函數

do_bootm_states 會調用 bootm_os_get_boot_func 來查找對應系統的啟動函數
image
image
image

3.6 do_bootm_linux函數

經過前面的分析,我們知道了 do_bootm_linux 就是最終啟動 Linux 內核的函數,此函數定義在文件 arch/arm/lib/bootm.c
image
image
第 293 行,變量 machid 保存機器 ID,如果不使用設備樹的話這個機器 ID 會被傳遞給Linux內核,Linux 內核會在自己的機器 ID 列表里面查找是否存在與 uboot 傳遞進來的machid 匹配的項目,如果存在就說明 Linux 內核支持這個機器,那么 Linux 就會啟動!如果使用設備樹的話這個 machid 就無效了,設備樹存有一個“兼容性”這個屬性,Linux 內核會比較“兼容性”屬性的值(字符串)來查看是否支持這個機器。
第 295 行,函數 kernel_entry,看名字“內核_進入”,說明此函數是進入 Linux 內核的,也就是最終的大 boos!!此函數有三個參數:zero,arch,params,第一個參數 zero 同樣為0;第二個參數為機器 ID;第三個參數 ATAGS 或者設備樹(DTB)首地址,ATAGS 是傳統的方法,用於傳遞一些命令行信息啥的,如果使用設備樹的話就要傳遞設備樹(DTB)。 第 299 行,獲取 kernel_entry 函數,函數 kernel_entry 並不是 uboot 定義的,而是 Linux 內核定義的,Linux 內核鏡像文件的第一行代碼就是函數 kernel_entry,而 images->ep 保存着Linux內核鏡像的起始地址,起始地址保存的正是 Linux 內核第一行代碼!
image
第 315~318 行是設置寄存器 r2 的值?為什么要設置 r2 的值呢?Linux 內核一開始是匯編代碼,因此函數 kernel_entry 就是個匯編函數。向匯編函數傳遞參數要使用 r0、r1 和 r2(參數數量不超過 3 個的時候),所以 r2 寄存器就是函數 kernel_entry 的第三個參數。
第 316 行,如果使用設備樹的話,r2 應該是設備樹的起始地址,而設備樹地址保存在 images的 ftd_addr 成員變量中。
第 317 行,如果不使用設備樹的話,r2 應該是 uboot 傳遞給 Linux 的參數起始地址,也就是環境變量 bootargs 的值,
第 328 行,調用 kernel_entry 函數進入 Linux 內核,此行將一去不復返,uboot 的使命也就完成了,它可以安息了!
image


免責聲明!

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



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