GNU LD 脚本学习笔记

Posted WindTaiL的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GNU LD 脚本学习笔记相关的知识,希望对你有一定的参考价值。

LD脚本(linker script)是什么

GNU ld是链接器,ld实际并不是GCC的一部分,ld属于binutils软件包。但是嵌入式开发时,下载的linaro GCC工具集中是包含 arm-linux-gnueabihf-ld 的。工作中我经常使用ARM的scatter文件,和这个LD脚本差不多,只不过scatter文件的功能要弱不少,这也是为什么ARM6中armclang也是推荐使用 GNU LD脚本的原因,ARM也不想维护自己特有的编译器了,只要专心把clang bytecode翻译成ARM指令的优化做好。

所有的链接过程都是由LD脚本控制的,写这个脚本的语言称为 linker command language,LD脚本的最主要的功能是描述如何将输入文件映射到输出文件以及输出文件的存储布局(memory layout)。在操作系统上开发时一般不会涉及到LD脚本,这是因为如果未使用命令行-T来指定脚本,ld会使用内置的默认脚本,这个脚本可以通过 ld --verbose 来查看,例如 arm-linux-gnueabihf-ld --verbose的输出如下

技术分享图片
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2017 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
	      "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SEARCH_DIR("=/home/tcwg-buildslave/workspace/tcwg-make-release/builder_arch/amd64/label/tcwg-x86_64-build/target/arm-linux-gnueabihf/_build/builds/destdir/x86_64-unknown-linux-gnu/arm-linux-gnueabihf/lib"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib");
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x00010000)); . = SEGMENT_START("text-segment", 0x00010000) + SIZEOF_HEADERS;
  .interp         : { *(.interp) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  .hash           : { *(.hash) }
  .gnu.hash       : { *(.gnu.hash) }
  .dynsym         : { *(.dynsym) }
  .dynstr         : { *(.dynstr) }
  .gnu.version    : { *(.gnu.version) }
  .gnu.version_d  : { *(.gnu.version_d) }
  .gnu.version_r  : { *(.gnu.version_r) }
  .rel.dyn        :
    {
      *(.rel.init)
      *(.rel.text .rel.text.* .rel.gnu.linkonce.t.*)
      *(.rel.fini)
      *(.rel.rodata .rel.rodata.* .rel.gnu.linkonce.r.*)
      *(.rel.data.rel.ro .rel.data.rel.ro.* .rel.gnu.linkonce.d.rel.ro.*)
      *(.rel.data .rel.data.* .rel.gnu.linkonce.d.*)
      *(.rel.tdata .rel.tdata.* .rel.gnu.linkonce.td.*)
      *(.rel.tbss .rel.tbss.* .rel.gnu.linkonce.tb.*)
      *(.rel.ctors)
      *(.rel.dtors)
      *(.rel.got)
      *(.rel.bss .rel.bss.* .rel.gnu.linkonce.b.*)
      PROVIDE_HIDDEN (__rel_iplt_start = .);
      *(.rel.iplt)
      PROVIDE_HIDDEN (__rel_iplt_end = .);
    }
  .rela.dyn       :
    {
      *(.rela.init)
      *(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
      *(.rela.fini)
      *(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
      *(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
      *(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
      *(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
      *(.rela.ctors)
      *(.rela.dtors)
      *(.rela.got)
      *(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
      PROVIDE_HIDDEN (__rela_iplt_start = .);
      *(.rela.iplt)
      PROVIDE_HIDDEN (__rela_iplt_end = .);
    }
  .rel.plt        :
    {
      *(.rel.plt)
    }
  .rela.plt       :
    {
      *(.rela.plt)
    }
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : { *(.plt) }
  .iplt           : { *(.iplt) }
  .text           :
  {
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf32.em.  */
    *(.gnu.warning)
    *(.glue_7t) *(.glue_7) *(.vfp11_veneer) *(.v4_bx)
  }
  .fini           :
  {
    KEEP (*(SORT_NONE(.fini)))
  }
  PROVIDE (__etext = .);
  PROVIDE (_etext = .);
  PROVIDE (etext = .);
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
  .rodata1        : { *(.rodata1) }
  .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) }
   PROVIDE_HIDDEN (__exidx_start = .);
  .ARM.exidx   : { *(.ARM.exidx* .gnu.linkonce.armexidx.*) }
   PROVIDE_HIDDEN (__exidx_end = .);
  .eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
  .eh_frame       : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gcc_except_table   : ONLY_IF_RO { *(.gcc_except_table
  .gcc_except_table.*) }
  .gnu_extab   : ONLY_IF_RO { *(.gnu_extab*) }
  /* These sections are generated by the Sun/Oracle C++ compiler.  */
  .exception_ranges   : ONLY_IF_RO { *(.exception_ranges
  .exception_ranges*) }
  /* Adjust the address for the data segment.  We want to adjust up to
     the same address within the page on the next page up.  */
  . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
  /* Exception handling  */
  .eh_frame       : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gnu_extab      : ONLY_IF_RW { *(.gnu_extab) }
  .gcc_except_table   : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
  .exception_ranges   : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) }
  /* Thread Local Storage sections  */
  .tdata	  : { *(.tdata .tdata.* .gnu.linkonce.td.*) }
  .tbss		  : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  }
  .init_array     :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
  .fini_array     :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
    KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
    PROVIDE_HIDDEN (__fini_array_end = .);
  }
  .ctors          :
  {
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn‘t matter if the user does not
       actually link against crtbegin.o; the
       linker won‘t look for a file to match a
       wildcard.  The wildcard also means that it
       doesn‘t matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don‘t want to include the .ctor section from
       the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
  }
  .dtors          :
  {
    KEEP (*crtbegin.o(.dtors))
    KEEP (*crtbegin?.o(.dtors))
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
    KEEP (*(SORT(.dtors.*)))
    KEEP (*(.dtors))
  }
  .jcr            : { KEEP (*(.jcr)) }
  .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
  .dynamic        : { *(.dynamic) }
  . = DATA_SEGMENT_RELRO_END (0, .);
  .got            : { *(.got.plt) *(.igot.plt) *(.got) *(.igot) }
  .data           :
  {
    PROVIDE (__data_start = .);
    *(.data .data.* .gnu.linkonce.d.*)
    SORT(CONSTRUCTORS)
  }
  .data1          : { *(.data1) }
  _edata = .; PROVIDE (edata = .);
  . = .;
  __bss_start = .;
  __bss_start__ = .;
  .bss            :
  {
   *(.dynbss)
   *(.bss .bss.* .gnu.linkonce.b.*)
   *(COMMON)
   /* Align here to ensure that the .bss section occupies space up to
      _end.  Align after .bss to ensure correct alignment even if the
      .bss section disappears because there are no input sections.
      FIXME: Why do we need it? When there is no .bss section, we don‘t
      pad the .data section.  */
   . = ALIGN(. != 0 ? 32 / 8 : 1);
  }
  _bss_end__ = . ; __bss_end__ = . ;
  . = ALIGN(32 / 8);
  . = SEGMENT_START("ldata-segment", .);
  . = ALIGN(32 / 8);
  __end__ = . ;
  _end = .; PROVIDE (end = .);
  . = DATA_SEGMENT_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_line.* .debug_line_end ) }
  .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) }
  /* DWARF 3 */
  .debug_pubtypes 0 : { *(.debug_pubtypes) }
  .debug_ranges   0 : { *(.debug_ranges) }
  /* DWARF Extension.  */
  .debug_macro    0 : { *(.debug_macro) }
  .debug_addr     0 : { *(.debug_addr) }
  .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
  .note.gnu.arm.ident 0 : { KEEP (*(.note.gnu.arm.ident)) }
  /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
