程序编译的几个阶段
一般而言,程序编译经历下图四个阶段,链接是编译的最后一步,无论是在PC上编译代码,还是在PC上使用嵌入式gcc工具交叉编译嵌入式代码,编译过程都是如下几步。深入理解链接过程是嵌入式工程师必要掌握的能力!
ld链接脚本的基础概念
链接过程是将各式各样的.o文件链接为一个文件的过程。链接脚本描述连接器如何将这些输入文件(.o)文件映射为一个输出文件的,并且定义了输出文件的memory layout。几乎所有的链接脚本都是在做这些事情。
下面给出一个简单的链接脚本实例,每行脚本都有相应的注解:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.bss : { *(.bss) }
}
上面提到的定位计数器就是点 ‘.’
这个链接脚本文件(Linker Scripty),用于告诉链接器如何将不同的代码和数据段(sections)组合在一起形成可执行文件。下面我会解释其中的每一部分:
1
. = 0x10000;
这行代码重新设置了定位计数器(location counter)的值为0x10000,即地址0x10000。
它告诉链接器在此处开始分配.text段的地址空间。
2
.text : { *(.text) }
这行代码定义了一个.text段,并告诉链接器将所有名为.text的数据节(section)放入这个段中。
*(.text)表示将所有输入文件中的.text段合并到输出文件的.text段中。
3
. = 0x8000000;
这行代码重新设置了定位计数器的值为 0x8000000,即地址 0x8000000。
它告诉链接器在此处开始分配.data和.bss段的地址空间。
4
.data : { *(.data) }
这行代码定义了一个.data段,并告诉链接器将所有名为.data的数据节放入这个段中。
*(.data)表示将所有输入文件中的.data段合并到输出文件的.data段中。
5
.bss : { *(.bss) }
这行代码定义了一个.bss段,并告诉链接器将所有名为.bss的数据节放入这个段中。
*(.bss)表示将所有输入文件中的.bss段合并到输出文件的.bss段中。
总体来说,这段链接脚本告诉链接器在特定的地址处分配.text、.data和.bss段,并将对应的数据节合并到这些段中。
链接脚本相关的概念
内存(Memory)
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
…
}
注解:这里的“attr”只能由以下特性组成:
‘R’ Read-only section
‘W’ -- Read/write section
‘X’ -- Executable section
‘A’ -- Allocatable section
‘I’ -- Initialized section
‘L’ -- Same as ‘I’
‘!’ -- Invert the sense of any of the attributes that follow
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000300, LENGTH = 36K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
}
注解:
“xrw”表示“RAM”区是可读、可写和可执行的,且RAM 的起始地址为“0x20000300”,长度为36K。
“rx”表示“FLASH”区是可读和可执行的,FLASH的起始地址为“0x08000000”,长度为128K。
段(Section)
Section有loadable(可加载)和allocatable(可分配)两种类型。不可加载也不可分配的内存段,通常包含某些调试信息。
loadable(可加载)是指:程序运行时,该段内容应该被加载到内存中。
allocatable(可分配)是指:该段的内容应该被预留出,但不应该加载任何别的内容(某些情况下,这些内存必须归零)。
“可加载”和“可分配”的section都有两个地址:“VMA”和“LMA”。
VMA(the virtual memory address):这是运行输出文件时,该section的地址。VMA是可选项,可以不设置。
LMA(load memory address):这是加载section时的地址。
在大多数情况下,这两个地址是相同的。当然也可以不相等,比如下面的例子就是LMA和VMA不同的案例:
数据段被加载到ROM中,然后在程序启动时复制到RAM中(通常用于初始化全局变量)。此时ROM地址就是LMA,RAM地址就是VMA。
语法:
SECTIONS
{
section [address] [(type)] :
{
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
…
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp] [,]
...
}
大多数的段仅使用了上述的一部分属性。
示例:
/* Sections */
SECTIONS
{
/* The startup code into "FLASH" Rom type memory */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
/* Initialized data sections into "RAM" Ram type memory */
.data:
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH
}
上述示例中“.isr_vector”的LMA与VMA是相等的。“.data”因为有“>RAM AT> FLASH”的修饰,表示.data段的VMA为RAM,LMA为FLASH。即.data段的内容会放在FLASH中,但是运行时,会加载到RAM中。
常用命令
ASSERT
语法:ASSERT(exp, message)
确保exp是非零值,如果为零,将以错误码的形式退出链接文件,并输出message。在必要的位置添加断言,可以清晰的定位问题。
/* The usage of ASSERT */
.test :
{
ASSERT ((_estack > (_Min_Stack_Size + _Min_Heap_Size)),"Error: There is an ERR occurred");
}
当示例中的“_estack”大于“_Min_Stack_Size + _Min_Heap_Size”时,就会打印“There is an ERR occurred”。
KEEP
用途:当链接器使用('--gc-sections')进行垃圾回收时,KEEP()可以使得被标记段的内容不被清除。
/* The startup code into "FLASH" Rom type memory */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
指定“变量”的输出地址:
可以定义如下的memory,然后将“变量”存放于该memory,就能控制“变量”的输出地址。
/* Memories definition */
MEMORY
{
FW_RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 0x300 /* 0x20000000 ~ 0x200002FF */
RAM (xrw) : ORIGIN = 0x20000300, LENGTH = 35K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
}
同时在c文件中,在定义“变量”时,添加如下对应的属性:
__attribute__((section(".FW_RAM"))) uint8_t key[8] = {0,1,2,3,4,5,6,7 };
变量将位于“0x20000000 ~ 0x200002FF”区域(如果仅仅只有key数组位于该区域,将从0x20000000开始存放,如果有多个变量存储于该区域,将按照编译的顺序,从0x20000000依次存放)。
指定“函数”的输出地址:
可以定义如下的memory和section,然后将“函数”存放于该section,就能控制“函数”的输出地址。
/* Memories definition */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x300 /* 0x08000000 ~ 0x080002FF */
CG_FLASH (rx) : ORIGIN = 0x08000300, LENGTH = 0x134 /* 0x08000300 ~ 0x08000433 */
RAM (xrw) : ORIGIN = 0x20000300, LENGTH = 0x900 /* 0x20000300 ~ 0x20001FFF */
}
SECTIONS
{
…
.SE_Call_Fun:
{
. = ALIGN(4);
. = . + 0x4;
*(.SE_Call_Fun)
. = ALIGN(4);
} > CG_FLASH
…
}
同时在c文件中, 在“函数”的实现部分,添加如下对应的属性:
__attribute__((section(".SE_Call_Fun"))) uint32_t call_fun(Callgate_Func_Type_t ftype, void *param)
函数“call_fun”将存放于0x08000304处(留意此处的位置计数器将产生0x04的内存间隙)。
指定“文件”输出地址:
可以定义如下的memory和section,然后将指定的文件存放于该section,就能控制“文件”的输出地址。
/* Memories definition */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x300 /* 0x08000000 ~ 0x080002FF */
FW_FLASH (rx) : ORIGIN = 0x08000434, LENGTH = 0x2BCC/* 0x08000434 ~ 0x08003000 */
RAM (xrw) : ORIGIN = 0x20000300, LENGTH = 0x900 /* 0x20000300 ~ 0x20001FFF */
}
/* Sections */
SECTIONS
{
…
.main_section :
{
. = ALIGN(4);
Core/Src/main.o(.text*)
. = ALIGN(4);
} >FLASH
…
}
示例中将main.o指定到FLASH区域中;更改FLASH的地址或者main_section的LMA,就可以实现将特定文件指定到特定内存区域。
案例:RZ/N2L把 .text, .data, .bss段从ATCM改到SYSTEM_RAM
这里描述的RZ/N2L的内存分配:
长按可保存查看大图
把.text段从ATCM改到SYSTEM_RAM:
长按可保存查看大图
把.data段从ATCM改到SYSTEM_RAM:
长按可保存查看大图
.bss段的改动也是类似的:
长按可保存查看大图
如您在使用瑞萨MCU/MPU产品中有任何问题,可识别下方二维码或复制网址到浏览器中打开,进入瑞萨技术论坛寻找答案或获取在线技术支持。
https://community-ja.renesas.com/zh/forums-groups/mcu-mpu/
1
END
1
推荐阅读
e² studio小技巧 - 创建仪表等其它视觉反馈
e² studio小技巧 - 如何在项目中更改设备
e² studio在Linux PC的应用快速入门指南 - 安装e² studio和RA GNU Arm嵌入式工具链