嵌入式入门5(代码重定位)

一、概述

JZ2440外接了各种控制器,它可以直接访问SDRAM、NOR Flash、SRAM(片内4K内存),以及各种控制器(包括Nand Flash控制器),但是不能直接访问Nand Flash。

我们程序可以放在Nor Flash上,也可以放在SDRAM上,CPU可以直接进行运行。但是如果我们将程序放到Nand Flash上,CPU就无法直接进行访问。那我们怎么通过Nand方式启动的呢?

Nand Flash启动时,物理硬件会将Nand Flash前4K内存copy到SRAM(片内4K内存),此时SRAM基地址为0,CPU从SRAM开始运行。

此时就涉及一个问题:如果我们程序大于4k,那如何进行Nand启动?

Nand Flash前4k内存,需要将所有程序放到SDRAM中,然后程序从SDRAM开始执行。

当我们通过Nor Flash启动时,虽然可以CPU可以直接访问超过4k的内容,但是由于Nor Flash的特性:能像普通内存那样直接读,但不能像普通内存那样直接写。这就意味着:

通过Nor Flash启动后,需要把全局变量和静态变量重定位到SDRAM里。

所以,无论是Nor启动,还是Nand启动,我们都需要代码重定位。

把一个程序从一个位置移动到另一个位置,称之为重定位


二、测试Nor Flash写入

main.c

#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文件:

嵌入式入门5(代码重定位)_第1张图片
image.png

代码中,各个段的含义:

  • text段:保存可执行的代码。
  • data段:保存全局变量和静态(局部/全局)变量
  • rodata段:固定的全局变量
  • bss段:保存无初值或初值为0的全局变量。
  • comment段:注释,如gcc编译信息。

我们可以清楚地发现:text段到data段之间,有一片很大的空白区域,我们称之为黑洞区

为了减少黑洞区的大小,使得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只能像普通内存那样读,而不能像普通内存那样写。


三、链接脚本

为什么通过Nor启动和Nand启动打印不一样呢?我们来分析一下原因。

  • Nor启动
嵌入式入门5(代码重定位)_第2张图片
image.png

当通过Nor启动时,0地址为Nor Flash的基地址,全局变量g_Char被放在0x800的地方,也属于Nor Flash的范围,所以此时g_Char++会无效。

  • Nand启动
嵌入式入门5(代码重定位)_第3张图片
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.1、链接脚本的引入

如果想使用重定位,需要使用链接脚本 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中,表示数据的组织形式:

1、.text表示存放代码段,0表示将内容放入0地址,{*(.text)}表示所有文件的代码段。所以这句话的意思是:把所有文件的代码段放到0地址。
2、第二句也类似,.rodata表示存放只读数据段,紧跟在.text内容后面,{ *(.rodata) }将表示所有文件的只读数据段。
3、第三句,bin文件中0x800的位置存放所有文件的数据段,这些内容理论上应该放到0x30000000。
4、第三句,将所有文件的bss段和common段,放到bss段。

我们编译源码,查看反编译内容:

Disassembly of section .rodata:

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


Disassembly of section .data:

30000000 :
30000000:   Address 0x30000000 is out of bounds.


Disassembly of section .bss:

30000004 :
30000004:   00000000    andeq   r0, r0, r0

30000008 :
30000008:   00000000    andeq   r0, r0, r0

可以看到,数据段中的g_Char变量,地址为0x30000000,可是我们bin文件只有3k大小。也就是说:

这些代码虽然存放在bin文件的0x800的位置,但是,后面这些代码会被在0x30000000位置进行执行。

那如何将这些存放在0x800的代码"搬运"到0x30000000呢?
这就需要我们手动去完成,修改start.S:

.text
.global _start

_start:
    /* 省略以下代码:
    1、关闭看门狗
    2、设置时钟
    3、设置栈指针
    */

    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.2、链接脚本优化

上个程序中,我们只重定位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;
}

此时,我们需要修改链接脚本,使得其更加通用。

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:
    /* 省略以下代码:
    1、关闭看门狗
    2、设置时钟
    3、设置栈指针
    */

    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.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整个文件,再将其余文件的代码段存入。

  • 1、链接到elf文件,包含地址信息(如:加载地址)。
  • 2、使用加载器,把elf文件读入内存

对应裸板,是JTAG调试工具
对于app,加载器也是app

  • 3、运行
  • 4、如果load addr != runtime addr,程序本身要重定位。

四、清除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:
    /* 省略以下代码:
    1、关闭看门狗
    2、设置时钟
    3、设置栈指针
    */

    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

这里我们做了以下优化:

  • 1、重定位和清除bss段的代码中,将原先每次操作1字节,改为每次操作4字节。
  • 2、做了4字节对齐操作。

假如我们不做4字节对齐操作,那么在清除bss段时,由于假如清除的地址不是4字节的整数倍,CPU会向下取整,可能会清除别的段中的数据。

所以在链接脚本中,我们添加了以下代码,用于段的4字节对齐。

. = ALIGN(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

五、代码重定位与位置无关码

之前也介绍过,解决黑洞区有2种方法:

1、只重定位数据段。
2、重定位整个程序。

在前面章节中,我们都是使用的第一种方法,但第二种方法其实对应Linux开发板来说,更加适用,原因如下:

1、Linux不同于单片机,它对内存大小没那么高的要求。
2、第一种方式只适用于能够运行程序的Flash,假如从Nand Flash、SD卡加载运行,就只能用第二种方式,将程序拷贝到内存中运行。
3、JTAG调试工具只支持第二种链接脚本,不支持第一种分体的链接脚本。

5.1、重定位整个程序

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:
    /* 省略以下代码:
    1、关闭看门狗
    2、设置时钟
    3、设置栈指针
    */

    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文件:

嵌入式入门5(代码重定位)_第4张图片
image.png

里面有一行代码eb000018 bl 300000c4 ,可是,此时SDRAM并未初始化,访问300000c4肯定会出问题,这是怎么回事呢?

原来,这句代码其实并没有直接跳到300000c4,而是跳到pc + offset位置,即该条指令与位置无关,无论放到哪个位置执行,都能正确被执行。

我们可以修改链接脚本的运行地址为0x32000000,这条指令的变成eb000018 bl 320000c4 ,机器码并没有改变,恰好验证我们的猜测。

在dis文件中,bl 0x3xxxxxxx只是起方便查看的作用,而不是真正的跳转到这个地址上。

那怎么写位置无关的程序?

  • 1、调用程序时,使用B/BL相对跳转指令。
  • 2、重定位之前,不可使用绝对地址,比如:
    不可访问全局变量、静态变量。
    不可访问有初始值数组(初始化值存放到rodata中,使用绝对地址来访问)
  • 3、重定位之后,使用ldr pc, =xxx,跳转到Runtime Add,比如:
    ldr pc, =main

所以现在,我们需要让CPU在SDRAM上执行,而不是原先的内存芯片上。这是我们只需要将相对跳转指令:

bl main  // bl相对跳转,程序仍在NOR/sram执行

修改为绝对跳转指令:

ldr pc, =main  // 绝对跳转,跳到SDRAM

编译烧写运行,发现程序运行速度明显变快。

六、C语言实现重定位和清除BSS段

start.S

.text
.global _start

_start:
    /* 省略以下代码:
    1、关闭看门狗
    2、设置时钟
    3、设置栈指针
    */

    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;
}

编译烧写运行 ,一切正常。

你可能感兴趣的:(嵌入式入门5(代码重定位))