嵌入式入门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;
}
复制代码

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

相关文章
相关标签/搜索