当前位置: 首页 > news >正文

链接脚本(Linker Scripts)

Linker Scripts官方文档

一、链接脚本的基本概念

GCC的编译过程通常包括四个主要步骤:预处理、编译、汇编和链接。编译过程的最后一步将所有的.o文件与系统库文件和其他库文件链接起来,生成最终的可执行文件。在链接过程中,由一个链接脚本控制,编译器会将程序所需的库函数代码添加到可执行文件中。

链接器必须要用一个链接脚本。如果不提供的话,链接器会使用默认的链接脚本。可以用“–verbose”命令行选项来显示默认的链接脚本。

一些固定的命令行选项,比如“-r”或“-N”会影响默认的链接脚本。也可以用“-T”命令行选项来指定自己的链接脚本。这时候,指定的链接脚本会替代默认的链接脚本。

链接脚本的主要目的是描述输入文件中的段如何映射在输出文件中,并控制输出文件的存储布局。绝大多数链接脚本都是这个作用。然而在必要的时,链接脚本也可以使用接下来描述的命令直接指定链接器执行许多其他的操作。

1.1 链接器脚本语言基本概念和术语

链接器将输入文件组合成一个输出文件。输出文件和每个输入文件都采用一种特殊的数据格式,称为目标文件格式。每个文件都称为一个目标文件。输出文件通常称为可执行文件,但为了我们的目的,我们也将它称为目标文件。每个目标文件包含其他内容,如一个段列表。我们有时将输入文件中的段称为输入段;类似地,输出文件中的段称为输出段。

每个目标文件中的段都有一个名称和大小。大多数段还包含一个与之关联的数据块,称为段内容。一个段可以被标记为可加载的,这意味着当输出文件运行时,其内容应该被加载到内存中。一个没有内容的段可能是可分配的,这意味着应该预留一块内存区域,但那里不需要加载特定内容(在某些情况下,这块内存必须清零)。一个既不可加载也不可分配的段通常包含某种调试信息。

每个可加载或可分配的输出段有两个地址。第一个是 VMA,即虚拟内存地址。这是当输出文件运行时该段将拥有的地址。第二个是 LMA,即加载内存地址。这是该段将被加载的地址。在大多数情况下,这两个地址是相同的。它们可能不同的情况的一个例子是当数据段被加载到 ROM 中,然后在程序启动时被复制到 RAM 中(这种技术通常用于在基于 ROM 的系统中初始化全局变量)。在这种情况下,ROM 地址将是 LMA,而 RAM 地址将是 VMA。

您可以通过使用 objdump命令的‘ -h ’选项来查看对象文件中的段。

每个目标文件还有一个符号列表,称为符号表。符号可以是已定义的或未定义的。每个符号都有一个名称,每个已定义的符号都有一个地址,以及其他信息。如果您将 C 或 C++程序编译成目标文件,您将为每个已定义的函数和全局或静态变量获得一个已定义的符号。输入文件中引用的每个未定义的函数或全局变量都将变成一个未定义的符号。

可以使用nm命令或者使用带 -t 选项的 objdump 命令来查看对象文件中的符号。

二、链接脚本格式

链接脚本是文本文件。

一个链接器脚本就是一系列命令。每条命令都是一个关键字,可能后面还跟有一个参数,或者一个符号的赋值。使用分号分割命令,空格通常被忽略。

文件名或格式名等字符串可以直接输入。如果文件名含有一个例如逗号的字符(逗号被用来分割文件名),你可以将文件名放在双引号内部。 因此文件名中不能使用双引号字符。

你可以像C语言一样在链接脚本内包含注释,由/**/分隔。和C一样,注释在句法上被当作空格。

三、最简单的链接脚本示例

最简单的链接脚本只有一个命令:SECTIONS该命令用于描述输出文件的内存布局。

SECTIONS命令是个很强大的命令,这里我们简单地描述一下它的使用。先假定程序只包含代码、初始化的数据和未初始化的数据,它们对应在“.text”、“.data”和“.bss”段中。我们假定在输入文件中也只有这些段。

在这个例子中,我们设定代码加载的地址为0x10000,数据开始的地址为0x8000000。这个链接就这么写:

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

你将‘ SECTIONS ’命令作为关键字‘ SECTIONS ’编写,后面跟着用花括号括起来的一系列符号赋值和输出段描述。

上述示例中SECTIONS命令内的第一行设置了特殊符号‘ . ’的值,该符号是位置计数器。如果你未以其他方式指定输出段的地址(其他方式将在后面描述),则地址将根据位置计数器的当前值设置。然后位置计数器会根据输出段的大小进行递增。在SECTIONS命令开始时,位置计数器的值为‘ 0 ’。

第二行定义了一个输出段.text。冒号是必需的格式。在输出段名称后的花括号内,你列出应放入此输出段的所有输入段的名称。*是一个通配符,匹配任何文件名。表达式*(.text)表示所有输入文件中的所有.text输入段。

由于在定义输出段.text时位置计数器为‘ 0x10000 ’,链接器将把输出文件中.text段的地址设置为‘ 0x10000 ’。

剩余的行定义了输出文件中的.data.bss部分。链接器将把.data输出部分放置在地址‘ 0x8000000 ’。在链接器放置.data输出部分后,位置计数器的值将是‘ 0x8000000 ’加上.data输出部分的大小。其效果是链接器将把.bss输出部分立即放置在内存中的.data输出部分之后。

链接器将通过增加位置计数器(如有必要)来确保每个输出部分具有所需的对齐方式。在这个例子中,.text.data部分指定的地址可能会满足任何对齐约束,但链接器可能需要在.data.bss部分之间创建一个小间隙。

这就是一个简单但完整的链接脚本。

四、简单的链接脚本命令

