链接脚本文件(.ld .lds)详解

链接脚本官方文档:

https://sourceware.org/binutils/docs-2.39/ld.html 

链接脚本实例:(STM32F407VG,RT-Thread Studio生成的工程所含)

/*
 * linker script for STM32F407ZG with GNU ld
 */

/* Program Entry, set to mark it as "used" and avoid gc */
MEMORY
{
    ROM (rx) : ORIGIN = 0x08000000, LENGTH =  1024k /* 1024K flash */
    RAM (rw) : ORIGIN = 0x20000000, LENGTH =  128k /* 128K sram */
}
ENTRY(Reset_Handler)
_system_stack_size = 0x400;

SECTIONS
{
    .text :
    {
        . = ALIGN(4);
        _stext = .;
        KEEP(*(.isr_vector))            /* Startup code */

        . = ALIGN(4);
        *(.text)                        /* remaining code */
        *(.text.*)                      /* remaining code */
        *(.rodata)                      /* read-only data (constants) */
        *(.rodata*)
        *(.glue_7)
        *(.glue_7t)
        *(.gnu.linkonce.t*)

        /* section information for finsh shell */
        . = ALIGN(4);
        __fsymtab_start = .;
        KEEP(*(FSymTab))
        __fsymtab_end = .;

        . = ALIGN(4);
        __vsymtab_start = .;
        KEEP(*(VSymTab))
        __vsymtab_end = .;

        /* section information for utest */
        . = ALIGN(4);
        __rt_utest_tc_tab_start = .;
        KEEP(*(UtestTcTab))
        __rt_utest_tc_tab_end = .;

        /* section information for at server */
        . = ALIGN(4);
        __rtatcmdtab_start = .;
        KEEP(*(RtAtCmdTab))
        __rtatcmdtab_end = .;
        . = ALIGN(4);

        /* section information for initial. */
        . = ALIGN(4);
        __rt_init_start = .;
        KEEP(*(SORT(.rti_fn*)))
        __rt_init_end = .;

        . = ALIGN(4);

        PROVIDE(__ctors_start__ = .);
        KEEP (*(SORT(.init_array.*)))
        KEEP (*(.init_array))
        PROVIDE(__ctors_end__ = .);

        . = ALIGN(4);

        _etext = .;
    } > ROM = 0

    /* .ARM.exidx is sorted, so has to go in its own output section.  */
    __exidx_start = .;
    .ARM.exidx :
    {
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)

        /* This is used by the startup in order to initialize the .data secion */
        _sidata = .;
    } > ROM
    __exidx_end = .;

    /* .data section which is used for initialized data */

    .data : AT (_sidata) // 设置加载首地址为 _sidata,位于ROM区
    {
        . = ALIGN(4);
        /* This is used by the startup in order to initialize the .data secion */
        _sdata = . ;

        *(.data)
        *(.data.*)
        *(.gnu.linkonce.d*)


        PROVIDE(__dtors_start__ = .);
        KEEP(*(SORT(.dtors.*)))
        KEEP(*(.dtors))
        PROVIDE(__dtors_end__ = .);

        . = ALIGN(4);
        /* This is used by the startup in order to initialize the .data secion */
        _edata = . ;
    } >RAM // 设置运行地址位于RAM区

    .stack : 
    {
        . = ALIGN(4);
        _sstack = .;
        . = . + _system_stack_size;
        . = ALIGN(4);
        _estack = .;
    } >RAM

    __bss_start = .;
    .bss :
    {
        . = ALIGN(4);
        /* This is used by the startup in order to initialize the .bss secion */
        _sbss = .;

        *(.bss)
        *(.bss.*)
        *(COMMON)

        . = ALIGN(4);
        /* This is used by the startup in order to initialize the .bss secion */
        _ebss = . ;
        
        *(.bss.init)
    } > RAM // bss段只设置运行地址位于RAM区,不占用ROM区
    __bss_end = .;

    _end = .;

    /* Stabs debugging sections.  */
    .stab          0 : { *(.stab) }
    .stabstr       0 : { *(.stabstr) }
    .stab.excl     0 : { *(.stab.excl) }
    .stab.exclstr  0 : { *(.stab.exclstr) }
    .stab.index    0 : { *(.stab.index) }
    .stab.indexstr 0 : { *(.stab.indexstr) }
    .comment       0 : { *(.comment) }
    /* DWARF debug sections.
     * Symbols in the DWARF debugging sections are relative to the beginning
     * of the section so we begin them at 0.  */
    /* DWARF 1 */
    .debug          0 : { *(.debug) }
    .line           0 : { *(.line) }
    /* GNU DWARF 1 extensions */
    .debug_srcinfo  0 : { *(.debug_srcinfo) }
    .debug_sfnames  0 : { *(.debug_sfnames) }
    /* DWARF 1.1 and DWARF 2 */
    .debug_aranges  0 : { *(.debug_aranges) }
    .debug_pubnames 0 : { *(.debug_pubnames) }
    /* DWARF 2 */
    .debug_info     0 : { *(.debug_info .gnu.linkonce.wi.*) }
    .debug_abbrev   0 : { *(.debug_abbrev) }
    .debug_line     0 : { *(.debug_line) }
    .debug_frame    0 : { *(.debug_frame) }
    .debug_str      0 : { *(.debug_str) }
    .debug_loc      0 : { *(.debug_loc) }
    .debug_macinfo  0 : { *(.debug_macinfo) }
    /* SGI/MIPS DWARF 2 extensions */
    .debug_weaknames 0 : { *(.debug_weaknames) }
    .debug_funcnames 0 : { *(.debug_funcnames) }
    .debug_typenames 0 : { *(.debug_typenames) }
    .debug_varnames  0 : { *(.debug_varnames) }
}

 

 特别注意:

