历史上的今天
返回首页

历史上的今天

今天是:2025年04月19日(星期六)

2020年04月19日 | s3c2440学习之路-011代码重定位

2020-04-19 来源:eefocus

1 基本原理

承接上1篇博客 s3c2440学习之路-010 sdram, sdram已经初始化完毕,现在可以正式的发挥SDRAM的价值了。(SDRAM 是可以随意读写的,后续的代码都会放到上面来运行)


1.1 程序段的划分

一个程序编译后,会有代码段、数据段、只读数据段、bss段和注释段

image.png?imageView2/2/w/550

这里主要讲一下bss段为什么可以减小bin文件的大小。bbs主要记录未初始化和初始化为0的全局(static 修饰的局部)变量的位置,而不会记录其具体数据,因为这部分的数值会默认设置为0。


这里看2个小程序


test1.c


#include


char a[1000];


int main(int argc, char *argv)

{


    return 0;

}


test2.c


#include


char a[1000] = {1};


int main(int argc, char *argv)

{


    return 0;

}


test1.c 定一个了1个char[1000]的数组,没有对其进行初始化,保存在bss段,最后编译出来的bin档大小为8756B

test2.c同样定义了1个chart1000]的数组,但是对其进行了初始化,保存在data段,最后编译出来的bin档大小为9592B


因为test1.c的数据保存在bss段,而bss只会记录其位置,所以不需要很大的存储空间。而test2.c需要把整个数组的值全部保存在数据段,所以至少需要比test1.c的bin大1000Byte。 这里要注意,减小的只是bin文件的大小,代码实际运行起来时消耗的内存大小是一样的。

在这里插入图片描述

1.2 为什么要需要重定位代码

当使用nand启动时,因为2440的特性,只能运行前4K的代码(前4K会自动拷贝到内部的sram里),因此想运行1个100K+ 的uboot.bin是不可以能的,所以需要把代码重定位到外部的SDRAM里。


当使用nor启动时,jz2440开发板上nor为2MB,足够放一般的程序。但是nor 可以随意读无法随意写,这意味着全局变量是不可修改的。因为修改全局变量需要把数值重新写到nor 里面,而nor 是不可随意写的,所以写数值会失败。因此也需要把代码重定位到外部的SDRAM里。(局部变量是可以修改的,因为局部变量是放在栈里面,栈一般会设置在2440内部的sram)


1.3 如何实现重定位

实现重定位很简单,第1把代码全部拷贝上SDRAM上,第2把代码跳到SDRAM上运行,也就是修改PC的值。


要把代码拷贝到SDRAM上,就需要弄清楚从哪里开始拷,拷多大,有什么顺序或规则吗。

1)代码是放在nor /nand里面,所以是从nor/nand开始拷。而烧写代码时都是从nor/nand的0地址开始的,所以要从nor/nand的0地址开始拷。

2)程序分成了代码段、数据段、只读数据段、bss段和注释段,因此要把这些数据全部拷贝到SDRAM里面,但是具体的大小是多少,后面会简单的讲一下lsd链接脚本,通过这个脚本可以知道程序的大小。

3)顺序可以通过lds链接脚本来设置,不过一般的顺序为代码段->只读数据段->数据段->bss段。


1.4 lds链接脚本

因为对lsd链接脚本没有做深入的研究,所以只懂得简单的运用,详细的使用请看 http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.es.html 官方说明文档。这里只对自己使用到的lds链接脚本做注释。


test.lds


SECTIONS {

// .代表当前的位置, 把当前的位置设置为0x30000000, 0x30000000就是SDRAM的起始地址

    . = 0x30000000 ;

// 4字节对齐

    . = ALIGN(4) ;

    //定义了一个变量 __copy_code_start = 当前的位置,也就是0x30000000

    __copy_code_start = . ;

    //代码段

    .text :

    {

    //先存放start.o的代码段,因为start.o是第1个运行的文件

        start.o

        //*表示所有,所有文件的代码段

        *(.text)

    }


    . = ALIGN(4) ;

    //所有文件的只读数据段

    .rodata : { *(.rodata) }


    . = ALIGN(4) ;

    //所有文件的数据段

    .data : { *(.data) }


    . = ALIGN(4) ;

    //定义变量__bss_start=当前位置,当前的位置等于0x30000000+所有文件的代码段+所有文件的只读数据段+所有文件的数据段

    __bss_start = . ;

    //文件文件的bss段和注释段

    .bss : { *(.bss) *(.COMMON) }

    //定义变量__end = 当前的位置,当前的位置等于__bss_start+所有文件的bss段和注释段

    __end = . ;

}