该节描述一些简单的链接脚本命令。

4.1 设置入口点

程序中第一条要执行的指令称为入口点。您可以使用 ENTRY 链接脚本命令来设置入口点。参数是一个符号名称:

ENTRY(symbol)

设置入口点有几种方法。链接器会按顺序尝试以下方法,并在其中一种方法成功时停止:
1)“-e”命令行选项。
2)链接脚本中的ENTRY(symbol)命令。
3)已经定义的目标特定符号的值。对于很多情况是start,但例如 PE 和 BeOS 系统会检查可能的入口符号列表,匹配找到的第一个符号。
4).text段中第一个字节的地址。
5)地址0。

4.2 处理文件的命令

以下是链接器脚本处理文件的几个常用命令:

1)INCLUDE命令

INCLUDE filename

使用INCLUDE命令的脚本将引用链接脚本filename。该文件将在当前目录中搜索,以及在用 -L 选项指定的任何目录中搜索。您可以嵌套调用 INCLUDE ,最多可达 10 层。

您可以将 INCLUDE 指令放置在顶层、 MEMORY 或 SECTIONS 命令中,或输出段描述中。

2)INPUT命令

INPUT(file, file, …)
INPUT(file file …)

INPUT命令指示链接器在链接中包括文件,就像在命令行中一样。

例如,如果你总是想在链接时包含 subr.o ,但又懒得在每个链接命令行上添加它,那么你可以在链接器脚本中放入‘ INPUT (subr.o) ’。

事实上,如果你愿意,你可以在链接器脚本中列出所有输入文件,然后仅使用‘ -T ’选项来调用链接器。

如果配置了sysroot 前缀,且文件名以‘/ ’符开头,并且正在处理的脚本位于sysroot 前缀内,则将在sysroot 前缀中查找文件名。也可以通过指定 = 作为文件名路径中的第一个字符,或在文件名路径前加上$ SYSROOT来强制使用sysroot 前缀。另请参阅命令行选项中对‘-L ’ 的描述(Command-line Options)。

如果未使用 sysroot 前缀,链接器将尝试在包含链接器脚本的目录中打开文件。如果找不到,链接器将接着在当前目录中搜索。如果仍然找不到,链接器将搜索归档库搜索路径。

如果你使用‘ INPUT (-lfile) ’, ld 会将名称转换为 libfile.a ,就像命令行参数‘ -l ’一样。

3)GROUP命令

GROUP(file, file, …) 
GROUP(file file …)

GROUP 命令与 INPUT 类似,但命名文件应该是归档文件,并且会反复搜索直到不再创建新的未定义引用。参见命令行选项中‘ -( ’的描述。

4)OUTPUT命令

OUTPUT(filename)

OUTPUT 命令命名输出文件。在链接脚本中使用 OUTPUT(filename) 与在命令行中使用‘ -o filename ’完全相同。如果两者都使用,命令行选项优先。

可以使用OUTPUT命令定义输出文件的默认名称,原本的默认名称是a.out。

5)SEARCH_DIR命令

SEARCH_DIR(path)

SEARCH_DIR 命令将路径添加到 ld 查找归档库的路径列表中。使用 SEARCH_DIR(path) 与在命令行上使用‘ -L path ’完全相同。如果两者都使用,则链接器将搜索这两个路径。使用命令行选项指定的路径将首先被搜索。

6)STARTUP命令

STARTUP(filename)

STARTUP 命令与 INPUT 命令非常相似,只是 filename 将成为链接的第一个输入文件,就像它在命令行中第一个被指定一样。当使用一个系统时,其中入口点总是第一个文件的开头,这可能会很有用。

4.3 处理目标文件格式的命令

有一些链接器脚本命令用于处理目标文件格式。

1)OUTPUT_FORMAT命令

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

OUTPUT_FORMAT 命令用于指定输出文件的 BFD 格式(参见 BFD)。使用 OUTPUT_FORMAT(bfdname) 与在命令行中使用 ‘ --oformat bfdname ’ 完全相同。如果两者都使用,则命令行选项优先。

您可以使用 OUTPUT_FORMAT 命令配合三个参数,根据 ‘ -EB ’ 和 ‘ -EL ’ 命令行选项使用不同的格式。这允许链接脚本根据期望的字节序(大小端)设置输出格式。

如果既不使用 ‘ -EB ’ 也不使用 ‘ -EL ’,则输出格式将是第一个参数,即默认格式。如果使用 ‘ -EB ’,输出格式将是第二个参数,即大端格式。如果使用 ‘ -EL ’,输出格式将是第三个参数,即小端格式。

例如,MIPS ELF 目标的默认链接脚本使用此命令:

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

这表示输出文件的默认格式是‘ elf32-bigmips ’,但如果用户使用‘ -EL ’命令行选项,输出文件将以‘ elf32-littlemips ’格式创建。

2)TARGET命令

TARGET(bfdname)

TARGET 命令指定读取输入文件时使用的 BFD 格式。它会影响后续的 INPUT 和 GROUP 命令。此命令类似于在命令行上使用‘ -b bfdname ’。如果使用了 TARGET 命令但未使用 OUTPUT_FORMAT ,则最后一个 TARGET 命令也将用于设置输出文件的格式。

4.4 为内存区域分配别名名称

使用MEMORY命令可以为存在的存储区域分配别名。每个名称最多对应一个存储区域。

1)REGION_ALIAS命令

REGION_ALIAS(alias, region)

REGION_ALIAS 函数为内存区域 region 创建别名名称 alias。这允许灵活地将输出段映射到内存区域。以下是一个示例。