1     .text :{}   .stack :{} 表示输出文件包含的 section

2     {}里面的 section,是输入文件的 section,比如 *(.isr_vector)    *(.text)    *(.rodata) 这些 .isr_vector section   .text section   .rodata section,都有指定输入文件,*表示所有的输入文件;所以 *(.isr_vector) 表示从所有的输入文件中获取所有 .isr_vector section 放在一块连续的地址空间;main.o(.data) 表示从 main.o文件中获取所有的 .data section 放在一块连续的地址空间

3     链接脚本从上往下,如果输入文件 A 已经被取出 .text section,此后输入文件 A 就没有 .text section,不能再被获取

4     关于 section 的命名,名字前可以包含 . ,也可以不包含,大多取名会包含 .

 

概述:

链接器:把一个或多个输入文件合并成一个输出文件,输入文件是目标文件或者链接脚本文件,输出文件是目标文件(库文件)或者可执行文件,链接器从链接脚本读完一个 section 后,将定位器符号的值增加该 section 的大小

链接脚本:控制输出文件内各部分在程序地址空间内的布局,地址空间包括 ROM 和 RAM

-T 选项用于指定自己的链接脚本,否则使用默认的链接脚本

 

语法:

定位符 . 

. 是定位器符号,可以对定位器符号赋值指定接下来内容的存储位置,如“ .= 0x10000”,也可以通过定位器符获取此位置的地址,比如 ”_stext = .;”,此后就可以用变量 _stext 表示此位置地址

 

入口地址

