GCC之(7)Linker链接脚本
Author: Once Day Date: 2024年10月25日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
本文档翻译自GNU LD链接脚本官方手册
参考文章:
- GNU LD Linker Scripts
- 链接脚本(Linker Scripts)语法和规则解析
- 链接脚本的作用及格式
文章目录
- GCC之(7)Linker链接脚本
- 1. 概述
- 2. 基本概念
- 3. 脚本命令
- 3.1 设置进入点
- 3.2 处理文件的命令
- 3.3 处理目标文件格式的命令
- 3.4 为内存区域分配别名
- 3.5 其他链接描述文件命令
- 4. 为符号分配值
- 4.1 简单赋值
- 4.2 隐藏符号(HIDDEN)
- 4.3 定义符号(PROVIDE)
- 4.4 源代码参考
- 5. SECTIONS 命令
- 5.1 输出部分描述
- 5.2 输出部分名称
- 5.3 输出段地址
- 5.4 基础输入部分描述
- 5.5 输入部分通配符模式
- 5.6 常用符号的输入部分
- 5.7 输入部分和垃圾收集
- 5.8 输入部分示例
- 5.9 输出部分数据
- 5.10 输出部分关键字
- 5.11 输出部分丢弃
- 5.12 输出部分类型
- 5.13 输出部分 LMA
- 5.14 强制对齐
- 5.15 输出部分约束
- 5.16 输出部分区域
- 5.17 输出部分 Phdr
- 5.18 输出部分填充
- 5.19 叠加描述
- 6. MEMORY命令
- 7. PHDRS命令
- 8. 符号版本命令
- 9. 链接器脚本中的表达式
- 9.1 常量
- 9.2 符号常量
- 9.3 符号名称
- 9.4 孤儿部分
- 9.5 位置计数器
- 9.6 运算符
- 9.7 表达式求值
- 9.8 表达式的Section
- 10. 内置函数
1. 概述
每个链接都由链接器脚本控制。此脚本以链接器命令语言编写。
链接器脚本的主要目的是描述输入文件中的部分应如何映射到输出文件中,并控制输出文件的内存布局。大多数链接器脚本仅此而已。但是,必要时,链接器脚本还可以使用下面描述的命令指示链接器执行许多其他操作。
链接器始终使用链接器脚本。如果自己不提供链接器脚本,链接器将使用编译到链接器可执行文件中的默认脚本。可以使用ld --verbose
命令行选项显示默认链接器脚本。某些命令行选项(例如-r
或-N
)会影响默认链接器脚本。
可以使用-T
命令行选项提供自己的链接器脚本。当执行此操作时,链接器脚本将替换默认链接器脚本。
还可以通过将链接器脚本命名为链接器的输入文件来隐式使用链接器脚本,就好像它们是要链接的文件一样。
2. 基本概念
链接器将输入文件组合成单个输出文件。输出文件和每个输入文件都采用一种称为目标文件格式(object file format
)的特殊数据格式。每个文件都称为目标文件(object file
)。输出文件通常称为可执行文件,但为了我们的目的,我们也将其称为目标文件。每个目标文件都包含一系列部分。我们有时将输入文件中的部分称为输入部分(input section
);同样,输出文件中的部分称为输出部分(output section
)。
目标文件中的每个section都有名称和大小。大多数section还具有关联的数据块,称为section contents。section可能被标记为可加载,这意味着在运行输出文件时应将内容加载到内存中。没有内容的section可能是可分配的,这意味着应该留出内存中的某个区域,但不应在那里加载任何特定内容(在某些情况下,必须将该内存清零)。既不可加载也不可分配的section通常包含某种调试信息。
每个可加载或可分配的输出节都有两个地址。第一个是 VMA 或虚拟内存地址。这是运行输出文件时section将具有的地址。第二个是 LMA 或加载内存地址。这是将加载节的地址。在大多数情况下,这两个地址是相同的。它们可能不同的一个例子是当数据节被加载到 ROM 中,然后在程序启动时复制到 RAM 中时(这种技术通常用于初始化基于 ROM 的系统中全局变量)。在这种情况下,ROM 地址将是 LMA,而 RAM 地址将是 VMA。
可以使用带有-h
选项的 objdump 程序查看目标文件中的节。
每个目标文件还有一个符号列表,称为符号表。符号可能是已定义的或未定义的。每个符号都有一个名称,每个已定义的符号都有一个地址,以及其他信息。如果将 C 或 C++ 程序编译为目标文件,则每个已定义函数和全局或静态变量都会获得一个已定义符号。输入文件中引用的每个未定义函数或全局变量都将成为未定义符号。
可以使用 nm 程序或使用带有-t
选项的 objdump 程序查看目标文件中的符号。
3. 脚本命令
链接器脚本是文本文件。链接器脚本是一系列命令。每个命令要么是关键字,后面可能跟有参数,要么是符号赋值。可以使用分号分隔命令。空格通常会被忽略。
字符串(例如文件或格式名称)通常可以直接输入。如果文件名包含逗号等字符(否则将用于分隔文件名),则可以将文件名放在双引号中。文件名中不能使用双引号字符。
链接器脚本中可以包含注释,就像在 C 中一样,注释由/*
和*/
分隔。与 C 一样,注释在语法上等同于空格。
许多链接器脚本相当简单。最简单的链接器脚本只有一个命令:“SECTIONS”。
可以使用SECTIONS
命令来描述输出文件的内存布局。
SECTIONS
命令是一个功能强大的命令。这里我们将描述它的简单用法。假设程序仅包含代码、初始化数据和未初始化数据。这些将分别位于.text
、.data
和.bss
部分中。让我们进一步假设这些是输入文件中出现的唯一部分。
对于此示例,假设代码应加载到地址 0x10000
,数据应从地址 0x8000000
开始。以下是将执行此操作的链接器脚本:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
可以将SECTIONS
命令写为关键字SECTIONS
,后跟一系列用花括号括起来的符号分配和输出section描述。
上述示例的SECTIONS
命令中的第一行设置特殊符号.
的值,即位置计数器。如果未以其他方式指定输出section的地址(其他方式稍后介绍),则地址将从位置计数器的当前值设置。然后,位置计数器将增加输出section的大小。在SECTIONS
命令的开头,位置计数器的值为0
。
第二行定义输出部分“.text”。冒号是必需的语法,目前可以忽略。在输出部分名称后的花括号内,可以列出应放入此输出部分的输入部分的名称。*
是与任何文件名匹配的通配符。表达式*(.text)
表示所有输入文件中的所有.text
输入部分。
由于定义输出部分.text
时位置计数器为0x10000
,因此链接器将输出文件中.text
部分的地址设置为0x10000
。
其余行定义输出文件中的.data
和.bss
部分。链接器将.data
输出部分放置在地址0x8000000
处。链接器放置.data
输出部分后,位置计数器的值将为0x8000000
加上.data
输出部分的大小。结果是链接器将.bss
输出部分放置在内存中.data
输出部分之后。
链接器将确保每个输出部分都具有所需的对齐,如有必要,将通过增加位置计数器来实现。在此示例中,.text
和 .data
部分的指定地址可能满足任何对齐约束,但链接器可能必须在 .data
和 .bss
部分之间创建一个小间隙。
3.1 设置进入点
程序中第一条要执行的指令被称为入口点(entry point)。可以使用链接器脚本命令 ENTRY 来设置入口点。该命令的参数是一个符号名:
ENTRY(symbol)
有几种方法可以设置入口点。链接器将按照以下顺序尝试每种方法,直到其中一种成功为止:
- 命令行选项
-e
指定的入口点; - 链接器脚本中的
ENTRY(symbol)
命令; - 目标特定符号的值(如果已定义)。对于许多目标,这个符号是
start
,但对于基于PE
和BeOS
的系统,链接器会检查一个可能的入口符号列表,并匹配找到的第一个符号;(PE(Portable Executable)是 Windows 操作系统使用的可执行文件格式,而 BeOS 是一个已经停止开发的操作系统。) - 如果存在代码段且正在创建可执行文件,则使用代码段的第一个字节的地址。代码段通常是 ‘.text’,但也可以是其他名称;
- 如果没有通过其他方式指定入口点,链接器会默认使用地址 0 作为入口点。。
3.2 处理文件的命令
INCLUDE filename
此时包含链接器脚本文件名。将在当前目录和使用 -L
选项指定的任何目录中搜索该文件。可以将对 INCLUDE 的调用嵌套最多 10 层。可以将 INCLUDE 指令放在顶层、MEMORY 或 SECTIONS 命令中或输出部分描述中。
INPUT(file, file, …)
INPUT(file file …)
INPUT 命令指示链接器将命名的文件包含在链接中,就像它们在命令行中命名一样。
例如,如果总是希望在每次进行链接时都包含 subr.o
,但又懒得在每个链接命令行上都放上它,那么您可以在链接器脚本中放入INPUT (subr.o)
。
事实上,可以在链接器脚本中列出所有输入文件,然后仅使用-T
选项调用链接器。
如果配置了 sysroot
前缀,并且文件名以/
字符开头,并且正在处理的脚本位于 sysroot
前缀内,则将在 sysroot
前缀中查找文件名。 还可以通过将 =
指定为文件名路径中的第一个字符或在文件名路径前加上 $SYSROOT
来强制使用 sysroot
前缀。
如果未使用 sysroot
前缀,则链接器将尝试打开包含链接器脚本的目录中的文件。如果未找到,则链接器将搜索当前目录。如果仍未找到,则链接器将搜索存档库搜索路径。
如果使用INPUT (-lfile)
,ld
会将名称转换为 libfile.a
,就像使用命令行参数-l
一样。
当在隐式链接器脚本中使用 INPUT
命令时,在链接器脚本文件的位置文件就会包含进来。这可能会影响存档搜索。
GROUP(文件,文件,…)
GROUP(文件文件…)
GROUP 命令类似于 INPUT,不同之处在于命名的文件应全部为档案,并且会重复搜索它们,直到没有创建新的未定义引用。
AS_NEEDED(file, file, …)
AS_NEEDED(file file …)
此构造只能出现在 INPUT 或 GROUP 命令中,以及其他文件名中。列出的文件将被视为直接出现在 INPUT 或 GROUP 命令中,但 ELF 共享库除外,这些共享库仅在实际需要时才会添加。此构造实质上为其中列出的所有文件启用 --as-needed
选项,然后恢复之前的 --as-needed
和 --no-as-needed
设置。
OUTPUT(filename)
OUTPUT 命令命名输出文件。在链接器脚本中使用 OUTPUT(filename)
与在命令行上使用-o filename
完全相同。如果同时使用两者,则命令行选项优先。
可以使用 OUTPUT 命令为输出文件定义一个默认名称,而不是通常的默认名称a.out。
SEARCH_DIR(path)
SEARCH_DIR 命令将 path 添加到 ld 查找存档库的路径列表中。使用 SEARCH_DIR(path) 与在命令行上使用-L path
完全相同。如果同时使用两者,则链接器将搜索这两个路径。首先搜索使用命令行选项指定的路径。
STARTUP(filename)
STARTUP 命令与 INPUT 命令一样,不同之处在于 filename 将成为要链接的第一个输入文件,就像在命令行上首先指定它一样。当使用入口点始终是第一个文件的开头的系统时,这可能很有用。
3.3 处理目标文件格式的命令
链接器脚本中有几个命令用于处理目标文件格式。
OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)
OUTPUT_FORMAT 命令指定输出文件的 BFD 格式。使用 OUTPUT_FORMAT(bfdname) 与在命令行中使用 --oformat bfdname
选项完全相同。如果两者都使用,则命令行选项优先。
可以使用带有三个参数的 OUTPUT_FORMAT 命令,根据 -EB
和 -EL
命令行选项使用不同的格式。这允许链接器脚本根据所需的字节序设置输出格式。
如果既没有使用 -EB
也没有使用 -EL
,则输出格式将是第一个参数 default。如果使用了 -EB
,输出格式将是第二个参数 big
。如果使用了 -EL
,输出格式将是第三个参数 little
。
例如,MIPS ELF 目标的默认链接器脚本使用以下命令:
OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)
这表示输出文件的默认格式为 elf32-bigmips
,但如果用户使用 -EL
命令行选项,则输出文件将以 elf32-littlemips
格式创建。
TARGET(bfdname)
TARGET 命令指定读取输入文件时使用的 BFD 格式。它会影响后续的 INPUT 和 GROUP 命令。此命令类似于在命令行上使用 -b bfdname
。如果使用了 TARGET 命令但没有使用 OUTPUT_FORMAT,则最后一个 TARGET 命令也用于设置输出文件的格式。参见 BFD。
3.4 为内存区域分配别名
可以为使用 MEMORY Command 命令创建的现有内存区域添加别名。每个名称最多对应一个内存区域。
REGION_ALIAS(alias, region)
REGION_ALIAS 函数为内存区域区域创建别名 alias。这允许灵活地将输出部分映射到内存区域。
假设我们有一个用于嵌入式系统的应用程序,它带有各种内存存储设备。所有设备都具有通用的易失性内存 RAM,允许执行代码或存储数据。有些可能具有只读的非易失性内存 ROM,允许执行代码和只读数据访问。最后一种变体是只读的非易失性内存 ROM2,具有只读数据访问功能,没有代码执行功能。我们有四个输出部分:
-
.text
program code; -
.rodata
read-only data; -
.data
read-write initialized data; -
.bss
read-write zero initialized data.
目标是提供一个链接器命令文件,其中包含一个定义输出部分的系统独立部分和一个将输出部分映射到系统上可用的内存区域的系统相关部分。我们的嵌入式系统有三种不同的内存设置 A、B 和 C:
Section Variant A Variant B Variant C
.text RAM ROM ROM
.rodata RAM ROM ROM2
.data RAM RAM/ROM RAM/ROM2
.bss RAM RAM RAM
RAM/ROM
或 RAM/ROM2
符号表示此部分分别加载到区域 ROM
或 ROM2
中。请注意,.data
部分的加载地址在所有三个变体中都从 .rodata
部分末尾开始。
处理输出部分的基本链接器脚本如下。它包括描述内存布局的系统相关 linkcmds.memory
文件:
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
}
现在我们需要三个不同的 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 = 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);
// C 程序代码进入ROM, 只读数据进入ROM2, 读写数据进入RAM, 初始化数据的映像加载到ROM2中, 并在系统启动时复制到RAM 中.
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);
可以编写一个通用的系统初始化例程,以便在必要时将 .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);
}
}
3.5 其他链接描述文件命令
(1) ASSERT(exp, message)
确保 exp 非零。如果为零,则使用错误代码退出链接器并打印消息。
请注意,在链接的最后阶段之前会检查断言。这意味着,如果用户未设置这些符号的值,则涉及节定义内提供的符号的表达式将失败。此规则的唯一例外是仅引用点的提供符号。因此,断言如下:
.stack :
{
PROVIDE (__stack = .);
PROVIDE (__stack_size = 0x100);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
如果 __stack_size
未在其他地方定义,则将失败。在节定义之外提供的符号会更早被评估,因此它们可以在 ASSERTions
内部使用。如下所示:
PROVIDE (__stack_size = 0x100);
.stack :
{
PROVIDE (__stack = .);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
will work.
(2) EXTERN(symbol symbol …)
强制将符号作为未定义符号输入到输出文件中。例如,这样做可能会触发从标准库链接其他模块。可以为每个 EXTERN 列出多个符号,也可以多次使用 EXTERN。此命令与-u
命令行选项具有相同的效果。
(3) FORCE_COMMON_ALLOCATION
此命令与-d
命令行选项具有相同的效果:即使指定了可重定位输出文件(-r
),也让 ld
为公共符号分配空间。
(4) INHIBIT_COMMON_ALLOCATION
此命令与--no-define-common
命令行选项具有相同的效果:使 ld
即使对于不可重定位的输出文件也省略对公共符号的地址分配。
(5) FORCE_GROUP_ALLOCATION
此命令与--force-group-allocation
命令行选项具有相同的效果:使 ld
将节组成员像普通输入节一样放置,并且即使指定了可重定位输出文件(-r
)也删除节组。
(6) INSERT [ AFTER | BEFORE ] output_section
此命令通常用于由-T
指定的脚本中,以使用覆盖等方式扩充默认 SECTIONS
。它会在 output_section
之后(或之前)插入所有先前的链接器脚本语句,并且还会导致-T
不覆盖默认链接器脚本。确切的插入点与孤立节相同。请参阅位置计数器。插入发生在链接器将输入节映射到输出节之后。在插入之前,由于-T
脚本在默认链接器脚本之前解析,因此-T
脚本中的语句出现在脚本的内部链接器表示中的默认链接器脚本语句之前。特别是,输入节分配将在默认脚本中的输入节分配之前分配给-T
输出节。以下是使用 INSERT 的-T
脚本的示例:
SECTIONS
{
OVERLAY :
{
.ov1 { ov1*(.text) }
.ov2 { ov2*(.text) }
}
}
INSERT AFTER .text;
请注意,当使用两次-T
时,一次用于覆盖默认脚本,一次用于使用 INSERT
扩充该脚本,解析和节分配的顺序与默认脚本相同。应首先在命令行上指定带有 INSERT
的脚本。
(7) NOCROSSREFS(section section …)
此命令可用于告诉 ld
发出有关某些输出部分之间任何引用的错误。在某些类型的程序中,特别是在使用覆盖的嵌入式系统中,当一个部分加载到内存中时,另一个部分不会加载。两个部分之间的任何直接引用都将是错误。例如,如果一个部分中的代码调用另一个部分中定义的函数,则会出现错误。
NOCROSSREFS 命令采用输出部分名称列表。如果 ld 检测到部分之间的任何交叉引用,它会报告错误并返回非零退出状态。请注意,NOCROSSREFS 命令使用输出部分名称,而不是输入部分名称。
(8) NOCROSSREFS_TO(tosection fromsection …)
此命令可用于告诉 ld 发出有关对来自其他部分列表的一个部分的任何引用的错误。
NOCROSSREFS 命令在确保两个或多个输出部分完全独立但在某些情况下需要单向依赖时很有用。例如,在多核应用程序中,可能存在可以从每个核心调用的共享代码,但为了安全起见,绝不能回调。
NOCROSSREFS_TO 命令采用输出部分名称列表。第一部分不能从任何其他部分引用。如果 ld 检测到任何其他部分对第一部分的任何引用,它会报告错误并返回非零退出状态。请注意,NOCROSSREFS_TO 命令使用输出部分名称,而不是输入部分名称。
(9) OUTPUT_ARCH(bfdarch)
指定特定的输出机器架构。该参数是 BFD 库使用的名称之一。您可以使用带有-f
选项的 objdump
程序查看对象文件的架构。
(10) LD_FEATURE(string)
此命令可用于修改 ld
行为。如果字符串为SANE_EXPR
,则脚本中的绝对符号和数字在任何地方都被视为数字。
4. 为符号分配值
4.1 简单赋值
可以使用任何 C 赋值运算符来赋值给符号:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
-
第一种情况将符号定义为表达式的值。在其他情况下,符号必须已经定义,并且值将相应调整。
-
特殊符号名称
.
表示位置计数器。只能在 SECTIONS 命令中使用它。 -
表达式后面的分号是必需的。
-
可以将符号赋值写为命令本身,也可以写为 SECTIONS 命令中的语句,也可以写为 SECTIONS 命令中输出部分描述的一部分。
-
符号的部分将从表达式的部分设置;
以下是一个示例,显示了可以使用符号赋值的三个不同位置:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
在此示例中,符号floating_point
将被定义为零。符号_etext
将被定义为最后一个.text
输入部分后面的地址。符号_bdata
将被定义为向上对齐到 4 字节边界的.text
输出部分后面的地址。
4.2 隐藏符号(HIDDEN)
对于 ELF 目标端口,定义一个将被隐藏且不会被导出的符号。语法为 HIDDEN(symbol = expression)
。
以下是来自简单分配的示例,重写为使用 :
HIDDEN(floating_point = 0);
SECTIONS
{
.text :
{
*(.text)
HIDDEN(_etext = .);
}
HIDDEN(_bdata = (. + 3) & ~ 3);
.data : { *(.data) }
}
在这种情况下,这三个符号都不会在该模块之外可见。
4.3 定义符号(PROVIDE)
在某些情况下,链接器脚本最好仅在引用符号且未由链接中包含的任何对象定义符号时才定义符号。
例如,传统链接器定义了符号etext
。但是,ANSI C 要求用户能够使用etext
作为函数名称而不会遇到错误。仅当引用但未定义符号时,才可以使用 PROVIDE 关键字来定义符号(例如etext
)。语法为 PROVIDE(symbol = expression)
。
以下是使用 PROVIDE 定义etext
的示例:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在此示例中,如果程序定义了_etext
(带有前导下划线),则链接器将给出多重定义诊断。另一方面,如果程序定义了etext
(没有前导下划线),则链接器将默默地使用程序中的定义。如果程序引用etext
但未定义它,则链接器将使用链接器脚本中的定义。
注意,PROVIDE
指令认为已定义通用符号,即使此类符号可以与PROVIDE
将创建的符号组合。在考虑构造函数和析构函数列表符号(例如__CTOR_LIST__
)时,这一点尤为重要,因为这些符号通常被定义为通用符号。
PROVIDE_HIDDEN
与 PROVIDE
类似。对于 ELF 目标端口,该符号将被隐藏,并且不会被导出。
4.4 源代码参考
从源代码访问链接器脚本定义的变量并不直观。特别是,链接器脚本符号不等同于高级语言中的变量声明,而是一个没有值的符号。
在进一步讨论之前,重要的是要注意,编译器经常将源代码中的名称转换为不同的名称,当它们存储在符号表中时。例如,Fortran 编译器通常在前面或后面添加下划线,而 C++ 执行广泛的“名称修改”。因此,源代码中使用的变量名称与链接器脚本中定义的相同变量的名称之间可能存在差异。例如,在 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);
请注意,使用此方法不需要使用&
运算符。
5. SECTIONS 命令
SECTIONS 命令告诉链接器如何将输入节映射到输出节,以及如何将输出节放置在内存中。
SECTIONS 命令的格式为:
SECTIONS
{
sections-command
sections-command
…
}
每个节命令可能是以下之一:
- ENTRY 命令
- 符号赋值
- 输出节描述
- 覆盖描述
允许在 SECTIONS
命令内使用 ENTRY
命令和符号分配,以方便使用这些命令中的位置计数器。这还可以使链接器脚本更容易理解,因为可以在输出文件布局中有意义的位置使用这些命令。
如果在链接器脚本中不使用 SECTIONS
命令,则链接器将按照节在输入文件中首次遇到的顺序将每个输入节放入同名的输出节中。例如,如果所有输入节都存在于第一个文件中,则输出文件中节的顺序将与第一个输入文件中的顺序匹配。第一个节将位于地址零。
5.1 输出部分描述
输出部分的完整描述如下:
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
…
} [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp] [,]
大多数输出部分不使用大多数可选部分属性。
部分周围的空格是必需的,以便部分名称明确。冒号和花括号也是必需的。如果使用 fillexp
并且下一个部分命令看起来像表达式的延续,则末尾的逗号可能是必需的。换行符和其他空格是可选的。
每个输出节命令可能是以下之一:
- 符号赋值
- 输入节描述
- 要直接包含的数据值
- 特殊输出节关键字
5.2 输出部分名称
输出节的名称是 section
。section
必须满足输出格式的限制。在仅支持有限数量节的格式(如 a.out)中,名称必须是该格式支持的名称之一(例如,a.out 仅允许.text
、.data
或.bss
)。如果输出格式支持任意数量的节,但包含数字而不是名称(如 Oasys 的情况),则名称应以带引号的数字字符串形式提供。节名称可以由任何字符序列组成,但包含任何不常见字符(如逗号)的名称必须用引号引起来。
输出节名称/DISCARD/
很特殊;输出节丢弃。
5.3 输出段地址
地址是输出节的 VMA(虚拟内存地址)的表达式。此地址是可选的,但如果提供了,则输出地址将完全按照指定的方式设置。
如果未指定输出地址,则将根据以下启发式方法为该节选择一个地址。将调整此地址以符合输出节的对齐要求。对齐要求是输出节中包含的任何输入节的最严格对齐。
输出节地址启发式方法如下:
-
如果为该节设置了输出内存区域,则将其添加到此区域,其地址将是该区域中的下一个空闲地址。
-
如果已使用 MEMORY 命令创建内存区域列表,则选择具有与该节兼容的属性的第一个区域来包含它。该节的输出地址将是该区域中的下一个空闲地址;MEMORY 命令。
-
如果没有指定内存区域,或者没有与该节匹配的内存区域,则输出地址将基于位置计数器的当前值。
例如:
.text . : { *(.text) }
.text : { *(.text) }
略有不同。第一个将把.text
输出节的地址设置为位置计数器的当前值。第二个将把它设置为与任何.text
输入节的最严格对齐方式对齐的位置计数器的当前值。
地址可以是任意表达式;链接器脚本中的表达式。例如,如果要将节对齐到 0x10 字节边界,以便节地址的最低四位为零,则可以执行以下操作:
.text ALIGN(0x10) : { *(.text) }
这是可行的,因为 ALIGN 返回向上对齐到指定值的当前位置计数器。
为某个部分指定地址将更改位置计数器的值,前提是该部分非空,(空部分将被忽略)。
5.4 基础输入部分描述
输入节描述由文件名组成,后面可选地跟着括号中的节名列表。文件名和节名可以是通配符模式。
最常见的输入节描述是将所有具有特定名称的输入节包含在输出节中。例如,要包含所有输入.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)
此结果与上一个示例相同。如果节列表包含多个节,则支持 EXCLUDE_FILE 的两种语法很有用,如下所述。
有两种方法可以包含多个节:
*(.text .rdata)
*(.text) *(.rdata)
两者之间的区别在于.text
和.rdata
输入节在输出节中出现的顺序。在第一个示例中,它们将混合在一起,以与在链接器输入中找到的顺序相同的顺序出现。在第二个示例中,所有.text
输入节将首先出现,然后是所有.rdata
输入节。
当使用 EXCLUDE_FILE 处理多个部分时,如果排除在部分列表内,则排除仅适用于紧随其后的部分,例如:
*(EXCLUDE_FILE (*somefile.o) .text .rdata)
将导致除 somefile.o
之外的所有文件的所有.text
部分被包含,而包括 somefile.o
在内的所有文件的所有.rdata
部分将被包含。要从 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
将由与名称 *(.text)
匹配且节头标志 SHF_MERGE 和 SHF_STRINGS 已设置的任何输入节组成。输出节.text2
将由与名称 *(.text)
匹配且节头标志 SHF_WRITE 已清除的任何输入节组成。
还可以通过编写与档案匹配的模式、冒号,然后编写与文件匹配的模式来指定档案中的文件,冒号周围没有空格。
-
archive:file
,匹配档案中的文件 -
archive
,匹配整个档案 -
:file
,匹配文件但不匹配档案中的文件
archive
和 file
中的一个或两个都可以包含 shell 通配符。在基于 DOS 的文件系统上,链接器将假定单个字母后跟冒号是驱动器说明符,因此 c:myfile.o
是一个简单的文件规范,而不是名为 c
的档案中的 myfile.o
。archive:file
文件规范也可以在 EXCLUDE_FILE 列表中使用,但不得出现在其他链接器脚本上下文中。例如,不能在 INPUT 命令中使用 archive:file
从档案中提取文件。
如果使用不带节列表的文件名,则输入文件中的所有节都将包含在输出节中。这种情况并不常见,但有时可能有用。例如:
data.o
当使用不是archive:file
说明符且不包含任何通配符的文件名时,链接器将首先查看是否还在链接器命令行或 INPUT 命令中指定了文件名。如果没有,链接器将尝试将文件作为输入文件打开,就像它出现在命令行中一样。请注意,这与 INPUT 命令不同,因为链接器不会在存档搜索路径中搜索文件。
5.5 输入部分通配符模式
在输入节描述中,文件名或节名或两者均可为通配符模式。
在许多示例中看到的文件名*
是文件名的简单通配符模式。
通配符模式与 Unix shell 使用的通配符模式类似。
-
*
匹配任意数量的字符 -
?
匹配任何单个字符 -
[chars]
匹配任何字符的单个实例;-
字符可用于指定字符范围,如[a-z]
匹配任何小写字母 -
\
引用以下字符
文件名通配符模式仅匹配在命令行或 INPUT 命令中明确指定的文件。链接器不会搜索目录来扩展通配符。
如果文件名与多个通配符模式匹配,或者文件名明确出现并且也与通配符模式匹配,则链接器将使用链接器脚本中的第一个匹配项。例如,此输入部分描述序列可能有误,因为不会使用 data.o
规则:
.data : { *(.data) }
.data1 : { data.o(.data) }
通常,链接器会按照链接期间出现的顺序放置与通配符匹配的文件和部分。可以使用 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 属性的升序数字顺序排序,然后再将它们放入输出文件中。在 .init_array.NNNNN
和 .fini_array.NNNNN
中,NNNNN 是 init_priority。在 .ctors.NNNNN
和 .dtors.NNNNN
中,NNNNN 是 65535 减去 init_priority。
SORT 是 SORT_BY_NAME 的别名。
REVERSE 表示应反转排序。如果单独使用,则 REVERSE 暗示 SORT_BY_NAME,否则它将反转封闭的 SORT..
命令。注意,目前不支持对齐的反向排序。
注意,排序命令仅接受单个通配符模式。因此,例如,以下代码将不起作用:
*(REVERSE(.text* .init*))
要解决此问题,请单独列出模式,如下所示:
*(REVERSE(.text*))
*(REVERSE(.init*))
注意,可以将 EXCLUDE_FILE 命令放在排序命令中,但不能反过来。例如:
*(SORT_BY_NAME(EXCLUDE_FILE(foo) .text*))
将起作用,但:
*(EXCLUDE_FILE(foo) SORT_BY_NAME(.text*))
将不起作用。
当链接器脚本中有嵌套的节排序命令时,节排序命令最多可以有 1 层嵌套。
-
SORT_BY_NAME (SORT_BY_ALIGNMENT (通配符节模式))。它将首先按名称对输入节进行排序,如果两个节具有相同的名称,则按对齐方式排序。
-
SORT_BY_ALIGNMENT (SORT_BY_NAME (通配符节模式))。它将首先按对齐方式对输入节进行排序,如果两个节具有相同的对齐方式,则按名称排序。
-
SORT_BY_NAME (SORT_BY_NAME (通配符节模式)) 与 SORT_BY_NAME (通配符节模式) 的处理方式相同。
-
SORT_BY_ALIGNMENT (SORT_BY_ALIGNMENT (通配符节模式)) 与 SORT_BY_ALIGNMENT (通配符节模式) 的处理方式相同。
-
SORT_BY_NAME (REVERSE (通配符节模式)) 按名称反向排序。
-
REVERSE (SORT_BY_NAME (通配符节模式)) 按名称反向排序。
-
SORT_BY_INIT_PRIORITY (REVERSE (通配符节模式)) 按初始化优先级反向排序。
-
所有其他嵌套节排序命令均无效。
-
当同时使用命令行节排序选项和链接器脚本节排序命令时,节排序命令始终优先于命令行选项。
如果链接器脚本中的节排序命令未嵌套,则命令行选项将使节排序命令被视为嵌套排序命令。
带有 --sort-sections
对齐的 SORT_BY_NAME (通配符节模式) 相当于 SORT_BY_NAME (SORT_BY_ALIGNMENT (通配符节模式))。SORT_BY_ALIGNMENT (通配符节模式) 和 --sort-section name
等效于 SORT_BY_ALIGNMENT (SORT_BY_NAME (通配符节模式))。如果链接器脚本中的节排序命令是嵌套的,则命令行选项将被忽略。
SORT_NONE 通过忽略命令行节排序选项来禁用节排序。
如果对输入节的去向感到困惑,请使用-M
链接器选项生成映射文件。映射文件精确显示了输入节如何映射到输出节。
此示例显示了如何使用通配符模式对文件进行分区。此链接器脚本指示链接器将所有.text
节放在.text
中,将所有.bss
节放在.bss
中。链接器会将所有以大写字母开头的文件中的.data
节放在.DATA
中;对于所有其他文件,链接器会将.data
部分放在.data
中。
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
5.6 常用符号的输入部分
公共符号需要特殊符号,因为在许多目标文件格式中,公共符号没有特定的输入部分。链接器将公共符号视为位于名为COMMON
的输入部分中。可以将文件名与COMMON
部分一起使用,就像任何其他输入部分一样。可以使用它将特定输入文件中的公共符号放在一个部分中,而将其他输入文件中的公共符号放在另一个部分中。
在大多数情况下,输入文件中的公共符号将放置在输出文件的.bss
部分中。例如:
.bss { *(.bss) *(COMMON) }
某些目标文件格式具有多种类型的公共符号。例如,MIPS ELF 目标文件格式区分标准公共符号和小公共符号。在这种情况下,链接器将对其他类型的公共符号使用不同的特殊部分名称。在 MIPS ELF 的情况下,链接器对标准公共符号使用COMMON
,对小公共符号使用.scommon
。这允许您将不同类型的通用符号映射到内存的不同位置。
有时会在旧的链接器脚本中看到[COMMON]
。此符号现在已过时。它相当于*(COMMON)
。
5.7 输入部分和垃圾收集
当使用链接时垃圾回收(--gc-sections
)时,标记不应消除的部分通常很有用。这可以通过用 KEEP()
包围输入部分的通配符条目来实现,例如 KEEP(*(.init))
或 KEEP(SORT_BY_NAME(*)(.ctors))
。
5.8 输入部分示例
以下示例是完整的链接器脚本。它告诉链接器从文件 all.o
读取所有部分,并将它们放在输出部分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 标识符,则链接器将自动看到 PROVIDE 两个符号:__start_SECNAME
和 __stop_SECNAME
,其中 SECNAME
是节的名称。它们分别表示输出节的起始地址和结束地址。注意:大多数节名称不能表示为 C 标识符,因为它们包含.
字符。
5.9 输出部分数据
可以使用 BYTE、SHORT、LONG、QUAD 或 SQUAD 作为输出部分命令,在输出部分中包含显式数据字节。每个关键字后面都跟一个括号中的表达式,提供要存储的值。表达式的值存储在位置计数器的当前值中。
BYTE、SHORT、LONG 和 QUAD 命令分别存储一个、两个、四个和八个字节。存储字节后,位置计数器将增加存储的字节数。
例如,这将存储字节 1,后跟符号addr
的四字节值:
BYTE(1)
LONG(addr)
当使用 64 位主机或目标时,QUAD 和 SQUAD 相同;它们都存储 8 字节或 64 位值。当主机和目标都是 32 位时,表达式将计算为 32 位。在这种情况下,QUAD 存储一个 32 位值,该值以零扩展为 64 位,而 SQUAD 存储一个 32 位值,该值以符号扩展为 64 位。
如果输出文件的目标文件格式具有明确的字节序(这是正常情况),则该值将以该字节序存储。 当目标文件格式没有明确的字节序时(例如,S 记录就是这样),该值将以第一个输入目标文件的字节序存储。
可以使用 ASCIZ 在输出部分中包含以零结尾的字符串。 关键字后跟一个字符串,该字符串存储在位置计数器的当前值中,并在末尾添加一个零字节。 如果字符串包含空格,则必须用双引号括起来。 字符串可能包含 \n
、\r
、\t
和八进制数。 不支持十六进制数。
例如,这个 16 个字符的字符串将创建一个 17 字节的区域
ASCIZ "This is 16 bytes"
注意,这些命令仅在节描述内部起作用,而不是在它们之间起作用,因此以下内容将导致链接器出错:
SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
而这将起作用:
SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }
可以使用 FILL 命令设置当前节的填充模式。它后面跟着一个括号中的表达式。节内任何其他未指定的内存区域(例如,由于输入节需要对齐而留下的间隙)都将用表达式的值填充,并根据需要重复。FILL 语句覆盖节定义中出现该语句的位置之后的内存位置;通过包含多个 FILL 语句,可以在输出部分的不同部分使用不同的填充模式。
此示例显示如何使用值“0x90”填充未指定的内存区域:
FILL(0x90909090)
FILL 命令类似于=fillexp
输出部分属性,但它仅影响 FILL 命令后面的部分,而不是整个部分。如果同时使用,则 FILL 命令优先。有关填充表达式的详细信息,请参阅输出部分填充。
注意,通常,表达式的值在用于填充间隙时会扩展到 4 个字节。因此,FILL(144)
将使用模式0 0 0 144
的重复来填充区域。该值被视为大端数字,例如,FILL(22 * 256 + 23)
将使用模式0 0 22 23
的重复来填充区域。如果表达式产生的值有超过 4 个有效字节,则只会使用该值的最少 4 个字节。
当表达式是简单的十六进制数时,上述规则不适用。在这种情况下,不执行零扩展,所有字节都有效。因此,FILL(0x90)
将用重复的0x90
填充一个区域,其中没有零字节,而FILL(0x9192)
将用重复的0x91 0x92
填充该区域。十六进制表达式中的零字节即使在开头也是有效的,因此FILL(0x0090)
将用重复的0x00 0x90
填充一个区域。
十六进制数可以长于 4 个字节,并且所有字节都有效,因此FILL(0x123456789a)
将用重复的 5 字节序列0x12 0x34 0x56 0x78 0x9a
填充一个区域。十六进制值中超出区域大小的多余字节将被忽略。
以上仅适用于指定为0x[0-9][a-f][A-F]
的十六进制数。以$
前缀或h
、H
、x
或X
后缀指定的十六进制数将遵循正常的填充值规则。这也适用于涉及十六进制数的表达式以及具有数值后缀的十六进制数。
LINKER_VERSION
命令插入一个字符串,其中包含当前点的链接器版本。注意,默认情况下,此指令被禁用并且不会执行任何操作。只有使用 --enable-linker-version
命令行选项时,它才会变为活动状态。
基于 ELF 的目标的内置链接器脚本已在其.comment
部分中包含此指令。
5.10 输出部分关键字
有几个关键字可以作为输出部分命令出现。
(1) CREATE_OBJECT_SYMBOLS
该命令告诉链接器为每个输入文件创建一个符号。每个符号的名称将是相应输入文件的名称。每个符号的部分将是 CREATE_OBJECT_SYMBOLS 命令出现的输出部分。
这对于 a.out 对象文件格式来说是常规的。它通常不用于任何其他对象文件格式。
(2) CONSTRUCTORS
当使用 a.out 对象文件格式进行链接时,链接器使用不寻常的集合构造来支持 C++ 全局构造函数和析构函数。当链接不支持任意部分的对象文件格式(例如 ECOFF 和 XCOFF)时,链接器将自动通过名称识别 C++ 全局构造函数和析构函数。对于这些对象文件格式,CONSTRUCTORS 命令告诉链接器将构造函数信息放在 CONSTRUCTORS 命令出现的输出部分中。对于其他对象文件格式,CONSTRUCTORS 命令将被忽略。
符号 __CTOR_LIST__
标记全局构造函数的开始,符号 __CTOR_END__
标记结束。同样,__DTOR_LIST__
和 __DTOR_END__
标记全局析构函数的开始和结束。列表中的第一个字是条目数,后面是每个构造函数或析构函数的地址,后面是零字。编译器必须安排实际运行代码。对于这些目标文件格式,GNU C++ 通常从子例程 __main
调用构造函数;对 __main
的调用会自动插入到 main 的启动代码中。GNU C++ 通常使用 atexit 或直接从函数出口运行析构函数。
对于支持任意节名的目标文件格式(如 COFF 或 ELF),GNU C++ 通常会安排将全局构造函数和析构函数的地址放入 .ctors 和 .dtors 节中。将以下序列放入链接器脚本中将构建 GNU 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__ = .;
如果使用 GNU C++ 对初始化优先级的支持(该支持对全局构造函数的运行顺序提供了一些控制),则必须在链接时对构造函数进行排序,以确保它们以正确的顺序执行。使用 CONSTRUCTORS 命令时,请改用SORT_BY_NAME(CONSTRUCTORS)
。使用 .ctors
和 .dtors
部分时,请使用*(SORT_BY_NAME(.ctors))
和*(SORT_BY_NAME(.dtors))
,而不是仅仅使用*(.ctors)
和*(.dtors)
。
通常,编译器和链接器会自动处理这些问题,无需担心。但是,如果您使用 C++ 并编写自己的链接器脚本,则可能需要考虑这一点。
5.11 输出部分丢弃
链接器通常不会创建没有内容的输出节。这是为了方便引用可能存在于或不存在于任何输入文件中的输入节。例如:
.foo : { *(.foo) }
仅当至少一个输入文件中有.foo
节,并且输入节并非全部为空时,才会在输出文件中创建.foo
节。在输出节中分配空间的其他链接脚本指令也会创建输出节。即使赋值不会创建空间,对点的赋值也是如此,但. = 0
、. = . + 0
、. = sym
、. = . + sym
和. = ALIGN (. != 0, expr, 1)
除外,此时sym
是脚本中定义的值为 0 的绝对符号。这允许使用. = .
强制输出空节。
链接器将忽略已丢弃输出节上的地址分配,除非链接器脚本在输出节中定义了符号。在这种情况下,链接器将遵循地址分配,即使该节被丢弃,也可能前进点。
特殊输出节名称/DISCARD/
可用于丢弃输入节。分配给名为/DISCARD/
的输出节的任何输入节均不包含在输出文件中。
这可用于丢弃标有 ELF 标志 SHF_GNU_RETAIN 的输入节,否则这些输入节将被从链接器垃圾收集中保存下来。
请注意,与/DISCARD/
输出节匹配的节将被丢弃,即使它们位于具有其他未被丢弃的成员的 ELF 节组中。这是故意的。丢弃优先于分组。
5.12 输出部分类型
每个输出节可能有一个类型。类型是括号中的关键字。定义了以下类型:
NOLOAD
该节应标记为不可加载,以便在程序运行时不会将其加载到内存中。
READONLY
该节应标记为只读。
DSECT / COPY / INFO / OVERLAY
这些类型名称是为了向后兼容而支持的,很少使用。它们都具有相同的效果:该节应标记为不可分配,以便在程序运行时不会为该节分配内存。
TYPE = type
将节类型设置为整数类型。生成 ELF 输出文件时,类型名称 SHT_PROGBITS、SHT_STRTAB、SHT_NOTE、SHT_NOBITS、SHT_INIT_ARRAY、SHT_FINI_ARRAY 和 SHT_PREINIT_ARRAY 也允许用于类型。用户有责任确保满足部分类型的任何特殊要求。
注意,仅当部分的部分或所有内容没有自己的隐式类型时才使用 TYPE。例如:
.foo . TYPE = SHT_PROGBITS { *(.bar) }
将把.foo
部分的类型设置为输入文件中.bar
部分的类型,这可能不是 SHT_PROGBITS 类型。而:
.foo . TYPE = SHT_PROGBITS { BYTE(1) }
将把.foo
的类型设置为 SHT_PROGBBITS。如果需要覆盖传入部分的类型并强制输出部分类型,则需要额外的无类型数据:
.foo . TYPE = SHT_PROGBITS { BYTE(1); *(.bar) }
READONLY ( TYPE = type )
此语法形式将 READONLY
类型与 type
指定的类型相结合。
链接器通常根据映射到输出部分的输入部分来设置输出部分的属性。可以使用部分类型覆盖此设置。例如,在下面的脚本示例中,ROM
部分位于内存位置0
,并且在程序运行时无需加载。
SECTIONS {
ROM 0 (NOLOAD) : { … }
…
}
5.13 输出部分 LMA
每个节都有一个虚拟地址 (VMA) 和一个加载地址 (LMA)。虚拟地址由前面所述的输出节地址指定。加载地址由 AT
或