1、文章背景:

上一章节:嵌入式内存学习二——通过官方文档讲解内存与代码之间的桥梁—ld链接脚本_多编译器链接脚本 ld iar-CSDN博客

        上一章中我们讲到了ld链接文件的各类讲解,但他只是告诉MCU如果我需要调用函数的话,我应该在哪找,但我们也知道,我们的各类指令和数据,都是存到FLASH里面的,因为RAM掉电就丢失数据,只有在FALSH里面才可以掉电保存。

        在上一章的时候,我说过了Reset_handler函数的作用是搬运代码从FLASH到RAM中的,因此本章我们就开始讲解RT-Thread是怎么将代码搬到指定的地方,让我们项目调用的时候能精准的找到函数/数据。

2、搬运代码讲解

2.1 ARM CC搬运流程 

首先我说明一点,如果你用的是ARM C库(也就是keil)的时候,你是不需要自己搬代码到RAM中的,你只需要设计好.sct分散加载文件,在上电的时候ARM C库会自动帮你搬到RAM中,我们从keil的Reset_Handler函数中也能看到。如下

; Reset handler
Reset_Handler    PROC
                 EXPORT  Reset_Handler                    [WEAK]
        IMPORT  SystemInit            ;相当于C语言的extern,告诉编译器有这个函数,要你自己去找
        IMPORT  __main

                 LDR     R0, =SystemInit    ;将SystemInit函数的地址加载到r0寄存器
                 BLX     R0                 ;跳转执行r0寄存器(还回来的)
                 LDR     R0, =__main        ;将__main函数的地址加载到r0寄存器
                 BX      R0                 ;跳转执行r0寄存器(不回来的,相当于单程票)
                 ENDP

等我们跳转到SystemInit函数的时候,发现都是一些配置寄存器的代码

这也印证了我上面的结论。

2.2 gcc库的搬运流程

        但对于RT-Thread studio的GCC库,他没有那么智能。GCC 的哲学是“显式优于隐式”,它不像 ARMCC 的 __main 也是个黑盒,GCC 把所有的搬运逻辑都写在汇编里让你看、让你改。因此你要手动实现搬运函数

2.2.1 .data段讲解

        我们先把.data段单独拉出来讲,因为他和RAM中的栈、堆、.bss段不一样。

        首先我们知道.data段存放的是赋非0初值的全局变量和添加了static的局部变量,你不像.bss段,他保存的是初始值为0或者没有初始化的全局变量,我一上电整个RAM就是0,变量数据根本不需要保存。因此,我们的.data的数据是保存在FLASH上面的,在MCU上电的时候,就会通过Reset_handler函数给他搬过来。

2.2.2 ld文件的设计

        好了,有了这个思路,我们就要知道三样数据,FLASH里存放.data段的起始地址_sidata,RAM里面存放的起始地址_sdata和结束地址_edata

        有的人就会好奇,我不需要知道FLASH的.data段的结束地址吗?

        答案是不需要,因为ld文件知道.data段在RAM的大小和FLASH的大小都是一样的,因此RAM里面保存的起始地址_sdata和结束地址_edata 之间的大小就是FLASH要搬运的大小

我们先查看RT-Thread的ld文件是怎么写的吧。

 .text :
    {
        . = ALIGN(4);
        _stext = .;
        KEEP(*(.isr_vector))            /* Startup code */

        . = ALIGN(4);
        *(.text)                        /* remaining code */
        *(.text.*)                      /* remaining code */
        *(.rodata)                      /* read-only data (constants) */
        *(.rodata*)  /**这里就是我们常说的.text段和.rodata段,他们紧密相连**/
        *(.glue_7)         
        *(.glue_7t)
        *(.gnu.linkonce.t*)

        /* section information for finsh shell */
        . = ALIGN(4);
        __fsymtab_start = .;
        KEEP(*(FSymTab))
        __fsymtab_end = .;

        . = ALIGN(4);
        __vsymtab_start = .;
        KEEP(*(VSymTab))
        __vsymtab_end = .;

        /* section information for utest */
        . = ALIGN(4);
        __rt_utest_tc_tab_start = .;
        KEEP(*(UtestTcTab))
        __rt_utest_tc_tab_end = .;

        /* section information for at server */
        . = ALIGN(4);
        __rtatcmdtab_start = .;
        KEEP(*(RtAtCmdTab))
        __rtatcmdtab_end = .;
        . = ALIGN(4);

        /* section information for initial. */
        . = ALIGN(4);
        __rt_init_start = .;
        KEEP(*(SORT(.rti_fn*)))
        __rt_init_end = .;

        . = ALIGN(4);

        PROVIDE(__ctors_start__ = .);
        KEEP (*(SORT(.init_array.*)))
        KEEP (*(.init_array))
        PROVIDE(__ctors_end__ = .);

        . = ALIGN(4);

        _etext = .;
    } > AXISRAM AT > ROM = 0

    /* .ARM.exidx is sorted, so has to go in its own output section.  */
    __exidx_start = .;
    .ARM.exidx :
    {
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)

        /* This is used by the startup in order to initialize the .data secion */
        _sidata = .;
    } > ROM
    __exidx_end = .;

    /* .data section which is used for initialized data */

    .data : AT (_sidata)
    {
        . = ALIGN(4);
        /* This is used by the startup in order to initialize the .data secion */
        _sdata = . ;

        *(.data)
        *(.data.*)
        *(.gnu.linkonce.d*)


        PROVIDE(__dtors_start__ = .);
        KEEP(*(SORT(.dtors.*)))
        KEEP(*(.dtors))
        PROVIDE(__dtors_end__ = .);

        . = ALIGN(4);
        /* This is used by the startup in order to initialize the .data secion */
        _edata = . ;
    } >DTCMSRAM

