link.ld手册备份

发布时间 2023-08-20 18:30:25作者: 大浪淘沙、

设定入口点

在程序中执行的第一条指令称为入口点。 您可以使用 ENTRY 链接器脚本命令来设置入口点。

ENTRY(symbol)

也可以通过其它方式设定入口点:

  • ‘-e ’输入命令行选项;
  • ‘.text ’ 部段的第一个字节的地址;

其它命令

命令1部分

  • INCLUDE filename
  • INPUT(file, file, …) / INPUT(file file …)
  • GROUP(file, file, …) / GROUP(file file …)
  • AS_NEEDED(file, file, …) / AS_NEEDED(file file …)
  • OUTPUT(filename)
  • SEARCH_DIR(path)
  • STARTUP(filename)
  • EXTERN(symbol symbol …)

命令2部分

  • FORCE_COMMON_ALLOCATION
  • INHIBIT_COMMON_ALLOCATION
  • FORCE_GROUP_ALLOCATION
  • INSERT [ AFTER | BEFORE ] output_section

此命令通常在‘-T ’ 指定的脚本中使用,用来增强默认的SECTIONS。例如,重复占位程序段。它将把所有此前的链接脚本的声明插入_output_section_的后面(或者前面),并且使 ’-T ’不要覆盖默认链接脚本。

SECTIONS
{
  OVERLAY :
  {
    .ov1 { ov1*(.text) }
    .ov2 { ov2*(.text) }
  }
}
INSERT AFTER .text;
  • NOCROSSREFS(section section …)

此命令可能被用来告诉 ld,如果引用了section的参数就报错。在特定的程序类型中,比如使用覆盖技术的嵌入式系统,当一个段被加载到内存中,另一个段不会被加载。任何两个段之间直接的引用都会带来错误。

  • NOCROSSREFS_TO(tosection fromsection …)

此命令可能被用来告诉 ld,从其他段列表中对某个段的任何引用就会引发错误。_NOCROSSREFS_TO_命令携带(给出)输出段名称的列表。 其他任何部分都不能引用第一部分。 如果 ld 从其他任何部分中检测到对第一部分的任何引用,它将报告错误并返回非零退出状态。

  • OUTPUT_ARCH(bfdarch)

指定一个特定的输出机器架构。该参数是BFD库使用的名称之一

  • LD_FEATURE(string)

可用于修改 ld 行为。如果字符串是“SANE_EXPR”,那么脚本中的绝对符号和数字将被在任何地方当作数字对待。

常用命令

OUTPUT_FORMAT

  • OUTPUT_FORMAT __(bfdname)
  • OUTPUT_FORMAT(default, big, little)

用来处理对象文件格式。例如:MIPS ELF目标的默认链接器脚本使用以下命令:

OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)

REGION_ALIAS(alias, region)

为内存区域创建别名 。

一个多文件的例子

第一行包括了其它的ld文件。

INCLUDE linkcmds.memory

SECTIONS
  {
    .text :
      {
        *(.text)
      } > REGION_TEXT
    .rodata :
      {
        *(.rodata)
        rodata_end = .;
      } > REGION_RODATA
    .data : AT (rodata_end)
      {
        data_start = .;
        *(.data)
      } > REGION_DATA
    data_size = SIZEOF(.data);
    data_load_start = LOADADDR(.data);
    .bss :
      {
        *(.bss)
      } > REGION_BSS
  }

设定所有的段都是RAM的形式,允许代码执行或数据存储。

MEMORY
{
	RAM : ORIGIN = 0, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", RAM);
REGION_ALIAS("REGION_RODATA", RAM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);

设定.text和.rodata段为ROM形式,允许代码执行和只读数据访问。

MEMORY
  {
    ROM : ORIGIN = 0, LENGTH = 3M
    RAM : ORIGIN = 0x10000000, LENGTH = 1M
  }

REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);

设定.rodata段为ROM2类型,允许对只读数据段读取,不允许代码执行。

MEMORY
  {
    ROM : ORIGIN = 0, LENGTH = 2M
    ROM2 : ORIGIN = 0x10000000, LENGTH = 1M
    RAM : ORIGIN = 0x20000000, LENGTH = 1M
  }
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM2);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);

断言检查

在段内使用PROVIDE的定义如果用户没有为其设置值,此表达式将无法通过检测。唯一的例外是PROVIDE的符号刚刚引用了’.’。。。。??????
ASSERT那部分,好像是判断成立,触发error。

.stack :
{
    PROVIDE (__stack = .);
    PROVIDE (__stack_size = 0x100);
    ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}

在其他地方定义stack_size,则会失败。在段外定义的符号会在此前被求值,可以在断言中使用它们,以下代码正常执行:。。。。。。???????

