嵌入式入門5(代碼重定位)

1、概述

由前幾章可知,JZ2440內存控制器能夠直接訪問SDRAM、NOR Flash、SRAM(片內4K內存),以及各類控制器(包括Nand Flash控制器),可是不能直接訪問Nand Flash。html

image.png

  • 當Nor啓動時,CPU能夠直接運行Nor Flash上的程序代碼,此時Nor Flash基地址爲0地址。
  • 當Nand啓動時,硬件會將Nand Flash前4K內容拷貝到SRAM(片內4K內存),CPU從SRAM開始運行,此時SRAM基地址爲0地址。

此時就涉及2個問題:linux

  • 一、若是咱們程序大於4k,那如何經過Nand啓動?
  • 二、Nor Flash是隻讀的,意味着咱們代碼中的全局變量和靜態變量都沒法進行修改。

解決辦法以下:c++

  • Nand啓動時,前4k代碼可以將代碼段拷貝到SDRAM中,而後讓CPU在SDRAM上繼續執行。
  • Nor啓動時,須要把數據段(保存全局變量和靜態變量)拷貝到SDRAM中。

因此,不管是Nor啓動,仍是Nand啓動,咱們都須要拷貝代碼到SDRAM中。數組

把一個程序從一個位置移動到另外一個位置,稱之爲重定位sass

2、測試Nor Flash寫入

main.cmarkdown

#include "uart.h"

void delay(volatile int d) {
    while (d--);
}

char g_Char = 'A';    //定義一個全局變量
const char g_Char2 = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;

int main(void) {
    uart0_init();

    while(1)
    {
        putchar(g_Char);
        g_Char++;
        delay(1000000);
    }

    return 0;
}
複製代碼

其他的Makefile、start.S、uart.c、uart.h都與前章基本一致。此時編譯出的bin文件大小有33k,顯然是不對的。 查看反編譯dis文件:app

image.png

代碼中,各個段的含義:工具

  • text段:保存可執行的代碼。
  • data段:保存全局變量和靜態(局部/全局)變量
  • rodata段:固定的全局變量
  • bss段:保存無初值或初值爲0的全局變量。
  • comment段:註釋,如gcc編譯信息。

咱們能夠清楚地發現:text段到data段之間,有一片很大的空白區域,咱們稱之爲黑洞區oop

爲了減小黑洞區的大小,使得bin文件小於4k,讓其可以支持Nand啓動,只須要修改Makefile,手動指定data段的位置爲0x800:測試

all:
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	arm-linux-ld -Ttext 0 -Tdata 0x800 start.o uart.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis
複製代碼

而後編譯,發現bin文件變爲3k大小,如何分別燒寫到Nor Flash和Nand Flash,並啓動,觀察打印內容:

Nor啓動時,打印AAAA......
Nand啓動時,打印ABCD......
複製代碼

可見:

Nor Flash只能像普通內存那樣讀,而不能像普通內存那樣寫。

3、連接腳本

爲何經過Nor啓動和Nand啓動打印不同呢?咱們來分析一下緣由。

  • Nor啓動

image.png

當經過Nor啓動時,0地址爲Nor Flash的基地址,全局變量g_Char被放在0x800的地方,也屬於Nor Flash的範圍,因此此時g_Char++會無效。

  • Nand啓動

image.png

當經過Nand啓動時,0地址爲SRAM(片內4k內存)的基地址,CPU上電後,硬件會將Nand Flash前4k內容所有拷貝到SRAM中,CPU開始從SRAM執行。而此時全局變量g_Char也被拷貝到SRAM中,因此此時g_Char++有效。

爲了解決Nor Flash裏面的變量不能寫的問題,咱們須要把變量所在的數據段放在SDRAM中。 但是若是隻是簡單的修改Makefile,指定數據段爲0x30000000,以下:

arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o uart.o main.o -o sdram.elf
複製代碼

編譯後,bin文件有700多M,這麼大的黑洞區,誰都難以忍受。

那如何才能將數據段在bin文件中,緊靠代碼段放置,而在運行時,放到0x30000000的地方?

有如下兩個辦法:

  • 一、第一個方法

把數據段的g_Char和代碼段靠在一塊兒;
燒寫在Nor Flash上面;
運行時把g_char(全局變量)複製到SDRAM,即0x3000000位置;

  • 二、第二個方法

讓文件直接從0x30000000開始,全局變量在0x3......;
燒寫Nor Flash上 0地址處;
運行會把整個代碼段數據段(整個程序)從0地址複製到SDRAM的0x30000000;

上面兩個方法的區別在於:前者只重定位了數據段,後者重定位了整個程序。

3.一、連接腳本的引入

若是想使用重定位,須要使用連接腳本 Using LD, the GNU linker

