前言
一直以來都在學習和開發嵌入式linux,但對于一些常用的工具和機制卻不甚了解,包括今天要說的uboot引導和啟動linux內核,最近打算啟動技術博客來學習和記錄探索這些過程中所獲得的知識。從事嵌入式linux開發的人應該都知道uboot,支持多種操作系統,多種硬件平臺的uboot在嵌入式linux界可是大名鼎鼎,我們今天就來談一談uboot如何啟動內核。這里我們不提供代碼,僅給出相關的關鍵結構體,其余的請各位自行查看uboot代碼
過程講解
do_bootm
在uboot引導Linux啟動時,使用的是bootm的命令。這個命令執行的函數就是do_bootm
, 這個函數的地址在cmd/bootm.c
中。
代碼如下:
int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
static int relocated = 0;
if (!relocated) {
int i;
/* relocate names of sub-command table */
for (i = 0; i < ARRAY_SIZE(cmd_bootm_sub); i++)
cmd_bootm_sub[i].name += gd->reloc_off;
relocated = 1;
}
#endif
/* determine if we have a sub command */
argc--; argv++;
if (argc > 0) {
char *endp;
simple_strtoul(argv[0], &endp, 16);
/* endp pointing to NULL means that argv[0] was just a
* valid number, pass it along to the normal bootm processing
*
* If endp is ':' or '#' assume a FIT identifier so pass
* along for normal processing.
*
* Right now we assume the first arg should never be '-'
*/
if ((*endp != 0) && (*endp != ':') && (*endp != '#'))
return do_bootm_subcommand(cmdtp, flag, argc, argv);
}
return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |
BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |
BOOTM_STATE_LOADOS |
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
BOOTM_STATE_RAMDISK |
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_MIPS)
BOOTM_STATE_OS_CMDLINE |
#endif
BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
BOOTM_STATE_OS_GO, &images, 1);
}
參數
我們來說說這個函數的參數
cmd_tbl_t *cmdtp
:目前筆者也不清楚它的來歷,從命名方式中可以看出大約是命令表之類結構體
int flag
:該參數筆者跟蹤了一下其傳入位置,目前并沒有發現需要它的地方
int argc
:不用說了,相信大家都知道這個就是bootm傳入參數的個數
char * const argv[]
:同上,這個就是傳入的參數了
函數講解
這里先略過前面的CONFIG_NEEDS_MANUAL_RELOC
宏定義的部分,這里筆者也不甚了解
然后如果在命令有傳入參數,則使用simple_strtoul
對參數進行字符串到長整型數據類型的轉換,這里解析的是傳入的第一個參數,并將其賦值給endp
,其實該參數就是在存儲介質中內核的地址,但這個變量似乎并沒有傳入到函數里面去,僅用作判斷。
執行函數do_bootm_subcommand
,這個函數中執行了do_bootm_states
,uboot分階段啟動,每一個階段稱之為subcommand
而do_bootm_states
執行的就是不同階段的subcommand
在這里我們可以見到,如果沒有傳入do_bootm
參數,也就是參數argc
為0,那么do_bootm_states
的state
參數將會是一大堆的標志宏,這些標志宏就是uboot啟動時需要的階段,每個階段都有一個宏來表示
do_bootm_states
我們現在假設沒有給bootm
命令傳入參數,那么我們現在進入do_bootm_states
函數了
代碼如下,有點長,節選部分出來
int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
int states, bootm_headers_t *images, int boot_progress)
{
boot_os_fn *boot_fn;
ulong iflag = 0;
int ret = 0, need_boot_fn;
images->state |= states;
/*
* Work through the states and see how far we get. We stop on
* any error.
*/
if (states & BOOTM_STATE_START)
ret = bootm_start(cmdtp, flag, argc, argv);
if (!ret && (states & BOOTM_STATE_FINDOS))
ret = bootm_find_os(cmdtp, flag, argc, argv);
if (!ret && (states & BOOTM_STATE_FINDOTHER))
ret = bootm_find_other(cmdtp, flag, argc, argv);
/* Load the OS */
if (!ret && (states & BOOTM_STATE_LOADOS)) {
ulong load_end;
iflag = bootm_disable_interrupts();
ret = bootm_load_os(images, &load_end, 0);
if (ret == 0)
lmb_reserve(&images->lmb, images->os.load,
(load_end - images->os.load));
else if (ret && ret != BOOTM_ERR_OVERLAP)
goto err;
else if (ret == BOOTM_ERR_OVERLAP)
ret = 0;
}
........
/* From now on, we need the OS boot function */
if (ret)
return ret;
boot_fn = bootm_os_get_boot_func(images->os.os);
need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);
if (boot_fn == NULL && need_boot_fn) {
if (iflag)
enable_interrupts();
printf("ERROR: booting os '%s' (%d) is not supported\n",
genimg_get_os_name(images->os.os), images->os.os);
bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
return 1;
}
/* Call various other states that are not generally used */
if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_BD_T))
ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_PREP)) {
#if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY)
if (images->os.os == IH_OS_LINUX)
fixup_silent_linux();
#endif
ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
}
#ifdef CONFIG_TRACE
/* Pretend to run the OS, then run a user command */
if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
char *cmd_list = getenv("fakegocmd");
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,
images, boot_fn);
if (!ret && cmd_list)
ret = run_command_list(cmd_list, -1, flag);
}
#endif
/* Check for unsupported subcommand. */
if (ret) {
puts("subcommand not supported\n");
return ret;
}
/* Now run the OS! We hope this doesn't return */
if (!ret && (states & BOOTM_STATE_OS_GO))
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
images, boot_fn);
/* Deal with any fallout */
}
參數
cmd_tbl_t *cmdtp
:同上
int flag
:同上
int argc
:同上
char * const argv[]
:同上
int states
:這個參數就是那一大堆的標志宏
bootm_headers_t *images
:這個數據結果就重要了,他傳入的是一個全局的結構體,這個結構體用于保存從存儲介質中讀到的linux內核頭部信息,同時這個全局結構體的也被命名為images
int boot_progress
:似乎無作用
函數講解
那么我們最先看到的是images
的成員被賦值為states
往下看是會將參數states
跟宏BOOTM_STATE_START
進行與操作,如果通過則執行
bootm_start
,那么這里就可以知道上面所說的每個階段都有一個宏來表示
接下來就會進入bootm_start
這個函數了
1、這里我們先設置一個斷點,直接跳到下面看bootm_start
,看完我們再回來
好,我們看完bootm_start
后,接下來往下面,接著執行bootm_find_os
,同樣,我們先跳到后面去查看它的函數講解,等下再回來
2、執行完bootm_find_os
,接著執行bootm_find_other
,這里不細講,主要是查詢是否有ramdisk
3、然后關閉中斷,執行bootm_load_os
,我們繼續跳到后面去看這個函數
4、跳過ramdisk
的代碼,我們直接查看bootm_os_get_boot_func
,這個函數很簡單,直接查看boot_os
變量,直接獲取我們使用的操作系統的啟動函數,uboot為每個不同的操作系統都編寫了不同的啟動函數。將其返回并賦值給變量boot_fn
。
終于要接近尾聲了,繼續跳過一些可選代碼。我們直接看boot_selected_os
,這函數里面就執行do_bootm_linux
跳轉到我們的內核去運行了,如無意外,到了這里一般情況下就不返回了。
這里我們使用的是linux,所以它的啟動函數是do_bootm_linux
這里會根據不同的階段去執行boot_fn
,需要執行的階段有以下這些
BOOTM_STATE_OS_CMDLINE
BOOTM_STATE_OS_BD_T
BOOTM_STATE_OS_PREP
BOOTM_STATE_OS_GO
5、在這里,我們講解的是ARM
架構,在這種結構中前2個階段是不用的,我們跳到文章后面查看do_bootm_linux
的BOOTM_STATE_OS_PREP
和 BOOTM_STATE_OS_GO
實現吧
bootm_start
代碼如下:
static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc,
char * const argv[])
{
memset((void *)&images, 0, sizeof(images));
images.verify = getenv_yesno("verify");
boot_start_lmb(&images);
bootstage_mark_name(BOOTSTAGE_ID_BOOTM_START, "bootm_start");
images.state = BOOTM_STATE_START;
return 0;
}
參數
cmdtp
、flag
、argc
、argv[]
這幾個參數相信不用我講大家也都知道他們是什么了
函數講解
最先看到的是清空images
結構體,然后獲取uboot的環境變量verify
,并復制給images
的成員
然后執行boot_start_lmb
,這個函數看起來想是初始化鏡像結構體的lmb
成員,
然后獲取環境變量中的某些變量并復制到images->lmb
中,具體其作用目前暫不明白
最后執行bootstage_mark_name
,大致就是記錄啟動階段的名字和記錄此時的一些數據
好,我們回去剛剛的do_bootm_states
bootm_find_os
代碼如下:
static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc,
char * const argv[])
{
const void *os_hdr;
bool ep_found = false;
int ret;
/* get kernel image header, start address and length */
os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,
&images, &images.os.image_start, &images.os.image_len);
if (images.os.image_len == 0) {
puts("ERROR: can't get kernel image!\n");
return 1;
}
/* get image parameters */
switch (genimg_get_format(os_hdr)) {
images.os.type = image_get_type(os_hdr);
images.os.comp = image_get_comp(os_hdr);
images.os.os = image_get_os(os_hdr);
images.os.end = image_get_image_end(os_hdr);
images.os.load = image_get_load(os_hdr);
images.os.arch = image_get_arch(os_hdr);
}
......
if (images.os.type == IH_TYPE_KERNEL_NOLOAD) {
images.os.load = images.os.image_start;
images.ep += images.os.load;
}
images.os.start = map_to_sysmem(os_hdr);
}
參數
同bootm_start
函數講解
這里需要說一下函數,該函數主要參數有images
,os_data
,os_len
boot_get_kernel
函數先執行genimg_get_kernel_addr_fit
來獲取內核鏡像在存儲介質中的位置,如果沒有傳入命令參數,則默認使用全局變量load_addr
并返回,該變量由每個硬件平臺自己定義宏并賦值,可能這里是移植需要做的工作之一。另外它似乎有掃描多個內核鏡像并找到啟動鏡像的功能,但這里暫且不表。
獲取到內核的存儲地址后,使用genimg_get_image
讀取內核到內存中,這個函數將內核頭部的64字節信息和內核全部讀到指定地址CONFIG_SYS_LOAD_ADDR
,然后返回內核所在的內存地址。
genimg_get_image
獲取頭部信息指針后,然后根據頭部指針來獲取內核的大小和內核目前所在的內存地址os_len
和os_data
,這2個指針指向的其實就是在imges
結構體中的成員。到了這里,boot_get_kernel
執行完畢,我們返回內核頭部信息指針
接著從內核頭部信息指針中獲取內核的格式,格式有傳統格式,FIT
格式和安卓格式等,這里我們使用傳統格式來講解。我們回到bootm_find_os
。
根據返回的頭部信息指針,我們去獲取到內核想信息并復制給images.os
的各個成員,包括內核類型type
,內核壓縮方式comp
,內核是什么操作系統os
,內核要裝載到內存的哪個位置load
,內核是什么體系架構arch
,為以后的工作做準備,這里要說明一下,現在內核所在的內存地址是uboot所指定,而內核啟動的內存地址不一定在這里,是在laod
成員所執行的地址,后面需要把整個鏡像拷貝到這里。
最后將images.os.load
賦值給images.ep
,其實就是內核的啟動地址了
下面內核頭部信息結構體,到了這里,我們就可以返回到do_bootm_states
。
typedef struct image_header {
__be32 ih_magic; /* Image Header Magic Number */
__be32 ih_hcrc; /* Image Header CRC Checksum */
__be32 ih_time; /* Image Creation Timestamp */
__be32 ih_size; /* Image Data Size */
__be32 ih_load; /* Data Load Address */
__be32 ih_ep; /* Entry Point Address */
__be32 ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;
bootm_load_os
主要參數
bootm_headers_t *images
:就在這一行字的上邊
代碼如下:
static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end,
int boot_progress)
{
image_info_t os = images->os;
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;
bool no_overlap;
void *load_buf, *image_buf;
int err;
load_buf = map_sysmem(load, 0);
image_buf = map_sysmem(os.image_start, image_len);
err = bootm_decomp_image(os.comp, load, os.image_start, os.type,
load_buf, image_buf, image_len,
CONFIG_SYS_BOOTM_LEN, load_end);
if (err) {
bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE);
return err;
}
flush_cache(load, ALIGN(*load_end - load, ARCH_DMA_MINALIGN));
debug(" kernel loaded at 0x%08lx, end = 0x%08lx\n", load, *load_end);
bootstage_mark(BOOTSTAGE_ID_KERNEL_LOADED);
no_overlap = (os.comp == IH_COMP_NONE && load == image_start);
if (!no_overlap && (load < blob_end) && (*load_end > blob_start)) {
debug("images.os.start = 0x%lX, images.os.end = 0x%lx\n",
blob_start, blob_end);
debug("images.os.load = 0x%lx, load_end = 0x%lx\n", load,
*load_end);
/* Check what type of image this is. */
if (images->legacy_hdr_valid) {
if (image_get_type(&images->legacy_hdr_os_copy)
== IH_TYPE_MULTI)
puts("WARNING: legacy format multi component image overwritten\n");
return BOOTM_ERR_OVERLAP;
} else {
puts("ERROR: new format image overwritten - must RESET the board to recover\n");
bootstage_error(BOOTSTAGE_ID_OVERWRITTEN);
return BOOTM_ERR_RESET;
}
}
return 0;
}
函數講解
首先調用bootm_decomp_image
,來解壓內核。查看其傳入參數,我們知道都是從上一步中獲取得到的各種數據,包括裝載地址,解壓類型等等。os.image_start
是內核未解壓時所在的地址,load_buf
是內核的啟動位置也就是解壓后內核所在的地址了。
我們繼續返回到do_bootm_states
do_bootm_linux
不同的硬件平臺有不同的實現,我們這里查看的ARM
架構的實現代碼
代碼如下
int do_bootm_linux(int flag, int argc, char * const argv[],
bootm_headers_t *images)
{
/* No need for those on ARM */
if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
return -1;
if (flag & BOOTM_STATE_OS_PREP) {
boot_prep_linux(images);
return 0;
}
if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
boot_jump_linux(images, flag);
return 0;
}
boot_prep_linux(images);
boot_jump_linux(images, flag);
return 0;
}
boot_prep_linux
首先先執行BOOTM_STATE_OS_PREP
的代碼,這里調用的是boot_prep_linux
,這個函數跟內核傳遞參數有關系,uboot向內核傳遞參數就是在這里做的準備
這里先調用char *commandline = getenv("bootargs");
從uboot的環境變量中獲取到我們傳入的啟動參數,并
使用指針指向了這串字符串,該字符串在后面會用到
再調用setup_start_tag
設置啟動要用到的 tag,在這里有一個全局變量params
,bd->bi_boot_params
的值賦給它,params
的結構如下
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
他本質是一個tag
結構。該結構包括hdr
和各種類型的tag_*
hdr
來標志當前的tag是哪種類型。
setup_start_tag
是初始化了第一個tag
,類型為tag_core
。
最后調用tag_next
跳到第一個tag
末尾,為下一個tag
賦值做準備。
接下來調用setup_serial_tag
,代碼如下,功能筆者覺得是設置控制臺串口號。其中get_board_serial
是各個硬件平臺的實現,其功能大概是獲取環境變量中的串口號,將該串口作為控制臺輸出。
static void setup_serial_tag(struct tag **tmp)
{
struct tag *params = *tmp;
struct tag_serialnr serialnr;
get_board_serial(&serialnr);
params->hdr.tag = ATAG_SERIAL;
params->hdr.size = tag_size (tag_serialnr);
params->u.serialnr.low = serialnr.low;
params->u.serialnr.high= serialnr.high;
params = tag_next (params);
*tmp = params;
}
接著,再調用setup_commandline_tag
,代碼如下,可以看出,這里調用了strcpy
來賦值字符串,賦值的字符串正是上面提到的,函數開頭使用getenv
獲取的啟動參數字符串
static void setup_commandline_tag(bd_t *bd, char *commandline)
{
char *p;
if (!commandline)
return;
/* eat leading white space */
for (p = commandline; *p == ' '; p++);
/* skip non-existent command lines so the kernel will still
* use its default command line.
*/
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;
strcpy (params->u.cmdline.cmdline, p);
params = tag_next (params);
}
接著下面還調用了setup_revision_tag
、setup_memory_tags
,同理都是設置不同的tag
而已。這里比較特殊的是setup_memory_tags
,如果有多片內存ram,會循環為每一片的ram
設置一個tag
繼續調用setup_board_tags
,這個是板級實現,如果沒有實現則跳過
最后將最末尾的tag
設置為ATAG_NONE
,標志tag
結束。
由此可知我們的啟動參數params
是一片連續的內存,這片內存有很多個tag
,我們通過調用不同的程序來設置這些tag
。
這樣整個參數的準備就結束了,最后在調用boot_jump_linux
時會將tags
的首地址也就是bi_boot_params
傳給內核,讓內核解析這些tag。當然我想也有朋友不懂內核傳遞的參數都是字符串,設置這些tag
跟傳遞的參數有什么關系呢,筆者也不明白,等到后面我們再來講解。
總結一下,uboot將參數以tag數組的形式布局在內存的某一個地址,每個tag代表一種類型的參數,首尾tag標志開始和結束,首地址傳給kernel供其解析。
我們回到do_bootm_linux
,其實這行到這里是跳出do_bootm_linux
回到我們的do_bootm_states
boot_jump_linux
kernel_entry
變量是個函數指針,我們會講images->ep
賦值給它作為跳轉到內核執行的入口。寄存器r2
會賦值為gd->bd->bi_boot_params
,就是我們之前所有是tag
啟動參數
然后傳入其余相關參數并執行kernel_entry
啟動內核
到了這里,uboot引導內核的啟動過程講解完畢
后記
總結來說,uboot啟動內核就是讀取內核,加載內核,解析內核頭部,解壓內核,裝載內核到執行啟動地址,準備啟動參數,啟動內核這幾個階段。
寫完該片,筆者對uboot的理解深了一層,當然該文所講的只是uboot引導的主要部分,還有很多細節我們跳過了(但大體上不影響)。以前筆者僅僅只是使用uboot,并沒有對它進行一個系統的理解,今天算是對uboot的有了一些更深的理解。當然了,這些都是理論層面,還需要各位去根據uboot的代碼進行實踐。實踐出真知,要了解uboot后的內核啟動,我們還需要對操作系統和編譯原理有一定的了解,關于我們后面有機會再繼續聊吧
本文參考:
uboot向kernel的傳參機制——bootm與tags https://blog.csdn.net/skyflying2012/article/details/35787971
什么是FIT uImage? https://blog.csdn.net/rikeyone/article/details/86594196