假设我们有一个嵌入式系统应用程序,它配备了各种内存存储设备。所有设备都包含一个通用、易失性内存 RAM ,用于代码执行或数据存储。有些设备可能还包含一个只读、非易失性内存 ROM ,用于代码执行和只读数据访问。最后一个是一个只读、非易失性内存 ROM2 ,仅支持只读数据访问且不具备代码执行能力。我们有四个输出部分:

  • .text 程序代码段;
  • .rodata 只读数据段;
  • .data 可读写已初始化数据段;
  • .bss 可读写的初始化为0数据段。

目标是为链接器命令文件提供一个系统无关部分,用于定义输出段;以及一个系统相关部分,用于将输出段映射到系统上可用的内存区域。我们的嵌入式系统有三种不同的内存配置 A 、 B 和 C :

SectionVariant AVariant BVariant C
.textRAMROMROM
.rodataRAMROMROM2
.dataRAMRAM/ROMRAM/ROM2
.bssRAMRAMRAM
  • 存储方式A 表示4个输出段都存放在RAM中。
  • 存储方式B 表示.text段和.rodata段存放在ROM中,.data段存放在RAM/ROM中,.bss段存放在RAM中。
  • 存储方式C 表示.text段存放在ROM中,.rodata段存放在ROM2中,.data段存放在RAM/ROM2中,.bss段存放在RAM中。

符号 RAM/ROM 或 RAM/ROM2 表示该段可以各自加载到区域 ROM 或 ROM2 中。请注意,无论哪种方式, .data 段的加载地址都从 .rodata 段的末尾开始。

下面是处理输出段的基链接脚本。它包括描述内存布局的系统相关文件 linkcmds.memory :

INCLUDE linkcmds.memorySECTIONS
{.text :{*(.text)} > REGION_TEXT.rodata :{*(.rodata)rodata_end = .;} > REGION_RODATA.data : AT (rodata_end){data_start = .;*(.data)} > REGION_DATAdata_size = SIZEOF(.data);data_load_start = LOADADDR(.data);.bss :{*(.bss)} > REGION_BSS
}

现在我们需要三个不同的 linkcmds.memory 文件来定义内存区域和别名名称。三种变体 A 、 B 和 C 的 linkcmds.memory 内容:

  • A:所有的内容都在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);
  • B:程序代码和只读数据放入 ROM 中。可读写数据放入 RAM 中。初始化数据存到 ROM 中,在系统启动时将被复制到 RAM 中。
MEMORY
{ROM : ORIGIN = 0, LENGTH = 3MRAM : ORIGIN = 0x10000000, LENGTH = 1M
}REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
  • C:程序代码放入 ROM 中。只读数据放入 ROM2 中。可读写数据放入 RAM 中。初始化数据存到 ROM2 中,在系统启动时将被复制到 RAM 中。
MEMORY
{ROM : ORIGIN = 0, LENGTH = 2MROM2 : ORIGIN = 0x10000000, LENGTH = 1MRAM : ORIGIN = 0x20000000, LENGTH = 1M
}REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM2);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);

可以编写一个通用的系统初始化例程,将 .data 段从 ROM 或 ROM2 复制到 RAM 中:

#include <string.h>extern char data_start [];
extern char data_size [];
extern char data_load_start [];void copy_data(void)
{if (data_start != data_load_start){memcpy(data_start, data_load_start, (size_t) data_size);}
}

4.5 其他链接器脚本命令

还有一些其他链接器脚本命令:
1)ASSERT

ASSERT(exp, message)

确保 exp 不为零。如果为零,则退出链接器并返回错误代码,同时打印消息。

请注意,断言会在链接的最终阶段之前进行检查。这意味着如果用户没有为那些符号设置值,那么在段定义中 PROVIDE 的符号表达式将会失败。这条规则的唯一例外是仅引用当前地址的 PROVIDE 符号。因此,像这样的断言:

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

如果 __stack_size 没有在其他地方定义,将会失败。在段定义外 PROVIDE 的符号会在更早的阶段被评估,因此它们可以在断言中使用。所以:

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

将会正常工作。

2)EXTERN

EXTERN(symbol symbol …)

强制将符号作为未定义符号输入到输出文件中。这样做可以从标准库中链接额外的模块。您可以为每个 EXTERN 列出多个符号,也可以多次使用 EXTERN 。此命令与命令行选项 ‘ -u ’ 具有相同的效果。

3)FORCE_COMMON_ALLOCATION
此命令与命令行选项 ‘ -d ’ 具有相同的效果:即使指定了可重定位的输出文件(‘ -r ’),也可使 ld 为 common symbols 分配空间。

4)INHIBIT_COMMON_ALLOCATION
此命令与命令行选项“ --no-define-common ”具有相同的效果:使 ld 在非可重定位的输出文件中忽略对 common symbols 的地址分配。

5)FORCE_GROUP_ALLOCATION
此命令与命令行选项“ --force-group-allocation ”具有相同的效果:使 ld 将段组成员像普通输入段一样放置,即使在指定可重定位输出文件时(“ -r ”)也会删除 section groups。

6)NOCROSSREFS

NOCROSSREFS(section section …)

这个命令可以用来告诉 ld 对某些输出段之间的任何引用发出错误。

在某些类型的程序中,特别是在使用内存覆盖的嵌入式系统中,当一个段被加载到内存中时,另一个段可能不会被加载。这两个段之间的任何直接引用都会产生错误。例如,如果一个段中的代码调用另一个段中定义的函数,这将是一个错误。

NOCROSSREFS 命令接受一个输出段名称列表。如果 ld 检测到这些段之间存在交叉引用,它会报告错误并返回非零退出状态。请注意, NOCROSSREFS 命令使用的是输出段名称,而不是输入段名称。

7)OUTPUT_ARCH

OUTPUT_ARCH(bfdarch)

指定特定的输出机器架构。参数是 BFD 库的参数之一。您可以通过使用带有‘ -f ’选项的 objdump 程序查看对象文件的架构。