根据ld文件的排队法则,我们知道在FLASH各个段的排队顺序是:

中断向量表 -> .text -> .rodata -> 各种表 -> 异常表 -> .data 初始值

得到了我们的_sidata之后我们就使用AT指令指定加载地址了。如.data : AT (_sidata)

如果我们没有用这个AT指令的话,我们的MCU就会默认LMA=VMA,到时候如果我们要找附初值的变量的时候只能去FLASH找了。

2.2.3 Reset_handler的搬运工作

我们在ld文件设置好了之后,我们就要在Reset_handle中将.data段搬运过去,也是本章的重点。如下

/* start address for the initialization values of the .data section.
defined in linker script */
.word  _sidata
/* start address for the .data section. defined in linker script */
.word  _sdata
/* end address for the .data section. defined in linker script */
.word  _edata
/* start address for the .bss section. defined in linker script */
.word  _sbss
/* end address for the .bss section. defined in linker script */
.word  _ebss
/* stack used for SystemInit_ExtMemCtl; always internal RAM used */

/**
 * @brief  This is the code that gets called when the processor first
 *          starts execution following a reset event. Only the absolutely
 *          necessary set is performed, after which the application
 *          supplied main() routine is called.
 * @param  None
 * @retval : None
*/

    .section  .text.Reset_Handler
  .weak  Reset_Handler
  .type  Reset_Handler, %function
Reset_Handler:
  ldr   sp, =_estack      /* set stack pointer */

/* Call the ExitRun0Mode function to configure the power supply */
  bl  ExitRun0Mode
/* Call the clock system initialization function.*/
  bl  SystemInit

/* Copy the data segment initializers from flash to SRAM */
  ldr r0, =_sdata     ;_sdata是在ld文件定义的.data段起始地址(是加载到RAM的地址),_edata同理

  ldr r1, =_edata     ;ldr汇编指令的意思是将_edata地址保存到r1中

  ldr r2, =_sidata    ;_sidata也是.data段的起始地址,只是是在FLASH中的

  movs r3, #0         ;将r3寄存器赋值0,这里r3的作用是作为偏移量出现。

  b LoopCopyDataInit  ;跳转到LoopCopyDataInit(不回来的那种)

CopyDataInit:
  ldr r4, [r2, r3]   ;[r2, r3] 的计算公式为:实际操作的内存地址 = r2 的值 + r3 的值,保存到r4
                     ;从这个地址读取 4个字节(32位) 的数据,暂存在 r4 寄存器里。

  str r4, [r0, r3]   ;把 r4 里的数据写入到这个地址。

  adds r3, r3, #4    ;将偏移值添加4


LoopCopyDataInit:
  adds r4, r0, r3    ;r0存的是加载到RAM的起始地址

  cmp r4, r1         ;检测r4是否大于等于r1,如果是,就退出

  bcc CopyDataInit   ;跳转到CopyDataInit执行(还回来的)

/* Zero fill the bss segment. */
  ldr r2, =_sbss
  ldr r4, =_ebss
  movs r3, #0
  b LoopFillZerobss

FillZerobss:
  str  r3, [r2]
  adds r2, r2, #4

LoopFillZerobss:
  cmp r2, r4
  bcc FillZerobss

/* Call static constructors */
    bl __libc_init_array
/* Call the application's entry point.*/
  bl  main
  bx  lr

        通过上面的解释应该很清楚了,汇编指令的搬运其实也是类似于mencpy,4字节4字节的把FLASH里面的.data代码一个个的搬到RAM里面的。

        其中_sidata保存的就是.data在FLASH存储的起始地址(相当于代码源地址 LMA,然后_sdata是加载到RAM的地址(相当于代码加载地址 VMA

3.重加载的用途

我们知道了重加载,除了搬运.data段到RAM,还有什么用呢?

还有的就是假如你觉得FLASH上运行代码太慢了,你想要把他全部都搬到RAM里面(比如32M的SDRAM),那么你就要使用重加载技术了。将整个代码搬到SDRAM里面。

这里先埋个坑嘿嘿嘿,不知道补不补。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