U-boot啓動內核流程的詳細分析

U-Boot屬於兩階段的Bootloader。第一階段的文件爲cpu/arm920t/start.S和board/smdk2410/lowlevel_init.S,
前一文件是平臺相關的,後一文件是開發板相關的。


Uboot第一階段分析:
1、硬件設備的初始化:將cpu的工作模式設置爲管理模式,關閉WATCHDOG,設置FCLK,HCLK,PCLK的比例,關閉mmu,cache.
2、爲加載bootloader的第二階段代碼準備RAM空間,即初始化內存芯片,使它可用,通過在start.S中調用lowlevel_init函數來
設置存儲控制器。代碼在board/smdk2410/lowlevel_init.S中。
3、複製第二階段代碼到RAM空間中。實際中可能把一二階段的代碼全部都複製到RAM空間中了。
自己寫bootloader的時候,應儘可能早地複製代碼,跳轉到複製後的代碼中執行,這樣不用考慮後面的代碼的位置無關的問題。
要注意弄清楚從哪裏複製到那裏去,複製多少的問題,根據鏈接腳本可以解決這些問題。(一般開發中會從主機上下載uboot到

內存上,再把它複製到flash中存儲着。開機時再從flash複製到內存上,鏈接地址上去運行)

	mov r0, #0	//這裏uboot存儲在flash 0地址上,從flash的 0地址開始複製
	ldr r1, =_start  //uboot鏈接地址,複製到這裏去,1.1.6內核中,鏈接地址是0x33f80000
	ldr r2, =__bss_start //程序最後的bss段不用複製,所以結束地址是__bss_start,而不是__bss_end
	sub r2, r2, r1	//複製的大小: __bss_start - _start
	//...接着調用複製的代碼
4、設置棧,棧放在哪裏?棧一般指向SDRAM較高的地址。
5、清BSS段,往BSS段中寫0。
6、跳轉到第二階段代碼的C入口點。
通過如下命令直接跳轉,它調用lib_arm/board.c中的start_armboot函數,這是第二階段的入口。
    ldr pc,_start_armboot
    _start_armboot: .word start_armboot

       插入講一下:環境變量有bootcmd,bootargs,serverip,ipaddr,等,獲取指定環境變量值函數是:getenv(),環境變量除了
可以在uboot命令行下設置,還可以在哪個文件中設置?可以編譯uboot之前在include\configs\smdk2410.h文件中設置。
啓動的時候,uboot main_loop()函數中主動讀取環境變量bootcmd的值(或稱引導命令),然後run_command:

      s = getenv(bootcmd)	
     //s 就是uboot下print出來的:bootcmd  = nand read.jffs2 0x30007fc0 kernel;bootm 0x30007cf0
     if (bootdelay >= 0 && s && !abortboot (bootdelay))
     //如果延時大於等於零,並且沒有在延時過程中接收到按鍵,則引導內核。
        run_command (s, 0);           //運行引導內核的命令。
在啓動的倒計時中按下空格鍵,將進入main_loop循環。
uboot界面下循環執行下面代碼(uboot命令行界面):
     readline()//讀入串口的數據,根據輸入對應地來run_command
     run_command()

無論是獲取bootcmd值自動啓動內核還是 uboot命令行下啓動內核,效果應該一樣。

下面分析通過環境變量bootcmd的值自動啓動內核過程:

bootcmd  = nand read.jffs2 0x30007fc0 kernel;bootm 0x30007cf0
    bootcmd的第一個命令nand read.jffs2 0x30007fc0 kernel 與 nand read.jffs2 0x30007fc0 0x00060000 0x00200000 作用是一樣的,就是把內核從nand的kernel分區讀到內存0x30007fc0,實際上是把kernel分區全部內容讀到0x30007fc0。實際上可以把內核讀到一個比較隨意的地址,然後bootm 0x30007fc0 命令會把真正的內核從這個隨意的地址移到內核頭部指定的加載地址處
(這裏真正的內核的加載地址是0x30008000,即真正的內核應該被加載到0x30008000),(內核頭部含有加載地址和入口地址,
頭部64字節,0x30007fc0+64 = 0x30008000,這裏讓頭部存於0x30007fc0,則真正的內核被存於0x30008000,剛好處於加載地址,不用移動)
    爲什麼用jffs2? 答:用jffs2則讀取的長度不需要頁對齊,其他格式的話,讀取的長度需要頁對齊或塊對齊限制。

bootcmd的第二個命令bootm 0x30007cf0 
bootm命令調用過程:  
bootm命令
 ...
  調用do_bootm()函數 // if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0)
移動內核
do_bootm_linux
char *commandline = getenv ("bootargs");
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
setup_start_tag (bd);
setup_memory_tags (bd);
setup_commandline_tag (bd, commandline);
setup_end_tag (bd);
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);

 
下面主要分析這個過程中的各種setup tag 和theKernel函數。
第一個設置的tag必須是用setup_start_tag()函數設置的start tag。在介紹setup_start_tag()函數之前,先介紹幾個結構體。