若是咱們不使用連接腳本,bin文件中,代碼存放的順序,是按照連接順序來存放的。

修改Makefile

all:
	arm-linux-gcc -c -o init.o init.c
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	#arm-linux-ld -Ttext 0 -Tdata 0x800 start.o uart.o main.o -o sdram.elf
	arm-linux-ld -T sdram.lds start.o init.o uart.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis
複製代碼

建立連接腳本sdram.lds:

SECTIONS {
    .text   0 : { *(.text) }
    .rodata   : { *(.rodata) }
    .data 0x30000000 : AT(0x800) { *(.data) }
    .bss      : { *(.bss) *(.COMMON)}
}
複製代碼

SECTIONS中,表示數據的組織形式:

  • 一、.text表示存放代碼段,0表示將內容放入0地址,{*(.text)}表示全部文件的代碼段。因此這句話的意思是:把全部文件的代碼段放到0地址。
  • 二、第二句也相似,.rodata表示存放只讀數據段,緊跟在.text內容後面,{ *(.rodata) }將表示全部文件的只讀數據段。
  • 三、第三句,bin文件中0x800的位置存放全部文件的數據段,這些內容理論上應該放到0x30000000。
  • 四、第四句,將全部文件的bss段和common段,放到bss段。

咱們編譯源碼,查看反編譯內容:

Disassembly of section .rodata:

000003a0 <g_Char2>:
 3a0:	Address 0x000003a0 is out of bounds.


Disassembly of section .data:

30000000 <g_Char>:
30000000:	Address 0x30000000 is out of bounds.


Disassembly of section .bss:

30000004 <g_A>:
30000004:	00000000 	andeq	r0, r0, r0

30000008 <g_B>:
30000008:	00000000 	andeq	r0, r0, r0
複製代碼

能夠看到,數據段中的g_Char變量,地址爲0x30000000,但是咱們bin文件只有3k大小。也就是說:

這些代碼雖然存放在bin文件的0x800的位置,可是,後面這些代碼會被在0x30000000位置進行執行。

那如何將這些存放在0x800的代碼"搬運"到0x30000000呢? 這就須要咱們手動去完成,修改start.S:

.text
.global _start

_start:
    /* 省略如下代碼:
    一、關閉看門狗
    二、設置時鐘
    三、設置棧指針
    */

    bl sdram_init

    /* 重定位data段,只copy 32位(4字節) */
    mov r1, #0x800
    ldr r0, [r1]
    mov r1, #0x30000000
    str r0, [r1]

    bl main

halt:
    b halt
複製代碼

因爲須要從data段拷貝4字節到SDRAM,因此須要提早初始化SDRAM。

再次編譯,而後燒寫到Nor和Nand Flash中,程序都正常執行。

3.二、連接腳本優化

上個程序中,咱們只重定位data段的4個字節,可是,當咱們程序有多個全局變量,就會出現問題:

main.c

#include "uart.h"

void delay(volatile int d) {
    while (d--);
}

char g_Char1 = 'A';    //定義一個全局變量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1';    //定義一個全局變量
// 因爲只copy 4字節,因此g_Char3無效
const char g_Char = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;

int main(void) {
    uart0_init();

    while(1)
    {
            putchar(g_Char1);
            putchar(g_Char2);
            putchar(g_Char3);
            g_Char1++;
            g_Char2++;
            g_Char3++;
            delay(1000000);
    }

    return 0;
}
複製代碼

運行上面程序,發現g_Char一、g_Char2運行正常,而g_Char3運行不正確,這是由於咱們未將g_Char3的內容重定位到SDRAM。

此時,咱們須要修改連接腳本,使得其能重定位多個字節。

sdram.lds

SECTIONS {
    .text   0   : { *(.text) }
    .rodata     : { *(.rodata) }
    .data 0x30000000 : AT(0x800)
    {
        data_load_addr = LOADADDR(.data);
        data_start = . ;
        *(.data)
        data_end = . ;
    }
    .bss        : { *(.bss) *(.COMMON)}
}
複製代碼

LOADADDR宏,能夠獲得data段在bin文件的地址,即加載地址。

data_start = .,表示運行地址。

也就是說,咱們只需將data_load_addr的內容copy到data_start上,拷貝長度爲data_end - data_start

從新修改start.S重定位的內容:

.text
.global _start

_start:
    /* 省略如下代碼:
    一、關閉看門狗
    二、設置時鐘
    三、設置棧指針
    */

    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]
    strb r4, [r2]
    add r1, r1, #1
    add r2, r2, #1
    cmp r2, r3
    bne cpy

    bl main

halt:
    b halt
複製代碼

編譯並燒寫,運行後,程序輸出預期結果。

3.三、連接腳本解析

