1. 程式人生 > >連結指令碼與重定位

連結指令碼與重定位

目錄


title: 連結指令碼與重定位
tags: ARM
date: 2018-10-12 19:25:53
---

連結指令碼與重定位

學習視訊 韋東山

總結

  1. 儘量使用一體式的連結指令碼,方便簡單,靈活
  2. 學會使用連結指令碼的值
  3. bss段和comm段是需要我們手動去清除的
  4. 位置無關碼相關:
    1. 區域性陣列是位置無關的,但是其陣列的初始值在連結地址中,所以over
    2. 不能使用全域性變數和區域性變數
    3. 跳轉使用bl
  5. 重定位程式碼的第一次跳轉需要使用ldr才能跳出去
  6. 重定位之前,只能使用相對地址,使用bl跳轉

連結指令碼格式

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}
  1. secname, contents是必須的,其他可選
  2. secname:段名,用以命名此段。
  3. contents:決定哪些內容放在本段,可以是整個目標檔案(.o),也可以是目標檔案中的某段(程式碼段、資料段等)。start.o或者這樣start.o *(.text)
  4. start:是段的重定位地址,即本段執行的地址。如果程式碼中有位置無關指令,程式執行時這個段必須放在這個地址上。start可以用任意一種描述地址的符號來描述。
  5. BLOCK(align) 指定塊對齊。比如,前一個段從0x30000000到0x300003F1,此處標記ALIGN(4),表示此處最小佔用4Bytes,即使下一個段是緊挨這個段,那麼下一個段的起始地址(也就是執行地址)為0x300003F4。
  6. NOLOAD:告訴載入器程式執行時不載入該段到記憶體。
  7. 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 連結指令碼的符號表

  1. 使用偽彙編指令ldr r1, =__bss_start

  2. 存放到一塊區域,再去讀取

    .global _bss_start
    _bss_start:
     .word __bss_start
    
    ldr r1, _bss_start   //讀取記憶體,這裡是讀取label所在記憶體的資料
    
    //c中這麼引用lable
    extern ulong _bss_start;
  3. 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_j
    Addr:xxx
    ------
    name:__bss_start //!< lds中,存的就是值了
    Addr:xxx
    ------

    img

    常規變數比如我們定義了int g_i,那麼就有一個int大小的記憶體分配出來,地址也就是&g_i,對於lds中的變數,我們也就要是需要先取址,再取值,也就是*(&__bss_start)

    為什麼要先取地址?

    1. 對於變數與lds中的變數,C都是先建立一個符號表,符號表正常情況存檔地址
    2. 因為在C的符號表中,正常變數g_i的操作是:1.尋找符號表中的g_i,獲得其記憶體地址,然後對記憶體操作
    3. 而Lds中的變數,實際上並沒有對應的記憶體單元,符號表中的__bss_start他的值(地址值)就是所需要的值.

    總結:

    1. 對於常規變數g_i,得到裡面的值,使用&g_i得到addr;
    2. 為了保持程式碼的一致,對於lds中的a1,使用&a1得到裡面的值;
    3. 藉助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=300005bc
30000050:   e1a0400c    mov r4, ip
30000054:   e8b4000f    ldmia   r4!, {r0, r1, r2, r3}
//從r4中指向的記憶體給r0~r3

//也就是說從 300005bc讀取一些資料,但是這個時候300005bc(sdram)並沒有被初始化且進行程式碼搬運,所以這裡的資料肯定有問題

//這裡的其實就是那個區域性陣列的值 mem_cfg_val
300005bc <.rodata>:
300005bc:   22011110    andcs   r1, r1, #4  ; 0x4
300005c0:   00000700    andeq   r0, r0, r0, lsl #14
300005c4:   00000700    andeq   r0, r0, r0, lsl #14
300005c8:   00000700    andeq   r0, r0, r0, lsl #14
300005cc:   00000700    andeq   r0, r0, r0, lsl #14
300005d0:   00000700    andeq   r0, r0, r0, lsl #14
300005d4:   00000700    andeq   r0, r0, r0, lsl #14
300005d8:   00018005    andeq   r8, r1, r5
300005dc:   00018005    andeq   r8, r1, r5
300005e0:   008c07a3    addeq   r0, ip, r3, lsr #15
300005e4:   000000b1    streqh  r0, [r0], -r1
300005e8:   00000030    andeq   r0, r0, r0, lsr r0
300005ec:   00000030    andeq   r0, r0, r0, lsr r0
Disassembly 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-分散載入(資料段)

引入(手動確認資料段大小)
  1. 將資料段與程式碼段在一起,全部燒寫到Nor的0地址上

  2. 執行時將全域性變數複製到sdram上,做資料段的重定位

  3. 這裡的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
    
    //lds
    SECTIONS {
       .text   0  : { *(.text) }//所有檔案的.text
       .rodata  : { *(.rodata) } //只讀資料段
       .data 0x30000000 : AT(0x800) { *(.data) } //放在0x700,但執行時在0x3000000
       .bss  : { *(.bss) *(.COMMON) }//所有檔案的bss段,所有檔案的.COMMON段
    }
  4. 需要手動初始化全域性變數的值,也就是說先提前複製儲存地址的值到執行地址,也就是將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-全域性重定位(一體式)

分散載入和全域性重定位的對比

  1. 分體式連結指令碼適合微控制器,微控制器自帶有flash,不需要再將程式碼複製到記憶體佔用空間。而我們的嵌入式系統記憶體非常大,沒必要節省這點空間,並且有些嵌入式系統沒有Nor Flash等可以直接執行程式碼的Flash,就需要從Nand Flash或者SD卡複製整個程式碼到記憶體;
  2. JTAG等偵錯程式一般只支援一體式連結指令碼;

程式碼段和資料段重定位,所以必須確保重定位前的程式碼必須是位置無關碼.具體步驟如下

  1. 讓檔案直接從0x30000000開始,全域性變數在0x3......;
  2. 燒寫Nor Flash上 0地址處;
  3. 執行會把整個程式碼段資料段(整個程式)從0地址複製到SDRAM的0x30000000(重定位);
  4. 修改連結指令碼如下:
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, #0
clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2
    ble clean

    //bl main   //這裡程式碼還是在nor上的,為什麼能執行?因為程式碼在nor其實也沒有什麼關係
    ldr pc, =main/*絕對跳轉,跳到SDRAM*/

halt:
    b halt

img

BL跳轉指令

在反彙編中的B或者Bl跳轉指令,其並不是跳轉的相應的地址

3000005c:   eb000106    bl  30000478 <sdram_init> 

30000060:   e3a01000    mov r1, #0  ; 0x0
30000064:   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, #0
clean:
    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,0002

clean:
    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, #0
clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2 //如果r1 =< r2則繼續拷貝
    ble clean

    bl main     //這裡程式碼還是在nor上的,為什麼能執行?因為程式碼在nor其實也沒有什麼關係

C處理

回頭看 獲得連線指令碼的值 上面的章節

  1. 使用匯編給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 */
  2. c直接從連結指令碼取值

    ///lds
    SECTIONS
    {
     . = 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 = .;
    }
    // c
    void 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;
     }
    }

位置無關碼

  1. 使用相對跳轉命令 b或bl;
  2. 重定位之前,不可使用絕對地址,不可訪問全域性變數/靜態變數,也不可訪問有初始值的陣列(因為初始值放在rodata裡,使用絕對地址來訪問);
  3. 重定位之後,使用ldr pc = xxx,跳轉到/runtime地址;

寫位置無關碼,其實就是不使用絕對地址,判斷有沒有使用絕對地址,除了前面的幾個規則,最根本的辦法看反彙編。