typedef struct bd_info {  
    int         bi_baudrate;      
    unsigned long   bi_ip_addr;   
    unsigned char   bi_enetaddr[6];   
    struct environment_s           *bi_env;  
    ulong           bi_arch_number;   	//機器ID
    ulong           bi_boot_params;   	//用於保存啓動參數鏈表的地址
    struct                
    {  
    ulong start;  
    ulong size;  
    }           bi_dram[CONFIG_NR_DRAM_BANKS];  
#ifdef CONFIG_HAS_ETH1  
      
    unsigned char   bi_enet1addr[6];  
#endif  
} bd_t  
對bd_t結構體,這裏主要涉及其bi_arch_number成員和bi_boot_parmas成員。
typedef struct  global_data {  
    bd_t        *bd;  
    unsigned long   flags;  
    unsigned long   baudrate;  
    unsigned long   have_console;     
    unsigned long   reloc_off;    
    unsigned long   env_addr;     
    unsigned long   env_valid;    
    unsigned long   fb_base;      
#ifdef CONFIG_VFD  
    unsigned char   vfd_type;     
#endif  
#if 0  
    unsigned long   cpu_clk;      
    unsigned long   bus_clk;  
    unsigned long   ram_size;     
    unsigned long   reset_status;     
#endif  
    void        **jt;         
} gd_t; 
include/am-arm/global_data.h頭文件的第64行:
64    #define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")

    它的作用是聲明一個全局的gd_t類型指針gd,並且gd是保存在ARM的r8這個寄存器裏面的,且有且僅有一個gd_t結構體,就是gd。gd結構體用來保存與平臺硬件相關的一些信息,或uboot與內核交互用的一些必要信息,這些信息在board.c和smdk2410.c文件中收集,主要在各種硬件的init函數中收集。
    在smdk2410.c中填充了gd結構體兩個重要的成員的成員:
	/* arch number of SMDK2410-Board */
	gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
	/* adress of boot parameters */
	gd->bd->bi_boot_params = 0x30000100;
對於gd_t結構體的分配與設置,可以參考board.c裏面start_armboot()函數。
下面是board.c裏面start_armboot()函數部分內容:

void start_armboot (void)
{
 ...
/* Pointer is writable since we allocated a register for it */
	gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));	//gd指向某個地址
	/* compiler optimization barrier needed for GCC >= 3.4 */
	__asm__ __volatile__("": : :"memory");

	memset ((void*)gd, 0, sizeof (gd_t));	//gd結構體清零
	gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));	//這個gd中的bd所在的地址。
	memset (gd->bd, 0, sizeof (bd_t));		//清零gd->bd

	monitor_flash_len = _bss_start - _armboot_start;

	for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {//調用init_sequence[]中的各初始化函數
		if ((*init_fnc_ptr)() != 0) {
			hang ();
		}
	}
  ...
}
啓動參數結構體:
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;
};
struct tag_header {
	__u32 size;
	__u32 tag;//tag結構體的的類型標誌,有值ATAG_CORE、ATAG_MEM、ATAG_CMDLINE
		  //和ATAG_NONE(用於最後一個空的tag,表示tag鏈表結束)
};
lib_arm\board.c部分代碼:
static struct tag *params;
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
		     ulong addr, ulong *len_ptr, int verify)
{
	
	void (*theKernel)(int zero, int arch, uint params);
	image_header_t *hdr = &header;
	bd_t *bd = gd->bd;
	//smdk2410.c中填充了gd兩個重要的成員的成員:
	//gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
	//gd->bd->bi_boot_params = 0x30000100;

#ifdef CONFIG_CMDLINE_TAG
	char *commandline = getenv ("bootargs");
#endif

#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
    defined (CONFIG_CMDLINE_TAG) || \
    defined (CONFIG_INITRD_TAG) || \
    defined (CONFIG_SERIAL_TAG) || \
    defined (CONFIG_REVISION_TAG) || \
    defined (CONFIG_LCD) || \
    defined (CONFIG_VFD)
	setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
	setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
	setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
	setup_memory_tags (bd);
#endif
#ifdef CONFIG_CMDLINE_TAG
	setup_commandline_tag (bd, commandline);
#endif


	setup_end_tag (bd);
#endif

	theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
}

//注意必須首先調用setup_start_tag(bd),最後調用setup_end_tag(bd)


static void setup_start_tag (bd_t *bd)
{	
	params = (struct tag *) bd->bi_boot_params;
	//ulong bd->bi_boot_params, tag第一個成員是tag_header hdr,是__u32類型

	params->hdr.tag = ATAG_CORE;
	params->hdr.size = tag_size (tag_core);

	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params);
}

...

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);
	}
}


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);
}
......