比如输入:arm-fsl-linux-gnueabi-objdump-f add可以查看add的输出机器架构为arm。

8)LD_FEATURE

LD_FEATURE(string)

此命令可用于修改 ld 的行为。如果字符串是 “SANE_EXPR” ,则脚本中的绝对符号和数字将在任何地方都被视为数字。

五、为符号赋值

您可以在链接器脚本中为符号赋值。这将定义该符号并将其置于具有全局作用域的符号表中。

5.1 简单的赋值

可以使用C语言的赋值操作来为符号赋值:

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

第一行将定义符号为表达式的值。在其他情况下,符号必须已经定义,并且值将相应调整。

特殊符号名称.表示位置计数器。您只能在 SECTIONS 命令中使用它。参见位置计数器。

表达式后面必须有一个分号。

您可以将符号赋值作为独立的命令,或在 SECTIONS 命令中的语句,或作为 SECTIONS 命令中输出段描述的一部分来编写。

符号的段将被设置为表达式的段;更多信息,请参阅The Section of an Expression。

这里是一个示例,展示了符号分配可能使用的三个不同位置:

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

在这个例子中,符号“ floating_point ”将被定义为零。符号“ _etext ”将被定义为最后一个“ .text ”输入段之后的地址。符号“ _bdata ”将被定义为“ .text ”输出段向上对齐到 4 字节边界之后的地址。

5.2 HIDDEN

对于针对 ELF 的目标,定义一个将被隐藏且不会导出的符号。语法为 HIDDEN(symbol = expression)

用HIDDEN重写上一节的例子:

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

这时候三个符号在该模块外面都是不可见的。

5.3 PROVIDE

在某些情况下,链接脚本需要仅在符号被引用且未在任何包含在链接中的对象中定义时才定义该符号。例如,传统的链接器定义了符号“ etext ”。然而,ANSI C 要求用户能够将“ etext ”用作函数名而不遇到错误。可以使用 PROVIDE 关键字仅在符号被引用但未定义时定义该符号,语法为 PROVIDE(symbol = expression)

这里是一个使用 PROVIDE 定义“ etext ”的示例:

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

在这个例子中,如果程序定义了‘ _etext ’(带有前导下划线),链接器会给出多重定义的诊断。另一方面,如果程序定义了‘ etext ’(没有下划线),链接器将静默地使用程序中的定义。如果程序引用了‘ etext ’但没有定义它,链接器将使用链接器脚本中的定义。

注意:PROVIDE 指令将一个 common symbols 视为已定义,即使这样的符号可以与 PROVIDE 会创建的符号合并。这在考虑构造函数和析构函数列表符号(如‘ CTOR_LIST ’)时尤其重要,因为这些符号通常定义为 common symbols。

5.4 PROVIDE_HIDDEN

PROVIDE_HIDDEN和PROVIDE类似,对于ELF目标,符号会被隐藏,不会被导出。

5.5 源代码引用

从源代码访问链接脚本定义的变量不是很直观。特别是链接脚本的符号不等同于高级语言的符号声明,取而代之的是没有值的一个符号。

编译器在符号表中存储源代码中的名称时,通常会将名称转换成不同的名称。例如,Fortran 编译器通常会在名称前或后添加下划线,而 C++ 会执行大量的“ name mangling ”。因此,源代码中使用的变量名称与链接器脚本中定义的相同变量名称之间可能会有差异。例如,在 C 语言中,链接器脚本变量可能被引用为:

  extern int foo;

但在链接器脚本中,它可能被定义为:

  _foo = 1000;

在下面的例子中假定不会有名称的转换。

当在 C 等高级语言中声明一个符号时,会发生两件事。第一,编译器在程序的内存中预留足够的空间来存储符号的值。第二,编译器在程序的符号表中创建一个条目,该条目包含符号的地址。也就是说,符号表包含存储符号值的内存块的地址。例如,以下 C 声明,在文件作用域中:

 int foo = 1000;

在符号表中创建一个名为“ foo ”的条目。该条目包含一个“ int ”大小内存块的地址,其中初始存储了数字 1000。

当程序引用编译器生成的代码时,首先访问符号表以找到符号内存块的地址,然后从该内存块读取值。所以:

foo = 1;

这条语句在符号表中查找符号foo得到符号相关的地址,然后将1写入这个地址。

然而:

int * a = & foo;

在符号表中查找符号foo,得到其地址,然后复制这个地址到变量a相关的内存块中。

相比之下,链接器脚本符号声明会在符号表中创建一个条目,但不会分配任何内存。因此它们是一个没有值的地址。例如,链接器脚本定义:

foo = 1000;

在符号表中创建一个名为‘ foo ’的条目,该条目存储内存地址为 1000,但在地址 1000 处没有存储任何特殊内容。这意味着你不能访问链接器脚本定义的符号的值,它没有值,你能做的只是访问链接器脚本定义的符号的地址。

因此,当你在源代码中使用链接器脚本定义的符号时,应该始终获取符号的地址,而绝不能尝试使用其值。例如,假设你想将名为.ROM 的内存段的內容复制到名为.FLASH 的内存段,并且链接器脚本包含以下声明:

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 命令告诉链接器如何将输入段映射到输出段,以及如何将输出段放置在内存中。

SECTIONS 命令的格式为:

SECTIONS
{sections-commandsections-command...
}

每个段命令可以是以下其中一种:

  • 一个 ENTRY 命令(参见Entry command)
  • 一个符号赋值(参见Assigning Values to Symbols)
  • 一个输出段描述
  • 一个覆盖描述

ENTRY 命令和符号分配允许在 SECTIONS 命令内部使用,以便在这些命令中方便地使用位置计数器。这也可以使链接器脚本更容易理解,因为您可以在输出文件布局的有意义位置使用这些命令。

