WhyCan Forum

本站无需注册,无需积分,无需回复可下载所有资料,如果真的喜欢小站,请您注册之后请至少回复一个帖子激活Id,谢谢支持! 站长QQ: 516333132 (挖坑网/填坑网) admin@whycan.cn

您尚未登录。

#1 2018-05-10 19:23:55

xinxiaoci
会员
注册时间: 2018-04-18
累计积分: 71

将程序搬运至SDRAM运行--链接脚本与代码重定位

《单片机小白转嵌入式Linux学习记录,基于S3C2440----目录》

-------------------------------------------------
A. NAND flash中的程序小于4K可以直接运行,如果超过4K则需要将代码从NAND flash中读取到SDRAM执行,这个过程需要代码重定位。

B. Nor flash 可以像内存一样读数据,但不能像内存一样直接写数据。
所以在Nor flash 上的程序中如果包含 全局/静态变量,将无法修改。因为全局变量也是保存在.bin 文件中。需要代码重定位,将代码放入SDRAM中。

把程序从一个位置移动到另一个位置,叫做代码的重定位,可以只重定位数据段,也可以重定位整个程序。

程序包含的段:
.text        代码段    程序代码
.data        数据段    存放全局变量和静态变量
.rodata        只读数据段    只读数据const
.bss        初值为0 或 无初值的全局变量
.comment    注释段    编译器描述信息

.bss和.commen 不保存在.bin 文件中


--------------------------------------------------

链接脚本简单测试

参考文档
http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html

arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf
直接指定数据段,将全局变量定义在SDRAM中,会导致编译后的.bin 文件非常大,不符合我们的要求

处理方法,将代码段写在数据段后面不远的位置,如0x800 ,然后在程序运行时,将数据段拷贝至SDRAM,重定位。需要引入链接脚本 .lds

Makefile:
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf

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

start.s        main函数前添加如下代码

    bl sdram_init            /* 初始化SDRAM 否则下面无法搬运数据 */

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

链接脚本解析:

runtime addr :运行时地址
relocate addr:重定位地址
链接地址

load addr :加载地址

程序运行时应该位于 runtime addr。如果load addr!=runtime addr 需要重定位:把load addr位置代码拷贝到对应的runtime addr.

SECTIONS {
   .text   0  : { *(.text) }                // 所有文件的代码段 runtime addr = load addr  无需重定位
   .rodata  : { *(.rodata) }                // 所有文件的.rodata 段
   .data 0x30000000 : AT(0x800)             // runtime addr = 0x30000000 也可以叫做链接地址  load addr = 0x800 
   {                                         // load addr!=runtime addr 需要重定位
      data_load_addr = LOADADDR(.data);        // data_load_addr 相当于一个变量 保存的是当前运行时的地址,传递给其他参数使用。
      data_start = . ;                        // . 表示当前地址
      *(.data)
      data_end = . ;
   }
   .bss  : { *(.bss) *(.COMMON) }            //.elf 和 .bin 文件中都不存在 .bss .COMMON 段。
}

1.链接得到.elf格式的文件,含有(load addr)地址信息
2.使用加载器把.elf文件读入内存,加载到指定位置(加载器:a.JTAG b.加载程序APP)
3.运行程序
4.如果load addr!=runtime addr 程序本身需要重定位

a. elf -> bin 
b. 没有加载器,硬件机制启动。
c. 如果.bin 中指令所在的位置!= runtime addr 那么程序本身需要实现重定位功能。

---------------------------------------------
.elf 和 .bin 文件中都不存在 .bss .COMMON 段。
所以我们要清空.bss段对应地址中的数值,使其初始值等于0;

SECTIONS
{
    .text   0  : { *(.text) }
    .rodata  : { *(.rodata) }
    .data 0x30000000 : AT(0x800)
    {
        data_load_addr = LOADADDR(.data);
        data_start = . ;
        *(.data)
        data_end = . ;
    }
    .bss  :
    {
        bss_start = .;
        *(.bss) *(.COMMON)
        bss_end = .;
    }
}

start.s 在重定位后面加上如下代码

