嵌入式内存学习三——从LMA->VMA代码搬运机制全解析
本文对比了ARMCC和GCC在嵌入式系统中代码搬运的实现方式。ARMCC通过.sct分散加载文件自动完成代码从FLASH到RAM的搬运,而GCC则需要开发者手动实现搬运逻辑。重点分析了RT-Thread使用GCC时的.data段搬运过程:通过ld链接脚本定义_sidata(FLASH源地址)、_sdata和_edata(RAM目标地址范围),在Reset_Handler中使用汇编指令实现类似mem
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里面。
这里先埋个坑嘿嘿嘿,不知道补不补。
更多推荐



所有评论(0)