連接腳本的通用格式以下:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}
複製代碼
  • secname,段名,這裏能夠隨便取
  • start ,起始地址:運行時地址,重定位地址
  • ldadr,加載地址,默認對於運行時地址。
  • contents,內容:

能夠直接指定某個文件(如start.o),把整個文件放在整個段中。
也能夠指定某個段(如*(.text)),把全部文件的這個段放到該段中。
或者start.o *(.text),先存放start.o整個文件,再將其他文件的代碼段存入。

  • 一、連接到elf文件,包含地址信息(如:加載地址)。
  • 二、使用加載器,把elf文件讀入內存

對應裸板,是JTAG調試工具 對於app,加載器也是app

  • 三、運行
  • 四、若是load addr != runtime addr,程序自己要重定位。

4、清除BSS段

BSS段:用來存放爲無初值或初值爲0的全局變量。

上節中,咱們重定位了數據段,但沒有清除BSS段。這會致使咱們訪問這些變量時,會獲得髒數據,因此咱們須要手動清除BSS段。

注意:bss段並不會保存在bin文件和elf文件中,由於這樣作是毫無心義的。

sdram.lds

SECTIONS {
    .text   0   : { *(.text) }
    .rodata     : { *(.rodata) }
    .data 0x30000000 : AT(0x800)
    {
        data_load_addr = LOADADDR(.data);
        . = ALIGN(4);
        data_start = . ;
        *(.data)
        data_end = . ;
    }

    . = ALIGN(4);
    bss_start = .;
    .bss        : { *(.bss) *(.COMMON)}
    bss_end = .;
}
複製代碼

start.S

.text
.global _start

_start:
    /* 省略如下代碼:
    一、關閉看門狗
    二、設置時鐘
    三、設置棧指針
    */

    bl sdram_init

    /* 重定位data段 */
    ldr r1, =data_load_addr /* data段在bin文件中的地址,加載地址 */
    ldr r2, =data_start     /* data段在重定位地址,運行時的地址 */
    ldr r3, =data_end       /* data段結束地址 */

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, =bss_end
    mov r3, #0

clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2
    ble clean

    bl main

halt:
    b halt
複製代碼

這裏咱們作了如下優化:

  • 一、重定位和清除bss段的代碼中,將原先每次操做1字節,改成每次操做4字節。
  • 二、作了4字節對齊操做。

假如咱們不作4字節對齊操做,那麼在清除bss段時,假如清除的地址不是4字節的整數倍,CPU會向下取整,可能會清除別的段中的數據。

因此在連接腳本中,咱們添加了. = ALIGN(4);,用於段的4字節對齊。

打印bss段中的變量,看看清除是否有效:

uart.c

//......
void printHex(unsigned int val) {
    int i;
    unsigned char arr[8];

    /* 先取每一位值 */
    for (i = 0; i < 8; i++)
    {
        arr[i] = val & 0xf;
        val >>= 4;
    }

    /* 打印 */
    puts("0x");
    for (i = 7; i >=0; i--)
    {
        if (arr[i] <= 9)
        {
            putchar(arr[i] + '0');
        }
        else
        {
            putchar(arr[i] - 10 + 'A');
        }
    }
}
複製代碼

main.c

#include "uart.h"

void delay(volatile int d) {
    while (d--);
}

char g_Char1 = 'A';    //定義一個全局變量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1';    //定義一個全局變量
//因爲只copy 4字節,因此g_Char3無效
const char g_Char = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;

int main(void) {
    uart0_init();

    puts("\n\rg_A = ");
    printHex(g_A);
    puts("\n\r");

    while(1)
    {
        putchar(g_Char1);
        putchar(g_Char2);
        putchar(g_Char3);
        g_Char1++;
        g_Char2++;
        g_Char3++;
        delay(1000000);
    }

    return 0;
}
複製代碼

運行後,結果與預期一致:g_A = 0x00000000

5、代碼重定位與位置無關碼

以前也介紹過,解決黑洞區有2種方法:

一、只重定位數據段。
二、重定位整個程序。

在前面章節中,咱們都是使用的第一種方法,但第二種方法其實對應Linux開發板來講,更加適用,緣由以下:

一、Linux不一樣於單片機,它對內存大小沒那麼高的要求。
二、第一種方式只適用於可以運行程序的Flash,假如從Nand Flash、SD卡加載運行,就只能用第二種方式,將程序拷貝到內存中運行。
三、JTAG調試工具只支持第二種連接腳本,不支持第一種分體式的連接腳本。

重定位整個程序

sdram.lds

SECTIONS
{
    . = 0x30000000;

    . = ALIGN(4);
    .text : { *(.text) }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    _end = .;
}
複製代碼

這裏直接指定程序開始的運行地址爲0x30000000。

start.S

.text
.global _start

