文章目錄
本文主要介紹ARM裸機代碼重定位的相關知識,以及重定位的實現過程。
下面將由ARM裸機(S3C2440)的啓動方式開始分析,引入段的概念,隨後介紹鏈接腳本的使用以及代碼重定位的操作,首先會使用匯編語言驗證代碼重定位的可行性,最後將使用C語言實現代碼重定位。
一.啓動方式
S3C2440的啓動方式有倆種:
- NOR FLASH啓動
- NAND FLASH啓動
說起ARM裸機的啓動方式,就是將程序的bin文件燒寫在ARM的存儲空間中,ARM從這些地址中讀取指令到CPU中執行,需要數據的時候去數據的存儲地址取數據。說白了啓動方式的不同就是bin文件燒寫地址的不同,可以燒在NOR FLASH中,也可以燒在NAND FLASH中,倆種FLASH本質上都是是存儲程序的,那爲什麼要區別呢?因爲倆種FALSH的性能不一樣,具體的不同在下面分析。
1.1 NAND FLASH 啓動
下圖是S3C2440的內存關係框圖:CPU(存儲控制器忽略)、SRAM、NAND FLASH控制器,SDRAM、NOR FLASH、NAND FLASH。
可以看出CPU可以直接對地址進行讀寫的外設有:SRAM、NOR FLASH、SDRAM等,不可以直接對NAND FLASH進行讀寫。
要知道,CPU直接對地址進行讀寫意味着CPU可以直接去執行此地址中的機器代碼,所以以NAND FLASH方式啓動的時候,bin文件雖然燒寫在NAND FLASH中,但是CPU無法直接去執行程序,所以硬件會在自動將NAND FLASH中的前4KB代碼拷貝在SRAM中(SRAM的大小爲4KB),CPU去SRAM中執行代碼。
簡單來說,CPU無法直接從NAND FLASH中取代碼來運行:
1.上電後,硬件自動把NAND FLASH的前4K內容拷貝到SRAM中。
2.CPU從0地址開始運行代碼(NAND FLASH啓動時SRAM的地址爲0x00000000)。
當程序的大小超過4KB時,SRAM就不足以放下整個程序,這時候就要用到代碼重定位了,簡單來講就是由程序自身將程序的代碼重新拷貝到SDRAM中去執行程序,接下來我們將仔細講解代碼重定位。
1.2 NOR FLASH 啓動
倆種啓動方式的對比:
- NAND FLASH雖然內存大,但是CPU不可以直接去讀寫,所以需要將前4KB代碼拷貝到SRAM中執行。
- NOR FLASH可以被CPU直接讀寫,意味着代碼可以直接在NOR FLASH中運行,而且NOR FLASH大小爲2MB內存足夠大,但是寫入NOR FLASH中的數據不可以被修改(寫進去的數據不可通過程序的代碼改變),這樣一來,如果有變量存儲在NOR FLASH中,那豈不是成了常量了。
簡單來說,雖然可以在NOR FLASH執行程序,但是其中的變量卻不可以被改變,所以我們有倆種解決方法:
1.將整個程序重定位到SDRAM中執行
2.只將NOR FLASH中的變量重定位到SDRAM中(使變量可以改變)
注意:
並不是程序中的所有變量都隨程序放在NOR FLASH中,局部變量是放在棧中的,而棧指向SRAM,所以局部變量不存在上述的情況。然鵝,全局變量是包含在bin文件中燒寫在NOR FLASH中,所以這樣看來全局變量是不可以被修改的,需要將全局變量重定位到SDRAM中,這涉及到段的概念,下面會講。
以下的討論是以NOR FLASH啓動作爲基礎的,因爲NAND FLASH啓動只要代碼小於4KB就可以不用重定位,NOR FLASH啓動時,只要程序中有全局變量,就要進行重定位,所以重定位的使用頻率較高。
二. 段的概念
上面說了,程序的局部變量存儲在棧(SRAM)中,全局變量跟着代碼包含在bin中燒寫在NOR FLASH中,而且上面說了要把全局變量單獨重定位在SDRAM中,所以我們知道,程序的bin文件是分段的:
-
.text:代碼段,存放代碼
-
.data:數據段,已初始化的全局變量
-
.rodata:只讀數據段,const修飾的全局變量,和代碼段一起寫在bin裏,值不需要改變
-
.bss:初值爲0以及無初值的全局變量,不保存在bin中( 並不給該段的數據分配空間,只是記錄數據所需空間的大小 bss段的大小從可執行文件中得到 ,然後鏈接器得到這個大小的內存塊,緊跟在數據段後面 )
-
.commen:註釋段,不保存在bin中
2.1 重定位數據段
以前我們通過Makefile中的鏈接指令來決定代碼段的位置:
arm-linux-ld -Ttext 0 start.o main.o -o relocation.elf
指令的意思是:通過鏈接指令,將start.o、 main.o倆個文件鏈接在一起生成relocation.elf文件,且代碼段.text存放在0地址。
這裏的-Ttext 0
所指的地址是代碼的運行地址,即CPU運行程序時,就去運行地址中取指令、數據。
注意:
這裏只是
-Ttext 0
,雖然只確定了代碼段的位置,但其他段的存儲地址都緊跟在.text的後面
現在我們將數據段的存儲地址添加進去,將數據段重定位到SDRAM(0x30000000)中:
arm-linux-ld -Ttext 0 -Tdata 0X30000000 start.o main.o -o relocation.elf
意思爲將代碼段放在0地址,將數據段放在0x30000000地址中,也就是SDRAM。(使用SDRAM前要初始化)
2.2 加載地址的引出
經過編譯後發現,生成的bin文件竟然大小爲800多MB,約爲0x30000001Byte,可以看出這是從代碼段到數據段的所有的內存大小,代碼段和數據段之間有一個0x30000000多Byte的內存空間,原因是-Ttext 0 -Tdata 0X30000000
間接確定.text和.data在bin文件中的地址,即確定加載地址。
加載地址:
arm-linux-ld -Ttext 0 -Tdata 0X30000000 start.o main.o -o relocation.elf
中確定的是.text和.data的運行地址,Makefile中默認加載地址=運行地址,加載地址是.text和.data在bin文件中的分佈地址,所以默認.data段在bin文件中的存儲地址就爲0x30000000。
由於bin文件中的段的加載地址,所以.data加載地址的大小影響了bin文件的大小,導致bin文件產生了0x30000000的地址。這樣的bin文件800多M,想都不要想了,肯定是行不通的!
看來Makefile中修改鏈接指令中的地址只能影響運行地址,默認運行地址=加載地址,而加載地址又影響了bin文件的大小,所以爲了不讓加載地址影響bin文件的大小,我們要找出另一種方法來進行重定位!!!這就引出了鏈接腳本!!!
三.鏈接腳本
參考文章:GUN ld
3.1 鏈接腳本的引入
首先要知道鏈接腳本的主要作業是鏈接,有輸入文件,有輸出文件,將輸入文件按照配置鏈接成爲輸出文件,一般輸入文件是.o文件,輸出爲.elf文件。
鏈接腳本的主要格式爲:
SECTIONS
{
...
secname start BLOCK(align)(NOLOAD) : AT ( ldadr )
{ contents}
...
}
解釋如下:
- secname:描述輸出文件的段,比如.text、.data
- start:規定輸出段的運行地址,即規定CPU從哪個地址去取指令、數據
- BLOCK(align):地址對齊,一般4Byte對齊,ALIGN(4)
- AT(ldadr):段在輸出文件中的物理地址,如果沒有使用AT(ldadr),加載地址=start
- contents:描述輸入文件的段從哪裏來,一般來自全部輸入文件的段,比如*(.data)
先用鏈接腳本的方法實現上面那個Makefile的鏈接指令:
arm-linux-ld -Ttext 0 -Tdata 0X30000000 start.o main.o -o relocation.elf
建立鏈接腳本文件:relocation.lds
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : {*(.data)}
.bss : {*(.bss) *(.COMMON)}
}
在Make file中使用relocation.lds 進行鏈接:
arm-linux-ld -T relocation.lds start.o uart.o main.o -o relocation.elf
得到的bin文件大小爲:0x30000001Byte,也證實了上面的分析。
3.2 鏈接腳本的正確打開方法
如果鏈接腳本僅僅是上面那種使用,那就和Makefile的鏈接命令沒有區別了,下面正式介紹鏈接腳本的正確打開方法:
現在修改relocation.lds:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT0x800 {*(.data)}
.bss : {*(.bss) *(.COMMON)}
}
值得注意的是:
.data 0x30000000 : AT0x800 {*(.data)}
將數據段,data的運行地址放在0x30000000,這代表CPU去0x30000000的地址去取.data,也就是SDRAM的地址;然後.data的加載地址則是0x800,即.data實際在bin文件中的位置是0x800,這樣的話現在bin文件的大小爲:0x801Byte(只定義了一個字符全局變量)
康起來好像沒毛病,運行一下,發現此時的運行結果還是輸出亂碼!
原因是.data 加載地址是0x800,但是運行地址是0x30000000,此時還沒有將數據段拷貝到SDRAM(0x30000000),所以CPU按照,data的運行地址直接去取數據,肯定是亂碼!
那要怎麼辦纔可以把 .data 拷貝到運行地址中呢,這才涉及到代碼重定位!說白了就是程序自己把.data從加載地址複製到運行地址!
3.3 鏈接腳本測試
重定位:start.S中,在進入main之前進行重定位,將0x800的內容複製到0x30000000(前提得初始化SDRAM)
修改relocation.lds來控制鏈接:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT
{
data_load_addr = LOADADDR(.data);
data_start = .;
*(.data)
data_end = .;
}
bss_start = .;
.bss :
{
*(.bss)
*(.COMMON)
}
bss_end = .;
}
-
. 代表當前地址
-
data_load_addr:.data段在bin文件中的源地址,即加載地址
-
data_start:是重定位地址,即運行時的地址
-
data_end:是結束地址
重定位就是將.data從data_load_addr地址拷貝到data_start地址
3.4 elf文件
鏈接腳本生成的文件是elf文件
elf文件裏含有這些地址信息,生成的bin文件中已經不含有地址信息了
1.鏈接得到elf文件,含有地址信息:加載地址(AT指定)
2.使用加載器把elf文件解析一下,寫入內存的加載地址:load addr
3.運行程序
4.若加載地址不是運行地址,程序本身要進行重定位
核心:程序運行時應該位於運行地址(或者說是relocate addr、鏈接地址)
3.5 bin文件
elf文件生成bin文件,bin文件可以直接燒寫在ARM中
1.elf生成bin文件
2.燒入裸機後(裸機沒有加載器)硬件機制來啓動
3.若加載地址位置不等於運行地址,程序本身實現重定位
四.重定位
重定位就是將.data從data_load_addr地址拷貝到data_start地址,即從加載地址拷貝到運行地址。
重定位根據連接腳本中的變量來確定各段的加載地址和運行地址:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT
{
data_load_addr = LOADADDR(.data);
data_start = .;
*(.data)
data_end = .;
}
bss_start = .;
.bss :
{
*(.bss)
*(.COMMON)
}
bss_end = .;
}
4.1 start.S 重定位數據段
對數據段.data進行重定位,從加載地址拷貝到運行地址:
ldr r1, = data_load_addr
ldr r2, = data_start
ldr r3, = data_end
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2,r3
bne cpy ;等於
看出來是ldrb從NOR FLASH中讀取1Byte,再strb寫入SDRAM,因爲NOR FLASH位寬16位,SDRAM是32位,所以在倆者之間拷貝數據會耗費CPU的,而且是以Byte爲單位的。
假設拷貝16Byte的數據,則會訪問16次NOR FLASH、訪問16次SDRAM。
利用位寬優勢進行改進:
我們可以使用ldr命令和str命令開拷貝程序,這樣就是以32Bit即4Byte爲單位進行讀寫,可以省很多事。
這樣情況下,拷貝16Byte數據時,執行4次ldr和str命令,訪問8次NOR FLASH、訪問4次SDRAM
這樣就充分利用了 NOR FLASH和SDRAM的位寬優勢,在數據段量大的時候,改進的優勢就會體現出來。
ldr r1, = data_load_addr
ldr r2, = data_start
ldr r3, = data_end
cpy:
ldr r4, [r1]
stb r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2,r3
ble cpy ;小於
這樣的話,加載地址就得對齊了,以4Byte對齊
4.2 start.S 清零.bss段
.bss段存放的是:未初始化的全局變量和初始值爲0的全局變量,實際bin文件中是不會存儲.bss段的,所以要對.bss段清零,不然未初始化的全局變量會打印一些亂碼,就是因爲.bss所指空間不爲零。
然鵝,在運行的過程中遇到問題,.data段的全局變量被清零了,原因是清零BSS段的時候把DATA段也清零了,原BSS段清零代碼如下:
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
str r3, [r1]
add r1, #1
cmp r1, r2
bne clean
因爲使用了str,str操作的單位是4Byte
比如BSS段的地址是30000002,這樣我們就需要 str r3, [30000002],但是實際上str會4Byte對齊的情況下進行str命令,即實際上str r3, [30000000],這樣一來就把.data段的數據也清零了一部分。
處理方法是:以四字節對齊進行清除!!!這就需要改進以下鏈接腳本了,因爲只有鏈接腳本中的加載地址以4Byte對齊,纔不會出現這種情況!
現在全部以4Byte爲單位進行拷貝和清除,提高效率
4.3 鏈接腳本改進
修改鏈接腳本來解決:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT
{
data_load_addr = LOADADDR(.data);
. = ALIGN(4)
data_start = .;
*(.data)
data_end = .;
}
. = ALIGN(4)
bss_start = .;
.bss :
{
*(.bss)
*(.COMMON)
}
bss_end = .;
}
. = ALIGN(4):先將當前地址向4取整,然後將當前地址給bss_start,這樣進行str命令就不會波及到其他段了。
4.4 C語言實現重定位
上面實現的代碼重定位和BSS段清除都是基於彙編語言的,而且也是比較簡單的彙編,這裏以C語言實現這些操作。
可以利用C語言的函數實現之後,在start.S中bl這些函數,通過r0、r1、r2等向C函數傳遞參數。
C語言實現.data重定位需要三個條件:
- 加載地址
- 運行地址
- 長度
void copy2sdram( volatile unsigned int *src, volatile unsigned int *dest, unsigned int len )
{
unsigned int i=0;
while( i<len )
{
*dest++ = *src++;
i += 4;
}
}
但是這樣需要彙編向C函數傳遞參數,可以改進一下,不需要彙編傳參。
需要去鏈接腳本里獲取參數
可以在鏈接腳本首里加入 _code_start = 0
要從lds文件中獲得_code_start,_bss_start
/*要從lds文件中獲得_code_start,_bss_start
*然後從0地址把數據複製到_code_start
*/
void copy2sdram( void )
{
extern int _code_start, _bss_start;
/* 利用符號表獲取加載地址 */
volatile unsigned int *dest = ( volatile unsigned int * )&_code_start;
volatile unsigned int *end = ( volatile unsigned int * )&_bss_start;
volatile unsigned int *src = ( volatile unsigned int * )0; //從0地址複製
while( dest<end )
{
*dest++ = *src++;
}
}
4.5 C語言實現清零.bss段
需要倆個條件:
- .bss的加載地址的起始
- 結束地址
void clean_bss( volatile unsigned int *start, volatile unsigned int *end )
{
while( start <= end )
{
*start++ = 0;
}
}
從鏈接腳本獲取參數:
/*從lds文件中獲取_bss_start、_bss_end
*/
void clean_bss( void )
{
extern int _bss_start, _bss_end;
/* 利用符號表獲取加載地址 */
volatile unsigned int *start = ( volatile unsigned int * )&_bss_start;
volatile unsigned int *end = ( volatile unsigned int * )&_bss_end;
while( start <= end )
{
*start++ = 0;
}
}
4.6 符號表
要注意的幾個點:
- 調用鏈接腳本里面的變量時要聲明爲外部變量extern
- 使用鏈接腳本里的變量時要加上取地址符號&(變量指段的地址)
- 彙編可以直接使用lds文件裏面的變量。
- 藉助符號表保存lds文件的變量,使用時加上&纔可以得到變量的值
符號表:
C程序中不保存lds文件中的變量,編譯程序時,有一個symbol table(符號表),包含了變量的名稱和地址。在本來放地址的地方可以放值,這樣就可以使用符號表保存lds的變量,這裏可以看作常量。使用的時候,常規變量是取地址來得到的,爲了保持代碼的一致,對於lds的變量取值,也使用取值操作得到變量的值,即volatile unsigned int *end = ( volatile unsigned int * )&_bss_end;
,這些變量的值來自鏈接腳本,在鏈接的時候確定。.符號表只是在編譯鏈接的時候輔助一下,最終不會存放在程序中的,所以符號表的大小無所謂。
五.位置無關碼(相對跳轉與絕對跳轉)
ARM啓動過程分析:
bin一開始是.text,緊接着是.rodata,然後是.data,bin文件燒在NOR FLASH上(從0地址開始),上電後從0地址開始運行。.text的前面一部分代碼會把程序拷貝到SDRAM實現重定位(整個程序的重定位)。然後start.S中實現了cpy和clean。
注意:
反彙編文件中,b或者bl某個值,只是起方便產看的作用,並不是實際跳到這個地址的,但是我們可以在反彙編文件中根據這個地址來查看跳轉到的部分是什麼樣子的,也就是說這個值只是反彙編文件中的位置參考值。實際中b或者bl跳轉的地址由當前的地址和一個固定的地址偏移決定(實際中採用偏移地址)。
重定位之前不使用絕對地址
就像上面分析的一樣,怎麼寫出與當前位置無關的代碼,也就是可以在任何條件允許的內存中運行的代碼:
1.使用相對跳轉命令:b、bl
但是有一個問題:我們在NOR FLASH(SRAM)中的代碼已經通過重定位拷貝在SDRAM中了,但是在start.S中使用的 bl main命令卻跳轉到了NOR FLASH(SRAM)中的main函數,沒有跳轉到SDRAM中的main函數,這是因爲bl main使用了相對跳轉命令,若想跳轉到SDRAM中,就要使用絕對跳轉命令:
ldr pc, =main
2.重定位之前不可以使用絕對地址,不可訪問全局變量,因爲全局變量是通過絕對地址來訪問的
3.重定位之後纔可以使用 ldr pc, =xxx
來跳轉到加載地址,即RunTimeAddr,若執意使用bl命令,則會跳轉到NOR FLASH(SRAM)上
使用絕對地址後,程序在SDRAM中運行,相對FLASH上快了一些
重定位之前一定要初始化SDRAM,重定位之後,才使用絕對地址,因爲此時已經將代碼拷貝到SDRAM上了!
小提示:
在寫位置無關碼的時候,初始化SDRAM不要使用有初始值數組的形式,因爲數組的初始值要用絕對地址來訪問,初始值放在.rodata