s3c2440学习之路-011代码重定位

硬件平台:jz2440
软件平台:Ubuntu16.04 arm-linux-gcc-3.4.5
源码位置: https://github.com/lian494362816/C/tree/master/2440/012_relocate/008

文章目录

  • 1 基本原理
    • 1.1 程序段的划分
    • 1.2 为什么要需要重定位代码
    • 1.3 如何实现重定位
    • 1.4 lds链接脚本
  • 2 源码讲解
    • 2.1 主要流程
    • 2.2 text、data、rodata段的拷贝
    • 2.3 清空bss段
    • 2.4 打印__copy_code_start, __bss_start, __end
    • 2.5 程序跳转
    • 2.6 测试
  • 2.7 总结

1 基本原理

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

1.1 程序段的划分

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

名称 作用
.txt 代码段 存储代码
.data 数据段 存储数据
.rodata 存储只读数据段,主要是使用const修饰过的变量
bss 未初始化和初始化为0的全局(static 修饰的局部)变量 (减小bin文件的大小)
.comment 注释段 注释作用,不占用bin文件的大小

这里主要讲一下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文件的大小,代码实际运行起来时消耗的内存大小是一样的。
s3c2440学习之路-011代码重定位_第1张图片

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
s3c2440学习之路-011代码重定位_第2张图片

test.dis
s3c2440学习之路-011代码重定位_第3张图片

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:%x\n", code_start);
    printf("bss_start:%x\n", bss_start);
    printf("end:%x\n", 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文件显示的。

s3c2440学习之路-011代码重定位_第4张图片

s3c2440学习之路-011代码重定位_第5张图片

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:%d\n", _g_a);
    printf("_g_b:%d\n", _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 总结

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

  1. 为什么需要代码重定位?
  2. bss为什么可以减小bin文件的大小
  3. 代码拷贝时为什么只拷贝text、rodata、data而不用拷贝bss段
  4. 为什么dis文件中显示程序的起始位置是0x30000000,而实际的PC值是从0开始。
  5. 为什么执行 ldr pc, =main 程序才真正的跳到SDRAM去执行了?

你可能感兴趣的:(s3c2440学习)