所謂的relocation,就是重定位,u-boot運行后會將自身代碼拷貝到SDRAM的另一個位置繼續運行。
但基於以前的理解,一個完整可運行的bin文件,link時指定的鏈接地址,load時的加載地址,運行時的運行地址,這3個地址應該是一致的。
relocation后運行地址不同於加載地址,特別是鏈接地址,ARM的尋址會不會出現問題?
u-boot啟動后會計算出一個靠近SDRAM頂端的地址,也就是gd->relocaddr,將自身代碼拷貝到該地址,繼續運行。
但是這樣會有一個問題,relocation后u-boot的運行地址跟其鏈接地址不一致,compiler會在link時確定了其中全局變量的絕對地址,鏈接地址 加載地址 運行地址應該一致,這樣看來,arm在尋址這些變量時找到的應該是relocation之前的地址,這樣relocation就沒有意義了!
因此我們在進行u-boot重定位需要包含代碼段、rodata、data以及全局變量、函數的重定位。
一、u-boot重定位
1.1 重定位原理
我們C代碼編譯成匯編后,我們需要思考以下幾個問題:
- 我們的函數跳轉轉為匯編代碼后是怎樣的;
- 全局變量又是如何尋址的;
實際上函數用b/bl相對跳轉,因此代碼進行重定位之后函數跳轉是沒有影響的。但是全局變量使用的是絕對地址,是有影響的。
這里我們以LCD項目為例,來介紹全局變量的尋址原理。
我們定義了const常量為例:
const u8 sunflower_320x240[] = { ... }
LCD項目的鏈接地址為0x30000000,代碼反匯編后,查看只讀數據段:
Disassembly of section .rodata: 30005634 <sunflower_320x240-0x18>: 30005634: aeb0d2ce cdpge 2, 11, cr13, cr0, cr14, {6} 30005638: f5c1e3c4 .word 0xf5c1e3c4 3000563c: 4920e0d1 stmdbmi r0!, {r0, r4, r6, r7, sp, lr, pc} 30005640: 564f4c20 .word 0x564f4c20 30005644: 4f592045 svcmi 0x00592045 30005648: 00002055 .word 0x00002055 3000564c <sunflower_320x240>: 3000564c: 73229322 .word 0x73229322 30005650: 73229322 .word 0x73229322 30005654: 93229322 .word 0x93229322 30005658: 93229322 .word 0x93229322 3000565c: 93227322 .word 0x93227322 30005660: 93229322 .word 0x93229322 ...
我們在lcd_test函數使用到了sunflower_320x240全局變量:
void lcd_test() { 300017f0: e92d4800 push {fp, lr} 300017f4: e28db004 add fp, sp, #4 ; 0x4 300017f8: e24dd010 sub sp, sp, #16 ; 0x10 /* 文件必須采用GBK編碼 */ lcd_draw_bmp(0,0,LCD_WIDTH,LCD_HEIGHT, sunflower_320x240); 300017fc: e59f3050 ldr r3, [pc, #80] ; 30001854 <lcd_test+0x64> 30001800: e58d3000 str r3, [sp] 30001804: e3a00000 mov r0, #0 ; 0x0 30001808: e3a01000 mov r1, #0 ; 0x0 3000180c: e3a02d05 mov r2, #320 ; 0x140 30001810: e3a030f0 mov r3, #240 ; 0xf0 30001814: ebfffe7c bl 3000120c <lcd_draw_bmp> char *data = "我愛你劉燕 I LOVE YOU "; 30001818: e59f3038 ldr r3, [pc, #56] ; 30001858 <lcd_test+0x68> 3000181c: e50b3008 str r3, [fp, #-8] lcd_draw_char_lib(100, 50, 0xFF00FF, data, strlen(data)); 30001820: e51b0008 ldr r0, [fp, #-8] 30001824: eb0003c8 bl 3000274c <strlen> 30001828: e1a03000 mov r3, r0 3000182c: e58d3000 str r3, [sp] 30001830: e3a00064 mov r0, #100 ; 0x64 30001834: e3a01032 mov r1, #50 ; 0x32 30001838: e3a028ff mov r2, #16711680 ; 0xff0000 3000183c: e28220ff add r2, r2, #255 ; 0xff 30001840: e51b3008 ldr r3, [fp, #-8] 30001844: ebffff83 bl 30001658 <lcd_draw_char_lib> } 30001848: e24bd004 sub sp, fp, #4 ; 0x4 3000184c: e8bd4800 pop {fp, lr} 30001850: e12fff1e bx lr 30001854: 3000564c .word 0x3000564c 30001858: 30005634 .word 0x30005634
我們可以發現:
- 在函數的后面有label,PC+offset找到label;
- label存放全局變量地址;
- 函數后面的label作為text段的一部分;
可以看到[pc,#80]地址處保存sunflower_320x240全局變量的地址0x3000564c。
30001854: 3000564c .word 0x3000564c
我們LCD項目鏈接地址為0x30000000,我們將代碼加載0x30000000地址處,並跳轉到0x30000000處運行。
我們在LCD項目初始化代碼中將.text、.rodata.、.data段代碼從0x30000000重新定位到A,重定位偏移為relocaddr=A-0x30000000,那么地址0x30001854(位於.text段)重定位后為:
30001854+relocaddr: 3000564c .word 0x3000564c => 我們需要將這個地址值+重定位偏移
此時我們會發現地址30001854+relocaddr處保存的sunflower_320x240全局變量的地址仍然是0x3000564c,很顯然這個地址不是sunflower_320x240全局變量重定位之后的地址。那么如何解決這個問題呢?
這個問題可以通過重定位.rel.dyn段解決。具體實現思路如下:
在代碼編譯的時候指定-pie選項,將會生成.rel.dyn段,.rel.dyn段將會保存label的地址,我們在重定位的過程中從.rel.dyn段中找到label的地址:
30006660: 30001854 .word 0x30001854
獲取lable地址中的值0x30001854,然后加上relocaddr得到地址30001854+relocaddr,這個地址存放的應該是重定位后sunflower_320x240全局變量的地址。我們取出這個地址的值0x3000564c然后加上偏移地址relocaddr,再寫回地址30001854+relocaddr,這樣我們就可以實現全局變量的重定位了。
30001854+relocaddr: 3000564c+relocaddr .word 0x3000564c+relocaddr
下面是別人繪制的一張簡圖,比較生動,我就直接貼過來了:

1.2 u-boot重定位
我們已經分析到了_main(arch/arm/lib/crt0.S)中board_init_f函數的執行,接着繼續分析_main。
#if ! defined(CONFIG_SPL_BUILD) /* * Set up intermediate environment (new sp and gd) and call * relocate_code(addr_moni). Trick here is that we'll return * 'here' but relocated. */ ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */ #if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */ mov r3, sp bic r3, r3, #7 mov sp, r3 #else bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ #endif ldr r9, [r9, #GD_BD] /* r9 = gd->bd */ sub r9, r9, #GD_SIZE /* new GD is below bd */ adr lr, here ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */ add lr, lr, r0 #if defined(CONFIG_CPU_V7M) orr lr, #1 /* As required by Thumb-only */ #endif ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ b relocate_code here: /* * now relocate vectors */ bl relocate_vectors /* Set up final (full) environment */ bl c_runtime_cpu_setup /* we still call old routine here */ #endif
其中GD_START_ADDR_SP 定義如下:
#define GENERATED_GBL_DATA_SIZE 176 /* (sizeof(struct global_data) + 15) & ~15 @ */ #define GENERATED_BD_INFO_SIZE 80 /* (sizeof(struct bd_info) + 15) & ~15 @ */ #define GD_SIZE 168 /* sizeof(struct global_data) @ */ #define GD_BD 0 /* offsetof(struct global_data, bd) @ */ #define GD_RELOCADDR 44 /* offsetof(struct global_data, relocaddr) @ */ #define GD_RELOC_OFF 64 /* offsetof(struct global_data, reloc_off) @ */ #define GD_START_ADDR_SP 60 /* offsetof(struct global_data, start_addr_sp) @ */
以上代碼主要做了以下事情:
- 設置棧指針為gd->start_addr_sp,並且8字節對齊;
- 設置r9=gd->bd,在內存圖上可以看出,新的gd結構在bd結構的下面緊挨着,所以減去gd的大小就是新的gd起始地址,r9變成了重定位后的新地址的gd結構了;
- 將here標號的地址值(adr取的是相對當前PC的偏移地址)讀取到LR中,將重定位偏移值gd->reloc_off加載到R0寄存器中;
- 鏈接寄存器LR加上偏移值R0后,LR的地址就變成重定位后的地址了;
- 將重定位地址gd->relocaddr加載到R0中,作為參數傳給relocate_code;
- 執行重定位,從relocate_code回來后,就直接運行在重定位后的u-boot中了,here標號已經是重定位后那個here標號了;

上面代碼主要是用來將u-boot從CONFIG_SYS_TEXT_BASE重定位到gd->relocaddr位置。
具體如圖所示:這里為了方便繪制,將內存地址拆分成了兩部分,右側地址存放的是u-boot的運行地址。
當Mini2440從NOR啟動時,0x00000000就是板上2MB NOR FLASH實際的起始地址,NOR FLASH中的程序就從這里開始運行。
需要注意的是:u-boot程序默認是從NOR啟動的,此時2MB的NOR FLASH足夠放下u-boot的所有代碼。
二、relocate_code(arch/arm/lib/relocate.S)
2.1.text、.rodata、.data、.rel.dyn重定位(arch/arm/lib/relocate.S)
/* * void relocate_code(addr_moni) * * This function relocates the monitor code. * * NOTE: * To prevent the code below from containing references with an R_ARM_ABS32 * relocation record type, we never refer to linker-defined symbols directly. * Instead, we declare literals which contain their relative location with * respect to relocate_code, and at run time, add relocate_code back to them. */ ENTRY(relocate_code) ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */ subs r4, r0, r1 /* r4 <- relocation offset */ beq relocate_done /* skip relocation */ ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */ copy_loop: ldmia r1!, {r10-r11} /* copy from source address [r1] */ stmia r0!, {r10-r11} /* copy to target address [r0] */ cmp r1, r2 /* until source end address [r2] */ blo copy_loop
根據u-boot.lds我們繪制各個段在內存中的分布圖:

以上代碼主要重定位__image_copy_start、__image_copy_end的數據到新的地址:
- r0設置為u-boot重定位后的位置gd->reloacaddr;
- r1設置為__image_copy_start,為u-boot鏈接起始地址(鏈接地址需要和運行地址一致,此時鏈接地址就是目前u-boot運行的起始地址);
- r4設置為u-boot重定位偏移地址gd->reloc_off;
- 如果r4=0,表示r0=r1,不再進行重定位;
- r2設置為__image_copy_end;
- 以r1為起始地址(也就是目前u-boot的起始地址),加載[r1],[r1+4]字到r10,r11;
- 以r0為起始地址(也就是重定位后的的新地址),加載r10,r11的值到[r0],[r0+4]中;
- 比較是否讀取到結束地址__image_copy_end,一直循環,直到拷貝結束;
除了代碼段外,這里我們重定位了只讀數據段和初始化數據段:
- rodata{}:聲明只讀數據段,簡稱rodata段,存放常量,字符常量,const常量,據說還存放調試信息;
- .data{}:聲明初始化數據段(Initialized data segment),簡稱data段,存放程序中已經初始化全局與初始化靜態變量;
2.2 .rel.dyn重定位(arch/arm/lib/relocate.S)
/* * fix .rel.dyn relocations */ ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */ ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */ fixloop: ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */ and r1, r1, #0xff cmp r1, #23 /* relative fixup? */ bne fixnext /* relative fix: increase location by offset */ add r0, r0, r4 ldr r1, [r0] add r1, r1, r4 str r1, [r0] fixnext: cmp r2, r3 blo fixloop relocate_done: #ifdef __XSCALE__ /* * On xscale, icache must be invalidated and write buffers drained, * even with cache disabled - 4.2.7 of xscale core developer's manual */ mcr p15, 0, r0, c7, c7, 0 /* invalidate icache */ mcr p15, 0, r0, c7, c10, 4 /* drain write buffer */ #endif /* ARMv4- don't know bx lr but the assembler fails to see that */ #ifdef __ARM_ARCH_4__ mov pc, lr #else bx lr #endif ENDPROC(relocate_code)
這里主要用於重定位.rel.dyn段,至於為什么需要定義這個段,實際上就是解決上面我們介紹的類似sunflower_320x240全局變量重定位存在的問題。這里仍然以sunflower_320x240為例:
- r2設置為__rel_dyn_start;
- r3設置為__rel_dyn_end;
- 以r2為起始地址(也就是動態符號表的起始地址),加載[r2],[r2+4]到r0,r1中,那么 r0得到的就是0x30001854;
- 取出r1中數據的低8位、23用來檢查這個符號是不是需要被重定位,不需要的話就跳過;
- r4是重定位偏移,設置r0=r0+r4,也就是新的u-boot里面sunflower_320x240全局變量的地址;
- 將r0地址內的數據存到r1中,即0x3000564c,這個值就是sunflower_320x240全局變量數據的存放地址;
- 給r1加上偏移r4,將加了偏移后的值(變量的地址)寫回,這樣新的u-boot就能正確訪問了;
- 跳轉到到lr,lr為u-boot重定位之后here標號的位置;
三、 relocate_vectors
接下來重定位向量表,這個很簡單,就是操作協處理器:
/* * Default/weak exception vectors relocation routine * * This routine covers the standard ARM cases: normal (0x00000000), * high (0xffff0000) and VBAR. SoCs which do not comply with any of * the standard cases must provide their own, strong, version. */ .section .text.relocate_vectors,"ax",%progbits .weak relocate_vectors ENTRY(relocate_vectors) #ifdef CONFIG_CPU_V7M /* * On ARMv7-M we only have to write the new vector address * to VTOR register. */ ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ ldr r1, =V7M_SCB_BASE str r0, [r1, V7M_SCB_VTOR] #else #ifdef CONFIG_HAS_VBAR /* * If the ARM processor has the security extensions, * use VBAR to relocate the exception vectors. */ ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */ #else /* * Copy the relocated exception vectors to the * correct address * CP15 c1 V bit gives us the location of the vectors: * 0x00000000 or 0xFFFF0000. */ ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */ ands r2, r2, #(1 << 13) ldreq r1, =0x00000000 /* If V=0 */ ldrne r1, =0xFFFF0000 /* If V=1 */ ldmia r0!, {r2-r8,r10} stmia r1!, {r2-r8,r10} ldmia r0!, {r2-r8,r10} stmia r1!, {r2-r8,r10} #endif #endif bx lr ENDPROC(relocate_vectors)
這里主要做了一下事情:
- r0設置為u-boot重定位后的位置gd->reloacaddr;
- 讀協處理器中的c1寄存器數據到ARM處理器的r2里面;
- 主要是控制bit[13]:V,對於支持高端異常向量表的系統,本控制位控制向量表的位置:
- 0 :選擇低端異常中斷向量 0x0~0x1c;
-
1 :選擇高端異常中斷向量0xffff0000~ 0xffff001c;
-
對於不支持高端異常向量表的系統,讀取時該位返回0,寫入時忽略;
- ands 后面的 s 會影響CPSR狀態的寄存器的標志位,若相與的結果為0,則CPSR的狀態標志位 Z = 1;反之,Z = 0;
-
If V=0,則Z=1,可執行 ldr r1,=0x00000000指令;
- If V=1,則Z=0,可執行 ldr r1,=0xFFFF0000指令;
- 最終,r1則為異常向量表的地址;
- 將r0地址的值加載到r2,r0+4地址的值加載到r3,...;
- 將r2-r8,r10寄存器的值街道地址r1,r1+4,...;
- 最終會將r0地址處的值寫到r1,長度為16*4個字節,即將重定位向量表和向量處理函數;
- !表示將地址寫回,IA:(Increase After)的含義每次傳送后地址加4

0x00-0x1c為向量表,0x20~0x40為向量處理函數。因此需要重定位64個字節的數據。
四、清bss段
介紹完代碼重定位以及終端向量重定位之后,_main函數還剩下一小部分沒有介紹,這部分主要涉及到bss段清0:
- .bss:簡稱bss段,存放程序中未初始化全局與未初始化靜態變量,該區域會需要清零;
#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK) # ifdef CONFIG_SPL_BUILD /* Use a DRAM stack for the rest of SPL, if requested */ bl spl_relocate_stack_gd cmp r0, #0 movne sp, r0 movne r9, r0 # endif ldr r0, =__bss_start /* this is auto-relocated! */ #ifdef CONFIG_USE_ARCH_MEMSET ldr r3, =__bss_end /* this is auto-relocated! */ mov r1, #0x00000000 /* prepare zero to clear BSS */ subs r2, r3, r0 /* r2 = memset len */ bl memset #else ldr r1, =__bss_end /* this is auto-relocated! */ mov r2, #0x00000000 /* prepare zero to clear BSS */ clbss_l:cmp r0, r1 /* while not at end of BSS */ #if defined(CONFIG_CPU_V7M) itt lo #endif strlo r2, [r0] /* clear 32-bit BSS word */ addlo r0, r0, #4 /* move to next */ blo clbss_l #endif #if ! defined(CONFIG_SPL_BUILD) bl coloured_LED_init bl red_led_on #endif /* call board_init_r(gd_t *id, ulong dest_addr) */ mov r0, r9 /* gd_t */ ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */ /* call board_init_r */ #if defined(CONFIG_SYS_THUMB_BUILD) ldr lr, =board_init_r /* this is auto-relocated! */ bx lr #else ldr pc, =board_init_r /* this is auto-relocated! */ #endif /* we should not return here. */ #endif
這一段代碼主要做了以下事情:
- 設置r0為__bss_start,設置r1為__bss_end,設置r2為0 ;
- 將r0~r1地址數據清零;
- 初始化led、點亮led;
- 設置r0=r9,r9為u-boot重定位后的新地址的gd結構;設置r1為重定位后的地址,作為參數傳給board_init_r;
- 調用board_init_r,進入u-boot第二階段;
參考文章:
