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为例来说明下,先贴上这文件内容

 MEMORY { .sram : ORIGIN = CONFIG_SPL_TEXT_BASE,\
LENGTH = CONFIG_SPL_MAX_SIZE }
MEMORY { .sdram : ORIGIN = CONFIG_SPL_BSS_START_ADDR, \
LENGTH = CONFIG_SPL_BSS_MAX_SIZE } OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
.text :
{
__start = .;
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
*(.text*)
} >.sram . = ALIGN();
.rodata : { *(SORT_BY_ALIGNMENT(.rodata*)) } >.sram . = ALIGN();
.data : { *(SORT_BY_ALIGNMENT(.data*)) } >.sram . = ALIGN();
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
} >.sram . = ALIGN();
__image_copy_end = .; .end :
{
*(.__end)
} _image_binary_end = .; .bss :
{
. = ALIGN();
__bss_start = .;
*(.bss*)
. = ALIGN();
__bss_end = .;
} >.sdram
}

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

  • MEMORY:描述板上的存储器位置,ORIGIN为起始地址,LENGTH为字节数,其中ORIGIN可缩写为org或o,LENGTH可缩写为len或l
  • 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 脚本学习笔记的更多相关文章

  1. GNU工具链学习笔记

    GNU工具链学习笔记 1..so为动态链接库,.a为静态连接库.他们在Linux下按照ELF格式存储.ELF有四种文件类型.可重定位文件(Relocatable file,*.o,*.a),包含代码和 ...

  2. shell脚本学习笔记(符号)

    shell脚本的学习: 1.Shell的作用是解释运行用户的命令,用户输入一条命令,Shell就解释运行一条,这样的方式称为交互式(Interactive),Shell还有 一种运行命令的方式称为批处 ...

  3. Window脚本学习笔记之BAT简介

    本篇文章不是直接讲技术,而是对我自己学习这些年来的一番感触和简单的介绍,其间也穿插着一些基本的知识,若是学习技术者可跳过,亦不妨碍学习其他. BAT简介 BAT是Windows的批处理脚本,即以后缀“ ...

  4. shell脚本学习笔记

    1.判断符号:中括号[ ] [ ]进行数据的判断,例如我想知道HOME这个变量是否为空,[ -z "$HOME" ],或者两个字符串是否相等,[ "$HOME" ...

  5. shell脚本学习笔记 (正則表達式)

    正則表達式一般有三个部分组成,他们各自是:字符类,数量限定符,位置限定符. 规定一些特殊语法表示字符类.数 量限定符和位置关系,然后用这些特殊语法和普通字符一起表示一个模式,这就是正則表達式(Regu ...

  6. shell脚本学习笔记 (流编辑器sed)

    sed意为流编辑器(Stream Editor),在Shell脚本和Makefile中作为过滤器使用很普遍,也就是把前一个程序的输出引入sed的输入,经过一系列编辑命令转换为另一种格式输出. sed不 ...

  7. Window脚本学习笔记之定时关闭进程

     定时关闭进程, 从字面上即可看出操作分为两个步骤,即: 1,结合“任务计划程序”,定时. “计算机->管理->计划任务程序”,作用是让系统定时启动脚本文件(bat脚本). 2,结合“nt ...

  8. Window脚本学习笔记之BAT调用设置

    用一句bat脚本调用window的系统设置: rem 调用回收站 explorer.exe ::{645FF040-5081-101B-9F08-00AA002F954E} rem 检查Windows ...

  9. Window脚本学习笔记之BAT文件处理

    BAT文件处理 列出盘中特定文件名的文件: @echo offdir C:\*.jpg /b/s>.\CDatejpg.txt dir C:\*.png /b/s>.\CDatepng.t ...

随机推荐

  1. centos 部署web项目

    Linux下安装Tomcat服务器和部署Web应用 一.上传Tomcat服务器

  2. MySql笔记之修改MySQL提示符

    首先,了解下MYSQL提示符是神马东东 就是每次登陆mysql后出现的提示符 如果我们不喜欢这个提示符呢,那我们就改成我们喜欢的样子. 系统参数提示符 举个栗子 就改成相应的提示符了,那么可否随意改名 ...

  3. C++ 二位数组做参数传递

    指针的强大功能,,,,简直牛逼!!! #include<iostream> #include<cstdio> #include<map> using namespa ...

  4. log4j - 1

    具体内容: 1.       如何在项目中配置log4j使得该系统可以输出web test的日志文件(自定义格式)到工程dist目录下的junitLog/WebTestLog.log目录下,输出508 ...

  5. [COCI2015]FUNGHI

    题目大意: 一个环上有8个数,从中选取连续的4个数使得和最大,求最大的和. 思路: 模拟. #include<cstdio> #include<cctype> #include ...

  6. CentOS 6.9安装类型选择(Basic Server/Web Server)

    Desktop :基本的桌面系统,包括常用的桌面软件,如文档查看工具. Minimal Desktop:基本的桌面系统,包含的软件更少. Minimal:基本的系统,不含有任何可选的软件包. Basi ...

  7. How to create an IPA (Xcode 5)

    This tutorial will walk you through the easiest way to generate an IPA using Xcode 5. We will be usi ...

  8. CountDownLatch模拟高并发测试代码

    直接上代码进行验证吧 /** * 通过countdownlatch的机制,来实现并发运行 * 模拟200个并发测试 * @author ll * @date 2018年4月18日 下午3:55:59 ...

  9. k8s restful API 结构分析

    k8s的api-server组件负责提供restful api访问端点, 并且将数据持久化到etcd server中. 那么k8s是如何组织它的restful api的? 一, namespaced ...

  10. python makestrans translate

    """ 1. makestrans()用法 语法: str.maketrans(intab, outtab]); Python maketrans() 方法用于创建字符映 ...