標籤:clean strong tail 分享圖片 main dde 串連 指令碼 最簡
目錄
- 連結指令碼與重定位
- 總結
- 連結指令碼格式
- COMM段BSS段
- elf和bin檔案
- 獲得連結指令碼的值
- 重定位
- 1-直接指定資料區段位置(代碼黑洞)
- 2-分散載入(資料區段)
- 3-全域重定位(一體式)
- BL跳轉指令
- bss段處理
- 位置無關碼
title: 連結指令碼與重定位
tags: ARM
date: 2018-10-12 19:25:53
---
連結指令碼與重定位
學習視頻 韋東山
總結
- 盡量使用一體式的連結指令碼,方便簡單,靈活
- 學會使用連結指令碼的值
- bss段和comm段是需要我們手動去清除的
- 位置無關碼相關:
- 局部數組是位置無關的,但是其數組的初始值在連結地址中,所以over
- 不能使用全域變數和局部變數
- 跳轉使用bl
- 重定位代碼的第一次跳轉需要使用
ldr才能跳出去
- 重定位之前,只能使用相對位址,使用bl跳轉
連結指令碼格式
SECTIONS {...secname start BLOCK(align) (NOLOAD) : AT ( ldadr ) { contents } >region :phdr =fill...}
- secname, contents是必須的,其他可選
- secname:段名,用以命名此段。
- contents:決定哪些內容放在本段,可以是整個目標檔案(.o),也可以是目標檔案中的某段(程式碼片段、資料區段等)。
start.o或者這樣start.o *(.text)
- start:是段的重定位地址,即本段啟動並執行地址。如果代碼中有位置無關指令,程式運行時這個段必須放在這個地址上。start可以用任意一種描述地址的符號來描述。
- BLOCK(align) 指定塊對齊。比如,前一個段從0x30000000到0x300003F1,此處標記ALIGN(4),表示此處最小佔用4Bytes,即使下一個段是緊挨這個段,那麼下一個段的起始地址(也就是運行地址)為0x300003F4。
- NOLOAD:告訴載入器程式運行時不載入該段到記憶體。
- AT(ldadr):定義本段儲存的地址,如果不使用這個選項,則載入地址等於運行地址,通過這個選項可以控制各段分別儲存於輸出檔案中不同的位置。
注意 一般需要4位元組對齊,如果程式碼片段和資料區段的重定位使用4位元組讀寫
最簡單的連結指令碼
SECTIONS { . = 0x33f80000; .text : { *(.text) } . = ALIGN(4); .rodata : {*(.rodata*)} . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(COMMON) } __bss_end = .;}
COMM段BSS段
對於全域變數來說,如果初始化了不為0的值,那麼該全域變數則被儲存在data段,如果初始化的值為0,那麼將其儲存在bss段,如果沒有初始化,則將其儲存在common段,等到連結時再將其放入到BSS段。關於第三點不同編譯器行為會不同,有的編譯器會把沒有初始化的全域變數直接放到BSS段。
參考連結
elf和bin檔案
elf檔案
1 連結得到elf檔案,含有地址資訊(load addr)
2 使用載入器(裸機程式是JTAG/應用程式的載入器本身也是個App去載入)
3 運行程式
4 如果loadaddr != runtimeaddr程式本身要重定位
核心程式運行時應該位於 runtimeaddr(reloate addr)或者連結地址
bin檔案
1 elf產生bin檔案
2 硬體機制啟動,此時沒有載入器
3 如果bin檔案所在位置 不等於runtimeaddr ,程式本身實現重定位
bin檔案/elf檔案都不儲存bss段 這些都是初始值為0 或者沒有初始化的全域變數,程式運行時把bss段對應的空間清零
獲得連結指令碼的值
https://sourceware.org/ml/binutils/2007-07/msg00154.html 連結指令碼的符號表
使用偽彙編指令ldr r1, =__bss_start
存放到一塊地區,再去讀取
.global _bss_start_bss_start: .word __bss_startldr r1, _bss_start //讀取記憶體,這裡是讀取label所在記憶體的資料//c中這麼引用lableextern ulong _bss_start;
C中擷取連結指令碼的值,連結的時候會根據lds與C所需要的lds中的值,產生一張記憶體表,記憶體表的標號也就是lds中的符號名(__bss_start).=========這裡是重點======
extern int __bss_start;int val =&__bss_start; //!< 獲得__bss_start的值int *p=&__bss_start; //!< p指向這個記憶體單元------------------------------------------------------------------下面是一個一個的符號表 ,都是 name + 地址的格式-----name: g_i //!<正常變數,存的是變數的地址Addr:xxx-----name: g_jAddr:xxx------name:__bss_start //!< lds中,存的就是值了Addr:xxx------
常規變數比如我們定義了int g_i,那麼就有一個int大小的記憶體配置出來,地址也就是&g_i,對於lds中的變數,我們也就要是需要先取址,再取值,也就是*(&__bss_start)
為什麼要先取地址?
- 對於變數與lds中的變數,C都是先建立一個符號表,符號表正常情況存檔地址
- 因為在C的符號表中,正常變數
g_i的操作是:1.尋找符號表中的g_i,獲得其記憶體位址,然後對記憶體操作
- 而Lds中的變數,實際上並沒有對應的記憶體單元,符號表中的
__bss_start他的值(地址值)就是所需要的值.
總結:
- 對於常規變數g_i,得到裡面的值,使用&g_i得到addr;
- 為了保持代碼的一致,對於lds中的a1,使用&a1得到裡面的值;
- 藉助symbol table儲存lds的變數,使用時加上"&"得到它的值,連結指令碼的變數要在C程式中聲明為外部變數,任何類型都可以;
重定位
全域變數
全域變數在放在連結地址指定的位置,所以其連結地址必須是可寫的.
局部變數
雖然局部變數是在棧中的,但是局部變數的數組他的初始值是從連結地址中取出來的.例如:
參考 JTAG調試中nand調試章節
void memsetup(){ unsigned long const mem_cfg_val[]={ 0x22011110, //BWSCON 0x00000700, //BANKCON0 0x00000700, //BANKCON1 0x00000700, //BANKCON2 0x00000700, //BANKCON3 0x00000700, //BANKCON4 0x00000700, //BANKCON5 0x00018005, //BANKCON6 0x00018005, //BANKCON7 0x008C07A3, //REFRESH 0x000000B1, //BANKSIZE 0x00000030, //MRSRB6 0x00000030, //MRSRB7 };}//mem_cfg_val是在棧,是位置無關的,但是他的初始值是去在連結地址取的,也就是0x3000000後面// ip=300005bc30000050: e1a0400c mov r4, ip30000054: e8b4000f ldmia r4!, {r0, r1, r2, r3}//從r4中指向的記憶體給r0~r3//也就是說從 300005bc讀取一些資料,但是這個時候300005bc(sdram)並沒有被初始化且進行代碼搬運,所以這裡的資料肯定有問題//這裡的其實就是那個局部數組的值 mem_cfg_val300005bc <.rodata>:300005bc: 22011110 andcs r1, r1, #4 ; 0x4300005c0: 00000700 andeq r0, r0, r0, lsl #14300005c4: 00000700 andeq r0, r0, r0, lsl #14300005c8: 00000700 andeq r0, r0, r0, lsl #14300005cc: 00000700 andeq r0, r0, r0, lsl #14300005d0: 00000700 andeq r0, r0, r0, lsl #14300005d4: 00000700 andeq r0, r0, r0, lsl #14300005d8: 00018005 andeq r8, r1, r5300005dc: 00018005 andeq r8, r1, r5300005e0: 008c07a3 addeq r0, ip, r3, lsr #15300005e4: 000000b1 streqh r0, [r0], -r1300005e8: 00000030 andeq r0, r0, r0, lsr r0300005ec: 00000030 andeq r0, r0, r0, lsr r0Disassembly of section .comment:
1-直接指定資料區段位置(代碼黑洞)
arm-linux-ld -Ttext 0 -Tdata 0x30000000// 資料區段如下Disassembly of section .data:30000000 <__data_start>:30000000: Address 0x30000000 is out of bounds.
這個時候會發現,程式碼片段從0開始,資料區段從0x3000,0000開始,bin檔案產生有0x3000,0000+1大小(1個全域變數),也就是產生了代碼黑洞.程式碼片段和資料區段有大量的空白.
在1中會產生巨大的代碼空白,所以需要解決這個問題,也就是資料區段和程式碼片段不能有大的空白,必須緊靠著.
2-分散載入(資料區段)
引入(手動確認資料區段大小)
將資料區段與程式碼片段在一起,全部燒寫到Nor的0地址上
運行時將全域變數複製到sdram上,做資料區段的重定位
這裡的Makefile需要使用連結指令碼了,這裡的關鍵就是實現了資料放置在A,但是實際啟動並執行時候是在B
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf// 重點 data 0x30000000 : AT(0x700) { *(.data) } //放在0x700,但運行時在0x3000000//ldsSECTIONS { .text 0 : { *(.text) }//所有檔案的.text .rodata : { *(.rodata) } //唯讀資料區段 .data 0x30000000 : AT(0x800) { *(.data) } //放在0x700,但運行時在0x3000000 .bss : { *(.bss) *(.COMMON) }//所有檔案的bss段,所有檔案的.COMMON段}
需要手動初始化全域變數的值,也就是說先提前複製儲存地址的值到運行地址,也就是將0x800的值複製0x3000,0000
// c語言調用全域變數char g_Char = ‘A‘;putchar(g_Char);// 彙編,從484擷取到連結的地址,也就是說從這個地方0x3000,0000取資料 450: e59f302c ldr r3, [pc, #44] ; 484 <.text+0x484> 454: e5d33000 ldrb r3, [r3] 458: e1a00003 mov r0, r3 45c: ebffff6f bl 220 <putchar> ... 484: 30000000 andcc r0, r0, r0
手動複製資料區段,注意需要先初始化sdram
/* 重定位data段 */ mov r1, #0x800 ldr r0, [r1] //讀取0x800 的內容到r0 mov r1, #0x30000000 str r0, [r1] //將r0存到0x3000,0000
自動確認資料區段大小
上述方式是看了反組譯碼,知道代碼就一個全域變數,這在具體應用肯定是不行的,需要從連結指令碼獲得資料區段的大小.修改連結指令碼如下,其中可以通過data_load_addr獲得其儲存地址
SECTIONS { .text 0 : { *(.text) } .rodata : { *(.rodata) } .data 0x30000000 : AT(0x700) { data_load_addr = LOADADDR(.data); data_start = . ;//等於當前位置 *(.data) //等於資料區段的大小 data_end = . ;//等於當前位置 } .bss : { *(.bss) *(.COMMON) }}
代碼中自動複製相關的重定位的資料區段
bl sdram_init /* 重定位data段 */ ldr r1, =data_load_addr /* data段在bin檔案中的地址, 載入地址 */ ldr r2, =data_start /* data段在重定位地址, 運行時的地址 */ ldr r3, =data_end /* data段結束位址 */cpy: ldrb r4, [r1] //從r1讀到r4 讀取一個位元組,讀取儲存地址的值 strb r4, [r2] //r4存放到r2 寫入到運行地址 add r1, r1, #1 //r1+1 儲存地址++ add r2, r2, #1 //r2+1 運行地址++ cmp r2, r3 //r2 r3比較 bne cpy //如果不等則繼續拷貝 bl main
3-全域重定位(一體式)
分散載入和全域重定位的對比
- 分體式連結指令碼適合單片機,單片機內建有flash,不需要再將代碼複製到記憶體佔用空間。而我們的嵌入式系統記憶體非常大,沒必要節省這點空間,並且有些嵌入式系統沒有Nor Flash等可以直接運行代碼的Flash,就需要從Nand Flash或者SD卡複製整個代碼到記憶體;
- JTAG等調試器一般只支援一體式連結指令碼;
程式碼片段和資料區段重定位,所以必須確保重定位前的代碼必須是位置無關碼.具體步驟如下
- 讓檔案直接從0x30000000開始,全域變數在0x3......;
- 燒寫Nor Flash上 0地址處;
- 運行會把整個程式碼片段資料區段(整個程式)從0地址複製到SDRAM的0x30000000(重定位);
- 修改連結指令碼如下:
SECTIONS{ . = 0x30000000; . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } _end = .;}
代碼搬運和清除bss如下
/* 重定位text, rodata, data段整個程式 */ mov r1, #0 ldr r2, =_start /* 第1條指令運行時的地址 */ ldr r3, =__bss_start /* bss段的起始地址 */cpy: ldr r4, [r1] str r4, [r2] add r1, r1, #4 add r2, r2, #4 cmp r2, r3 ble cpy /* 清除BSS段 */ ldr r1, =__bss_start ldr r2, =_end mov r3, #0clean: str r3, [r1] add r1, r1, #4 cmp r1, r2 ble clean //bl main //這裡代碼還是在nor上的,為什麼能運行?因為代碼在nor其實也沒有什麼關係 ldr pc, =main/*絕對跳轉,跳到SDRAM*/halt: b halt
BL跳轉指令
在反組譯碼中的B或者Bl跳轉指令,其並不是跳轉的相應的地址
3000005c: eb000106 bl 30000478 <sdram_init> 30000060: e3a01000 mov r1, #0 ; 0x030000064: e59f204c ldr r2, [pc, #76] ; 300000b8 <.text+0xb8>30000068: e59f304c ldr r3, [pc, #76] ; 300000bc <.text+0xbc>
當我們修改連結指令碼的時候,修改了連結地址,機器碼也是不變的.實際上的跳轉其實是相對pc跳轉.由連結器決定.
假設程式從0x30000000執行,當前指令地址:0x3000005c ,那麼就是跳到0x30000478;如果程式從0運行,當前指令地址:0x5c 調到:0x00000478
跳轉到某個地址並不是由bl指令所決定,而是由當前pc值決定。反組譯碼顯示這個值只是為了方便讀代碼。
重點: 反組譯碼檔案裡, B或BL 某個值,只是起到方便查看的作用,並不是真的跳轉。
bss段處理彙編處理
初始值為0的全域變數是存放在bss段的,需要自己寫代碼清0
SECTIONS { .text 0 : { *(.text) } .rodata : { *(.rodata) } .data 0x30000000 : AT(0x700) { data_load_addr = LOADADDR(.data); data_start = . ; *(.data) data_end = . ; } bss_start = .; //bss開始地址是當前位置 .bss : { *(.bss) *(.COMMON) } bss_end = .; //bss結束位址也是當前位置}
代碼如下:
/* 清除BSS段 */ ldr r1, =bss_start ldr r2, =bss_end mov r3, #0clean: strb r3, [r1] add r1, r1, #1 cmp r1, r2 bne clean bl main //這裡代碼還是在nor上的,為什麼能運行?因為代碼在nor其實也沒有什麼關係halt: b halt
上述代碼是位元組讀寫的,cpu是32位的,sdram是32位寬的,可以直接使用32位訪問,這裡需要對連結指令碼進行4位元組對齊,否則在clean清bss段的時候,bss的起始地址不是4位元組對齊的,那麼他向4取整,也就是
ldr r1, =bss_start 這裡地址如果不是4取整,那麼比如0x3000,0002clean: strb r3, [r1] //這裡是將0 寫入 0x3000,0002 ,但是這是str會向4去整,也就是破壞bss段上面的 //2個位元組
SECTIONS { .text 0 : { *(.text) } .rodata : { *(.rodata) } .data 0x30000000 : AT(0x700) { data_load_addr = LOADADDR(.data); . = ALIGN(4); data_start = . ; *(.data) data_end = . ; } . = ALIGN(4);//讓當前地址向4對齊 bss_start = .; .bss : { *(.bss) *(.COMMON) } bss_end = .;}
改進代碼搬運為4位元組訪問
cpy: ldr r4, [r1] str r4, [r2] add r1, r1, #4 //r1加4 add r2, r2, #4 //r2加4 cmp r2, r3 //如果r2 =< r3繼續拷貝 ble cpy/* 清除BSS段 */ ldr r1, =bss_start ldr r2, =bss_end mov r3, #0clean: str r3, [r1] add r1, r1, #4 cmp r1, r2 //如果r1 =< r2則繼續拷貝 ble clean bl main //這裡代碼還是在nor上的,為什麼能運行?因為代碼在nor其實也沒有什麼關係
C處理
回頭看 獲得串連指令碼的值 上面的章節
使用彙編給c傳遞參數,也就是先使用彙編ldr r1,=__bss_start獲得連結指令碼的參數
/* 重定位text, rodata, data段整個程式 */ mov r0, #0 ldr r1, =_start /* 第1條指令運行時的地址 */ ldr r2, =__bss_start /* bss段的起始地址 */ sub r2, r2, r1 /*長度*/ bl copy2sdram /* src, dest, len */ /* 清除BSS段 */ ldr r0, =__bss_start ldr r1, =_end bl clean_bss /* start, end */
c直接從連結指令碼取值
///ldsSECTIONS{ . = 0x30000000; __code_start = .; //定義__code_start地址位當前地址 . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } _end = .;}// cvoid copy2sdram(void){ /* 要從lds檔案中獲得 __code_start, __bss_start * 然後從0地址把資料複製到__code_start */ 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; while (dest < end) { *dest++ = *src++; }}void clean_bss(void){ /* 要從lds檔案中獲得 __bss_start, _end */ extern int _end, __bss_start; volatile unsigned int *start = (volatile unsigned int *)&__bss_start; volatile unsigned int *end = (volatile unsigned int *)&_end; while (start <= end) { *start++ = 0; }}
位置無關碼
- 使用相對跳轉命令 b或bl;
- 重定位之前,不可使用絕對位址,不可訪問全域變數/靜態變數,也不可訪問有初始值的數組(因為初始值放在rodata裡,使用絕對位址來訪問);
- 重定位之後,使用ldr pc = xxx,跳轉到/runtime地址;
寫位置無關碼,其實就是不使用絕對位址,判斷有沒有使用絕對位址,除了前面的幾個規則,最根本的辦法看反組譯碼。
連結指令碼與重定位