链接脚本ld

必要性

作为嵌入式开发,随着工作的深入,会接触到链接脚本,掌握基础链接脚本语法,对于嵌入式开发非常有必要。

什么是链接脚本

链接脚本实质上是一个规则文件,程序员用来指示链接器工作的,当我们编写了多个C文件,调用了大量库,如何编译成一个可执行文件呢?这是链接脚本文件就会发挥他的作用了:将多个目标文件(xx.o)、库文件(xx.a)、动态库(.so)等等链接成一个可执行文件。

链接脚本规则

链接配置

OUTPUT_FORMAT("elf32-little")
OUTPUT_ARCH("riscv")
ENTRY(_start)

其中,前两句指定了输出格式和输出架构,ENTRY指定整个程序入口,一般_start是startup.S的第一个lable。另外,要知道程序入口并不是说它位于存储介质的起始位置。

内存分布定义

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

段列表定义

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所在的地址

链接脚本关键字

‘.’ 定位符号
‘.’ 表示当前地址,可以作为左值也可作为右值。

. = 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 变量存放在数据段)

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区间

参考链接

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值