如果您在链接器脚本中不使用 SECTIONS 命令,链接器将每个输入段放置到输入文件中首次遇到的顺序中具有相同名称的输出段中。例如,如果所有输入段都存在于第一个文件中,输出文件中的段顺序将与第一个输入文件中的顺序相匹配。第一个段将位于地址零处。

6.1 输出段描述

一个输出段的完整描述如下:

section [address] [(type)] :[AT(lma)][ALIGN(section_align) | ALIGN_WITH_INPUT][SUBALIGN(subsection_align)][constraint]{output-section-commandoutput-section-command...} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp] [,]

大多数输出段都不会用到上面的大部分可选的段属性。

section边上一定要有空格,以便段名不产生歧义。冒号和花括号也是必需的。如果使用 fillexp 并且下一个段命令看起来像是表达式的延续,那么末尾的逗号可能是必需的。行尾和其他空白是可选的。

每个输出部分命令可以是以下之一:

  • 一个符号赋值
  • 一个输入部分描述
  • 直接包含的数据值
  • 一个特殊的输出部分关键词

6.2 输出段名称

输出段的名称是section,它一定要满足输出格式的约束。在只支持有限个段数目的格式中,比如a.out,名称必须是格式支持的(比如a.out只支持“.text”、“.data”和“.bss”等)。

6.3 输出段地址

该地址是输出段的 VMA(虚拟内存地址)的表达式。这个地址是可选的,但如果提供了它,输出地址将精确地按照指定设置。

如果未指定输出地址,则将根据以下方法为该段选择一个地址。此地址将调整以符合输出段的对齐要求。对齐要求是输出段中包含的任何输入段的最严格对齐。

输出段地址的选择方法如下:

  • 如果为该段设置了输出内存区域,则它会被添加到该区域,并且其地址将是该区域内下一个空闲地址。
  • 如果使用 MEMORY 命令创建了内存区域列表,则选择具有与该段兼容属性的第一个区域来包含它。该段的输出地址将是该区域内下一个空闲地址。
  • 如果未指定内存区域,或者没有区域与该段匹配,则输出地址将基于位置计数器的当前值。

例如:

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

略有不同。第一个将“ .text ”输出段的地址设置为位置计数器的当前值。第二个将其设置为位置计数器对齐到所有“ .text ”输入段中最严格的对齐方式后的值。

地址可以是一个任意表达式。例如,如果你想将段对齐到 0x10 字节边界,使得段地址的最低四位为零,你可以这样做:

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

ALIGN会返回当前位置计数器上边沿对齐的特定值。

假定一个段是非空的,为该段指定地址会改变位置计数器的值。

6.4 输入段描述

最通常的输出段命令就是一个输入段描述。

输入段描述是最基本的链接器脚本操作。你使用输出段来告诉链接器如何安排你的程序在内存中的布局。也可以使用输入段描述来告诉链接器如何将输入文件映射到你的内存布局中。

6.4.1 输入段基础

一个输入段描述由一个文件名和跟在后面的在圆括号中的一串段名组成。

文件名和段名可能是通配符模式。

最常见的输入段描述是在输出段中包括带有特定名字的所有输入段。例如,为了包括所有的输入“.text”段,可以这么写:

*(.text)

在这里,‘ * ’是一个通配符,它匹配任何文件名。要从文件名通配符匹配中排除一组文件,可以使用 EXCLUDE_FILE 来匹配除 EXCLUDE_FILE 列表中指定的文件之外的所有文件。例如:

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

这将导致除了 crtend.o 和 otherfile.o 之外的所有文件中的.ctors 段被包含。EXCLUDE_FILE 也可以放在段列表中,例如:

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

下面是两种包含多个段的方法:

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

这两种方法的区别在于‘ .text ’和‘ .rdata ’输入段在输出段中出现的顺序。在第一个示例中,它们将交错出现,按照在链接器输入中找到的顺序出现。在第二个示例中,所有‘ .text ’输入段将首先出现,然后是所有‘ .rdata ’输入段。

当使用 EXCLUDE_FILE 包含多个段时,如果排除在段列表内,则排除仅适用于紧随其后的段,例如:

*(EXCLUDE_FILE (*somefile.o) .text .rdata)

这将导致除了 somefile.o 之外的所有文件中的所有‘ .text ’段被包含,而所有文件中的所有‘ .rdata ’段,包括 somefile.o ,将被包含。要排除 somefile.o 中的‘ .rdata ’段,示例可以修改为:

*(EXCLUDE_FILE (*somefile.o) .text EXCLUDE_FILE (*somefile.o) .rdata)

或者,将 EXCLUDE_FILE 放在段列表之外,在输入文件选择之前,将导致排除适用于所有段。因此,前面的示例可以重写为:

EXCLUDE_FILE (*somefile.o) *(.text .rdata)

您可以指定一个文件名,以包含来自特定文件的段。如果您的一个或多个文件包含需要在内存中的特定位置的特殊数据,您就会这样做。例如:

data.o(.data)

为了根据输入段的段标识来细化包含的段,可以使用 INPUT_SECTION_FLAGS。

这里有一个使用段头标志来处理 ELF 段的简单示例:

SECTIONS {.text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }.text2 :  { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }
}

在这个例子中,输出段‘ .text ’由所有设定SHF_MERGE和SHF_STRINGS段头标志的符合*(.text)名称的所有输入段组成。输出段‘ .text2 ’由未设定SHF_WRITE段头标志的符合*(.text)的所有输入段组成。

6.4.2 输入段的通配符模式

在一个输入段描述中,文件名或段名都可能是通配符格式。

