嵌入式Linux入门-代码重定位和清除bss段讲解

本文主要讲解代码重定位,以及清除bss段。

在开始本文内容之前,需要学习一下,链接脚本的编写。 

参考文章:链接器介绍和链接脚本的编写

 嵌入式Linux学习系列全部文章:嵌入式Linux学习—从裸机到应用教程大全

一、重定位引入

我们都知道S3C2440如下的内存模型

假如是Nand启动

上电后,S3C2440内部的Nand启动硬件会自动把Nand Flsh前4K复制到SRAM;

CPU从0地址运行SRAM;

如果程序大于4K,则前4K的代码需要把整个程序读出来放到SDRAM(即代码重定位)。

假如是Nor Flash启动

此时CPU认为的 0地址 在Nor Flash上面,片内内存SRAM的基地址就变成了0x40000000

由于Nor Flash特性:可以像内存一样读,但不能像内存直接写因此需要把全局变量和静态变量重定位放到SDRAM里。

我们来看这样一段代码

文件名:sdram.c

#include "stdio.h"

char g_Char = 'A';  //定义一个全局变量

int sdram_init()
{
    ....../*略,sdram初始化*/
}
int main(void)
{
    sdram_init();

    while (1)
    {
        putchar(g_Char); /*输出g_Char*/
        g_Char++;         
        delay(1000000);
    }
    return 0;
}

这段代码如果是Nand启动,就是正常的,如果是nor启动,就不正常,因为nor flash只能像sdram一样读,却不能正常写。

二、重定位

一个程序里面有

  • .text 代码段
  • .data 数据段
  • rodata 只读数据段(const全局变量)
  • bss段 (初始值为0,无初始值的全局变量)
  • commen 注释

其中bss段和commen 注释不保存在bin文件中。

为了解决Nor Flash里面的变量不能写的问题,我们把变量所在的数据段放在SDRAM里面,看行不行。
修改Makefile 指定数据段为0x30000000 -Tdata 0x30000000:

arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o  sdram.o -o sdram.elf

这样的话编译出来的bin文件 从0地址 到 0x30000000地址 文件大小有700多MB,代码段和数据段直接有间隔,称之为黑洞


解决黑洞有两个办法:

方法一:只重定位数据段

把数据段和代码段靠在一起;烧写在Nor Flash上面;运行时把全局变量复制到SDRAM,即0x3000000位置(重定位);

步骤:

修改Makefile,使用链接脚本sdram.lds指定。

arm-linux-ld -T sdram.lds start.o sdram.o -o sdram.elf

我们需要依次排列 代码段、只读数据段、数据段、.bss段、.common。

其中数据段放在0x700,但运行时在0x3000000:

SECTIONS {
   .text   0  : { *(.text) }//所有文件的.text
   . = ALIGN(4);
   .rodata  : { *(.rodata) } //只读数据段
   . = ALIGN(4);
   .data 0x30000000 : AT(0x700) { *(.data) } //放在0x700,但运行时在0x3000000
   . = ALIGN(4);
   .bss  : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}

重新编译后烧写bin文件,发现启动后显示乱码。

原因:

我们从0x30000000处获取g_Char,但在这之前,并没有在0x30000000处准备好数据(0x30000000处没有初始化)。

因此需要重定位数据段,将0x700的数据移动到0x30000000处,在start.S加入:

 bl sdram_init

 /* 重定位data段 */
 mov r1, #0x700 
 ldr r0, [r1]
 mov r1, #0x30000000
 str r0, [r1]

 bl main

上面的这种方法,只能复制0x700处的一位数据,不太通用,下面我们利用链接脚本中的符号,写一个更加通用的复制方法:

链接脚本修改如下:

SECTIONS {
   .text   0  : { *(.text) }
   . = ALIGN(4);
   .rodata  : { *(.rodata) }
   . = ALIGN(4);
   .data 0x30000000 : AT(0x700) 
   { 
      data_load_addr = LOADADDR(.data);
      data_start = . ;//等于当前位置
      *(.data)  //等于数据段的大小
      data_end = . ;//等于当前位置
   }
   . = ALIGN(4);
   .bss  : { *(.bss) *(.COMMON) }
}

data_load_addr = LOADADDR(.data);这句话的意思就是data_load_addr=0x700

data_start = . ;的意思就是data_start =0x30000000

data_end = . ;的意思是data_end =0x30000000+数据段大小