默认的脚本

 

大家可以看到,默认的脚本比自己写的还要复杂得多,原来GCC会输出这么多的section!

LD脚本的理解

LD脚本由许多命令组成,下面我们以u-boot中的am335x的u-boot-spl.lds为例来说明下,先贴上这文件内容

 1 MEMORY { .sram : ORIGIN = CONFIG_SPL_TEXT_BASE, 2         LENGTH = CONFIG_SPL_MAX_SIZE }
 3 MEMORY { .sdram : ORIGIN = CONFIG_SPL_BSS_START_ADDR,  4         LENGTH = CONFIG_SPL_BSS_MAX_SIZE }
 5 
 6 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
 7 OUTPUT_ARCH(arm)
 8 ENTRY(_start)
 9 SECTIONS
10 {
11     .text      :
12     {
13         __start = .;
14         *(.vectors)
15         arch/arm/cpu/armv7/start.o    (.text*)
16         *(.text*)
17     } >.sram
18 
19     . = ALIGN(4);
20     .rodata : { *(SORT_BY_ALIGNMENT(.rodata*)) } >.sram
21 
22     . = ALIGN(4);
23     .data : { *(SORT_BY_ALIGNMENT(.data*)) } >.sram
24 
25     . = ALIGN(4);
26     .u_boot_list : {
27         KEEP(*(SORT(.u_boot_list*)));
28     } >.sram
29 
30     . = ALIGN(4);
31     __image_copy_end = .;
32 
33     .end :
34     {
35         *(.__end)
36     }
37 
38     _image_binary_end = .;
39 
40     .bss :
41     {
42         . = ALIGN(4);
43         __bss_start = .;
44         *(.bss*)
45         . = ALIGN(4);
46         __bss_end = .;
47     } >.sdram
48 }

 