ENTRY(SYMBOL):将符号 SYMBOL 的值设置为入口地址,入口地址是进程执行的第一条指令在进程地址空间的地址(比如 ENTRY(Reset_Handler) 表示进程最开始从复位中断服务函数处执行

 有多种方法设置进程入口地址,以下编号越小,优先级越高

1、ld 命令行的 -e 选项

2、链接脚本的 ENTRY(SYMBOL) 命令

3、在汇编程序中定义了 start 符号,使用 start 符号值

4、如果存在 .text section,使用 .text section 首地址的值

5、使用地址 0 的值

 

 注:对于使用BIN文件的STM32,芯片决定了固定从0x0000_0004地址取值初始化PC,然后运行,因为0x0000_0004存的是复位中断地址,所以程序从复位中断开始运行;对于可执行文件是ELF,可以通过ENTRY()设置入口地址。

 

 

内存布局

脚本中以MEMORY命令定义了存储空间,其中以ORIGIN定义地址空间的起始地址,LENGTH定义地址空间的长度。

结构:

MEMORY {
NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2
NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2
…
}

 

NAME :存储区域的名字。(自己可以随意命名)

ATTR :定义该存储区域的属性。ATTR属性内可以出现以下7 个字符:

R 只读section
W 读/写section
X 可执行section
A 可分配的section
I 初始化了的section
L 同 I
! 不满足该字符之后的任何一个属性的section
ORIGIN :关键字,区域的开始地址,可简写成 org 或 o

LENGTH :关键字,区域的大小,可简写成 len 或 l

例子:

MEMORY
{
    ROM (rx) : ORIGIN = 0x08000000, LENGTH =  1024k /* 1024K flash */
    RAM (rw) : ORIGIN = 0x20000000, LENGTH =  128k /* 128K sram */
}

 

在链接文件中定义的变量(符号)可以在目标文件中使用

在链接文件中定义变量 

  _init_start = .;
  .application_init  : { *(.application_init) }
  _init_end = .;

 在源文件中使用变量 _init_start

#include <stdio.h>
#include <string.h>

struct _s_application_init {
    int(*function)(void);
};

extern struct _s_application_init _init_start;//段".application_init"的起始地址,在*.lds文件中定义
extern struct _s_application_init _init_end;//段".application_init"的末尾地址,在*.lds文件中定义

#define __app_init_section __attribute__((section(".application_init")))
#define __application_init(function) \
    struct _s_application_init _s_a_init_##function  __app_init_section = {function}

static int application_init_a(void)
{
    printf("execute funtion : %s\n", __FUNCTION__);
    return 0;
}
__application_init(application_init_a); // 函数地址存在指定的段

int main(int argc, char **argv) { /* * 从段的起始地址开始获取数据,直到段的末尾地址 */ struct _s_application_init *pf_init = &_init_start; do { printf("Load init function from address %p\n", pf_init); pf_init->function(); ++pf_init; } while (pf_init < &_init_end); return 0; }

PROVIDE 关键字(感觉有无 PROVIDE 修饰都可以被输入文件引用,看上面的例子)

该关键字定义一个(输入文件内被引用但没定义)符号。相当于定义一个全局变量的符号表,其他C文件可以通过该符号来操作对应的存储内存。

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

在目标文件内定义的符号可以在链接脚本内被赋值

此时该符号被定义为全局的. 每个符号都对应了一个地址, 此处的赋值是更改这个符号对应的地址.

e.g. 通过下面的程序查看变量a的地址:

/* a.c */  
#include <stdio.h>
#include <stdlib.h>

int a = 100;  
int main(void)  
{  
    printf( “&a=0x%p“, &a );  
    return 0;  
}

文件a.lds

/* a.lds :注意格式有要求*/  
a = 3;  

编译命令:$gcc -Wall -o a-without-lds a.c,执行./a-without-lds输出&a = 0×8049598.

编译命令:$gcc -Wall -o a-with-lds a.c a.lds,执行a-with-lds,输出&a = 0×3

对符号的赋值只对全局变量起作用! 一些简单的赋值语句,能使用任何c语言内的赋值

 

除了可以在 C源文件中指定函数属于某个 section,汇编文件也可以,比如 startup.s

  .section  .text.Reset_Handler
  .weak  Reset_Handler
  .type  Reset_Handler, %function
Reset_Handler:  
  ldr   sp, =_estack     /* set stack pointer */
  bl  entry
  bx  lr    
.size  Reset_Handler, .-Reset_Handler

 

  .section  .isr_vector,"a",%progbits
  .type  g_pfnVectors, %object
  .size  g_pfnVectors, .-g_pfnVectors
    
g_pfnVectors:
  .word  _estack
  .word  Reset_Handler

 section 结构

SECTIONS
{
       ...
      secname [start_ADDR] [(TYPE)] : [AT (LMA_ADDR)]
      { 
        contents 
      } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
      ...
}

[ ]内的内容是可选选项

secname: 表示输出文件的 section 名,即输出文件中有哪些 section。而contents就是描述输出文件的这个 section 内容从哪些输入文件的哪些 section 里抽取而来。

输出section名字必须符合输出文件格式要求,比如:a.out格式的文件只允许存在.text、.data和.bss section名。而有的格式只允许存在数字名字,那么此时应该用引号将所有名字内的数字组合在一起;另外,还有一些格式允许任何序列的字符存在于 section名字内,此时如果名字内包含特殊字符(比如空格、逗号等),那么需要

用引号将其组合在一起。

如下,将输入文件的数据段存放在输出文件的数据段(section 名自己定义,section 名前后必须要有空格)

SECTIONS
{
       ...
      .data :
      { 
        main.o(.data)
        *(.data)
      } 
      ...
}

其中 *(.data) 表示将所有的输入文件的 .data section 链接到输出文件 .data section 中, 特别注意的是,之前链接的就不会再链接,这样做的目的是可以将某些特殊的输入文件链接到地址前面。

 

start_addr :表示将某个段强制链接到的地址( VMA ),start_addr 改变定位符的值。

SECTIONS
{
   .bss :
   {
      . = ALIGN(4);        /* Align the start of the section */
      _sbss = .;           /* Provide the name for the start of this section */
      
      *(.bss)
      *(.bss.*)
      . = ALIGN(512);
      USB_RAM_START = .;
    . += USB_RAM_GAP;
      
      . = ALIGN(4);        /* Align the end of the section */
   } > RAM
   _ebss = .;              /* Provide the name for the end of this section */

   m_usb_bdt USB_RAM_START (NOLOAD) :
   {
     *(m_usb_bdt)
     USB_RAM_BDT_END = .;
   }

   m_usb_global USB_RAM_BDT_END (NOLOAD) :
   {
     *(m_usb_global)
   }
}

TYPE:每个输出section都有一个类型,如果没有指定TYPE类型,那么链接器根据输出section引用的输入section的类型设置该输出section的类型。它可以为以下五种值

  • NOLOAD 该section在程序运行时,不被载入内存。
  • DSECT,COPY,INFO,OVERLAY :这些类型很少被使用,为了向后兼容才被保留下来。这种类型的section必须被标记为“不可加载的”,以便在程序运行不为它们分配内存。

AT( LAM_ADDR ):输出 section 的 LMA,默认情况下 LMA 等于 VMA,但可以通过关键字 AT() 指定 LMA。

用关键字 AT()指定,括号内包含表达式,表达式的值用于设置LMA。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围。这个属性主要用于构建ROM镜像。

[>REGION]:这个region就是前面说的MEMORY命令定义的位置信息,用于指定section在哪个memory执行,也就是VMA。如果不指定LMA,LMA = VMA。

VMA 和 LMA

section包含两个地址:VMA(virtual memory address虚拟内存地址)和LMA(load memory address加载内存地址)。通常VMA和LMA是相同的。

  • VMA是执行输出文件时section所在的地址
  • LMA是加载输出文件时section所在的地址

要作为运行地址,首先PC指针要能在这个地址空间内跑动,所以这段地址空间必须是可随机寻址的,也就是说可以访问想要访问的地址,如RAM,NorFlash(Norflash带有SRAM接口,有足够的地址引脚来寻址,可以很容易地存取其内部的每一个字节);如果是NandFlash,则不行,因为NandFlash只能通过一个块一个块的读/写,不能做到随机寻址。

VMA和LMA大多数情况下是相等的,但也可以不相等。通常,当LMA地址空间不支持随机寻址,或者是嫌弃LMA地址空间的访问速度比较慢时(比如NorFlash速度比SDRAM慢),则会将VMA设置到RAM中,这时,VMA与LMA就不相等了。

例:NandFlash因为不能随机访问想要访问的每个地址,不能作为运行地址,所以这里想要把在NandFlash的代码复制到SDRAM中。这里除了代码中要加入复制模块外,还要在链接脚本中使NandFlash部分的VMA设为SDRAM地址。大多数情况下,是在起始代码中初始化时就将需要复制的部分复制的到VMA地址空间中。


还有一种情况就是当嵌入式系统中先都将代码和数据加载到了ROM中,此时的地址就是LMA,但是当开始运行之后,需要将data数据部分拷贝到RAM中,此时数据的地址就是VMA,本文最开始的链接脚本,就是把.data的LMA设置在ROM,VMA设置在RAM。

 

KEEP 关键字

在链接命令行内使用了选项 -gc-sections 后,链接器可能将某些它认为没用的 section 过滤掉,此时就有必要强制让链接器保留一些特定的 section,可用 KEEP() 关键字达此目的。如 KEEP(* (.text)) 或 KEEP(SORT(*)(.text))。说的通俗易懂就是:防止被优化。

ALIGN 关键字

表示字节对齐, 如 “ . = ALIGN(4);”表示从该地址开始后面的存储进行4字节对齐。

实例详解:

 SECTIONS
 {
   .= 0x10000;
   .text : { *(.text) }
   .= 0×8000000;
   .data : { *(.data) }
   .bss : { *(.bss) }
 }

 

解释一下上诉的例子:

.= 0x10000:把定位器符号置为 0x10000(若不指定,则该符号的初始值为0)

.text : { *(.text) }:*符号代表所有的输入文件,此句表示获取所有输入文件的 .text section放在一块连续的地址空间,首地址由上一句的定位器符号确定,即 0x10000

.= 0x8000000:把定位器符号置为 0x8000000

.data : { *(.data) }:获取所有输入文件的 .data section 放在一块连续的地址空间,该 section 的首地址为 0x8000000

.bss : { *(.bss) }:获取所有输入文件的 .bss section 放在一块连续的地址空间,该 section 的首地址为 0x8000000 + .data section 的大小

 输出文件包含 .text section     .data section     .bss section

 

posted @ 2021-06-12 21:41  流水灯  阅读(19924)  评论(0编辑  收藏  举报