PROVIDE (__stack_size = 0x100);
  .stack :
{
    PROVIDE (__stack = .);
    ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}

符号赋值

简单赋值,和C相同

symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;

三个不同位置为符号赋值的示例:(1, 7, 9 行)。
符号’ _bdata '将被定义为在 ’.text’ 输出段后面的一个4字节向上对齐的地址。

floating_point = 0;
SECTIONS
{
  .text :
    {
      *(.text)
      _etext = .;
    }
  _bdata = (. + 3) & ~ 3;
  .data : { *(.data) }
}

HIDDEN

语法HIDDEN(symbol = expression)为ELF目标的端口定义一个符号,符号将被隐藏并且不会被导出。在本例中,这三个符号在此模块之外都不可见

HIDDEN(floating_point = 0);
SECTIONS
{
  .text :
    {
      *(.text)
      HIDDEN(_etext = .);
    }
  HIDDEN(_bdata = (. + 3) & ~ 3);
  .data : { *(.data) }
}

PROVIDE

在某些情况下,仅当一个符号被引用了却没有定义在任何链接目标中,才需要为链接脚本定义一个符号。例如,传统链接器定义了符号‘etext’。然而,ANSI C要求用户能够使用’ etext '作为函数名而不会引发错误。PROVIDE关键字可以用来定义一个符号,比如‘etext’ ,只有当它被引用但没有被定义时才使用。
下例:如果程序定义了’ _etext** ‘(带有前导下划线),链接器将给出重复定义错误。另一方面,如果程序定义了’ _etext_ **‘(没有前导下划线),链接器会默认使用程序中的定义。如果程序引用了’ etext '但没有定义它,链接器将使用链接器脚本中的定义。

SECTIONS
{
  .text :
    {
      *(.text)
      _etext = .;
      PROVIDE(etext = .);
    }
}

PROVIDE_HIDDEN

PROVIDE 类似。对于ELF目标的端口,符号将被隐藏且不会被输出。

代码对应关系

extern int foo;  // c
  _foo = 1000;   // 对应的链接

C语言,声明了一个符号,会发生两件事。

  • 编译器在程序内存中保留足够的空间来保存符号的值。
  • 编译器在程序的符号表中创建一个条目,用来保存符号的地址。

**C语言 ****int * a = & foo; 在符号表中查找符号’ foo ',获取它的地址,然后将这个地址复制到与变量 ’ a ’ 相关联的内存块中。
Link脚本中 foo = 1000; **在符号表中创建一个名为’ foo '的条目,该条目保存内存位置1000的地址,但地址1000上没有存储任何特殊内容。这意味着您无法访问链接程序脚本定义的符号的值-它没有值。

内存的 .ROM 拷贝到 .FLASH 中

在Link脚本中:

  start_of_ROM   = .ROM;
  end_of_ROM     = .ROM + sizeof (.ROM);
  start_of_FLASH = .FLASH;

在C语言中:

extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);

  extern char start_of_ROM[], end_of_ROM[], start_of_FLASH[];
  memcpy (start_of_FLASH, start_of_ROM, end_of_ROM - start_of_ROM);

SECTIONS 命令

如果在链接脚本中未使用 SECTIONS 命令,则链接器将会照输入文本的顺序,将每个输入段放置到名称相同的输出段中。例如,如果所有输入段出现在第一个文件中,输出文件的段的顺序将会与第一个输入文件保持一致。第一个段被放在地址0。?????

输出段对齐

第一个将‘.text’ 输出段的地址设置为位置计数器的当前值。
第二个参数会将其设置为位置计数器的当前值,但是该值与所有‘.text’ 输入段中最严格的对齐方式对齐。

.text . : { *(.text) }
//.text : { *(.text) }

如果要在0x10字节(16字节)边界上对齐段,以使节地址的最低四位为零,则可以执行以下操作:

.text ALIGN(0x10) : { *(.text) }

包括多个段的方法

以下两种:
两种方法的区别是输入段的 ’.text’ 和 ’.rata’ 段出现在输出段中的顺序。第一个例子里,他们将被混合在一起,按照链接器找到它们的顺序存放。另一个例子中,所有 ’.text’ 输入段将会先出现,后面是 ’.rdata’ 输入段。

*(.text .rdata)
*(.text) *(.rdata)

输入段除去文件EXCLUDE_FILE

把所有输入段放入’.text’段:

*(.text)

使用 EXCLUDE_FILE** **来匹配除 _EXCLUDE_FILE_列表中指定的文件以外的所有文件。以下将得到除去 crtend.ootherfile.o 以外的所有文件的所有 .ctors 段。

EXCLUDE_FILE (*crtend.o *otherfile.o) *(.ctors)

**EXCLUDE_FILE **也可以放在段的列表中:

*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)