static void setup_end_tag (bd_t *bd)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}
do_bootm_linux()函數中設置好這些tag(啓動參數)後
接着調用: theKernel (0, bd->bi_arch_number, bd->bi_boot_params);啓動內核。
在介紹theKernel()函數之前,先來看一下bootm命令與內核的加載地址和入口地址的關係。
uImage包含頭部header結構體和zImage(真正的內核)。
typedef struct image_header {  
    uint32_t    ih_magic;     
    uint32_t    ih_hcrc;      
    uint32_t    ih_time;      
    uint32_t    ih_size;      
    uint32_t    ih_load;      //真正的內核的加載地址,應該加載到這裏
    uint32_t    ih_ep;        //入口地址,正常運行時,ih_ep應該指向真正的內核,否則無法正常啓動。
    			      //當真正的內核的實際加載地址等於ih_load時,
    uint32_t    ih_dcrc;      //ih_ep 應等於ih_load(即指向真正的內核的實際加載地址)才能正常啓動。
    uint8_t     ih_os;        
    uint8_t     ih_arch;      
    uint8_t     ih_type;      
    uint8_t     ih_comp;      
    uint8_t     ih_name[IH_NMLEN];    
} image_header_t;  
    bootm會判斷是否需要移動內核,若需要移動,則僅僅把真正的內核移動到header指定的ih_load中。
但源碼中bootm xxx會根據xxx是否與ih_load相同來判斷是否需要移動真正的內核,相同則不移動,不同則移動。
這是有點不合理的,xxx 是整個內核(包含頭部)下載到內存中的位置。
舉幾個啓動內核例子說明他們的關係:
1、假設:真正的內核應該的  加載地址  ih_load   : 30008000
                                          入口地址  ih_ep     :30008040
    a、若把內核(包含頭部)下載到30008000,則真正的內核此時處於30008040,與入口地址ih_ep 30008040相等。
     bootm 30008000啓動內核,這個地址與ih_load 30008000相等,不移動真正的內核,而入口地址ih_ep 300080040正確
     指向真正的內核30008040,可以正常啓動。
    b、如若內核(包含頭部)下載地址不是30008000,而是其他的xxx,則應bootm xxx啓動,此時bootm判斷出 內核(包含頭部)下       載地址xxx 與ih_load 30008000 不相等,則移動真正的內核至 ih_load 30008000,此時入口地址ih_ep 30008040 不能正確指     向真正的內核ih_load 30008000,不能正常啓動。
2、假設:真正的內核應該的  加載地址  ih_load  : 30008000
                                          入口地址  ih_ep    :30008000
    a、若把內核(包含頭部)下載到30008000,則真正的內核此時處於30008040,與入口地址ih_ep 30008000不相等。
     bootm 30008000啓動內核,這個地址與ih_load 30008000相等,不移動真正的內核,而入口地址ih_ep 300080000不能正        確指向真正的內核30008040,不可以正常啓動。
    b、如果只把內核(包含頭部)下載地址改爲0x30007cf0,則真正的內核此時處於30008000,與真正的內核應該的加載地址         ih_load 30008000相等。bootm 判斷出內核(包含頭部)下載地址0x30007cf0與真正的內核應該的加載地址ih_load                 30008000不相等,則把真正的內核移植ih_load 30008000(實際上下載時,真正的內核剛好處於30008000,剛好重疊,不需     移動),此時入口地址ih_ep 30008000能正確指向真正的內核 ih_load 30008000,可以正常啓動。

      可以看到,真正的內核的加載地址ih_load其實沒有什麼用,無論bootm 是否移動真正的內核,最終啓動時只要保證ih_ep指向真正的內核最終在內存中的實際加載地址即可正常啓動。

好了,看theKernel,
對於theKernel,do_bootm_linux()函數裏面有下面3條語句:
void (*theKernel)(int zero, int arch, uint params);//聲明一個函數指針
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);//初始化這個函數指針
//ntohl()用來將參數指定的32 位netlong 轉換成主機字符順序,在將這個東東強制轉換爲對應的函數指針賦給theKernel
//ntohl(hdr->ih_ep)相當於0x30008000(假設ih_ep爲0x30008000,ih_load和ih_ep在用mkimage製作內核時通過-a,-e選

       //項指定),不管真正的內核的實際加載地址是否與ih_load相等,此時的ih_ep應指向真正的內核在內存中的實際加載地址。

 thekernel(0,bd->bi_arch_number,bd->bi_boot_parmas);
/* theKernel(0, bd->bi_arch_number, bd->bi_boot_params);這相當於彙編:
* mov r0,#0
* ldr r1,=362
* ldr r2,=0x30000100
* mov pc,#0x30008000
*/
  theKernel()第一個參數指定爲0;第二個參數bd->bi_arch_number是機器ID,smkd2410是MACH_TYPE_SMDK2410=193,而MACH_TYPE_S3C2440 =362;第三個參數保存着的是啓動參數的地址0x30000100。而theKernel指針本身是0x30008000,指向真正的內核。

    theKernel函數會把它的三個參數分別賦給r0,r1,r2寄存器,並把thekernel指向的地址0x30008000賦給pc寄存器,跳轉到0x30008000執行。

    內核代碼中會處理r0,r1,r2。

    再一次說明一下,theKernel()參數中的bd實際上來自是gd->bd,在smdk2410.c中填充了gd兩個重要的成員的成員:
//gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
//gd->bd->bi_boot_params = 0x30000100; 
    即可在smdk2410.c中設置機器ID和bi_boot_params,內核機器碼和uboot機器碼必須一致才能啓動內核。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章