这个文件涉及如下几个命令

  • OUTPUT_FORMAT:指定输出文件的格式,这里指定了无论命令是否选择了大小端,都输出ARM的小端格式的指令
  • OUTPUT_ARCH:指定输出的架构
  • ENTRY:指定入口地址,注意这里使用的是代码中定义的_start符号,也就是说脚本中可以直接访问符号表中的符号
  • SECTIONS:这是脚本中最重要的命令了,所有的LD脚本都会有这个命令,用来指定如何将输入文件映射到输出文件等等,要看懂SECTIONS的内容需要许多概念,下面来一一说明

object

链接器的目的是把多个输入文件组成一个输出文件,这些文件都叫做object文件(包括最终生成的可执行文件)。object文件有很多内容,但最重要的是它包含一组段(section),输入文件中的段称为输入段(input section),而输出文件中的段称为输出段(output section)。

section

每个section都有自己的名字(如.text/.data/.bss)和大小,大部分section还有自己的数据(.bss就是一种有大小无数据的section)。

  • 有些section是loadable,如.text段,运行时需要把它们的数据加载到内存中去
  • 还有一些section是allocatable,如.bss段,运行时需要在内存中为它们留空间,但是不用加载任何数据到这段内存中去(一般会清零)
  • 除此之外的section,一般只是包含一些调用信息

VMA/LMA

loadable 或 allocatable 的section都有两个地址

  • VMA:Virtual Memory Address,这是运行时section的地址(操作系统上,程序运行的时候的地址一般经过MMU映射过的虚拟地址)
  • LMA:Load Memory Address,这是还未开始运行时,section处在的位置

如果是在操作系统上,可执行程序由外部加载器加载,VMA和LMA一般来说是相同的,但是在嵌入式裸机代码中,LMA可能是在ROM中,程序从ROM中开始运行,初始化代码(VMA==LMA的代码,或与位置无关的代码)负责把其他VMA和LMA不同的代码加载到VMA中。

location counter

SECTIONS命令中,"."是一个特殊的符号,表示当前VMA,"."的初始值是0,通过给"."赋值可以增加它的值,每创建一个新的输出section时,"."也会根据其大小相应地增加。通过直接赋值给"."可能会产生空洞,这些空洞会被填充上(可以指定填充值)。需要注意的是,通过赋值不可以使"."回退,如果ld检测到这种情况,就会报错。

有了上面这些概念,我们再来分析下SECTIONS

  • 输入段的指定方式:file-name(section-name)或 archive-name:file-name(section-name),所有这些名称都可以使用 *和?等通配符
  • 输出段的指定方式:section-name { input-sections },输出段的名称与可执行文件的格式相关
  • 表达式:以分号结尾的表达式,用于直接创建符号或改变"."
  • >指定VMA(运行时)在哪个存储器中,为了方便映射到不同的内存,还可以创建REGION_ALIAS
  • ALIGN(size)返回"."对齐到size字节的值,但是不会改变"."
  • SORT_BY_ALIGNMENT是按对齐大小倒序排列,对齐大的放前面,以减少padding
  • SORT是SORT_BY_NAME简写,按照名称顺序排列
  • KEEP是即使没有代码引用,也保留下来(汇编或其他外部代码会使用这些初始化数据)

这样我们就基本能看懂上面的脚本了,另外我觉得还有几点比较重要:

  • LD脚本直接创建的符号,也会放到符号表中,但是要注意这个符号不同于C代码中的符号。符号表是一个名称到地址的映射,代码中的符号都会被分配到一个存储器的位置,运行时会有值的概念,但LD脚本中创建的符号是不会分配内存的,所以它只有地址,而没有存储位置(值),在C语言中引用时,可以extern为变量,然后使用&获取其地址,或者直接extern为数组,使用数组名即可
  • 当".=exp"在输出段的{}中时,".=exp"就相当于所在的".=输出段的起始VMA+exp"
  • 在输出段的{}以外的地方给符号赋值是有风险的,".=."的方式可以限制ld旋转orphan section的位置,参见这里

 

要更详细地了解LD脚本,请参见下面的文献。相比arm的scatter文件,gnu ld script更加灵活,我最喜欢的就是可以给各种位置定义符号名,而不是用Image$$RO$$Base之类的magic名称。


参考文献

[1] https://sourceware.org/binutils/docs/ld/Scripts.html#Scripts

[2] https://wiki.osdev.org/Linker_Scripts

以上是关于GNU LD 脚本学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

使用 GNU ld 链接器脚本包含二进制文件

Gnu make学习笔记

GNU工具链学习笔记

JSP学习笔记:JSP语法和指令

程序员的自我修养

学习笔记:python3,代码片段(2017)