1.為什么需要重定位
位置無關編碼(PIC,position independent code):匯編源文件被編碼成二進制可執行程序時編碼方式與位置(內存地址)無關。
位置有關編碼:匯編源碼編碼成二進制可執行程序后和內存地址是有關的。
我們在設計一個程序時,會給這個程序指定一個運行地址(鏈接地址)。就是說我們在編譯程序時其實心里是知道我們程序將來被運行時的地址(運行地址)的,而且必須給編譯器鏈接器指定這個地址(鏈接地址)才行。最后得到的二進制程序理論上是和你指定的運行地址有關的,將來這個程序被執行時必須放在當時編譯鏈接時給定的那個地址(鏈接地址)下才行,否則不能運行(就叫位置有關代碼)。但是有個別特別的指令他可以跟指定的地址(鏈接地址)沒有關系,也就是說這些代碼實際運行時不管放在哪里都能正常運行。
對於位置有關代碼來說:最終執行時的運行地址和編譯鏈接時給定的鏈接地址必須相同,否則一定出錯。
之前的裸機程序中,Makefile中用 -Ttext 0x0 來指定鏈接地址是0x0。這意味着我們認為這個程序將來會放在0x0這個內存地址去運行。
但是實際上我們運行時的地址是0xd0020010(我們用dnw下載時指定的下載地址)。這兩個地址看似不同,但是實際相同。這是因為S5PV210內部做了映射,把SRAM映射到了0x0地址去。
分清楚這兩個概念:
鏈接地址:鏈接時指定的地址(指定方式為:Makefile中用-Ttext,或者鏈接腳本)
運行地址:程序實際運行時地址(指定方式:由實際運行時被加載到內存的哪個位置說了算)
2.再解S5PV210的啟動過程:三星推薦和uboot的實現是不同的
三星推薦的啟動方式中:bootloader必須小於96KB並大於16KB,假定bootloader為80KB,啟動過程是這樣子:先開機上電后BL0運行,BL0會加載外部啟動設備中的bootloader的前16KB(BL1)到SRAM中去運行,BL1運行時會加載BL2(bootloader中80-16=64KB)到SRAM中(從SRAM的16KB處開始用)去運行;BL2運行時會初始化DDR並且將OS搬運到DDR去執行OS,啟動完成。
uboot實際使用的方式:uboot大小隨意,假定為200KB。啟動過程是這樣子:先開機上電后BL0運行,BL0會加載外部啟動設備中的uboot的前16KB(BL1)到SRAM中去運行,BL1運行時會初始化DDR,然后將整個uboot搬運到DDR中,然后用一句長跳轉(從SRAM跳轉到DDR)指令從SRAM中直接跳轉到DDR中繼續執行uboot直到uboot完全啟動。uboot啟動后在uboot命令行中去啟動OS。
鏈接地址和運行地址有時候必須不相同,而且還不能全部用位置無關碼,這時候只能重定位。
擴展:
分散加載:把uboot分成2部分(BL1和整個uboot),兩部分分別指定不同的鏈接地址。啟動時將兩部分加載到不同的地址(BL1加載到SRAM,整個uboot加載到DDR),這時候不用重定位也能啟動。
評價:分散加載其實相當於手工重定位。重定位是用代碼來進行重定位,分散加載是手工操作重定位的。
3.對比操作系統下的程序與裸機程序
linux中的應用程序。gcc hello.c -o hello,這時使用默認的鏈接地址就是0x0,所以應用程序都是鏈接在0地址的。因為應用程序運行在操作系統的一個進程中,在這個進程中這個應用程序獨享4G的虛擬地址空間。所以應用程序都可以鏈接到0地址,因為每個進程都是從0地址開始的。(編譯時可以不給定鏈接地址而都使用0)
210中的裸機程序。運行地址由我們下載時確定,下載時下載到0xd0020010,所以就從這里開始運行。(這個下載地址也不是我們隨意定的,是iROM中的BL0加載BL1時事先指定好的地址,這是由CPU的設計決定的)。所以理論上我們編譯鏈接時應該將地址指定到0xd0020010,但是實際上我們在之前裸機程序中都是使用位置無關碼PIC,所以鏈接地址可以是0。
4.關於鏈接
從源碼到可執行程序的步驟:預編譯、編譯、鏈接、strip
預編譯:預編譯器執行。譬如C中的宏定義就是由預編譯器處理,注釋等也是由預編譯器處理的。
編譯: 編譯器來執行。把源碼.c .S編程機器碼.o文件。
鏈接: 鏈接器來執行。把.o文件中的各函數(段)按照一定規則(鏈接腳本來指定)累積在一起,
形成可執行文件。
strip: strip是把可執行程序中的符號信息給拿掉,以節省空間。(Debug版本和Release版本)
objcopy:由可執行程序生成可燒錄的鏡像bin文件。
程序段的概念:代碼段、數據段、bss段(ZI段)、自定義段
段就是程序的一部分,我們把整個程序的所有東西分成了一個一個的段,給每個段起個名字,然后在鏈接時就可以用這個名字來指示這些段。也就是說給段命名就是為了在鏈接腳本中用段名來讓段站在核實的位置。
段名分為2種:一種是編譯器鏈接器內部定好的,先天性的名字;一種是程序員自己指定的、自定義的段名。
先天性段名:
代碼段:(.text),又叫文本段,代碼段其實就是函數編譯后生成的東西
數據段:(.data),數據段就是C語言中有顯式初始化為非0的全局變量
bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,對應C語言中初始化為0的全局變量。
后天性段名:
5、鏈接腳本究竟要做什么?
鏈接腳本其實是個規則文件,他是程序員用來指揮鏈接器工作的。鏈接器會參考鏈接腳本,並且使用其中規定的規則來處理.o文件中那些段,將其鏈接成一個可執行程序。
鏈接腳本的關鍵內容有2部分:段名 + 地址(作為鏈接地址的內存地址)
鏈接腳本的理解:
SECTIONS {} 這個是整個鏈接腳本
. 點號在鏈接腳本中代表當前位置。
= 等號代表賦值
6.代碼重定位實戰(理論分析)
任務:在SRAM中將代碼從0xd0020010重定位到0xd0024000
任務解釋:本來代碼是運行在0xd0020010的,但是因為一些原因我們又希望代碼實際是在0xd0024000位置運行的。這時候就需要重定位了。
注解:本練習對代碼本身運行無實際意義,我們做這個重定位純粹是為了練習重定位技能。但是某些情況重定位就是必須的,譬如在uboot中。
思路:
第一點:通過鏈接腳本將代碼鏈接到0xd0024000
第二點:dnw下載時將bin文件下載到0xd0020010
第一點加上第二點,就保證了:代碼實際下載運行在0xd0020010,但是卻被鏈接在0xd0024000。從而為重定位奠定了基礎。
當我們把代碼鏈接地址設置為0xd0024000時,實際隱含意思就是我這個代碼將來必須放在0xd0024000位置才能正確執行。如果實際運行地址不是這個地址就要出事(除非代碼是PIC位置無關碼),當以上都明白了后,就知道重定位代碼的作用就是:在PIC執行完之前(在代碼中第一句位置有關碼執行之前)必須將整個代碼搬移到0xd0024000位置去執行,這就是重定位。
第三點:代碼執行時通過代碼前段的少量位置無關碼將整個代碼搬移到0xd0024000
第四點:使用一個長跳轉跳轉到0xd0024000處的代碼繼續執行,重定位完成
總結:
重定位實際就是在運行地址處執行一段位置無關碼PIC,讓這段PIC(也就是重定位代碼)從運行地址處把整個程序鏡像拷貝一份到鏈接地址處,
完了之后使用一句長跳轉指令從運行地址處直接跳轉到鏈接地址處去執行同一個函數(led_blink),這樣就實現了重定位之后的無縫連接。
7.代碼重定位實戰(代碼)
adr與ldr偽指令的區別
ldr和adr都是偽指令,區別是ldr是長加載、adr是短加載。
重點:adr指令加載符號地址,加載的是運行時地址;ldr指令在加載符號地址時,加載的是鏈接地址。
第一步:重定位(代碼拷貝)
重定位就是匯編代碼中的copy_loop函數,代碼的作用是使用循環結構來逐句復制代碼到鏈接地址。
復制的源地址是SRAM的0xd0020010,復制目標地址是SRAM的0xd0024000,復制長度是bss_start減去_start
所以復制的長度就是整個重定位需要重定位的長度,也就是整個程序中代碼段+數據段的長度。
bss段(bss段中就是0初始化的全局變量)不需要重定位。
第二步:清bss段
清除bss段是為了滿足C語言的運行時要求(C語言要求顯式初始化為0的全局變量,或者未顯式初始化的全局變量的值為0,實際上C語言編譯器就是通過清bss段來實現C語言的這個特性的)。一般情況下我們的程序是不需要負責清零bss段的(C語言編譯器和鏈接器會幫我們的程序自動添加一段頭程序,這段程序會在我們的main函數之前運行,這段代碼就負責清除bss)。但是在我們代碼重定位了之后,因為編譯器幫我們附加的代碼只是幫我們清除了運行地址那一份代碼中的bss,而未清除重定位地址處開頭的那一份代碼的bss,所以重定位之后需要自己去清除bss。
第三步:長跳轉
清理完bss段后重定位就結束了。然后當前的狀況是:
1、當前運行地址還在0xd0020010開頭的(重定位前的)那一份代碼中運行着。
2、此時SRAM中已經有了2份代碼,1份在d0020010開頭,另一份在d0024000開頭的位置。
然后就要長跳轉了。
/* * 文件名: led.s * 作者: 朱老師 * 描述: 演示重定位(在SRAM內部重定位) */ #define WTCON 0xE2700000 #define SVC_STACK 0xd0037d80 .global _start // 把_start鏈接屬性改為外部,這樣其他文件就可以看見_start了 _start: // 第1步:關看門狗(向WTCON的bit5寫入0即可) ldr r0, =WTCON ldr r1, =0x0 str r1, [r0] // 第2步:設置SVC棧 ldr sp, =SVC_STACK // 第3步:開/關icache mrc p15,0,r0,c1,c0,0; // 讀出cp15的c1到r0中 //bic r0, r0, #(1<<12) // bit12 置0 關icache orr r0, r0, #(1<<12) // bit12 置1 開icache mcr p15,0,r0,c1,c0,0; // 第4步:重定位 // adr指令用於加載_start當前運行地址 adr r0, _start // adr加載時就叫短加載 // ldr指令用於加載_start的鏈接地址:0xd0024000 ldr r1, =_start // ldr加載時如果目標寄存器是pc就叫長跳轉,如果目標寄存器是r1等就叫長加載 // bss段的起始地址 ldr r2, =bss_start // 就是我們重定位代碼的結束地址,重定位只需重定位代碼段和數據段即可 cmp r0, r1 // 比較_start的運行時地址和鏈接地址是否相等 beq clean_bss // 如果相等說明不需要重定位,所以跳過copy_loop,直接到clean_bss // 如果不相等說明需要重定位,那么直接執行下面的copy_loop進行重定位 // 重定位完成后繼續執行clean_bss。 // 用匯編來實現的一個while循環 copy_loop: ldr r3, [r0], #4 // 源 將地址為r0的數據給r3.然后r0=r0+4 str r3, [r1], #4 // 目的 這兩句代碼就完成了4個字節內容的拷貝 cmp r1, r2 // r1和r2都是用ldr加載的,都是鏈接地址,所以r1不斷+4總能等於r2 bne copy_loop // 清bss段,其實就是在鏈接地址處把bss段全部清零 clean_bss: ldr r0, =bss_start ldr r1, =bss_end cmp r0, r1 // 如果r0等於r1,說明bss段為空,直接下去 beq run_on_dram // 清除bss完之后的地址 mov r2, #0 clear_loop: str r2, [r0], #4 // 先將r2中的值放入r0所指向的內存地址(r0中的值作為內存地址), cmp r0, r1 // 然后r0 = r0 + 4 bne clear_loop run_on_dram: // 長跳轉到led_blink開始第二階段 ldr pc, =led_blink // ldr指令實現長跳轉 // 從這里之后就可以開始調用C程序了 //bl led_blink // bl指令實現短跳轉 // 匯編最后的這個死循環不能丟 b .
led_blink在c語言程序里,與上章代碼相同不再展開。
鏈接腳本
link.lds
SECTIONS { . = 0xd0024000; //指定鏈接地址 .text : { start.o * (.text) } .data : { * (.data) } bss_start = .; .bss : { * (.bss) } bss_end = .; }
Makefile
led.bin: start.o led.o arm-linux-ld -Tlink.lds -o led.elf $^ //-T后面跟的為鏈接腳本 arm-linux-objcopy -O binary led.elf led.bin arm-linux-objdump -D led.elf > led_elf.dis gcc mkv210_image.c -o mkx210 ./mkx210 led.bin 210.bin %.o : %.S arm-linux-gcc -o $@ $< -c -nostdlib %.o : %.c arm-linux-gcc -o $@ $< -c -nostdlib clean: rm *.o *.elf *.bin *.dis mkx210 -f