链接脚本(Linker Script)解析

每一个嵌入式开发工程师,在工作越来越深入后,都会慢慢接触到链接脚本,那么什么是链接脚本,我们又该如何结合实际去做自己的链接脚本呢?

一、什么是链接脚本

链接脚本实质上是一个规则文件,程序员用来只会链接器工作的,当我们编写了多个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
}
//Note:在这里开辟堆栈空间,在ram段最后取4k空间,二者相对增加

//Note:由于此“链接脚本”意图是让数据存储在Flash之中,而将数据段上载至DLM中进行运行,所以数据段物理地址和运行地址不同,所以,上述.data数据段的物理地址是flash区间,而运行地址为ram区间

你可能感兴趣的:(gcc,计算机,编译,汇编)