在许多示例中看到的‘ * ’的文件名是一个简单的文件名通配符模式。

通配符格式与UNIX shell下的类似:

  • “*” 匹配任意个字符。
  • “?” 匹配任意单个字符。
  • “[chars]” 匹配chars中的任意一个字符;“-”字符用于设定字符的范围,比如“[a-z]”匹配任意一个小写字母。
  • “\” 引用后面的字符。

当一个文件名与通配符相匹配,匹配字符不能匹配一个“/”字符(在UNIX中用于分隔目录名的)。由一个单一“*”字符构成的样式是例外,它会匹配任何文件名,不管它是否包含一个“/”。在一个段名中,通配符字符会匹配一个“/”字符。

文件名通配符样式只匹配那些在命令行或 INPUT命令中明确指定的文件。链接器不会搜索目录来扩展匹配。

如果一个文件名匹配多个通配符样式,或者如果一个文件名很明确而且它还匹配一个通配符样式,链接器会使用在链接脚本中最先匹配的。下面这个例子中,输入段描述系列可能是错误的,因为data.o规则没有用到。

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

一旦一个输入段匹配了某个规则,它就会被放置到对应的输出段中,并且不会再被后续的规则处理。.data 输出段包含了 data.o 的 .data 段以及所有其他文件的 .data 段。所以.data1 输出段是空的。

通常,链接器将按照通配符匹配的文件和段的顺序在链接过程中放置它们。您可以通过使用括号中出现的 SORT_BY_NAME 关键字来改变这一点(例如, SORT_BY_NAME(.text*) )。当使用 SORT_BY_NAME 关键字时,链接器将在将文件或段放置到输出文件之前,按名称升序对它们进行排序。

SORT_BY_ALIGNMENT 与 SORT_BY_NAME 类似。 SORT_BY_ALIGNMENT 将在将段放入输出文件之前,按对齐的降序对段进行排序。将较大的对齐放在较小的对齐之前可以减少所需的填充量。

SORT_BY_INIT_PRIORITY和SORT_BY_NAME非常相似,不同之处是SORT_BY_INIT_PRIORITY 在将段放入输出文件前先按照GCC的 init_priority 属性的数值进行升序排序。

SORT是SORT_BY_NAME的别名。

当在链接脚本中存在嵌套的段排序命令时,最多只能有一层嵌套。所有的嵌套的段排序命令组合有以下几种:
1)SORT_BY_NAME(SORT_BY_ALIGNMENT(wildcard section pattern))。优先按照输入段名排序,如果两个段有相同的名称,再按照对齐排序。
2)SORT_BY_ALIGNMENT(SORT_BY_NAME(wildcard section pattern))。优先按照对齐排序,如果两个段有相同的对齐,再按照名称排序。
3)SORT_BY_NAME(SORT_BY_NAME(wildcard section pattern))和SORT_BY_NAME(wildcard section pattern)一样。
4)SORT_BY_ALIGNMENT(SORT_BY_ALIGNMENT(wildcard section pattern))和SORT_BY_ALIGNMENT(wildcard section pattern)一样。
5)所有其他的嵌套排序命令是无效的。

SORT_NONE会忽略命令行中的段排序选项来关闭段排序。

当同时使用命令行段排序选项和链接器脚本段排序命令时,链接器脚本中的段排序命令始终优先于命令行选项。如果链接脚本中的段排序命令嵌套,命令行选项将被忽略。

如果你弄不清楚输入段的排序,可以使用-M链接器选项产生一个map文件,它将精确地显示出输入段是如何显示到输出段。

这个示例展示了如何使用通配符模式来分区文件。这个链接器脚本指示链接器将所有‘ .text ’段放置在‘ .text ’,所有‘ .bss ’段放置在‘ .bss ’。链接器将从所有以大写字母开头的文件中放置‘ .data ’段到‘ .DATA ’;对于其他文件,链接器将放置‘ .data ’段到‘ .data ’。

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

6.4.3 通用符号的输入段

通用符号(common symbols)需要一个特定的标记法,因为在很多目标文件格式中通用符号没有一个独有的输入段。链接器在处理通用符号时好像它们都在一个名为COMMON的输入段中。

大多数情况下,输入文件中的common符号会在输出文件中的“.bss”段中。例如:

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

有些对象文件格式有多种common符号类型。比如MIPS ELF对象文件格式区分标准common符号和小的common符号。这种情况下,链接器为其他类型的common符号使用特别的段名。对于MIPS ELF,标准common符号使用“COMMON”而小的common符号使用“.scommon”。这就允许你将不同类型的common符号放入内存中的不同位置。

在老的链接脚本中有时会遇到“[COMMON]”,这已经是过时的记法,它等价于“*(COMMON)”。

6.4.4 输入段和垃圾回收

当使用链接时的垃圾回收(“–gc-sections”)时,通常去标记不应该去掉的段。可以用KEEP()和输入段的通配符来完成这一点,例如

 KEEP(*(.init)) 或者 KEEP(SORT_BY_NAME(*)(.ctors))

6.4.5 输入段示例

下面的示例是一个完整的链接脚本。它告知链接器从文件all.o中读取所有的段,并将它们放置在以“0x10000”为起始地址的输出段“outputa”的开头处。在“outputa”输出段中,文件“foo.o”中的所有“.input1”段都紧跟其后。文件“foo.o”的所有“input2”段都放进输出段“outputb”中,后面紧跟着文件“foo1.o”的“.input1”段。最后,任何文件的所有剩余的“.input1”段和“.input2”段都放置在输出段“outputc”当中。

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

6.5 输出段数据

可以在一个输出段命令中使用BYTE、SHORT、LONG、QUAD或SQUAD关键字在输出段中包含确切字节数的数据。每个关键字后面跟着包含在圆括号里面的表达式数值。表达式数值存在位置计数器的当前值处。