_start:
    /* 省略如下代碼:
    一、關閉看門狗
    二、設置時鐘
    三、設置棧指針
    */

    bl sdram_init

    /* 重定位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

halt:
    b halt
複製代碼

上述的代碼在運行時,前面部分會將整個程序複製到SDRAM,並清除BSS段。可是在編譯腳本里,整個程序的運行地址被指定到SDRAM上,這就引出一個問題:

爲何當咱們經過Nor啓動或Nand啓動時,運行地址爲SDRAM的程序,也能在Nor Flash或SRAM上正常運行?

此時CPU依舊運行在原先的內存芯片上,並未在SDRAM上執行。也就是說:

  • 當Nor啓動時,CPU在Nor上執行代碼,而後去SDRAM上讀寫全局變量。
  • 當Nand啓動時,CPU在SRAM上執行代碼,而後去SDRAM上讀寫全局變量。

這就意味着,前面這部分的代碼,必須與位置無關。查看反編譯dis文件:

image.png

裏面有一行代碼eb000018 bl 300000c4 <sdram_init>,但是,此時SDRAM並未初始化,訪問300000c4確定會出問題,這是怎麼回事呢?

原來,這句代碼其實並無直接跳到300000c4,而是跳到pc + offset位置,即該條指令與位置無關,不管放到哪一個位置執行,都能正確被執行。

咱們能夠修改連接腳本的運行地址爲0x32000000,這條指令的變成eb000018 bl 320000c4 <sdram_init>,機器碼並無改變,剛好驗證咱們的猜想。

在dis文件中,bl 0x3xxxxxxx只是起方便查看的做用,而不是真正的跳轉到這個地址上。

那怎麼寫位置無關的程序?

  • 一、調用程序時,使用B/BL相對跳轉指令。
  • 二、重定位以前,不可以使用絕對地址,好比:

不可訪問全局變量、靜態變量。
不可訪問有初始值數組(初始化值存放到rodata中,使用絕對地址來訪問)

  • 三、重定位以後,使用ldr pc, =xxx,跳轉到Runtime Add,好比:ldr pc, =main

因此如今,咱們須要讓CPU在SDRAM上執行,而不是原先的內存芯片上。這是咱們只須要將相對跳轉指令:

bl main  // bl相對跳轉,程序仍在NOR/sram執行
複製代碼

修改成絕對跳轉指令:

ldr pc, =main  // 絕對跳轉,跳到SDRAM
複製代碼

編譯燒寫運行,發現程序運行速度明顯變快。

6、C語言實現重定位和清除BSS段

start.S

.text
.global _start

_start:
    /* 省略如下代碼:
    一、關閉看門狗
    二、設置時鐘
    三、設置棧指針
    */

    bl sdram_init

    bl copy2sdram

    bl clean_bss

    bl uart0_init

    //bl main  /*bl相對跳轉,程序仍在NOR/sram執行*/
    ldr pc, =main  /*絕對跳轉,跳到SDRAM*/

halt:
    b halt
複製代碼

init.c

void copy2sdram() {
    extern int _start, __bss_start;
    unsigned int* src   = (unsigned int*)0;
    unsigned int* start = (unsigned int*)&_start;
    unsigned int* end   = (unsigned int*)&__bss_start;

    while(start < end)
    {
        *start++ = *src++;
    }
}

void clean_bss() {
    extern int __bss_start, _end;
    unsigned int* start = (unsigned int*)&__bss_start;
    unsigned int* end   = (unsigned int*)&_end;

    while(start < end)
    {
        *start++ = 0;
    }
}
複製代碼

這裏,咱們在c語言中使調用了lds連接腳本和start.S啓動文件中的變量。這些變量,被保存在了symbol table符號表中。

  • 對於c文件:在編譯時,symbol table裏面存放了c變量的名字(name) 。在連接時,肯定變量的地址。
  • 對於lds文件:爲了在C程序中使用lds中的值,藉助了symbol table保存lds的變量的值,一樣是在編譯時,在symbol table裏面存放了lds中變量的名字(name),在連接時肯定變量的值(注意:不是地址)。

這些變量,在彙編代碼中能夠直接使用,而在c語言裏,須要經過extern關鍵字引入,而後取址得到。

main.c

#include "uart.h"

void delay(volatile int d) {
    while (d--);
}

char g_Char1 = 'A';    //定義一個全局變量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1';    //定義一個全局變量
//因爲只copy 4字節,因此g_Char3無效
const char g_Char = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;

int main(void) {
    puts("\n\rg_A = ");
    printHex(g_A);
    puts("\n\r");

    while(1)
    {
        putchar(g_Char1);
        putchar(g_Char2);
        putchar(g_Char3);
        g_Char1++;
        g_Char2++;
        g_Char3++;
        delay(1000000);
    }

    return 0;
}
複製代碼

編譯燒寫運行 ,一切正常。

相關文章
相關標籤/搜索