链接脚本(Linker Script)解析
原文链接:https://blog.csdn.net/weixin_43083491/article/details/127095711
每一个嵌入式开发工程师,在工作越来越深入后,都会慢慢接触到链接脚本,那么什么是链接脚本,我们又该如何结合实际去做自己的链接脚本呢?
一、什么是链接脚本
链接脚本实质上是一个规则文件,程序员用来只会链接器工作的,当我们编写了多个C文件,调用了大量库,如何编译成一个可执行文件呢?这是链接脚本文件就会发挥他的作用了:将多个目标文件(xx.o)、库文件(xx.a)、动态库(.so)等等链接成一个可执行文件。
二、链接脚本规则
为了方便学习,可以将链接脚本规则分为三部分,后面再拿一个实例分析下。
2.1 链接配置
OUTPUT_FORMAT("elf32-little")
OUTPUT_ARCH("riscv")
ENTRY(_start)
其中,前两句指定了输出格式和输出架构,ENTRY指定整个程序入口,一般_start是startup.S的第一个lable。另外,要知道程序入口并不是说它位于存储介质的起始位置
2.2 内存分布定义
MEMORY
{
sram (rxa!w) : ORIGIN = 0x1c000000, LENGTH = 256K
ilm (rxa!w) : ORIGIN = 0x08000000, LENGTH = 64K
FLASH (wxa!r) : ORIGIN = 0x08010000, LENGTH = 64K
}
MEMORY表示对内存分布的定义,拿sram (rxa!w) : ORIGIN = 0x1c000000, LENGTH = 256K来说:
sram是内存块的名字
rxa!w是该内存的属性ATTR
ORIGIN 是区域的起始地址
LENGTH是内存区域的大小
ATTR :定义该存储区域的属性。ATTR属性内可以出现以下7 个字符:
R 只读section
W 读/写section
X 可执行section
A 可分配的section
I 初始化了的section
L 同I
! 不满足该字符之后的任何一个属性的section
2.3 段列表定义
SECTIONS命令功能非常强大,可以通过对SECTIONS命令定义一些段(.text、.data、.bss等段)链接分布。
SECTIONS
{
.text :
{
*(.text*)
} > FLASH
}
其中:
.text段即代码段,* (.text*)指示将工程中所有目标文件的.text段,> FLASH 表示将.text链接到FLASH中。
更加通用模板如下:
SECTIONS
{
...
secname [start_ADDR] [(TYPE)] : [AT (LMA_ADDR)]
{
contents
} [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
...
}
其中contents和secname是必须的,
secname为段名字
contents表示将所有的目标的secname段链接到输出文件secname
start_ADDR:表示将某个段强制链接到的地址。
AT( LAM_ADDR ):实现存放地址和加载地址不一致的功能,AT表示在文件中存放的位置,而在内存里呢,按照普通方式存储。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围
REGION 这个region就是前面说的MEMORY命令定义的位置信息。
TYPE每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。
PS
一般情况下section的VMA == LMA;但在嵌入式系统中,为了节省MCU的面积,要减小片上SRAM的大小,会将加载地址和执行地址定义在不同地方:比如将文件加载烧录到开发板的flash中(由LMA指定),而在运行时将位于flash中的输出文件复制到SDRAM中去执行(由VMA指定)。
VMA(virtual memory address虚拟内存地址或程序地址空间地址)
LMA(load memory address加载内存地址或进程地址空间地址)
VMA是执行输出文件时section所在的地址
LMA是加载输出文件时section所在的地址
三、链接脚本关键字
上面在介绍sections时已经说了一些,这里介绍链接脚本常见一些修饰关键字如下:
‘.’ 定位符号
‘.’ 表示当前地址,可以作为左值也可作为右值。
. = 0x08000000; /当前地址设为0x08000000/
RAM_START = .; /将当前地址赋值给RAM_START/
KEEP
连接命令行内使用了选项–gc-sections后,连接器可能将某些它认为没用的section过滤掉,要强制连接器保留一些特定的section
PROVIDE
该关键字定义一个(输入文件内被引用但没定义)符号。相当于定义一个全局变量的符号表,其他C文件可以通过该符号来操作对应的存储内存。
ALIGN
表示字节对齐, 如 “ . = ALIGN(4);”表示从该地址开始后面的存储进行4字节对齐。
SECTIONS
{
.text :
{
*(.text) PROVIDE(_etext = .);
}
}
如上,在链接脚本中声明了_etext 符号。特别注意的是_etext 只是一个符号,没有存储内存,并不是一个变量,该符号对应(映射)的是一个地址,其地址为 .text section 之后的第一个地址。C文件中引用用法如下:
int main()
{
//引用该变量
extern char _etext;
char *p = &_etext;
//...
}
若在链接脚本中 " _etext = 0x100; ",即表示符号_etext对应的地址为0x100, 此时 & _etext的值为 0x100, char a= *p;表示为 从0x100地址取存储的值赋值给变量a
NOLOAD 就是上面我们提到的TYPE类型中的一类,表示该section在程序运行时,不被载入内存。
另外,还有四种类型DSECT,COPY,INFO,OVERLAY,只是这些类型很少被使用,为了向后兼容才被保留下来。
常见段含义
.text 代码段 存放程序执行代码区域
.data 初始化数据段 存放程序中已初始化的全局变量
.bss 未初始化的数据段 存放程序中未初始化的全局变量
.rodata 只读数据段 存放C中的字符串和#define定义的常量
.heap 堆 存放进程运行中被动态分配的内存段(malloc)
.stack 栈 存放程序临时创建的局部变量及函数调用压栈入栈(static 变量存放在数据段)
NOTE:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据。局部变量没有初始化是随机的,全局变量没有初始化是 0。
这里很有感悟,前面接收了同事写的代码,main函数里用来配置寄存器的一个局部变量未初始化,后面调试总是各类问题,verdi仿真没问题硬件也没问题,调试了两天,后面fpga上板把所有寄存器初始化前后都打印出来才定位到这里。所以嘛。。。
另外链接脚本还可以在startup.S中定义,
四、 链接脚本实例
链接脚本里注释用 /* */(同C,但不可以用//哦)
额其实看了前三节的介绍,自己应该很容易看懂,就是最后堆栈那块,后面有时间解释下。
ENTRY( _start ) //指明程序入口为_start标签
MEMORY
{
//定义了三块地址区间,分别名为flash,ilm和ram,对应Flash,ILM和DLM的地址区间
flash (rxai!w) : ORIGIN = 0x00020000, LENGTH = 4M
ilm (rxai!w) : ORIGIN = 0x00080000, LENGTH = 64K
ram (wxa!ri) : ORIGIN = 0x00090000, LENGTH = 64K
}
//起一组别名,和日常认知对应
REGION_ALIAS("ROM", flash)
REGION_ALIAS("RAM", ram)
SECTIONS
{
.init :
{
KEEP (*(SORT_NONE(.init)))
} >ROM AT>flash
//Note:上述语法中AT前的一个flash表示该段的运行地址,AT后的flash表示该段的物理地址。
//物理地址是该程序要被存储在的存储器地址(调试器下载程序之时会遵从此物理地址进行下载),运行地址却是指程序真正运行起来后所处于的地址,所以程序中的相对寻址都会遵从此运行地址。
//Note:上述.init段为上电引导程序所处的段,所以它直接在Flash里面执行,所以其运行地址和物理地址相同,都是flash区间。
.ilalign :
{
. = ALIGN(4);
PROVIDE( _ilm_lma = . ); //创建一个标签名为_ilm_lma,地址为flash地址区间的起始地址
} >flash AT>flash
.ialign :
{
PROVIDE( _ilm = . ); //创建一个标签名为_ilm,地址为ilm地址区间的起始地址
} >ilm AT>flash
.text :
{
*(.vtable_ilm) //中断向量表(ILM)
*(.text.unlikely .text.unlikely.*)
*(.text.startup .text.startup.*)
*(.text .text.*)
*(.gnu.linkonce.t.*)
} >ilm AT>flash
//Note:由于此“链接脚本”意图是让程序存储在Flash之中,而上载至ILM中进行运行,所以其物理
//地址和运行地址不同,所以,上述.text代码段的物理地址是flash区间,而运行地址为ilm区间
.data :
{
*(.rdata)
*(.rodata .rodata.*)
*(.gnu.linkonce.r.*)
*(.data .data.*)
*(.gnu.linkonce.d.*)
. = ALIGN(8);
PROVIDE( __global_pointer$ = . + 0x800 ); //创建一个标签名为__global_pointer$
*(.sdata .sdata.* .sdata*)
*(.gnu.linkonce.s.* )
. = ALIGN(8);
*(.srodata.cst16)
*(.srodata.cst8)
*(.srodata.cst4)
*(.srodata.cst2)
*(.srodata .srodata.*)
} >RAM AT>ROM
.bss (NOLOAD) : ALIGN(8)
{
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss .bss.*)
*(.gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(4);
} >RAM AT>RAM
PROVIDE( _end = . );
PROVIDE( end = . );
PROVIDE(__STACK_SIZE = 2K);
PROVIDE(__HEAP_SIZE = 2K);
.heap (NOLOAD) : ALIGN(16) //heap need to be align at 16 bytes
{
. = ALIGN(16);
PROVIDE( __heap_start = . ); //define __heap_start
. += __HEAP_SIZE; //heap space :__HEAP_SIZE
. = ALIGN(16);
PROVIDE( __heap_limit = . );
} >RAM AT>RAM
.stack ORIGIN(RAM) + LENGTH(RAM) - __TOT_STACK_SIZE (NOLOAD) :
{
. = ALIGN(16);
PROVIDE( _heap_end = . );
PROVIDE( __heap_end = . ); //define __heap_end
PROVIDE( __StackLimit = . );
PROVIDE( __StackBottom = . );
. += __TOT_STACK_SIZE;
. = ALIGN(16);
PROVIDE( __StackTop = . ); //栈顶位置,地址增长方向为从大到小
PROVIDE( _sp = . );
} >RAM AT>RAM
}