BYTE、SHORT、LONG、QUAD命令分别存储1、2、4和8个字节。存储这些字节后,位置计数器增加存储字节数的数目。

例如,这将在一个字节后放着符号“ addr ”的四个字节值:

BYTE(1)
LONG(addr)

当使用64位的主机或目标机时,QUAD和SQUAD是一样的,它们都存储一个8字节的值。当主机和目标机都是32位的,表达式按照32位计算。在这种情况下QUAD存储的32位数值扩展到64位,SQUAD存储的32位有符号数值扩展到64位。

如果输出文件的目标文件格式有明确的大小端,这个值就按照该字节顺序存储。当输出文件的目标文件格式没有指定的大小端,这个值就按照第一个输入对象文件的字节顺序存储。

注意,这些命令只有在段描述内才能正常工作。
如下写法会产生错误:

SECTIONS {.text : { *(.text) }LONG(1).data : { *(.data) }
} 

而这是可以工作的:

SECTIONS {.text : { *(.text) ; LONG(1) }.data : { *(.data) }
} 

可以使用FILL命令来设置当前段的填充样式,后面跟着放在括号里面的表达式(这个表达式就类似于FILL(0x90909090)中的0x90909090)。该段中任何其他未指定内容的内存区域(比如说按照字对齐要求留下的缺口)都填满表达式的值。一个 FILL 语句覆盖该段定义中该语句出现点之后的内存位置;通过包含多个 FILL 语句,您可以在输出段的不同部分使用不同的填充模式。

用“0x90”来填充内存中位指定的区域:

FILL(0x90909090)

FILL命令和“=fillexp”输出段属性很类似,但是它是影响段中跟在FILL命令之后的部分,而不是整个段。如果二者都使用了,FILL命令优先级更高。

6.6 输出段关键字

下面介绍一些在输出段命令中出现的关键字。

1)CREATE_OBJECT_SYMBOLS
该命令告诉链接器为每个输入文件创建一个符号。每个符号的名字就是输入文件的名字。每个符号的段将是CREATE_OBJECT_SYMBOLS命令出现的输出段。

该命令在a.out对象文件格式中较常见,但在其他对象文件格式上不常用。

2)CONSTRUCTORS
当使用a.out对象文件格式进行链接时,链接器会使用一个不常用的设置construct来支持C++的全局构造函数和析构函数。当链接的对象文件格式不支持arbitrary段,比如ECOFF和XCOFF,链接器会根据名称自动识别C++的全局构造函数和析构函数。对于这些对象文件格式,CONSTRUCTORS命令会告知链接器将构造函数信息放在输出段CONSTRUCTORS命令出现的地方;其他格式则忽略CONSTRUCTORS命令。

6.7 输出段丢弃

链接器不会创建没有内容的输出段。当引用在任何输入文件中可能有的输入段时是很方便的。比如:

.foo : { *(.foo) }

如果在至少一个输入文件中有一个“.foo”段,并且输入段不是全空的条件下,将会在输出文件中创建一个“.foo”段。其余指明在输出段分配空间的链接脚本也会创建输出段。

链接器在丢弃的输出段中会忽略地址对齐,除非当链接器在输出段中定义符号。在这种情况下链接器会遵循地址对齐。

特殊的输出段名称“/DISCARD/”可以用于丢弃输入段。分配到名为“/DISCARD/”输出段的任何输入段都不会包括在输出文件中。

6.8 输出段属性

输出段的完全描述如下:

section [address] [(type)] :[AT(lma)][ALIGN(section_align) | ALIGN_WITH_INPUT][SUBALIGN(subsection_align)][constraint]{output-section-commandoutput-section-command...} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

6.8.1.输出段类型 type

每个输出段都有一个类型,类型是在圆括号中的关键字。定义了下面的类型:

  1. NOLOAD
    该段标记是不可加载的,所以当程序运行时它不会加载到内存中。

  2. READONLY
    该段应标记为只读。

  3. DSECT
    COPY
    INFO
    OVERLAY

    这些类型支持向后兼容,但是很少用。它们有相同的效果:段不可标记为可分配的,即当程序运行时不为该段分配内存。

链接器通常按照映射到输出段的输入段来设置输出段的属性。你可以使用段类型来覆盖它。比如在下面的示例脚本中,“ROM”段放在内存地址0处,并且在程序运行时无需加载。

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

6.8.2 输出段LMA

每个段都有一个虚拟地址(VMA)和加载地址(LMA),加载地址由AT或AT>关键字指定,指定加载地址是可选的。

AT关键字的参数是一个表达式,它会指定段的准确加载地址。AT>关键字的参数是内存区域的名称。段的加载地址为区域的后面一个空闲地址,并满足段的对齐要求。

如果一个可分配的段没有使用AT和AT>,链接器将按照下面的顺序来决定加载地址:
1)如果段有一个指定的VMA地址,那么把这个地址也当做LMA地址。
2)如果段是不可分配的,那么它的LMA设为VMA的值。
3)如果一个存储区域和当前段是相容的,并且该区域包含至少一个段,那么按照VMA和LMA之间的不同值与区域中最后一个段VMA和LMA之间的不同值相等的准则来设定LMA。
4)如果没有申请内存区域,那么在上一步中用包含整个地址空间的一个默认区域。
5)如果没有找到合适的区域,或者没有前面的段,那么LMA设为VMA的值。

设计输出段LMA属性是为了简单地创建一个ROM镜像。比如,下面的链接脚本创建三个输出段:一个名为“.text”,从0x1000开始;一个名为“.mdata”,它加载到“.text”段后面,其VMA是0x2000;一个名为“.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镜像中拷贝已经初始化的数据到运行地址。应注意该段代码如何使用链接脚本中定义的符号。

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;

