1、前言
在前面的文章《Uboot啟動流程分析(四)》,鏈接如下:
https://www.cnblogs.com/Cqlismy/p/12147411.html
已經對board_init_f() 函數作出了簡單的分析,該函數對一些早期的外設進行了初始化,例如調試串口,並填充了gd_t結構體中的成員變量,最主要的是對整個DRAM的內存進行了分配,以便uboot的重定位,接下來,先回顧一下_main函數的大概流程,如下:
_main | board_init_f_alloc_reserve-->reserve gd and early malloc area | board_init_f_init_reserve-->initialize global data | board_init_f-->initialize ddr,timer...,and fill gd_t | relocate_code-->relocate uboot code | relocate_vectors-->relocate vectors | board_init_r-->calling board_init_r
在_main函數中,調用完了board_init_f()函數后,將DRAM的內存分配好,填充gd_t結構體成員變量,接下來,就是調用relocate_code()函數重定位uboot代碼,調用relocate_vectors()函數重定位中斷向量表,本篇文章將簡單分析uboot的大概重定位過程。
2、uboot段相關變量
在分析relocate_code函數之前,先來總結一下相關的uboot段相關變量,這些段的地址在uboot代碼重定位的時候需要用到,將uboot源碼進行編譯后,會在源碼根目錄生成u-boot.lds鏈接文件和u-boot.map內存映射文件,通過這兩個文件,可以尋找到uboot段的地址,一些重要的段地址如下表格所示:
變量 | 數值 | 描述 |
_start | 0x87800000 | uboot入口地址 |
__image_copy_start | 0x87800000 | uboot拷貝的開始地址 |
__image_copy_end | 0x87868960 | uboot拷貝的結束地址 |
__rel_dyn_start | 0x87868960 | .rel.dyn段開始地址 |
__rel_dyn_end | 0x87871af0 | .rel.dyn段結束地址 |
_image_binary_end | 0x87871af0 | |
__bss_start | 0x87868960 | .bss段開始地址 |
__bss_end | 0x878b4c34 | .bss段結束地址 |
在上面尋找到的變量值中,除了_start和__image_copy_start值不會該變,當修改了uboot的源碼或改變了編譯條件,其他的變量都可能會發生改變,因此分析時,一定要以實際編譯時進行uboot分析。
3、relocate_code函數
在調用relocate_code函數之前的分析,可以參考《文章Uboot啟動流程分析(二)》,鏈接如下所示:
https://www.cnblogs.com/Cqlismy/p/12002764.html
也就是_main函數執行的第二部分,relocate_code函數會傳入一個參數,該參數為gd->relocaddr,也就是uboot重定位的目的地址。
接下來,對relocate_code函數進行分析,該函數用於重定位uboot,函數的定義在下面的匯編文件中:
uboot/arch/arm/lib/relocate.S
relocate_code函數的定義如下所示:
/* * 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 */ //r1保存源image開始地址 //r0 = gd->relocaddr,r4 = r0 - r1 = 0x8ff3b000 - 0x87800000 = 0x873b000 subs r4, r0, r1 /* r4 <- relocation offset */ //r4 = gd->reloc_off,保存偏移地址 beq relocate_done /* skip relocation */ //如果r0和r1相等,則不需要uboot重定位 ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */ //r2保存源image結束地址 copy_loop: ldmia r1!, {r10-r11} /* copy from source address [r1] */ //拷貝uboot代碼到r10和r11 stmia r0!, {r10-r11} /* copy to target address [r0] */ //將uboot代碼寫到目的地址 cmp r1, r2 /* until source end address [r2] */ //判斷uboot是否拷貝完成 blo copy_loop //循環拷貝 /* * fix .rel.dyn relocations //修復.rel.dyn段 */ 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: ENDPROC(relocate_code)
relocate_code函數的分析主要分兩個部分,第一部分是從__image_copy_start到__image_copy_end的重定位,該部分的代碼如下:
ldr r1, =__image_copy_start/* r1 <- SRC &__image_copy_start */ //r1保存源image開始地址 //r0 = gd->relocaddr,r4 = r0 - r1 = 0x8ff3b000 - 0x87800000 = 0x873b000 subs r4, r0, r1 /* r4 <- relocation offset */ //r4 = gd->reloc_off,保存偏移地址 beq relocate_done /* skip relocation */ //如果r0和r1相等,則不需要uboot重定位 ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */ //r2保存源image結束地址 copy_loop: ldmia r1!, {r10-r11} /* copy from source address [r1] */ //拷貝uboot代碼到r10和r11 stmia r0!, {r10-r11} /* copy to target address [r0] */ //將uboot代碼寫到目的地址 cmp r1, r2 /* until source end address [r2] */ //判斷uboot是否拷貝完成 blo copy_loop //循環拷貝
函數傳入的參數為r0 = 0x8ff3b000,uboot重定位的目的地址,函數進來后,將__image_copy_start的數值賦值給r1,也就是uboot復制開始地址,從表格可以知道r1 = 0x87800000,接下來進行一個減法操作,此時r4將保存着uboot的偏移量,也就是r4 = gd->reloc_off。
接下來會進行一個判斷,判斷uboot重定位的目的地址是否和uboot源地址是否相等,如果相等的話,表示不需要重定位,跳到relocate_done處,繼續運行,如果不相等的話,則是需要進行重定位。
將__image_copy_end的值賦值給r2,也就是uboot復制結束的地址,從表格中可以知道,此時r2 = 0x87868960,接下來,開始進行uboot代碼的拷貝,進入到copy_loop循環,從r1,也就是__image_copy_start開始,讀取uboot代碼到r10和r11寄存器,一次就拷貝兩個32位的數據,r1自動更新,保存下一個需要拷貝的地址,然后將r10和r11里面的數據,寫到r0保存的地址,也就是uboot重定位的目的地址,r0自動更新。
接下來,比較r1和r2是否相等,也就是判斷uboot代碼是否拷貝完成,如果沒完成的話,跳轉到copy_loop處,繼續循環,直到uboot拷貝完成。
函數relocate_code()的第二部分是修復.rel.dyn的定位問題,.rel.dyn段存放了.text段中需要重定位地址的集合,uboot重定位就是uboot將自身拷貝到DRAM的另外一個地址繼續運行(DRAM的高地址),一個可執行的bin文件,它的鏈接地址和運行地址一定要相等,也就是鏈接到哪個地址,運行之前就需要拷貝到哪個地址中進行運行,在前面的重定位后,運行地址和鏈接地址就不一樣了,這樣在進行尋址就會出問題,對.rel.dyn的定位問題進行修復,就是為了解決該問題。
下面,使用一個簡單的例子進行分析:
先在新適配的板級文件中添加測試代碼,文件如下:
uboot/board/freescale/mx6ul_comp6ul_nand/mx6ul_comp6ul_nand.c
添加的內容如下:
static int rel_a = 0; void rel_test(void) { rel_a = 100; printf("rel_test\n"); } /* 在board_init()函數中調用rel_test()函數 */ int board_init(void) { ... ... rel_test(); return 0; }
接下來對uboot源碼重新編譯,並將u-boot文件進行反匯編分析,使用命令如下:
$ cd uboot $ make $ arm-linux-gnueabihf-objdump -D -m arm u-boot > u-boot.dis
然后,打開u-boot.dis文件,並在文件中,找到rel_a變量、rel_test函數和board_init函數相關的匯編代碼,如下所示:
/* rel_test尋址分析 */ 8780424c <rel_test>: 8780424c: e59f300c ldr r3, [pc, #12] ; 87804260 <rel_test+0x14> 87804250: e3a02064 mov r2, #100 ; 0x64 87804254: e59f0008 ldr r0, [pc, #8] ; 87804264 <rel_test+0x18> 87804258: e5832000 str r2, [r3] 8780425c: ea00f1a9 b 87840908 <printf> 87804260: 87868994 ; <UNDEFINED> instruction: 0x87868994 87804264: 878494ce strhi r9, [r4, lr, asr #9] ... ... 878043e4: e59f2038 ldr r2, [pc, #56] ; 87804424 <board_init+0x1bc> 878043e8: e5923068 ldr r3, [r2, #104] ; 0x68 878043ec: e3833030 orr r3, r3, #48 ; 0x30 878043f0: e5823068 str r3, [r2, #104] ; 0x68 878043f4: ebffff94 bl 8780424c <rel_test> ... ... 87868994 <rel_a>: 87868994: 00000000 andeq r0, r0, r0
在0x878043f4處,也就是board_init()函數中調用了rel_test()函數,在匯編代碼中,可以看到是使用了bl指令,而bl指令是相對尋址的(pc + offset),為位置無關指令,因此,可以知道,uboot中的函數調用時與絕對位置無關的。
接下來,分析rel_test()函數對全局變量rel_a的調用過程,在0x8780424c處開始,先設置r3的值為pc + 12地址處的值,由於ARM流水線的原因,當前pc的值為當前的地址加8,所以pc = 0x8780424c + 8 = 0x87804254,因此r3 = pc + 12 = 0x87804254 + 12 = 0x87804260,從反匯編的代碼中可以看到0x87804260處的值為0x87868994,所以,最終r3的值為0x87868994,而且從反匯編代碼中可以看到0x87868994就是變量rel_a的地址,rel_test()函數繼續執行,將100賦值到r2寄存器,然后通過str指令,將r2的值寫到r3的地址處,也就是將0x87868994地址處的值賦值100,就是函數中調用的rel_a = 100;語句。
從上面的分析,可以知道rel_a = 100;的執行過程為:
- 在函數rel_test()的末尾處有一個地址為0x87804260的內存空間,此內存空間中保存着變量rel_a的地址;
- 函數rel_test()想要訪問變量rel_a,首先需要訪問0x87804260內存空間獲取變量rel_a的地址,而訪問0x87804260是通過偏移來訪問的,與位置無關的操作;
- 通過訪問0x87804260獲取變量rel_a的地址,然后對變量rel_a進行賦值操作;
- 函數rel_test()對變量rel_a的訪問並沒有直接進行,而是通過一個偏移地址0x87804260,找到變量rel_a真正的地址,然后才對其操作,偏移地址的專業術語為Label。
從分析中,可以知道,該偏移地址就是uboot重定位運行是否會出錯的原因,在上面可以知道,uboot重定位的偏移量為0x8ff3b000,將上面給出的反匯編代碼進行重定位后,也就是加上地址偏移量0x873b000后,如下:
/* rel_test尋址分析(重定位后) */ 8ff3f24c <rel_test>: 8ff3f24c: e59f300c ldr r3, [pc, #12] 8ff3f250: e3a02064 mov r2, #100 ; 0x64 8ff3f254: e59f0008 ldr r0, [pc, #8] 8ff3f258: e5832000 str r2, [r3] 8ff3f25c: ea00f1a9 b 87840908 <printf> 8ff3f260: 87868994 ; <UNDEFINED> instruction: 0x87868994 8ff3f264: 878494ce strhi r9, [r4, lr, asr #9] ... ... 8ff3f3e4: e59f2038 ldr r2, [pc, #56] 8ff3f3e8: e5923068 ldr r3, [r2, #104] ; 0x68 8ff3f3ec: e3833030 orr r3, r3, #48 ; 0x30 8ff3f3f0: e5823068 str r3, [r2, #104] ; 0x68 8ff3f3f4: ebffff94 bl 8780424c <rel_test> ... ... 8ffa3994 <rel_a>: 8ffa3994: 00000000 andeq r0, r0, r0
函數rel_test()假設調用后對rel_a變量進行訪問,分析和上面一樣,先通過pc和偏移量找到0x8ff3f260,該地址是重定位后的地址,可此時該地址的值為0x87868994,也就是沒有重定位后的rel_a變量的地址,但是從上面可以知道,uboot重定位后,rel_a變量的新地址為0x8ffa3994,因此,我們可以知道,如果uboot重定位后,要想正常訪問rel_a變量,必須要將0x8ff3f260地址中的值0x87868994加上偏移量0x873b000,才能保證uboot重定位后,能正常訪問到rel_a變量,將.rel.dyn段進行重定位修復,就是為了解決鏈接地址和運行地址不一致的問題。
在uboot中,對於重定位后鏈接地址與運行地址不一致的解決辦法就是使用位置無關碼,在uboot編譯使用ld鏈接的時候使用參數"-pie"可生成與位置無關的可執行程序,使用該參數后,會生成一個.rel.dyn段,uboot則是靠該段去修復重定位后產生的問題的,在uboot的反匯編文件中,有.rel.dyn段代碼,如下:
Disassembly of section .rel.dyn: 87868988 <__rel_dyn_end-0x91a0>: 87868988: 87800020 strhi r0, [r0, r0, lsr #32] 8786898c: 00000017 andeq r0, r0, r7, lsl r0 87868990: 87800024 strhi r0, [r0, r4, lsr #32] 87868994: 00000017 andeq r0, r0, r7, lsl r0 ... ... 87868f08: 87804260 strhi r4, [r0, r0, ror #4] 87868f0c: 00000017 andeq r0, r0, r7, lsl r0 87868f10: 87804264 strhi r4, [r0, r4, ror #4] 87868f14: 00000017 andeq r0, r0, r7, lsl r0 ... ...
.rel.dyn段的格式如下,類似下面的兩行就是一組:
87868f08: 87804260 strhi r4, [r0, r0, ror #4] 87868f0c: 00000017 andeq r0, r0, r7, lsl r0
也就是兩個4字節數據為一組,其中高4字節是Label地址的標識0x17,低4字節就是Label的地址,首先會判斷Label地址標識是否正確,也就是判斷高4字節是否為0x17,如果是的話,低4字節就是Label地址,在上面給出的兩行代碼中就是存放rel_a變量地址的Label,0x87868f0c的值為0x17,說明低4字節是Label地址,也就是0x87804260,需要將0x87804260 + offset處的值改為重定位后的變量rel_a地址。
在對.rel.dyn段以及Label的相關概念有一定的了解后,接下來,我們分析函數relocate_code()的第二部分代碼,修復.rel.dyn段重定位問題,代碼如下:
/* * fix .rel.dyn relocations //修復.rel.dyn段 */ 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_code()函數第二部分代碼調用后,將__rel_dyn_start的值賦給r2,也就是r2中保存着.rel.dyn段的起始地址,然后將__rel_dyn_end的值賦給r3,也就是r3中保存着.rel.dyn段的結束地址。
接下來,進入到fixloop處執行,使用ldmia指令,從.rel.dyn段起始地址開始,每次讀取兩個4字節數據存放到r0和r1寄存器中,其中r0存放低4字節的數據,也就是Label的地址,r1存放高4字節的數據,也就是Label的標識,然后將r1的值與0xff相與,取r1值的低8位,並將結果保存到r1中,接下來,判斷r1中的值是否等於23(0x17),如果r1不等於23的話,也就說明不是描述Label,跳到fixnext處循環執行,直到r2和r3相等,也就是遍歷完.rel.dyn段。
如果r1的值等於23(0x17)的話,繼續執行,r0保存着Label地址,r4保存着uboot重定位的偏移,因此,r0 + r4就得到了重定位后的Label地址,也就是上面分析的0x87804260 + 0x873b000 = 0x8ff3f260 = r0,此時r0已經保存着重定位后的Label地址,然后使用ldr指令,讀取r0中保存的值到r1中,也就是Label地址所保存的rel_a變量的地址,但是此時,該rel_a變量的地址仍然是重定位之前的地址0x87868994,所以,此時r1 = 0x87868994,接下來,使用add指令,將r1中的值加上r4,也就是加上uboot重定位偏移量,所以,此時r1 = 0x87868994 + 0x873b000 = 0x8ffa3994,這不就是前面分析的uboot重定位后的rel_a變量的地址嗎?接下來使用str指令,將r1中的值寫入到r0保存的地址中,也就是將Label地址中的值設置為0x8ffa3994,就是重定位后rel_a變量的新的地址。
比較r2和r3的值,查看.rel.dyn段重定位修復是否完成,循環直到完成,才能執行完relocate_code()函數。
第二部分執行完成后,就解決了.rel.dyn段的重定位問題,從而解決了uboot重定位后,鏈接地址和運行地址不一致的問題。
4、relocate_vectors函數
執行完relocate_code函數后,接下來就是執行relocate_vectors函數,該函數用於重定位中斷向量表,該函數的定義同樣在匯編文件:
uboot/arch/arm/lib/relocate.S
函數的定義如下所示:
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)
在該函數中,對於i.mx6ul芯片來說CONFIG_CPU_V7M沒有定義,接下來,判斷是否定義了CONFIG_HAS_VBAR,表示向量表偏移,在.config文件有該配置,因此,會執行該定義相關的代碼,首先是將gd->relocaddr的值賦給r0,也就是r0里面保存重定位后uboot的首地址,中斷向量表也是從這個首地址開始存儲的,接下來,使用mcr指令,將r0的值寫到CP15的VBAR寄存器中,其實就是將新的中斷向量表首地址寫到VBAR寄存器中,設置中斷向量表偏移。
5、小結
本篇文章主要是對Uboot啟動流程中,uboot重定位的流程進行簡單分析,核心是對relocate_code函數和relocate_vectors函數的分析。