以下:

  1. 将EXCLUDE_FILE与多个段一起使用时,这个排除命令仅仅对紧随其后的段有效。导致包含除 somefile.o 以外的所有文件的所有‘.text’段,而包括somefile.o在内的所有文件的所有‘.rdata’ 段都将被包含。
  2. 将1修改,添加包含除 somefile.o 以外的所有文件的所有‘.rdata’段。
  3. 将EXCLUDE_FILE放在段列表之外(在选择输入文件之前),将导致排除操作对所有段有效。
  4. 可以指定一个文件名来包含特定文件的段。如果一个或者多个你的文件需要被放在内存中的特定位置,你可能需要这么做。
*(EXCLUDE_FILE (*somefile.o) .text .rdata)
*(EXCLUDE_FILE (*somefile.o) .text EXCLUDE_FILE (*somefile.o) .rdata)
EXCLUDE_FILE (*somefile.o) *(.text .rdata)
data.o(.data)

输入段标志INPUT_SECTION_FLAGS

使用段标志来选择输入文件的段。输入段是冒号后面的部分。

  1. 输入段1:与 *(.text) 能匹配的段(名字)且段头部标志设置了SHF_MERGE和SHF_STRINGS的段。
  2. 输入段2:与 *(.text) 能匹配的段(名字)且段头部标志未设置SHF_WRITE的段。
SECTIONS {
  .text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }
  .text2 :  { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }
}

输入段通配

  • ‘*’ 匹配任意数量字符
  • ‘?’ 匹配任意单字
  • ‘[chars]’ 匹配任何字符的单个实例;‘-’ 字符可被用来指出一个字符的范围,例如 ‘[a-z]’ 可以用来匹配所有小写字母
  • ‘\’ 引用后面的字符

如果一个文件名匹配多个通配符,或者一个文件名被显示指定了,且又被通配符匹配了,则链接器将使用链接器脚本中的第一个匹配项。
下面的输入段描述(第二段)可能有错误,因为 data.o 的规则不会被应用:

.data : { *(.data) }
.data1 : { data.o(.data) }

通常情况下,链接器将按照链接过程中出现通配符的顺序放置文件和段。
以下:链接器将会把所有以大写字母开头的文件的 ’.data’ 段放入 ’.DATA’ ,其他文件的 ’.data’ 段放入 ’.data’ 。

SECTIONS {
  .text : { *(.text) }
  .DATA : { [A-Z]*(.data) }
  .data : { *(.data) }
  .bss : { *(.bss) }
}

排序关键字

使用 SORT_BY_NAME** 关键字时,链接器将按名称按升序对文件或段进行排序,然后将它们放入输出文件中。
_
SORT_BY_ALIGNMENT_ 将在将段放入输出文件之前,按对齐方式的降序对段进行排序。大的对齐被放在小的对齐前面可以减少所需的填充量。
_
SORT_BY_INIT_PRIORITY把段按照GCC的嵌入在段名称的 init_priority 数字属性值升序排列后放入输出文件。
SORT_ SORT_BY_NAME 的别名。
嵌套的段排序命令时,段排序命令最多可以有1个嵌套级别。其它所有嵌套段排序命令都是无效的。
_
SORT_NONE_ **通过忽略命令行部段排序选项来禁用段排序。

GNU C的特殊部分

attribute 机制

attribute 用法_wiggens的博客-CSDN博客___attribute__
attribute 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )
大致有六个参数值可以被设定,即:aligned, packed, transparent_union, unused, **deprecated **和 **may_alias **。

例子section

attribute___youngseaz的博客-CSDN博客___attribute
整型变量var存放到.text段

int var __attribute__((section(".text"))) = 0;

将字符串str存放到.data段

char str[] __attribute__((section(".data"))) = "hello world";

将函数存放到.mysection段(这个是自定义的段)

void __attribute__((section(".mysection"))) myfunc()
{
	printf("111!");
}

例子aligned

gcc之__attribute__简介及对齐参数介绍

__attribute__((aligned(64))),该部分添加到数组后面,可以64字节对其。

例子packed

实现结构体的压缩方法。
gcc之__attribute__简介及对齐参数介绍 - 若离相惜 - 博客园

#include <stdio.h>

struct unpacked_struct {
    char c;
    int i;
};

struct packed_struct_1 {
    char c;
    int i;
} __attribute__((__packed__));

struct packed_struct_2 {
    char c;
    int i;
    struct unpacked_struct us;
} __attribute__((__packed__));

