美文网首页
链接脚本(Linker Script)解析

链接脚本(Linker Script)解析

作者: 炎鸿 | 来源:发表于2023-11-18 02:00 被阅读0次

    原文链接: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
    }
    

    相关文章

      网友评论

          本文标题:链接脚本(Linker Script)解析

          本文链接:https://www.haomeiwen.com/subject/fevuwdtx.html