修改start.S

    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 //r1加4
    add r2, r2, #4 //r2加4
    cmp r2, r3 //如果不等继续拷贝
    ble cpy

    bl main

方法二:重定位数据段和代码段

让文件直接从0x30000000开始,全局变量在0x3……;烧写Nor Flash上 0地址处;

运行会把整个代码段数据段(整个程序)从0地址复制到SDRAM的0x30000000(重定位);

先梳理下把整个程序复制到SDRAM需要哪些技术细节:
1. 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
2. 编译链接生成的bin文件,需要在SDRAM地址上运行,但上电后却必须先在0地址运行,这就要求重定位之前的代码与位置无关(是位置无关码);

修改链接脚本:

SECTIONS
{
    . = 0x30000000;

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

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

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

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

现在我们写的这个链接脚本,称为一体式链接脚本,对比前面的分体式链接脚本区别在于代码段和数据段的存放位置是否是分开的。

例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的。
分体式链接脚本则是代码段、只读数据段,中间相关很远之后才是数据段、bss段。

我们以后的代码更多的采用一体式链接脚本,原因如下:
1. 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
2. JTAG等调试器一般只支持一体式链接脚本;

修改start.S段

    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

    ldr pc, =main  

halt:
    b halt

将修改后的代码重新编译烧写在Nor Flash上,上电运行。

注意了,为什么用 ldr pc, =main来跳转至main?而不用bl main?

因为bl是相对跳转

B命令的本质是跳转到: pc + offset,PC的值是当前指令,offset是跳转目标位置到当前位置的距离

总结怎么写位置无关码

使用相对跳转命令 b或bl;

重定位之前,不可使用绝对地址,不可访问全局变量/静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);

重定位之后,使用ldr pc = xxx,跳转到runtime地址;

三、清除BSS段

bss段 (初始值为0,无初始值的全局变量)

bin文件/elf文件都不保存bss段 这些都是初始值为0 或者没有初始化的全局变量,不保存能缩小文件。

如果没有清除bss段操作,则初始值为0的全局变量的值不一定为0,所以需要程序去清除。

修改lds链接文件

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结束地址也是当前位置

修改start.s,清除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

现在的代码全局变量就是为0,如果初始值为0的全局变量很多,通过几行代码,就可以少几十个甚至上千个全局变量的存储空间。

四、完整代码

makefile

all:	
	arm-linux-gcc -c -o start.o start.S
	arm-linux-gcc -c -o sdram.o sdram.c
	arm-linux-gcc -c -o main.o main.c
	arm-linux-ld -T sdram.lds start.o sdram.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
clean:
	rm *.bin *.o *.elf 

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
	 */

	ldr sp, =0x40000000+4096
	
	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:
	strb r3, [r1]
	add r1, r1, #1
	cmp r1, r2
	bne clean

	ldr pc, =main

halt:
	b halt

sdram.c

#define BWSCON (*(volatile unsigned int *)0x48000000)
#define BANKCON6 (*(volatile unsigned int *)0x4800001C)
#define REFRESH (*(volatile unsigned int *)0x48000024)
#define BANKSIZE (*(volatile unsigned int *)0x48000028)
#define MRSRB6 (*(volatile unsigned int *)0x4800002C)

int sdram_init(void)
{
	BWSCON = 0x22000000;
	BANKCON6 = 0x18001;
	REFRESH = 0x008404f5;
	BANKSIZE = 0xb1;
	MRSRB6 = 0x00000020;
}

main.c

#include <stdio.h>
#define GPFCON (*(volatile unsigned int *)0x56000050)
#define GPFDAT (*(volatile unsigned int *)0x56000054)
char g_char='A';
int g_int=0;
void delay(volatile int d)
{
	while (d--);
}
int main(void)
{	
	int i=0;
	while(i!=5)
	{
		if(g_char=='A')
		{
		GPFCON = 0x00000100;
		GPFDAT = 0;
		}
		delay(200000);
		if(g_int==0)
		{
		GPFDAT = 0xff;
		}
		delay(200000);
		i++;
	}
	g_char='B';
	while(1)
	{
		if(g_char=='B')
		{
		GPFDAT = 0;
		}
	}		
	return 0;
}

功能:验证全局变量g_char是否成功重定位,成功则灯亮

验证全局变量g_int是否成功被清0,成功则灯灭,

循环5次,验证g_char写入是否正常。

全部正常则:灯循环亮灭5次,然后长亮。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闪耀大叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值