int main(int argc, char** argv)
{
    printf("sizeof(struct unpacked_struct) = %lu
", sizeof(struct unpacked_struct));
    printf("sizeof(struct packed_struct_1) = %lu
", sizeof(struct packed_struct_1));
    printf("sizeof(struct packed_struct_2) = %lu
", sizeof(struct packed_struct_2));

    return 0;
}

/*
 * 输出:
 * sizeof(struct unpacked_struct) = 8
 * sizeof(struct packed_struct_1) = 5
 * sizeof(struct packed_struct_2) = 13
*/

volatile关键字

开发者告诉编译器该变量是易变的,无非就是希望编译器去注意该变量的状态,时刻注意该变量是易变的,每次读取该变量的值都重新从内存中读取。
C语言volatile关键字详解_ora___的博客-CSDN博客_c语言volatile

学习link.ld文件编写。
链接脚本(Linker Scripts)语法和规则解析(翻译自官方手册)_BSP-路人甲的博客-CSDN博客_链接脚本的作用和编写规则

输入段的公共符号

普通符号需要一个特别的标记,因为很多目标文件格式中没有特定的普通符号输入段。链接器把普通符号当作位于一个名为 ’COMMON’ 的输入段中。
大多数情况下,输入文件的普通符号会被放到输出文件的 ’.bss’ 段里面。

.bss { *(.bss) *(COMMON) }

在MIPS ELF中,链接器为普通符号使用 ’COMMON’ 以及为小型普通符号使用 ’.scommon’ 。这样就可以把不同类型的普通符号映射到内存中的不同位置。

把一个输入段的通配符入口使用** _KEEP()_ **实现链接时垃圾收集(‘–gc-sections’)的功能。

输入段的例子

下面是一个完整的链接脚本的例子。它告诉链接器从 all.o 读取所有段,把它们放到输出段 ’outputa’ 的开头位置,’outputa’ 的起始地址为 ’0x10000’ 。所有文件 foo.o 中的 ’.input1’ 段紧跟其后。所有文件 foo.o 中的 ’input2’ 段放入输出文件的 ’outputb’ 中,跟着是 foo1.o 中的 ’input1’ 段。所有其它的 ’.input1” 和 .input2’ 段被放入输出段 ’outputc’ 。

SECTIONS {
  outputa 0x10000 :
    {
    all.o
    foo.o (.input1)
    }
  outputb :
    {
    foo.o (.input2)
    foo1.o (.input1)
    }
  outputc :
    {
    *(.input1)
    *(.input2)
    }
}

大多数段名不能表示为C标识符,因为它们包含 ‘.’ 字符。

输出段的数据

使用输出段命令_BYTE**, **SHORT**, **LONG**, **QUAD, 或者 _SQUAD在输出段显式的包含几个字节的数据。每个关键字后面跟着一个括号包裹的表达式指出需要存储的数值。表达式的值被存储在当前位置计数器值的地方。BYTE, _SHORT**, **LONG**, **QUAD命令分别存储1,2,4,8字节。在存储字节后,位置计数器会按照存储的字节数增加。
当使用64位主机或目标时,
QUAD** **和SQUAD是相同的;它们都存储一个8字节或64位的值。主机和目标都是32位时,表达式被当作32位计算。在这种情况下QUAD存储一个32位的值,并使用0扩展到64位,SQUAD保存32位值并使用符号位扩展到64位。
一个
FILL_语句仅会覆盖它本身在段定义中出现的位置后面的所有内存区域,以下例子显示了如何使用 ’0x90’ 填充未定义内存区域:

FILL(0x90909090)

输出段的关键字

CREATE_OBJECT_SYMBOLS
此命令告诉链接器为每个输入文件创建一个符号。每个符号的名字为对应输入文件的名字。每个符号出现的位置位于包含_CREATE_OBJECT_SYMBOLS_命令的输出段中。这个命令常常是 a.out 目标文件格式特有的。 它一般不为其它的目标文件格式所使用。
CONSTRUCTORS
使用一个特殊构造集来支持C++ 全局构造函数和析构函数。在链接不支持任意段的文件格式时,例如 ECOFF 和 XCOFF ,链接器将会通过名字自动识别C++全局构造函数和析构函数。对于这些格式的目标文件,CONSTRUCTORS命令告诉链接器把构造函数信息放到出现 CONSTRUCTORS 命令的输出段中。其它文件格式中CONSTRUCTORS命令被忽略。
符号CTOR_LIST 标记全局构造函数的开始,
符号
CTOR_END
标记结束。
DTOR_LISTDTOR_END分别标记全局析构函数的开始和结束。
GNU C++通常把全局构造函数和析构函数放入 .ctors 和 .dtors 段。把下面的代码放入你的链接脚本,将会创建GUN C++运行时期望的表。....????

      __CTOR_LIST__ = .;
      LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
      *(.ctors)
      LONG(0)
      __CTOR_END__ = .;
      __DTOR_LIST__ = .;
      LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
      *(.dtors)
      LONG(0)
      __DTOR_END__ = .;

输出段的丢弃

链接器通常不会创建没有内容的输出段。这是为了方便引用那些有可能出现或者不出现任何输入文件中的段。
以下的定义中,只有在至少有一个输入文件含有 ’.foo’ 段且 ’.foo’ 段不为空的时候才会在输出文件创建一个 ’.foo’ 段。可以强制一个空的输出段使用 ‘. = .’

.foo : { *(.foo) }

输出段的类型

每个输出段可以有一个类型。类型是圆括号中的关键字。NOLOAD此段应标记为不可加载,以便在程序运行时不会将其加载到内存中。DSECT COPY INFO OVERLAY 都具有相同的效果:该段应该标记为不可分配,以便在程序运行时不会为该段分配内存。
在下面的脚本示例中,’ ROM ’ 部分位于内存位置 ’ 0 ',在程序运行时不需要加载它。

SECTIONS {
  ROM 0 (NOLOAD) : { … }
  …
}

输出段LMA

每个段有一个虚拟地址(VMA)和一个加载地址(LMA)。加载地址由 AT** 或 _AT>**_ 关键字指定。指定加载地址是可选的。AT 关键字把一个表达式当作自己的参数。这将指定段的实际加载地址 。段的加载地址被设置为该区域的当前空闲位置,并且按照段对齐要求对齐。
如果没有为可分配段使用 ATAT>,链接器会使用下面的方式尝试来决定加载地址:

  • 如果段有一个特定的VMA地址,则LMA也使用该地址。
  • 如果段为不可分配的则LMA被设置为它的VMA。
  • 否则如果可以找到符合当前段的一个内存区域,且此区域至少包含了一个段,则设置LMA在那里。如此VMA和LMA的区别类似于VMA和LMA在该区域的上一个段的区别。
  • 如果没有声明内存区域且默认区域覆盖了整个地址空间,则采用前面的步骤。
  • 如果找不到合适的区域或者没有前面存在的段,则LMA被设置为等于VMA。

例子

以下链接器脚本创建三个输出段:一个名为“.text”,从0x1000开始;一个名为“.mdata”,即使其VMA为0x2000,也加载在“.text”节的末尾;另一个名为“.bss”,用于在地址0x3000保存未初始化的数据。符号’_data’被定义为值0x2000,这表明位置计数器保存VMA值,而不是LMA值。

SECTIONS
  {
  .text 0x1000 : { *(.text) _etext = . ; }
  .mdata 0x2000 :
    AT ( ADDR (.text) + SIZEOF (.text) )
    { _data = . ; *(.data); _edata = . ;  }
  .bss 0x3000 :
    { _bstart = . ;  *(.bss) *(COMMON) ; _bend = . ;}
}

把初始化数据从ROM镜像复制到运行时地址。把m_data段的数据从LMA地址放置到VMA的地址空间中。初始化bss段空间为0。

extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;

/* ROM has data at end of text; copy it.  */
while (dst < &_edata)
  *dst++ = *src++;

/* Zero bss.  */
for (dst = &_bstart; dst< &_bend; dst++)
  *dst = 0;

部分关键字x

使用ALIGN增加输出段的对齐。
通过ALIGN_WITH_INPUT属性强制VMA与LMA自始至终保持它们之间的区别。
使用_SUBALIGN来强制输出段中的输入段对齐。
使用关键字 ONLY_IF_RO** **和
ONLY_IF_RW_,可以指定只有在所有输入段都是只读或所有输入段都是读写的情况下才创建输出段。

使用 ’>region’ 把一个段指定到此前设置的内存区域内。

MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

使用 ':phdr** **’ 将一个段分配给先前定义的程序段。??????

PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }

输出段填充

使用’=fillexp’为整个段设置填充模板。_fillexp_是一个表达式。任何其它的未被特殊指定的输出段的内存区域(例如,因为对其输入段产生的缝隙)将会被用_fillexp_的值填充,如果有需要可以重复填充。

SECTIONS { .text : { *(.text) } =0x90909090 }

覆盖描述

覆盖描述使用_OVERLAY_命令。_OVERLAY_命令和_SECTIONS_命令一起使用,就像一个输出段描述符。

OVERLAY [start] : [NOCROSSREFS] [AT ( ldaddr )]
  {
    secname1
      {
        output-section-command
        output-section-command
        …
      } [:phdr…] [=fill]
    secname2
      {
        output-section-command
        output-section-command
        …
      } [:phdr…] [=fill]
    …
  } [>region] [:phdr…] [=fill] [,]

覆盖描述 将(????)作为单个内存映像的一部分加载但将在相同内存地址上运行的段。在运行时,某种类型的覆盖管理器将根据需要从运行时内存地址复制覆盖的段,可能通过简单地操作寻址位来实现。这种方法可能很有用,例如,当某个内存区域比另一个区域更快时。

例子

把 ’.text0’ 和 ’.text1’ 的起始地址设置为地址 0x1000。’.text0’ 的加载地址为0x4000, ’.text1’ 会加载到 ’.text0’ 后面。

  OVERLAY 0x1000 : AT (0x4000)
   {
     .text0 { o1/*.o(.text) }
     .text1 { o2/*.o(.text) }
   }

下面的符号如果被引用则会被定义: __load_start_text0, __load_stop_text0, __load_start_text1, __load_stop_text1。C代码拷贝覆盖._text1_到覆盖区域可能像下面的形式。

  extern char __load_start_text1, __load_stop_text1;
  memcpy ((char *) 0x1000, &__load_start_text1,
          &__load_stop_text1 - &__load_start_text1);

OVERLAY’命令只是为了语法上的便利,因为它所做的所有事情都可以用更加基本的命令加以代替:

  .text0 0x1000 : AT (0x4000) { o1/*.o(.text) }
  PROVIDE (__load_start_text0 = LOADADDR (.text0));
  PROVIDE (__load_stop_text0 = LOADADDR (.text0) + SIZEOF (.text0));
  .text1 0x1000 : AT (0x4000 + SIZEOF (.text0)) { o2/*.o(.text) }
  PROVIDE (__load_start_text1 = LOADADDR (.text1));
  PROVIDE (__load_stop_text1 = LOADADDR (.text1) + SIZEOF (.text1));
  . = 0x1000 + MAX (SIZEOF (.text0), SIZEOF (.text1));

MEMORY命令

MEMORY 命令描述目标中内存块的位置和大小。使用它来描述链接器可以使用哪些内存区域,以及链接器必须避免使用哪些内存区域。把段放到特定的内存区域里。链接器将会基于内存区域设置段地址,如果区域趋于饱和将会产生警告信息。链接器不会为了把段更好的放入内存区域而打乱段的顺序。
属性如下:

  • R’ 只读段
  • W’ 读写段
  • X’ 可执行段
  • A’ 可分配段
  • I’ 已初始化段
  • L’ 类似于’I’
  • ‘!’ 反转其后面的所有属性

格式如下:name** 是链接器脚本中用于引用内存区域的名称。区域名称在链接器脚本之外没有任何意义,不会与符号名、文件名或段冲突。attr 字符是一个可选的属性列表,用于指定是否对链接器脚本中未显式映射的输入段使用特定的内存区域。origin 是内存区域起始地址的数值表达式,关键字_ORIGIN可以缩写为_org 或 _o_。len** 是内存区域的字节大小的表达式,关键字长度可以缩写为 len 或** _l**_。

MEMORY
  {
    name [(attr)] : ORIGIN = origin, LENGTH = len
    …
  }

指定有两个内存区域可供分配:一个从“0”开始空间大小为256k字节,另一个从“0x40000000”开始空间大小为4M字节。链接器将把未显式映射到内存区域的每个部分放入“rom” 内存区域,这些部分要么是只读的,要么是可执行的。链接器会将未显式映射到内存区域的其他部分放入 “ram” 内存区域。

MEMORY
  {
    rom (rx)  : ORIGIN = 0, LENGTH = 256K
    ram (!rx) : org = 0x40000000, l = 4M
  }

使用 >region**’ **输出段属性指引链接器把特殊输出段放到该内存区域。例如,如果有一个名为 ‘mem’ 的内存区域,你可以在输出段定义中使用 ’>mem’。

MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

通过 ORIGIN(memory)LENGTH(memory) 函数获得内存区域的起始地址以及长度:

 _fstack = ORIGIN(ram) + LENGTH(ram) - 4;

PHDRS 命令

ELF对象文件格式使用程序头,类似于段。程序头描述了如何将程序加载到内存中。在本地运行ELF程序时,系统加载程序将读取程序头以确定如何加载程序。只有当程序头设置正确时,这才会工作。默认的链接器将会创建合适的程序头部。有些情况下,可能需要更加精确地指定程程序头。可以使用 PHDRS** **命令达到此目的。

PHDRS
{
  name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
        [ FLAGS ( flags ) ] ;
}

类型可以是以下之一,数字表示关键字的值。:

  • PT_NULL (0)表示未使用的程序头。
  • PT_LOAD (1)表示此程序头描述了要从文件中加载的段。
  • PT_DYNAMIC (2)表示可以找到动态链接信息的段。
  • PT_INTERP (3)表示可以在其中找到程序解释器名称的段。
  • PT_NOTE (4)表示包含注释信息的段。
  • PT_SHLIB (5)保留的程序头类型,由ELF ABI定义但未指定。
  • PT_PHDR (6)表示可以在其中找到程序头的段。
  • PT_TLS(7)指示包含线程本地存储的段。

可以在程序头类型之后使用 FILEHDR** 和 _PHDRS_ **关键字来进一步描述段的内容。 _FILEHDR_关键字意味着该段应包含ELF文件头。 _PHDRS_关键字意味着该段应包括ELF程序头本身。

例子

PHDRS
{
  headers PT_PHDR PHDRS ;
  interp PT_INTERP ;
  text PT_LOAD FILEHDR PHDRS ;
  data PT_LOAD ;
  dynamic PT_DYNAMIC ;
}

SECTIONS
{
  . = SIZEOF_HEADERS;
  .interp : { *(.interp) } :text :interp
  .text : { *(.text) } :text
  .rodata : { *(.rodata) } /* defaults to :text */
  …
  . = . + 0x1000; /* move to a new page in memory */
  .data : { *(.data) } :data
  .dynamic : { *(.dynamic) } :data :dynamic
  …
}

版本命令

链接器支持符号版本。符号版本仅在使用共享库时有用。当动态链接器运行的程序可能已链接到共享库的早期版本时,动态链接器可以使用符号版本来选择函数的特定版本。

  • 第一个版本节点是 VERS_1.1’;它没有其他依赖项。脚本将符号 ‘foo1’ 绑定到 ‘VERS_1.1’ 。脚本把一些符号缩减到局部可见,因此在共享库外部它们将是不可见的;这是使用通配符模式完成的,因此以’old’,’original’,’new’开头的符号将被匹配上。
  • 版本脚本定义节点 ‘VERS_1.2’ 。此节点依赖于 ‘VERS_1.1’。脚本将符号 ‘foo2’绑定到版本节点 ‘VERS_1.2’。
  • 版本脚本定义节点 ‘VERS_2.0’ 。此节点依赖于 ‘VERS_1.2’ 。脚本将符号 ‘bar1’ 和 ‘bar2’ 绑定到版本节点 ‘VERS_2.0’。
VERS_1.1 {
	 global:
		 foo1;
	 local:
		 old*;
		 original*;
		 new*;
};

VERS_1.2 {
		 foo2;
} VERS_1.1;

VERS_2.0 {
		 bar1; bar2;
	 extern "C++" {
		 ns::*;
		 "f(int, double)";
	 };
} VERS_1.2;

通过在版本脚本的某个地方使用 ‘*global: _;_’,可以将所有其他未指定的符号绑定到给定的版本节点。
GNU扩展允许同一个函数的多个版本出现在给定的共享库中。通过这种方式,您可以在不增加共享库的主要版本号的情况下对接口进行不兼容的更改,同时仍然允许与旧接口链接的应用程序继续运行。为此,必须在源文件中使用多个‘.symver’ 指令。

__asm__(".symver original_foo,foo@");
__asm__(".symver old_foo,foo@VERS_1.1");
__asm__(".symver old_foo1,foo@VERS_1.2");
__asm__(".symver new_foo,foo@@VERS_2.0");

链接脚本中的表达式

常数

链接器接受后缀 ‘h’ 或 ‘H’ 表示十六进制,‘o’ 或 ‘O’ 表示八进制,‘b’ 或 ‘B’ 表示二进制,‘d’ 或 ‘D’ 表示十进制。以下均为同一数量:

_fourk_1 = 4K;
_fourk_2 = 4096;
_fourk_3 = 0x1000;
_fourk_4 = 10000o;

符号常数

使用 CONSTANT(name) 操作符来引用特定于目标的常量,其中 _name_为:
MAXPAGESIZE:目标的最大页面大小。
COMMONPAGESIZE:目标的默认页大小。
创建一个对齐到目标支持的最大页边界的代码段:

  .text ALIGN (CONSTANT (MAXPAGESIZE)) : { *(.text) }

符号名

除引号外,符号名称以字母、下划线或句点开始,可以包括字母、数字、下划线、句点和连字符。
由于符号可以包含许多非字母字符,用空格分隔符号是最安全的。例如,‘A-B’ 是一个符号,而 ‘A - B’ 是一个包含减法的表达式。

地址(位置)计数器 .

位置计数器不能在一个输出段内向回移动,也不能在段外回退,如果这么做了将会创建重叠的LMA。

例子

文件 file1 的 ’text’ 段位于输出段 output 的起始位置。其后有个1000字节的缝隙。此后 file2 的 ’.text’ 段出现在输出段内,其后也有1000字节的缝隙,最后是 file3 的 ’.text’ 段。标记 ’=0x12345678’ 指定了应当向缝隙中填充的内容。

SECTIONS
{
  output :
    {
      file1(.text)
      . = . + 1000;
      file2(.text)
      . += 1000;
      file3(.text)
    } = 0x12345678;
}

’.’ 实际上是指从当前包含对象开始的字节偏移量。通常为 SECTIONS 声明,起始地址为0,因此 ’.’ 可以被当作一个绝对地址使用。但是如果 ’.’ 被在段描述符内使用,它表示从该段开始的偏移地址,不是一个绝对地址。

例子2

.text’ 段将会被安排到起始地址0x100,实际大小为0x200字节,即便 ’.text’ 输入段没有足够的数据填充该区域(反之如果数据过多,将会产生一个错误,因为将会尝试向前回退 ’.’ )。段 ’.data’ 将会从0x500开始,并且输出段会有额外的0x600字节空余空间在输入段’.text’。

SECTIONS
{
    . = 0x100
    .text: {
      *(.text)
      . = 0x200
    }
    . = 0x500
    .data: {
      *(.data)
      . += 0x600
    }
}

例子3

如果链接器需要放置孤儿段,则将符号设置为输出段语句外部的位置计数器的值可能会导致意外的值。给定如下:

SECTIONS
{
    start_of_text = . ;
    .text: { *(.text) }
    end_of_text = . ;

    start_of_data = . ;
    .data: { *(.data) }
    end_of_data = . ;
}

’.rodata’ 没有在脚本中提及,可能会被选择放到 ’.text’ 和 ’.data’ 段中间。假设所有定义或者其他声明属于前面的输出段,除了特殊情况设定 ’.’。链接器将会类似于下面的脚本放置孤儿段:

SECTIONS
{
    start_of_text = . ;
    .text: { *(.text) }
    end_of_text = . ;

    start_of_data = . ;
    .rodata: { *(.rodata) }
    .data: { *(.data) }
    end_of_data = . ;
}

一种影响孤儿段放置的办法是为位置计数器指定自身的值,链接器会认为一个 ’.’ 的设置是设定一个后面段的起始地址,因此该段应为一个组。因此可以这么写。这样,孤儿段 ’.rodata’ 将会被放置在 end_of_text 和 _start_of_data_之间。

SECTIONS
{
    start_of_text = . ;
    .text: { *(.text) }
    end_of_text = . ;

    . = . ;
    start_of_data = . ;
    .data: { *(.data) }
    end_of_data = . ;
}

内置函数

ABSOLUTE(exp)
返回表达式exp的绝对值(不可重定位,非负)。主要用于在段定义中为符号赋绝对值,其中符号值通常是段相对的。
ADDR(section)
返回名为 ’section’ 的段的地址(VMA)。你的脚本必须事先为该段定义了位置。
ALIGN(align)
ALIGN(exp,align)
返回位置计数器(.)或任意表达式对齐到下一个align指定边界的值。单操作数_ALIGN_并不会改变位置计数器的值,它只是对其进行算术运算。两个操作数_ALIGN_允许任意表达式向上对齐:ALIGN(ALIGN)等价于_ALIGN_(ABSOLUTE(.),ALIGN)
ALIGNOF(section)
如果section已分配,返回名为_section_的对齐字节。如果段还没被分配,链接器会报错。
BLOCK(exp)
这是_ALIGN_的同义词,用于与旧的链接器脚本兼容。
DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)

(ALIGN(maxpagesize) + (. & (maxpagesize - 1)))

DATA_SEGMENT_END(exp)
为DATA_SEGMENT_ALIGN运算定义了数据段的结尾。

  . = DATA_SEGMENT_END(.);

DATA_SEGMENT_RELRO_END(offset, exp)
此命令为使用 ’-z relro’ 命令的情况定义了_PT_GNU_RELRO_段的结尾。
DEFINED(symbol)
如果符号在链接器全局符号表中,并且在脚本中定义的语句之前定义,则返回1,否则返回0。
全局符号 ‘begin’ 设置为 ‘.text’ 段中的第一个位置,但如果名为 ‘begin’ 的符号已经存在,则其值将被保留:

SECTIONS { …
  .text : {
    begin = DEFINED(begin) ? begin : . ;
    …
  }
  …
}

LENGTH(memory)
返回名为 memory 内存区域的长度。
LOADADDR(section)
返回名为 section 的段的LMA绝对地址
LOG2CEIL(exp)
返回 exp 的二进制对数,取整为无穷大。
MAX(exp1, exp2)
返回 exp1exp2 的最大值。
MIN(exp1, exp2)
返回 exp1exp2 的最小值。
NEXT(exp)
返回下一个未分配的地址,它是 exp 的倍数。
ORIGIN(memory)
返回名为 memory 的内存区域的起始地址。
SEGMENT_START(segment, default)
返回命名段的基址。如果已经为此段指定了显式值(使用命令行 ‘-T’ 选项),则将返回该值,否则该值将为默认值。
SIZEOF(section)
返回名为 section 段的字节数。
SIZEOF_HEADERS
sizeof_headers
返回输出文件头的大小(以字节为单位)。