1. 程式人生 > >ARM+Linux嵌入式開發05:【uboot-2017移植】重定位

ARM+Linux嵌入式開發05:【uboot-2017移植】重定位

概述

上一節初始化好了串列埠和LED,我們可以用它們進行除錯;也設定好了時鐘和DRAM,為uboot的重定位做好準備。之前所做的所有工作都是在BL1中,也就是uboot的前16KB,而大部分uboot的程式碼還在SD卡中沒有載入記憶體,沒有載入記憶體的原因是之前我們使用的是內部SRAM,容量不足以放下整個uboot,而現在已經初始化好了外部DRAM,擁有了512MB記憶體,是完完全全夠放下整個uboot,所以這裡的重定位就是將SD卡中BL2的部分載入到DRAM中,然後跳轉到DRAM中去執行。
需要注意的是,uboot-2017在啟動的後期也會進行一次重定位,目的是將uboot搬運到DRAM末尾部分,方便在核心的載入,這個重定位是uboot事先設定好的,不用我們自己實現。也就是說我們uboot的啟動總共需要進行兩次重定位,這裡講的是第一次重定位。

程式碼段和資料段

重定位的概念比較難以理解,為了講清楚我儘量詳細一些。首先需要清楚什麼是程式碼段和資料段。
首先回顧一下CPU是如何執行指令的,來看如下一段彙編:

	ldr	r0, =0x30000000
	ldr	r1,	=0xe10
	str	r1,	[r0]

這段程式碼就是將0xe10這個數字寫入到0x30000000這個地址,而根據前面分析,0x30000000是我們的DRAM,因此段程式碼就是將0xe10寫入DRAM中。假如說這段程式碼就是在我們的BL1當中,我們來詳細分析一下CPU執行這段指令的過程。
首先BL1是載入到s5pv210的SRAM中的,而SRAM和DRAM一樣是接到ARM核心上的,ARM核心通過訪問一個固定的地址就可以訪問到SRAM,這個地址在哪裡呢?重新貼一下上一節的圖:
在這裡插入圖片描述


可以看到,SRAM(也就是IRAM)的起始地址是0xD0000000(實際上是0xD0020000),也就是說以上這段程式碼實際上是被載入到了0xD0000000 - 0xDFFFFFFF的某個位置當中,假設是0xD0021000吧,CPU通過PC指標的值訪問0xD0021000就可以載入ldr r0, =0x30000000這段程式碼,然後執行,隨後r0暫存器的值被設定為0x30000000,PC指標指向下一個地址0xD0021004(32位指令寬,先不考慮流水線),載入指令ldr r1, =0xe10,隨後r1被設定為0xe10,PC再指向下個地址0xD0021008,載入指令str r1, [r0],CPU訪問r0指向的地址,也就是0x30000000,最後將r1的值存入其中。
這裡可以明顯地看到,程式碼(也就是我們輸入的指令)和資料(這裡是0xe10)是分開儲存在兩個儲存器中的,程式碼在SRAM中,地址是0xD0021000
,資料在DRAM中,地址是0x30000000。我們就稱儲存程式碼的那段記憶體為程式碼段,而儲存資料的那段記憶體為資料段

注意
程式碼段和資料段只是為了便於區分而人為定的一個叫法,它們對應的都是記憶體,沒有硬體上的區別,只是用途不同,因此程式碼段和資料段並不一定要分開在兩個儲存器中,只有能保證它們不相互覆蓋,在同一個儲存器(如DRAM)中也是完全可行的。

連結地址和載入地址

接下來介紹連結地址載入地址,首先需要明白的是連結地址和載入地址都是針對程式碼段而言的。
首先是連結地址,我們先來看uboot的反彙編檔案,在uboot根目錄執行以下命令反彙編uboot:

arm-linux-objdump -S u-boot > u-boot.dmp

開啟u-boot.dmp,我從中隨便擷取一段:

33e00060:	e51fd028 	ldr	sp, [pc, #-40]	; 33e00040 <IRQ_STACK_START_IN>
33e00064:	e58de000 	str	lr, [sp]
33e00068:	e14fe000 	mrs	lr, SPSR
33e0006c:	e58de004 	str	lr, [sp, #4]
33e00070:	e3a0d013 	mov	sp, #19
33e00074:	e169f00d 	msr	SPSR_fc, sp
33e00078:	e1a0e00f 	mov	lr, pc
33e0007c:	e1b0f00e 	movs	pc, lr

以第一行為例,其中33e00060是程式碼段地址,e51fd028是機器碼,ldr sp, [pc, #-40]是對應的指令。這裡的程式碼段地址0x33e00060就是連結地址
我們可以看到,0x33e00060還在16KB的範圍內,應該屬於BL1的部分,而經過上面的分析,BL1的地址應該在0xD0000000 - 0xDFFFFFFF的範圍內,和連結地址不符,為什麼會出現這種情況呢?

這是因為上面分析的地址是程式碼的實際載入地址

講到這裡,必須分析一下我們程式碼編譯的過程。我們目前寫的程式碼都是彙編程式碼,彙編程式碼會被編譯器翻譯為ARM的機器碼,在這個翻譯的過程中,編譯器需要知道程式碼的將來會被載入的地址,這個地址是需要程式設計師手動指定的(若沒有指定則預設為0),因為編譯器在程式執行之前,是無法知道程式會被載入到那個地址執行的,這個手動指定的地址就是連結地址
uboot中的連結地址在include/configs/x210.h檔案中的32行指定:

#define CONFIG_SYS_TEXT_BASE		0x33E00000

指定了連結地址過後,編譯器就認為第一條程式碼的地址是0x33E00000,並以此來計算其他程式碼的地址。
而我們BL1實際上是載入到SRAM中執行的,地址也不是0x33E00000,也就是說連結地址和載入地址是不一致的,這就會導致一個問題,那就是如果執行位置有關碼將會造成錯誤。

位置有關碼和位置無關碼(PIC)

編譯器編譯彙編程式碼的同時,會需要用到程式碼的地址,這些地址通過函式名和標號體現,舉個簡單的例子:

	ldr r0, =main

main:
	...

該指令會將main函式的地址載入到r0暫存器中,而編譯器在編譯這段程式碼時不知道main函式的實際載入地址,因此r0中儲存的就是main函式的連結地址,也就是說如果將來程式沒有按連結地址載入,那麼r0中儲存的地址就是一個錯誤的地址,因此這是一個位置有關碼。
關於具體哪些指令是位置有關碼,那些是位置無關碼,以及它們的詳細分析可以參考部落格:https://blog.csdn.net/lizuobin2/article/details/52049892

載入BL2到DRAM

程式執行到當前為止都是在SRAM中執行,執行的程式為uboot的前16KB,也就是BL1,而我們最終的目的是在DRAM中執行整個uboot。現在DRAM已經初始化完畢可以使用,現在需要做的是將SD卡的BL2拷貝到DRAM中,然後跳轉到DRAM中執行。完成這個拷貝工作的是lowlevel_init.S的296行的movi_bl2_copy函式,該函式定義在board/samsung/x210/movi.c檔案中:

typedef u32(*copy_sd_mmc_to_mem)
(u32 channel, u32 start_block, u16 block_size, u32 *trg, u32 init);

void movi_bl2_copy(void)
{
	ulong ch;
#if defined(SET_EVT1)
	ch = *(volatile u32 *)(0xD0037488);
	copy_sd_mmc_to_mem copy_bl2 =
	    (copy_sd_mmc_to_mem) (*(u32 *) (0xD0037F98));

	#if defined(CONFIG_SECURE_BOOT)
	ulong rv;
	#endif
#else
	ch = *(volatile u32 *)(0xD003A508);
	copy_sd_mmc_to_mem copy_bl2 =
	    (copy_sd_mmc_to_mem) (*(u32 *) (0xD003E008));
#endif
	u32 ret;
	if (ch == 0xEB000000) {
		ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
			(u32 *)CONFIG_SYS_TEXT_BASE, 0);
#if defined(CONFIG_SECURE_BOOT)
		/* do security check */
		rv = Check_Signature( (SecureBoot_CTX *)SECURE_BOOT_CONTEXT_ADDR,
				      (unsigned char *)CONFIG_SYS_TEXT_BASE, (1024*512-128),
			              (unsigned char *)(CONFIG_SYS_TEXT_BASE+(1024*512-128)), 128 );
		if (rv != 0){
				while(1);
			}
#endif
	}
	else if (ch == 0xEB200000) {
		ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
			(u32 *)CONFIG_SYS_TEXT_BASE, 0);
		//ret = copy_bl2(2, 49, 1024, (u32 *)CONFIG_SYS_TEXT_BASE, 0);
#if defined(CONFIG_SECURE_BOOT)
		/* do security check */
		rv = Check_Signature( (SecureBoot_CTX *)SECURE_BOOT_CONTEXT_ADDR,
				      (unsigned char *)CONFIG_SYS_TEXT_BASE, (1024*512-128),
			              (unsigned char *)(CONFIG_SYS_TEXT_BASE+(1024*512-128)), 128 );
		if (rv != 0) {
			while(1);
		}
#endif
	}
	else {
		return;
	}

	if (ret == 0) {
		while (1);
	} else {
		return;
	}
}

注意,這裡是一段C語言程式碼,這是因為在上一節最後已經把棧設定在了0x33E00000-12的位置,因此能夠使用C語言了,這裡SET_EVT1是定義了的,因此執行:

	ch = *(volatile u32 *)(0xD0037488);
	copy_sd_mmc_to_mem copy_bl2 =
	    (copy_sd_mmc_to_mem) (*(u32 *) (0xD0037F98));

0xD0037488是一個暫存器,如果從MMC啟動,裡面存有啟動的通道號,這裡是從SD2啟動的,裡面的資料是0xEB200000,從SD0啟動則為0xEB000000

copy_sd_mmc_to_mem是我們定義的一個函式指標型別,copy_bl2指向0xD0037F98地址,該地址是iROM區域,其中固化一段讀取SD卡並拷貝到指定記憶體地址的程式:
在這裡插入圖片描述
我們能夠直接呼叫copy_bl2來拷貝SD卡中的資料,接下來正式進行拷貝:

	ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
			(u32 *)CONFIG_SYS_TEXT_BASE, 0);

