MEMORY 命令
链接器的默认配置允许分配所有可用内存。 您可以使用MEMORY命令覆盖它。
MEMORY命令描述目标中内存块的位置和大小。您可以使用它来描述链接器可能使用的存储区域,以及必须避免的存储区域。
然后,您可以将section分配给特定的内存区域。链接器将根据存储区域设置section地址,并警告太满的区域。
链接描述文件最多可以包含MEMORY命令的一种用法。
但是,您可以根据需要在其中定义尽可能多的内存块。 语法为:
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
...
}
name是链接描述文件中用于引用该区域的名称。区域名称在链接描述文件之外没有任何意义。
区域名称存储在单独的名称空间中,不会与符号名称,文件名称或section名称冲突。
每个内存区域在MEMORY命令中必须有一个不同的名称。但是你可以为已经存在的内存区域,增加一个别名
attr字符串是可选的属性列表,用于指定是否对链接器脚本中未显式映射的输入section使用特定的内存区域。
如前面所述,如果未为某些输入section指定输出section,则链接器将创建一个与该输入section同名的输出section。
如果定义区域属性,则链接器将使用它们为它创建的输出section选择存储区域。
attr字符串必须仅包含以下字符:
‘R’ Read-only section
‘W’ Read/write section
‘X’ Executable section
‘A’ Allocatable section
‘I’ Initialized section
‘L’ Same as ‘I’
‘!’ Invert the sense of any of the preceding attributes
如果未映射的section与“!”以外的任何列出的属性匹配,它将被放置在这个内存区域中。
“!”属性会翻转属性,因此,只有未映射的section与列出的任何属性都不匹配时,才会将其放置在这个内存区域中。
origin是存储区起始地址的数字表达式。该表达式必须计算为常数,并且不能包含任何符号。
关键字ORIGIN可以缩写为org或o(但不能缩写为ORG)。
len是存储区大小的表达式。与origin表达式一样,该表达式必须仅是数字,并且必须计算为常数。关键字LENGTH可以缩写为len或l。
在以下示例中,我们指定有两个可分配的存储区域:一个从“ 0”开始,大小为256 KB,另一个从“ 0x40000000”开始,大小为4兆字节。
链接器会将未显式映射到存储区中的每个section都放置在“ rom”存储区中,并且它是只读的或可执行的。
链接器会将其他未明确映射到内存区域的section放置到“ ram”内存区域中。
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
定义内存区域后,您就可以使用“> region”输出section属性来指导链接器将特定的输出section放置到该内存区域中。
例如,如果您有一个名为“ mem”的内存区域,则可以在输出section定义中使用“> mem”。
如果没有为输出section指定地址,则链接器会将地址设置为内存区域中的下一个可用地址。
如果指向一个内存区域的输出section对于该区域太大,则链接器将发出错误消息。
可以通过ORIGIN(memory)和LENGTH(memory)函数访问表达式中的存储器的起始位置和长度:
_fstack = ORIGIN(ram) + LENGTH(ram) - 4;
PHDRS 命令
ELF目标文件格式使用程序头,也称为段。程序头说明如何将程序加载到内存中。您可以通过将objdump程序与“ -p”选项一起使用来打印出来。
在本机ELF系统上运行ELF程序时,系统加载程序会读取程序标头,以了解如何加载程序。
仅在正确设置程序头的情况下才能使用。
本手册没有详细描述系统加载程序如何解释程序头。 有关更多信息,请参见ELF ABI。
默认情况下,链接器将创建合理的程序头。但是,在某些情况下,您可能需要更精确地指定程序头。为此,您可以使用PHDRS命令。当链接器在链接器脚本中看到PHDRS命令时,除了指定的程序头以外,它不会创建任何程序头。
链接器仅在生成ELF输出文件时注意PHDRS命令。 在其他情况下,链接器将仅忽略PHDRS。
这是PHDRS命令的语法。 关键字PHDRS,FILEHDR,AT和FLAGS是关键字
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS ( flags ) ] ;
}
name仅用于链接脚本的SECTIONS命令中的引用。它不会放入输出文件中。程序头名称存储在单独的名称空间中,不会与符号名称,文件名称或section名称冲突。每个程序头必须有一个不同的名称。
程序头按顺序处理,通常以升序加载地址的顺序映射到section。
某些程序头类型描述了系统加载程序将从文件中加载哪一个段。在链接器脚本中,可以通过将可分配的输出section放置在段中来指定这些段的内容。
您可以使用’:phdr’输出section属性将section放置在特定段中。
通常将某些section放在一个以上的段中。这仅意味着一个内存段包含另一段。一旦某个段,想要包含某个section时,你就可以重复使用“:phdr”.
如果您使用’:phdr’将section放在一个或多个段中,则链接器会将所有未指定’:phdr’的后续可分配的section放在同一段中。
这是为了方便起见,因为通常将一整套连续section放在单个段中。
您可以使用:NONE覆盖默认段,并告诉链接器不要将该段放在任何段中。
您可以在程序头类型之后使用FILEHDR和PHDRS关键字来进一步描述段的内容。
FILEHDR关键字意味着该段应包含ELF文件头。PHDRS关键字意味着该段应包括ELF程序头本身。如果应用于可加载段(PT_LOAD),则它必须是第一个可加载段。注意,此处的文件头和程序头的区别。
类型可以是以下之一。 数字表示关键字的值:
PT_NULL (0)
表示未使用的程序头。
PT_LOAD (1)
表示此程序头描述了一个段,这个要从文件中加载。
PT_DYNAMIC (2)
表示可以找到动态链接信息的段
PT_INTERP (3)
表示可以在其中找到程序解释器名称的段。
PT_NOTE (4)
表示包含注释信息的段。
PT_SHLIB (5)
保留的程序头类型,由ELF ABI定义但未指定。
PT_PHDR (6)
指示可以在其中找到程序头的段。
expression
该表达式给出程序头的数字类型。 这可能用于上面未定义的类型
您可以使用AT表达式指定将段加载到内存中的特定地址。这与用作输出section属性的AT命令相同.程序头的AT命令将覆盖输出section属性。
链接程序通常会根据构成段的section来设置段标记。您可以使用FLAGS关键字来显式指定段标记。标志的值必须是整数。它用于设置程序头的p_flags区域。
这是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
...
}
VERSION命令
使用ELF时,链接器支持符号版本。符号版本仅在使用共享库时有用。动态链接器可以通过符号版本,选择指定的函数。
您可以直接在主链接程序脚本中包含版本脚本,也可以将版本脚本作为隐式链接程序脚本进行调用。您也可以使用“ --version-script”链接器选项。
VERSION命令的语法很简单
VERSION { version-script-commands }
版本脚本命令的格式与Solaris 2.5中Sun链接程序使用的格式相同。
版本脚本定义了版本节点树。您可以在版本脚本中指定节点名称和相互依赖性。
您可以指定将哪些符号绑定到哪些版本节点,并且可以将一组指定的符号作用域减少到本地范围,以使它们在共享库之外不全局可见
演示版本脚本语言的最简单方法是使用一些示例。
VERS_1.1 {
global:
foo1;
local:
old*;
original*;
new*;
};
VERS_1.2 {
foo2;
} VERS_1.1;
VERS_2.0 {
bar1; bar2;
extern "C++" {
ns::*;
"int f(int, double)";
}
} VERS_1.2;
此示例版本脚本定义了三个版本节点。定义的第一个版本节点是“ VERS_1.1”; 它没有其他依赖项。该脚本将符号“ foo1”绑定到“ VERS_1.1”。它将一些符号缩小到局部范围,这样它们在共享库之外是不可见的;这是使用通配符模式完成的,因此任何名称以“ old”,“ original”或“ new”开头的符号都将匹配。
可用的通配符模式与匹配文件名时在shell中使用的通配符模式相同。但是,如果在双引号中指定名称,则该名称将被视为字面量常量,而不是需要匹配的模式。
接下来,版本脚本定义节点“ VERS_1.2”。 该节点取决于“ VERS_1.1”。 该脚本将符号“ foo2”绑定到版本节点“ VERS_1.2”。
最后,版本脚本定义了节点“ VERS_2.0”。 该节点取决于“ VERS_1.2”。 脚本将符号“ bar1”和“ bar2”绑定到版本节点“ VERS_2.0”。
当链接器在库中找到一个未绑定到任何版本的符号时,会将其绑定到一个未指定的基本的版本中。
您可以使用版本脚本中的“ global:*;”将所有其他未指定的符号绑定到给定的版本节点。
请注意,在全局作用域中使用通配符有点疯狂,但最后一个版本节点除外。其他地方的全局通配符具有这样一种风险:将符号意外添加到旧版本的集合中。这是错误的,因为较旧的版本应具有一组固定的符号。
版本节点的名称除了可能向阅读它们的人建议以外,没有其他特定含义。“ 2.0”版本也可能出现在“ 1.1”和“ 1.2”之间。
但是,这将是一种编写版本脚本的混乱方式。
如果节点名称是版本脚本中唯一的版本节点,则可以省略。这样的版本脚本不会为符号分配任何版本,只会选择哪些符号在全局范围内可见,哪些不会:
{ global: foo; bar; local: *; };
当您将应用程序链接到具有符号版本控制的共享库时,应用程序本身知道它需要的每个符号的版本,并且还知道链接到的每个共享库中所需的版本节点。
因此,在运行时,动态加载程序可以进行快速检查,以确保所链接的库确实提供了应用程序所需的所有版本节点。
这样,动态链接器就可以确定地知道它需要的所有外部符号都可以解析,而不必搜索每个符号引用
符号版本控制实际上是SunOS进行次要版本检查的一种更为复杂的方法。此处要解决的基本问题是,通常在需要时绑定对外部函数的引用,并且在应用程序启动时不会全部绑定。
如果共享库已过期,则可能缺少所需的接口; 当应用程序尝试使用该接口时,它可能会突然失败。
使用符号版本控制时,如果与应用程序一起使用的库太旧,则用户在启动程序时将收到警告。
Sun的版本控制方法有多个GNU扩展。其中第一个功能是将符号绑定到源文件中的版本节点的能力,源文件中已定义符号而不是在版本控制脚本中。
这样做主要是为了减轻库维护人员的负担。您可以通过输入以下内容来做到这一点:
__asm__(".symver original_foo,foo@VERS_1.1");
这会将函数“ original_foo”重命名为绑定到版本节点“ VERS_1.1”的“ foo”的别名。“ local:”指令可用于防止导出符号“ original_foo”。“ .symver”指令优先于版本脚本。
第二个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");
在此示例中,“ foo @”代表符号“ foo”绑定到该符号的未指定基本版本中。包含该示例的源文件将定义4 个C函数:“ original_foo”,“ old_foo”,“ old_foo1”和“ new_foo”。
如果给定符号有多个定义,则需要某种方式指定默认版本,该符号的外部引用将绑定到该默认版本。您可以使用“ .symver”指令的“ foo @@ VERS_2.0”类型执行此操作。
这样只能将一个版本的符号声明为默认版本。 否则,您将有效地使用同一符号的多个定义。
如果您希望在共享库中将引用绑定到特定版本的符号中,则可以使用方便的别名(即“ old_foo”),也可以使用“ .symver”指令来特定地绑定到外部有关函数的版本。
您还可以在版本脚本中指定语言:
VERSION extern "lang" { version-script-commands }
受支持的“ lang”是“ C”,“ C ++”和“ Java”。链接器将在链接时遍历符号列表,并根据“ lang”对它们进行还原,然后再将它们与“ version-script-commands”中指定的模式进行匹配。
杂乱的名称可能包含空格和其他特殊字符。如上所述,您可以使用通配符来匹配名称,也可以使用双引号引起来的字符串来完全匹配该字符串。
在后一种情况下,请注意,版本脚本和实际真正输出之间的细微差别(例如,空白)将导致不匹配。
由于实际生成的确切字符串将来可能会更改,即使名称不正确,您也应检查所有版本指令的行为是否符合升级后的预期。
下一篇文章,介绍脚本中的表达式和隐式链接脚本