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
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
函式返回。