6.8.3 强制输出对齐

可以使用ALIGN强制输出段的对齐。

6.8.4 强制输入对齐

使用SUBALIGN可以强制输入段和输出段对齐。

6.8.5 输出段约束

可以使用ONLY_IF_RO关键字设定输出段在它所有的输入段都是只读的条件下才能创建。

可以使用ONLY_IF_RW关键字设定输出段在它所有的输入段都是可读写的条件下才能创建。

6.8.6 输出段区域

可以使用“>region”在先前定义的内存区域分配一个段。

下面是一个简单的例子:

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

6.8.7 输出段的填充

可以使用“=fillexp”设置整个段的填充样式,fillexp是一个表达式。输出段中任何没有指定的存储区域都填充为该值,比如因为输入段对齐要求导致的缝隙。如果填充表达式是一个简单的十六进制数,比如以“0x”开头而且不以“k”或“M”结尾的十六进制数字字符串,那么一个任意长的十六进制数字串将用于填充样式,前导零也是样式的一部分(这里的前导零指的是类似于0x0000ffff中的前面四个零)。对于其他情况,包括额外的圆括号或一元的加号,填充样式是表达式值的4个最低有效字节。在所有情况下,数字都是大端的。

在输出段命令中使用FILL命令也可以改变填充值。下面是个简单的例子:

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

七、内存命令

链接器的默认配置是所有可用的内存都是可分配的。使用MEMORY命令来修改这个配置。

MEMORY命令描述在目标中内存块的位置和空间大小。可以使用MEMORY命令来描述哪个内存区域可以被链接器使用,哪个内存区域不可以被链接器使用。还可以为特殊的内存区域分配段。链接器按照内存区域来设定段地址,而且当区域快饱和时会发出通知。链接器不会为了将段放入合适的区域而打乱其位置。

一个链接脚本最多包含一个MEMORY命令。然而,可以定义足够多的内存块。语法格式是:

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

name是用在链接脚本中引用区域的名称,在链接脚本外区域名称是没有意义的。区域名称存储在一个独立的命名空间,不会与符号名、文件名或者段名冲突。MEMORY命令的每个内存区域要有一个确切的名称,然而可以使用REGION_ALIAS命令为存在的内存区域添加别名。

attr字符串是一个可选的属性列表,用于指定是否为一个在链接脚本中没有明确映射的输入段使用一个特殊的内存区域。在SECTIONS小节中描述过,如果没有为输入段指定一个输出段,那么链接器会按照输入段的名称创建同名的输出段。如果定义了区域属性,链接器会使用它们。

attr字符串一定只能由下面的字符组成:
R 只读段
W 可读写段
X 可执行段
A 可分配段
I 已初始化段
L 和I相同
! 反转跟在后面的所有属性

origin是内存区域起始地址的数值表达式。该表达式必须求值为常量,并且不能包含任何符号。关键字 ORIGIN 可以缩写为 org 或 o (但例如不能缩写为 ORG )。

len 是一个表示内存区域大小(以字节为单位)的表达式。与 origin 表达式类似,该表达式必须仅包含数字,并且必须计算为常量。关键字 LENGTH 可以缩写为 len 或 l 。

在以下示例中,我们指定有两个内存区域可供分配:一个从‘ 0 ’开始,大小为 256 千字节,另一个从‘ 0x40000000 ’开始,大小为 4 兆字节。链接器会将所有未明确映射到内存区域的、且为只读或可执行的部分放置到‘ rom ’内存区域中。链接器会将其他未明确映射到内存区域的段放置到‘ ram ’内存区域中。

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

可以通过 ORIGIN(memory) 和 LENGTH(memory) 函数在表达式中访问内存的 origin 和 length:

  _fstack = ORIGIN(ram) + LENGTH(ram) - 4;
http://www.dtcms.com/a/601444.html

相关文章:

  • 素材网站整站下载WordPress做图床
  • 企业网站相关案例网站建设域名怎么用
  • 太原专业做网站wordpress主体开发
  • 零基础新手小白快速了解掌握服务集群与自动化运维(十八)Ansible自动化模块--安装与入门
  • 【C++11】Lambda表达式+新的类功能
  • C语言编译工具 | 探讨常用C语言编译工具的选择与使用
  • SCT2A26——5.5V-100V Vin,4A峰值电流限制,高效率非同步降压DCDC转换器,兼容替代LM5012
  • 手机网站搜索框代码网上做网站怎么防止被骗
  • 滑动窗口(同向双指针)
  • C语言嵌入式编程实战指南(四):进阶技术和未来展望
  • Mac上的C语言编译软件推荐与使用指南 | 如何选择适合你需求的C语言编译器
  • 做建站较好的网站wordpress edit.php
  • 【大语言模型】-- Function Calling函数调用
  • STM32项目分享:花房环境监测系统
  • 第1章 认识Qt
  • JDK 25 重大兼容性 Bug
  • MyBatis多表联查返回List仅一条数据?主键冲突BUG排查与解决
  • c 做网站方便吗手机企业wap网站
  • el-table有固定列时样式bug
  • Vue项目中 安装及使用Sass(scss)
  • 珠海本地网站设计公司什么网站可以发布信息
  • UEFI+GPT平台一键安装Windows方法
  • GPT‑5 全面解析与开发者接入指南
  • 站优云seo优化页面模板这样选
  • dism++功能实操备份与还原
  • 动态型网站建设哪里便宜app开发需要用到哪些工具
  • 网站建设的什么是网站建设的第一阶段佛山市房产信息网
  • React 18
  • CVPR 2025|电子科大提出渐进聚焦Transformer:显著降低超分辨率计算开销
  • CTFHub Web进阶-Linux:动态装载