其中有:

/* BL2開始扇區為49號扇區,大小為512KB,也就是1024個扇區 */
#define MOVI_BL2_POS 49
#define MOVI_BL2_BLKCNT ((512 * 1024) / 512)
#define CONFIG_SYS_TEXT_BASE		0x33E00000

因此這裡就是把SD卡中的49扇區開始的1024個扇區,拷貝到0x33E00000地址處,共512KB。
低49扇區就是我們uboot在SD卡中的位置,這是我們將uboot映象燒寫到SD卡中的位置,在build.sh檔案中:

UBOOTPOS=49
dd iflag=dsync oflag=dsync if=u-boot.bin of=$SDDEV seek=$UBOOTPOS

而之前檢視過uboot大小為380KB,因此是把整個uboot拷貝到了DRAM中。

重定位

拷貝完成後就可以進行最重要的重定位了,我這裡重定位是選擇跳轉到_start函式,見lowlevel_init.S的304行:

	/* 完成重定位 */
	ldr pc, =_start

經過前面分析,這一個位置有關碼,會將_start標號的地址載入PC指標,從而實現一個長跳轉_start標號在arch/arm/lib/vector.S檔案的48行,這個檔案是連結指令碼的第一個檔案,也就是說_start標號的地址是0x33E00000,CPU從DRAM中載入程式執行,完成重定位。
注意,這裡跳轉完成後,勢必會從_start處開始執行,將前面的執行過的程式重新執行一遍,這是沒有關係的,分岔點在於進入lowlevel_init函式之後,根據之前分析,lowlevel_init函式開頭會進行一個是否已經重定位的判斷:

	// 檢查是否需要重定位
	ldr	r0, =0x0000ffff
	bic	r1, pc, r0					// 實際載入地址(SRAM)的高16位存到r1
	ldr	r2, =CONFIG_SYS_TEXT_BASE
	bic	r2, r2, r0					// 連結地址的高16位存到r2
	cmp     r1, r2
	beq     after_copy

如果已經完成重定位,則執行到此處PC指標的偏移量還在16KB的範圍內,也就是說PC的範圍是0x33E00000 - 0x33E03FFF,那麼遮蔽PC指標的低16位,可以得到0x33E00000,這和連結地址一致,說明已經完成重定位。
否則PC指標的範圍在0xD0020000 - 0xD0038000,遮蔽低16位後不等於連結地址,說明沒有重定位。

在判斷重定位完成後,就會直接跳轉到after_copy處執行,跳過之前的硬體初始化:

after_copy:

	/* 串列埠列印'K' */
	ldr	r0, =ELFIN_UART_CONSOLE_BASE
	ldr	r1, =0x4b4b4b4b
	str	r1, [r0, #UTXH_OFFSET]

	b	1f

...

1:
	mov	lr, r11
	mov	pc, lr

串列埠列印’K’,然後lowlevel_init函式返回。