1. 鏈接腳本u-boot.lds
指定鏈接的首地址在哪里,哪一行代碼是第一行。所以需要先編譯。

打開該源碼,可知u-boot的入口地址是_start;
可以搜索_start. 在文件 arch/arm/lib/vectors.S 中有定義:


如代碼中定義的,這里面包含復位和中斷向量表的起始地址;
繼續回到u-boot.lds:

.text是代碼段,中間有個__image_copy_start,可以通過全局搜索在

可以看到,是0x87800000,.vectors也存在這里,它是中斷向量表,和裸機中斷時一樣的;
繼續回到u-boot.lds:

對應的map為(復位和初始化CPU,就類似裸機開發必須要存放的兩個,剩下的代碼段就可以隨意存放了):

繼續回到u-boot.lds:

鏡像拷貝結束,它和image_copy_start是成對的,因為需要重定位,所以要拷貝copy,搜索一下它的位置:

由圖中可以看到它的鏡像結束地址,不同的編譯可能最后的低嗎段結束不一樣,這樣就得到代碼的大小了,差不多300多kb
繼續回到u-boot.lds:

rel_dyn_start在map中的地址為:

(end就不找了),差不多30多k
__end段

再找找bss段


自此,我們每個段的地址都找到了,不同編譯產生的地址是不一樣的,除了開始的87800000
2. u-boot啟動流程
剛才說到了,入口地址是_start,所以匯編從這里分析:

2.1 reset函數
上一張圖是vector.S主要定義了一些中斷向量表,我們找rest函數,是在start.s

由圖中可以看到,reset是跳轉函數,不用往下搜索了,跳着跳着還是回到下面的函數里了:

注:HYP模式是超級監視者模式,比超級管理員模式第一點,主要用來做一些虛擬化擴展
繼續往下看:

如果沒有定義這兩個變量,就執行下面語句,#if后面都是注釋的顏色,那肯定沒有定義

這個寄存器如圖所示:

綜上所述:就是設置偏移為0,重定位向量表,再把中斷向量表遷移到87800000

接下來有兩個函數,一個看名字就知道是初始化cp15,比如關閉MMU啥的,這些都比較固定,就不分析了,
一個是cpu_init_crit,最后一個是main
cpu_init_crit是跳到lowlevel_init
2.2 lowlevel_init函數
函數 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定義


GENERATED_GBL_DATA_SIZE 256



總結:這個函數就是設置sp指針和R9寄存器,保存棧指針
接下來我們來看s_init
2.3 s_init函數
我們知道 lowlevel_init 函數后面會調用 s_init 函數,s_init 函數定義在文件arch/arm/cpu/armv7/mx6/soc.c

由圖可知,相當於空函數
2.4 __main函數

上面分析可知,該進入main函數了_main 函數定義在文件 arch/arm/lib/crt0.S 中


我們再看board_init_f_alloc_reserve這個函數,此函數定義在文件common/init/board_init.c 中




gd_t 是個結構體,在 include/asm-generic/global_data.h 里面有定義,gd_t里面還有一個bd_t也是非常重要的函數
我們再回到89行crt0.S,board_init_f_init_reserve函數,

此時和上面對起來了

我們再回到crt0.S,

這里借助bd計算出新的gd位置,然后將其保存在r9里面,bd可以直接獲得,而gd緊挨着bd,所以減去gd大小就是gd的新地址
這里面有個問題,就是bd的大小可以直接獲得

接下來可以重定位代碼
在整個ddr中,從87800000開始,是uboot的代碼,要將這部分代碼移植到高地址,也就是最下面,這樣把其他的代碼考到87800000上就還是會運行,因為uboot去高地址了
here函數中:
重定位中斷向量表;
調用函數 c_runtime_cpu_setup,此函數定義在文件 arch/arm/cpu/armv7/start.S

關閉ICache和DCache

這段代碼清除bss段


由圖可知,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 的時候就會用到這個內存“分配圖”。

再看一下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,
};
最后的內存分布圖:

2.6 relocate_code函數
目前分析的uboot代碼還沒有拷貝到gd->relocaddr中,這個函數是拷貝用的
調用這個函數之前,參數r0=0x9ff47000

此函數定義在文件 arch/arm/lib/relocate.S 中


這里面有一個問題:
我原來的函數的地址(沒拷貝之前)在那,有另一個函數會調用它,但是你把它拷貝走了再調用,就沒了,全局變量引用也會出問題。uboot對於這個處理方法,就是位置無關方法,借助.rel.dyn段




如上圖可知:
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。

並沒有直接讀取變量的地址,而是借助了另一個地址,間接讀取,+8+12偏移地址,也就是說這個過程也沒有用到絕對地址;
繼續往下:

這個段的作用是,給需要偏移的加上label,判斷是label之后就要偏移了


2.7 relocate_vectors函數
中斷向量表偏移

第 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

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 中



2.10 cli_loop函數









2.12 cmd_process函數
在學習cmd_process 之前先看一下uboot中命令是如何定義的。uboot使用宏U_BOOT_CMD來定義命令,宏 U_BOOT_CMD 定義在文件 include/command.h 中








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


3. u-boot啟動linux內核函數過程
3.1 images全局變量

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;

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

3.3 bootz_start函數


先看看bootz_setup:

bootm_find_images:

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








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



3.6 do_bootm_linux函數
經過前面的分析,我們知道了 do_bootm_linux 就是最終啟動 Linux 內核的函數,此函數定義在文件 arch/arm/lib/bootm.c


第 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 內核第一行代碼!

第 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 的使命也就完成了,它可以安息了!

