重定位和連結
連結和重定位是嵌入式C中很重要的部分,對於這一塊掌握的越精細越好。
指令位置分類
指令分為兩種:
- 位置無關編碼(PIC):彙編原始檔被編碼成二進位制可執行程式時編碼方式與位置(記憶體地址)無關
- 位置相關編碼:彙編原始檔被編碼成二進位制可執行程式時編碼方式與位置(記憶體地址)相關
在程式設計編譯連結過程會給程式一個執行地址,而且必須給編譯聯結器指定這個地址,最後得到的二進位制程式是和指定的連結地址相關的,這個地址叫做”連結地址”。
所以我們在程式編譯時其實就已經知道程式將來執行時的地址,這個地址叫做”執行地址”,執行地址和連結地址相關,但是不一定是同一個,程式執行時必須放在指定的連結地址下,否則不能執行,這些程式指令就是位置相關程式碼,我們之前使用的ld連結器指定的“-Ttext 0x0”就是這個作用,意味著這個程式將來會放在0x0地址去執行。
但是有個別的指令可以和連結地址無關,這些程式碼在實際執行時放在哪裡都可以正常執行,這些指令就是位置無關指令。
連結地址和執行地址可能相同也可能不同,例如我們的“-Ttext 0x0”期望在0x0地址執行,但是實際上我們程式是在記憶體地址0xd0020010位置執行的,這兩個地址看起來不同,但實際上是同一個,因為S5PV210內部做了對映,把SRAM0xd0020000位置對映到了0x0這個地址,因為我們把程式碼燒寫在了這個記憶體區域,但是大部分的指令都是位置相關程式碼,這就決定了執行地址和連結地址必須相同,否則一定出錯。
在S5PV210中,三星推薦的啟動方式,bootloader必須小於96K,並且必須大於16K,如果bootloader是80K,則啟動過程為:
- 上電執行iROM中的BL0,BL0載入bootloader中的前16K到SRAM中作為BL1來執行,
- BL1執行時載入剩餘的BL2部分,也就是80-16=64K,到SRAM中去執行
- BL2執行會初始化DDR並將OS搬運到DDR中去執行,
在Uboot啟動過程中,Uboot的大小是沒有限制的,假設Uboot是200K,則啟動過程為:
- 上電執行iROM中的BL0,BL0載入Uboot中的前16K到SRAM中作為BL1來執行,
- BL1執行時初始化DDR,然後將整個Uboot搬運到DDR中,然後使用長跳轉指令從SRAM跳轉到DDR中繼續執行Uboot16K之後剩餘的部分,直到Uboot完全啟動。
- Uboot啟動後,在Uboot中使用命令啟動OS。
為什麼需要重定位?
連結地址和執行地址有時候不能相同,而且不能全部使用位置無關指令,則需要重定位來解決該問題,
執行地址和連結地址
執行地址是在程式執行時決定的,在編譯連結階段沒有權利也沒有辦法決定程式的執行地址。
連結地址是程式在編譯連結階段由-Ttext或者lds連結檔案決定,程式在編譯連線階段,程式設計師期望程式執行在一個合適的地址,就把這個地址作為程式的連結地址。
程式編譯步驟
- 預編譯:執行巨集擴散,檔案包含,條件編譯和註釋處理等動作,為編譯做準備
- 編譯:將原始碼編譯為機器目的碼,檔案以.o結尾,目的碼中包含了程式最初的二進位制程式,只是還沒有被連結起來
- 連結:將目的碼按照-Ttext或者lds連結成可執行的二進位制程式整體,連結時按照連結指令碼指定的規則來進行
- (可選)strip:刪除二進位制可執行程式中的符號表,也就是函式表,以減少程式體積,符號表可以在程式反編譯的時候提供程式的函式資訊,但是在實際執行中符號表是沒有用的
- (可選)objcopy:將二進位制可執行程式轉換為可下載燒寫的bin檔案,以便燒寫到NAND Flash或者其他介質中
程式段
程式段是程式在編譯之後由原始碼得到的程式的組成部分,每個二進位制程式被分成了若干段,並對每個段命名,段名分為兩種:
- 內建段名:由編譯聯結器內部的命名規則對段的命名
- 自定義段名:由程式設計師自己命名的段名
內建段名一般有以下幾種:
- 程式碼段(.text):又稱作文字段,程式中函式被編譯之後會被放在程式碼段中
- 資料段(.data):程式中被顯示初始化且值不為0的全域性變數會被編譯進資料段
- BSS段(.bss):又稱作ZI(zero initial)程式中,沒有被初始化的全域性變數或者初始化為預設值的全域性變數會被編譯進BSS段
連結指令碼
連結指令碼指定了連結的地址和規則,整個連結操作都按照連結指令碼指定的規則進行,程式設計師通過連結指令碼來指揮連結器來處理.o目的碼的段,將其連結到合適的位置,從而形成可執行的程式,連結指令碼中關鍵內容有:
- 段名:定位.o目的碼中的段
- 地址:作為連結地址的記憶體地址,
將指定的段名防止到指定的連線地址,就完成了該段的連結.
連線指令碼示例:
SECTIONS
{
. = 0xd0024000; // .代表當期位置
.text : {
start.o
* (.text)
}
.data : {
* (.data)
}
bss_start = .;
.bss : {
* (.bss)
}
bss_end = .;
}
連線指令碼由一個或者多個SECTIONS{}組成:
. = 0xd0024000;
表示將當前位置的地址設定為0xd0024000,.text : {}
表示該段為程式碼段,下面的.data : {},.bss : {}等同start.o
表示start.o在前面,所以start.S會被首先執行*(.text)
匹配模式,表示剩餘的程式碼都屬於程式碼段並依次排列bss_start = .;
表示將bss_start的值設定為當前地址,bss_end等同,這兩個符號之所以定義在這裡是為了可以在別的檔案中引用,以知道bss段的起止地址
程式碼重定位
目標:將程式碼從0xd0020010重定位到0xd0024000,
程式碼本來是從0xd0020010地址開始執行的,因為BL1從這裡開始執行,但是因為特殊原因,我們又需要程式碼從0xd0024000開始執行,這時候就需要重定位,在某些情況下,重定位是必須的,例如Uboot.
思路
- 通過連結指令碼把程式碼連結到0xd0024000,因為這個地址是我們期望執行的地址,將上面的連結程式碼作為link.lds連結,原來的-Ttext 0x0修改為-Tlink.lds即可
- 燒寫的時候,將程式碼燒寫到0xd0020010,在dnw的options中設定燒寫地址,這樣程式碼實際執行在0xd0020010,但是卻連結在0xd0024000,為重定位做了準備
- 在位置無關程式碼執行完畢之前和位置相關程式碼開始之前,必須將程式碼搬移到連結地址上去,否則後面的位置相關程式碼將會出錯,所以才需要進行重定義
- 使用一個長跳轉跳轉到0xd0024000處的程式碼繼續執行,重定位完成
長跳轉
長跳轉也就是一個跳轉,在ARM中常用分支指令(b,bl)來完成短跳轉,跳轉通常是指令通過給PC暫存器賦值,從而完成程式碼跳轉執行,長跳轉和普通跳轉的區別在於長跳轉的目標地址和當前地址的距離較大,範圍較寬
重定位之後為什麼要使用長跳轉?
程式碼重定位之後,我們需要去程式碼被拷貝的目的地的程式碼區域執行,這個 目標地址通常會距離我們當前地址較遠,所以才需要使用重定位,長跳轉示例:ldr pc,=led_blink
,這樣我們就會跳轉到目的地的led_blink函式來執行了,這裡不能使用短跳轉(b,bl),短跳轉會跳轉到執行地址附近的led_blink函式,只有使用長跳轉,才能跳轉到連結地址上的led_blink
重定位
重定位其實就是,在執行地址位置執行一段位置無關程式碼,這段程式碼將整個程式拷貝到連結地址上,然後使用一個長跳轉,跳轉到連結地址的對應函式上去,在連結地址上繼續執行,從而實現重定位之後的無縫連線。
重定位實現
重定位程式碼如下:
// 重定位開始
adr r0,_start // adr 用於載入_start執行地址到r0中
ldr r1,=_start // ldr 載入_start的連結地址到r1中
// 獲取BSS段起始地址,等於重定位程式碼的結束地址
ldr r2,=bss_start // bss_start是在lds連結指令碼中指定的
cmp r0,r1 // 比較_start的執行地址和連結地址
beq clean_bss // 如果相等就直接跳轉到clean_bss函式
// 如果上一句不相等,則進入程式碼拷貝階段
copy_loop:
ldr r3,[r0],#4 // 拷貝源
str r3,[r1],#4 // 儲存到目的,長度是bss_start - _start,程式碼段+資料段的長度
cmp r1,r2 // 比較拷貝是否完成,也就是到沒到重定位程式碼的結束地址
bne copy_loop // 拷貝沒有完成就繼續迴圈
// 如果拷貝完成,進入clean_bss函式,該函式用於清理bss
clean_bss:
ldr r0,bss_start // 為了滿足C語言的要求,需要清理BSS的
ldr r1,bss_end // 一般是不需要手動清理的,這裡我們做了重定位,所以需要手動清理
cmp r0,r1 // 比較bss段起始地址和結束地址
beq run_on_dram // 如果相等則直接進入run_on_dram函式
mov r2,#0 // 不相等的話就把r2設定為0
clear_loop: // 開始清理bss
str r2,[r0],#4 // 把r2賦值給r0然後r0+=4
cmp r0,r1
bne clear_loop // 迴圈,直到bss_end
// 從這一句開始就是位置相關程式碼了,必須在指定的連線地址開始執行
// 所以在這一句之前,必須保證程式碼已經拷貝完畢
run_on_dram:
// 長跳轉到led_blink開始第二階段
ldr pc,=led_blink // 長跳轉
// 重定位完畢
重定位放置在icache開關之後。
SDRAM
SDRAM是DRAM的一種,前面的S代表Sync,表示具有可同步性,常見的DDR就是SDRAM的一種,SDRAM屬於動態記憶體,需要進行初始化之後才能使用,這點和SRAM是有很大的不同。
SDRAM屬於SOC外部外設,通過地址匯流排和資料匯流排和SOC通訊。
SDRAM記憶體地址
SDRAM使用之前必須要初始化,S5PV210共有兩個記憶體埠,分別是DRAM0和DRAM1,地址範圍分別是:
- DRAM0對應Port1:0x20000000-0x3fffffff,大小為512MB,引腳為xm1
- DRAM1對應Port2:0x40000000-0x7fffffff,大小為1G,引腳為xm2
可以看到整個S5PV210最多支援1.5G記憶體,X210開發板上共有512MB記憶體,DRAM0/1上各有256MB,所以可以得到X210上記憶體地址為:
- DRAM0:0x20000000到0x2fffffff,大小為256MB
- DRAM1:0x40000000到0x4fffffff,大小為256MB
DDR初始化完畢之後,這些地址都是可用的,其他地址則不可用。
每個DDR埠都有三類匯流排組成,分別是地址匯流排ADDR14個,控制匯流排,資料匯流排32個。
X210開發板共使用了4片記憶體,每片128MB,資料匯流排為16bit,通過兩個記憶體之間進行並聯,從而實現了資料匯流排32bit。X210上的每片記憶體分為8個Bank,CPU通過DDR埠中的BA0-BA2來選擇Bank,每個Bank有128Mbit的範圍,通過行地址和列地址進行定址,定址範圍是2的24次方(16MB = 128Mbit)。
程式碼初始化SDRAM
在進行重定位之前,先去執行SDRAM初始化,可以直接跳轉到SDRAM的初始化函式:
// 初始化SDRAM
bl sdram_asm_init // bl短跳轉到sdram_asm_init函式進行SDRAM初始化
SDRAM的初始化需要先初始化DRAM0,然後初始化DRAM1,步驟在手冊的1.2.1.3
- 設定IO埠驅動強度,DDR和SOC通過匯流排連結,物理表現上就是引腳,工作時需要驅動訊號,驅動強度就是設定了該訊號的強度
- 設定埠時鐘,設定PHYCONTROL暫存器,從而使記憶體晶片的時鐘和CPU的時鐘保持一致
- 設定DDR2,DMC_CONCONTROL,DMC_MEMCONTROL,DMC_MEMCONFIG等一系列暫存器
DRAM初始化之後可以使用DRAM進行重定位,步驟和之前完全一致。