通过test.lds脚本可以知道,程序的存储顺序为:start.o的text->text->rodata->data->bss。总共定义了3个变量,__copy_code_start、__bss_start 、__end。


在编译的时候,使用-T就可以使用链接脚本了。因为链接脚本第1句“ . = 0x30000000 ;”,把当前的位置设置为0x30000000,所以反汇编看到dis文件的起始位置就是0x30000000。当你把生成的bin档烧录到nor/nand 里面时,还是会从nor/nand的0位置开始拷贝进去。复位后PC=0, 程序依旧是从0地址开始运行。因此dis文件中的0x30000000不会对代码产生任何影响。其中的__copy_code_start、__bss_start 、__end才是后面编程需要用到的。


Makefile

在这里插入图片描述

test.dis

在这里插入图片描述

2 源码讲解

2.1 主要流程

start.s


/* 初始化外部的sdram */

    bl bank6_sdram_init


/* 将代码从nor 拷贝到 sdram */

    /* copy .data .rodata. .text to SDRAM */

    bl copy2sdram


/* 清空bss段 */

    /* clear bss segment */

    bl clear_bss

/* 初始化串口 */

    bl uart0_init

/* 打印位置的数值 */

    bl print_positon

/* 跳到main去执行 */

    ldr pc, =main /* abs jump to main */


start.s 前面会做一些其他的初始化,不过全部略去了,只看最重要的几个部分。SDRAM的初始化在上一篇博客讲了,这里就不做介绍了。


2.2 text、data、rodata段的拷贝

init.c


void copy2sdram(void)

{

    extern int __copy_code_start, __bss_start;


    volatile unsigned int *src = (unsigned int *)0;

    volatile unsigned int *dst = (unsigned int *)&__copy_code_start;

    volatile unsigned int *end = (unsigned int *)&__bss_start;


    while(dst < end)

    {

        *dst++ = *src++;

    }

}


__copy_code_start, __bss_start 是链接脚本里面的变量,引用外部变量,所以需要extern。现在的程序只支持nor 的重定位,而2440可以直接访问nor, 所以src=0 表示nor的0地址。&__copy_code_start表示取变量__copy_code_start的数值,也是0x30000000。这里的表达跟C语言不一样,C语言是取变量的地址,而__copy_code_start是链接脚本的变量,因此使用起来也不一样。可以把这个当做一种规则来记忆。同理,end = &__bss_start 也是取__bss_start的数值0x30001000。


这段代码的主要作用就是把nor 0地址的数据拷贝到0x30000000的地址去,长度为end - dst = &__bss_start - &__copy_code_start = 0x30001000 - 0x30000000 = 0x1000。这里只拷贝了text、data、rodata,bss段没有拷贝,接下来就处理bss段。(0x30000000就是SDRAM的起始地址,拷贝到这个地址就是拷贝到SDRAM里面,不理解的麻烦看看上一篇博客)


2.3 清空bss段

init.c


void clear_bss(void)

{

    extern int __bss_start, __end;

    volatile unsigned int *src = (unsigned int *)&__bss_start;

    volatile unsigned int *end = (unsigned int *)&__end;


    while(src <= end)

    {

        *src ++ = 0;

    }

}


同理,引用 __bss_start, __end链接脚本变量需要使用extern。这里的&__bss_start 和 &__end也是获取其数值。src = &__bss_start = 0x30001000, end = &__end = 30001008。这段代码的主要作用就是把0x30001000~0x30001008上的数据清空。而拷贝代码时不要拷贝bss段,因此这段数据都是0,所以只需要记录bss的起始位置和终止位置,然后将其清空即可。这也照应了前面说的,bss段可以减小bin文件的大小。


2.4 打印__copy_code_start, __bss_start, __end

init.c


void print_positon(void)