/* 清除 .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

-------------------------------------------------

链接脚本的改进
目的:提高代码拷贝的效率,减少对外部芯片的读写次数
a. 将数据的读写改为 32bit 位宽 4字节对齐
    ldrb r4, [r1]    -> ldr r4, [r1]
    strb r4, [r2]    -> str r4, [r2]
b. 由于将读写修改为 32 bit宽 4字节对齐,所以在清除.bss段时有可能 .bss的起始地址不是4字节对齐,会导致清除掉.data段的数据。所以要修改链接脚本使其 bss_start 地址 4字节对齐

    . = ALIGN(4);        当前地址4字节对齐
    bss_start = .;        将当前地址传递给 bss_start
   
    由于链接脚本中 .data 段我们指定的地址类型是4字节对齐,所以不需要修改,如果不放心,也可以在data_start = .; 前添加 . = ALIGN(4);
   
-------------------------------------------------
整个程序的重定位

数据和代码分离的重定位常用在单片机中或有可执行代码的器件中如nor,如果没有外接nor设备,我们就需要将整个程序重定位。

步骤:
1. 在连接脚本中指定runtime addr 为 SDRAM
2.重定位之前的代码用位置无关码编写。这样程序在Nor和SDRAM中都可执行。
位置无关代码不能使用 :绝对地址、全局变量、静态变量、有初值的数组,具体查看反汇编代码。

链接脚本可以参考 u-boot 重定位代码进行修改。

知识点:

b/bl 是相对跳转 = 当前pc值 + offset(偏移地址)

反汇编代码中的 bl _0x30000000<sdram_init> 并不是跳转到 0x30000000 位置。 _0x30000000<sdram_init>只是为了便于分析代码才这样写的。


怎么写位置无关吗程序:
a. 调用程序时用B/BL相对跳转指令
b. 重定位之前不可以使用绝对地址:全局变量、静态变量、由初值的数组,具体要看反汇编结果。
c. 重定位之后,使用绝对跳转指令跳转到Runtime Addr : ldr pc,=main 如果使用bl main 程序还是在Nor或片内SRAM执行。

有初值的数组:数组保存在栈中,数组中的初值保存在 .rodata 段中。


修改后的链接脚本 sdram.lds

SECTIONS
{
	. = 0x30000000;

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

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

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

	. = ALIGN(4);
	__bss_start = .;
	.bss : { *(.bss) *(.COMMON) }
	_end = .;
}

修改后的start.s

.text
.global _start

_start:

	/* 关闭看门狗 */
	ldr r0, =0x53000000
	ldr r1, =0
	str r1, [r0]

	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
	ldr r0, =0x4C000000
	ldr r1, =0xFFFFFFFF
	str r1, [r0]

	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
	ldr r0, =0x4C000014
	ldr r1, =0x5
	str r1, [r0]

	/* 设置CPU工作于异步模式 */
	mrc p15,0,r0,c1,c0,0
	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
	mcr p15,0,r0,c1,c0,0

	/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) 
	 *  m = MDIV+8 = 92+8=100
	 *  p = PDIV+2 = 1+2 = 3
	 *  s = SDIV = 1
	 *  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
	 */
	ldr r0, =0x4C000004
	ldr r1, =(92<<12)|(1<<4)|(1<<0)
	str r1, [r0]

	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
	 * 然后CPU工作于新的频率FCLK
	 */
	
	

	/* 设置内存: sp 栈 */
	/* 分辨是nor/nand启动
	 * 写0到0地址, 再读出来
	 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
	 * 否则就是nor启动
	 */
	mov r1, #0
	ldr r0, [r1] /* 读出原来的值备份 */
	str r1, [r1] /* 0->[0] */ 
	ldr r2, [r1] /* r2=[0] */
	cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
	ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
	moveq sp, #4096  /* nand启动 */
	streq r0, [r1]   /* 恢复原来的值 */

	bl sdram_init
	//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 */

	/* 重定位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  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
	ldr pc, =main  /* 绝对跳转, 跳到SDRAM */

halt:
	b halt
	

------------------------------------------------------

用C语言替换代码重定义和.bss 段清除

a. 汇编向C函数传递运行时地址,调用c函数
start.s 改进

	/* 重定位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语言代码

void copy2sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)  /* src, dest, len */
{
	unsigned int i = 0;

	while (i < len)
	{
		*dest++ = *src++;
		i += 4;
	}
}


void clean_bss(volatile unsigned int *start, volatile unsigned int *end)  /* start, end */
{
	while (start <= end)
	{
		*start++ = 0;
	}
}

b. 直接从链接脚本中获取 对应的地址

starts.s 改进

	/* 重定位text, rodata, data段整个程序 */
	bl copy2sdram

	/* 清除BSS段 */
	bl clean_bss
	

sdram.lds 改进 传递起始地址

SECTIONS
{
	. = 0x30000000;

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

原理:
文件编译时有一个 symbol table 符号表

存放各种变量

name1:global_a:addr1
name2:global_b:addr2
....
....
__code_start::addr

c语言中用取址符号 & 即可获取对应的地址

使用方法:
1. 声明为外部变量
2. 用取值符号得到地址

最近编辑记录 xinxiaoci (2018-05-10 19:25:41)

离线

#2 2018-05-10 19:40:25

超级萌新
会员
注册时间: 2018-05-04
累计积分: 173

Re: 将程序搬运至SDRAM运行--链接脚本与代码重定位

搬小凳子学习!

离线

#3 2019-01-19 21:14:27

jw__liu
会员
注册时间: 2019-01-18
累计积分: 40

Re: 将程序搬运至SDRAM运行--链接脚本与代码重定位

学习中。

离线

#4 2019-01-28 13:41:51

xinyu_khan
会员
注册时间: 2019-01-15
累计积分: 31

Re: 将程序搬运至SDRAM运行--链接脚本与代码重定位

学习中

离线

快速回复

填写内容后点击按钮提交

页脚