{

    extern int __copy_code_start, __bss_start, __end;


    volatile unsigned int *code_start = (unsigned int *)&__copy_code_start;

    volatile unsigned int *bss_start= (unsigned int *)&__bss_start;

    volatile unsigned int *end= (unsigned int *)&__end;


    printf("code_start:%xn", code_start);

    printf("bss_start:%xn", bss_start);

    printf("end:%xn", end);

}


print_positon 的作用就是打印__copy_code_start、__bss_start、__end的数值,因此需要先调用bl uart0_init来初始化串口。

在这里插入图片描述

2.5 程序跳转

start.s 最后执行 ldr pc, =main 将pc赋值成main的数值。通过dis文件可以看到, ldr pc, =main对应的语句为ldr pc, [pc, #16]。pc + 16 = 0x30000088, 0x30000088位置上的数值为0x30000af8,刚好main的数值就是0x30000af8。因此ldr pc, =main就等于pc=0x30000af8,此时程序就开始运行在SDRAM上了。


这里要特别说明,在执行ldr pc, =main之前,程序都是运行在nor上的,所以程序的地址不可能超过2M。虽然看到程序的起始地址是0x30000000,不过实际PC的值都需要减去0x30000000。虽然显示的地址为0x30000000+,不过程序是从nor的0地址开始烧录。复位后程序从nor 0地址开始取指令,也是对应的0x30000000。因此PC是从0开始往上增加,而不是从0x30000000开始往上增加,直到执行ldr pc, =main之后PC的值才真正的等于dis文件显示的。

在这里插入图片描述

在这里插入图片描述

2.6 测试

main.c


#include "uart.h"

//#include "key.h"

//#include "led.h"

#include "delay.h"

#include "my_printf.h"

#include "init.h"


//2018.9.1 测试bss 段的初始数据是否是0


char _g_char1 = 'A';

char _g_char2 = 'a';

int _g_a;

int _g_b = 0;

const char _g_const_char = 'B';


int main(int argc, char *argv[])

{

    uart0_init();


    printf("_g_a:%dn", _g_a);

    printf("_g_b:%dn", _g_b);


    while(1)

    {

        printf("%c", _g_char1);

        _g_char1 ++;


        printf("%c ", _g_char2);

        _g_char2 ++;

        delay(500000);

    }

return 0

}


程序定义了_g_char1和 _g_char2 两个全局变量,如果程序可以正常的跑到main函数并且这个2个变量的数值可以正常的增加,说明重定位成功。(目前只能测试nor)


2.7 总结

看完后需要懂得以下几个问题才算是弄懂了代码重定位:


为什么需要代码重定位?

bss为什么可以减小bin文件的大小

代码拷贝时为什么只拷贝text、rodata、data而不用拷贝bss段

为什么dis文件中显示程序的起始位置是0x30000000,而实际的PC值是从0开始。

为什么执行 ldr pc, =main 程序才真正的跳到SDRAM去执行了?

推荐阅读

史海拾趣

Elpakco Inc公司的发展小趣事

Elpakco Inc公司始终将产品质量视为企业的生命线。公司建立了严格的质量控制体系,从原材料采购到生产流程,再到产品出厂检测,每一个环节都严格把关。正是这种对品质的坚持,让Elpakco Inc的产品在市场上赢得了良好的口碑,越来越多的客户开始选择信赖Elpakco Inc的品牌。

EECO Switch公司的发展小趣事

为了满足全球客户的需求,EECO Switch公司积极实施全球化战略。公司在墨西哥、台湾和中国等地设立了制造工厂,并在英国剑桥设立了销售办事处。这些海外机构不仅为公司提供了更广阔的市场空间,还帮助公司更好地了解当地市场的需求和趋势。展望未来,EECO Switch将继续秉承创新、质量、服务的核心价值观,致力于成为全球领先的人机界面产品提供商。

Econais公司的发展小趣事

随着技术的不断成熟和市场的日益扩大,Econais开始将目光投向全球市场。公司积极参加各类国际展会和研讨会,与全球各地的客户和合作伙伴建立联系。同时,Econais还针对不同地区的市场需求,推出定制化的产品和解决方案。这些努力使得Econais在全球范围内的市场份额逐渐扩大。

Ememory Technology Inc公司的发展小趣事

在2022年,eMemory宣布加入英特尔晶圆代工服务加速器(Intel Foundry Services Accelerator)计划。这一合作标志着eMemory的技术得到了国际半导体巨头的认可。通过这一计划,eMemory为使用英特尔晶圆代工服务的客户提供全球领先的安全IP解决方案,进一步提升了其产品在全球市场的竞争力。

ETTINGER公司的发展小趣事

Ettinger,这个源于英国的皮具奢侈品牌,由Gerry Ettinger在1934年创立。自创立之初,Ettinger就致力于提供高品质的皮具产品,凭借其精湛的皮具制作工艺和独特的英格兰式设计风格,逐渐在市场上树立了良好的品牌形象。初期,Ettinger主要专注于手工制作皮具,通过不断提升产品质量和设计水平,赢得了消费者的青睐。

Dielectric Laboratories公司的发展小趣事

在电子元件市场竞争日益激烈的背景下,DLI意识到必须不断创新才能保持竞争优势。于是,公司开始研发多层陶瓷电容器(MLCC)技术。经过数年的努力,DLI成功推出了具有高性能、高可靠性特点的多层陶瓷电容器,广泛应用于军事、航空等领域。这一技术的突破不仅提升了DLI的市场地位,也为整个电子行业的发展做出了重要贡献。

问答坊 | AI 解惑

32个最热cpld_fpga网站

本帖最后由 paulhyde 于 2014-9-15 03:38 编辑 32个最热cpld_fpga网站  …

查看全部问答>

发明器,让人们专门搞发明的设备

发明器,让人们专门搞发明的设备小编记得有互联网投资皇帝之称的孙正义先生在年轻时就曾经应用了一个有趣的发明器来赞助自己搞创新,它的原理是可以将很多风马牛不相及的东西自由排列组合在一起,使你产生新的想法解决人们新的需求。成果依附这个发 ...…

查看全部问答>

物联网------之------智能家居

       前提:“我们在家里安装了红外幕帘探头、门磁感应传感器、燃气泄漏报警器、烟雾报警器等各种设备,形成了一个移动视频智能系统。”        早上出门前,开启安防报警系 ...…

查看全部问答>

在proteus中能否调试编译c代码?

在proteus中能否调试编译c代码? …

查看全部问答>

如何对ARM的IO进行操作

ARM芯片上有一些管脚没有寄存器对其进行控制,在WinCE驱动程序中,请问这样的管脚应该如何操作呢?如对扩展总线进行读写控制的nROE、nRWE,还有扩展的地址线RADDR(GPA有些管脚是RADDR,可以操作,但其它的应如何赋值并操作呢?)等。 …

查看全部问答>

弱问,关于网络驱动程序

首先,我是新手,没有接触过网卡驱动程序开发,仅看过windows上一个中间层的驱动代码。 我有些问题:    ndis仅仅是windows上的东西吗?    windows上开发一个驱动程序必须按照ndis吗?    网卡制造商如果仅有wi ...…

查看全部问答>

微型编程器电路

本帖最后由 jameswangsynnex 于 2015-3-3 19:57 编辑 …

查看全部问答>

swimerror[30004]:commtimeout

评估板原先还好好的,就先前调触摸按键拆了充放电的电阻,有个脚的3.3M还没焊接回去。这段时间一直用板子的5V供电。刚刚想再看看这块板子,就出现这种情况,STVD的help里也找不到相关信息额。 STLINK是没问题的,因为我还可以用它debug。 评 ...…

查看全部问答>

AD9850信号源模块_原理图+测试程序

本帖最后由 paulhyde 于 2014-9-15 03:42 编辑 原理图、PCB、测试程序和使用说明,适合做信号源的TX。  …

查看全部问答>

天运板子的DMA怎么调通啊?

我板子用的是天运科技的CC2530板子。例程是sesor demo,这个例程在我原来用的板子上用的时候电脑是可以接受到数据的,但是吧这个例程下到天运的板子就是接受不到数据。协议栈我没有用他们资料里带的。还是以前用的版本,,我弄了好几天了不知道怎么 ...…

查看全部问答>