原文
注:文中大部分文章参考引用都是自身的引用,为了不产生混淆,各个章节标题使用英文原称,同时参考引用也用英文原称。
每个链接都由一个链接脚本控制。这个脚本由链接命令语言编写。
链接脚本的主要目的是描述输入文件中的段应当如何映射到输出文件中,并控制输出文件的内存布局。多数链接脚本都执行类似功能。但是,如果需要,链接脚本也可以使用下面所描述的命令指挥链接器进行很多其他操作。
链接器通常使用一个链接脚本。如果没有为其提供一个,链接器将会使用默认的编译在链接器执行文件内部的脚本。可以使用命令’–verbose’显示默认的链接脚本。一些命令行选项,例如’-r’,’-N’会影响默认的链接脚本。
你可以通过在命令行使用’-T’命令使用自己的脚本。如果使用此命令,你的链接脚本将会替代默认链接脚本。
也可以通过将脚本作为链接器输入文件隐式的使用链接脚本,参考Implicit Linker Scripts。
- Basic Script Concepts: 基本链接器脚本概念
- Script Format: 链接器脚本格式
- Simple Example: 简单的链接器脚本例子
- Simple Commands: 简单的链接器脚本命令
- Assignments: 为符号指定数值
- SECTIONS: 段命令
- MEMORY: 内存命令
- PHDRS: PHDRS命令
- VERSION: 版本命令
- Expressions: 链接脚本的表达式
- Implicit Linker Scripts: 隐式链接脚本
3.1 Basic Linker Script Concepts
为了描述链接脚本语言,我们需要定义一些基本概念和词汇。
链接器将许多输入文件组合成一个输出文件。输出文件和每个输入文件都有一个特定的已知格式成为目标文件格式。每个文件都被称为目标文件。输出文件通常叫做可执行文件,但我们仍将其称为目标文件。每个目标文件在其他东西之间,都有一个段列表。有时把输入文件的段称作输入段,类似的,输出文件的段称作输出段。
每个目标文件中的段都有名字和大小。多数段还有一个相关的数据块,称为 段内容。一个段可能被标记为可加载,表示当输出文件运行时,段内容需要先加载到内存中。一个没有内容的段可能是可分配段,即在内存中留出一段空间(有时还需要清零)。一个即不是加载又不是可分配的段,通常含有一些调试信息。
每个加载或可分配输出段有两个地址。第一个地址为VMA,或者叫做虚地址。这是当输出文件运行时段所拥有的地址。第二个地址是LMA,或者叫加载内存地址。这是段将会被加载的地址。一个它们会产生区别的例子是,当一个数据段加载到ROM, 此后在程序启动时被复制到RAM中(这个技术通常被用来初始化全局变量)。此种情况下,ROM使用LMA地址,RAM使用VMA地址。
如果想查看目标文件中的段,可以用objdump程序的’-h’选项。
每个目标文件还有一个符号列表,称为符号列表。一个符号可能是被定义的或者未定义的。每个符号都有一个名字,且所有已定义的符号在其他信息中间都有一个地址。如果将一个c或者c++程序编译成目标文件,会将所有定义过的函数和全局变量以及静态变量作为已定义符号。所有输入文件引用的未定义的函数或者全局变量会成为未定义符号。
你可以参看目标文件中的符号,使用nm程序或使用objdump程序的’-t’选项。
3.2 Linker Script Format
链接脚本是文本的文件。
一个链接器脚本是一系列的命令。每个命令都是一个关键字,可能后面还跟有一个参数,或者一个符号的赋值。使用分号分割命令,空格通常被忽略。
类似于文件名或者格式名的字串可以直接输入。如果文件名含有一个字符例如逗号,(逗号被用来分割文件名)你可以将文件名放在双引号内部。这里禁止文件名内使用双引号字符。
你可以像C语言一样在链接脚本内包含注释,由’/*’和’*/’划分。和C一样,注释在句法上被当作空格。
3.3 Simple Linker Script Example
多数脚本链接都很简单。
一个最简单的可能的脚本只有一个命令:’SECTIONS’。你使用’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’段之间创建一个小缝(为了使’.bss’段对齐)。
如上,这是一个简单完整的链接脚本。
3.4 Simple Linker Script Commands
本章我们将介绍一些简单的脚本命令。
- Entry Point: 设置入口点
- File Commands: 控制文件的命令
- Format Commands: 控制目标文件格式的命令
- REGION_ALIAS: 为内存区域设置别名
- Miscellaneous Commands: 其他链接脚本命令
3.4.1 Setting the Entry Point
第一个在程序中执行的指令被称为入口点(entry point)。可以用ENTRY脚本命令设置入口点,参数是一个符号名:
ENTRY(symbol)
这里有几种方法设置入口点。链接器会依照下面的方法依次尝试设置入口点,直到其中一种方法成功:
- 命令行的’-e’选项指定的值
- 脚本中的ENTRY(symbol)命令
- 一个目标约定的特殊符号(如果有定义的话);例如大多数目标符号为start,但PE和BeOS系统会检查一个可能入口符号列表,以第一个碰到的为准 - ‘.text’的第一个字节的地址,如果存在的话
- 地址0
注:也就是说,优先级为:命令>脚本文件>自定义start
3.4.2 Commands Dealing with Files
一些脚本命令用来处理文件。
INCLUDE filename
在命令处包含链接脚本文件’filename’。文件将会在当前目录搜索,以及任何’-L’命令行命令指定的路径。INCLUDE可以嵌套调用10层。
可以直接把INCLUDE放到顶层,MEMORY或者SECTIONS命令中,或者在输出段描述中。
**INPUT(file, file, …)
INPUT(file file …)**
INPUT命令引导链接器包含列出的文件,与在命令行上使用的一样。
例如,如果每次链接时你总是想包含subr.o,但不想麻烦的输入每个链接命令,那么你可以把’INPUT(subr.o)’放入你的链接脚本。
事实上,如果你愿意,可以把所有输入文件列在链接脚本内,然后仅使用’-T’命令调用链接器。
在sysroot前缀被设置的情况下,且filename以’/’字符开始,且正在运行的脚本也处于sysroot前缀范围内,filename将会在sysroot前缀范围内查找。否则链接器会尝试在当前目录打开。如果没有找到,链接器会搜索库搜索路径。sysroot前缀也可以通过把filename的第一个字符设置为’=’强制使用(’=’替换为sysroot)。参照Command Line Options的’-L’命令。
如果使用’INPUT (-lfile)’ld将会将名字转化为libfile.a,就像命令行参数’-l’。
当你使用INPUT命令在隐式链接脚本中,文件在链接脚本文件被包含的时刻才会被加入。这可能会影响库的搜索。
**GROUP(file, file, …)
GROUP(file file …)**
GROUP命令与INPUT命令勒斯,除了所有file指出的名字都应该为库,并且所有库将会被重复搜索直到没有新的未定义引用被创建。参见Command Line Options对于’-(‘的描述。
**AS_NEEDED(file, file, …)
AS_NEEDED(file file …)**
此构造仅可以出现在INPUT或GROUP命令中,位于其他命令中间。此命令中的文件将会以类似于直接出现在INPUT或者GROUP命令中的文件一样处理,除了ELF共享库,ELF共享库仅在真正需要使用时才被添加。这个构造本质上使能了列表中文件的’–as-needed’选项,并且恢复此前的–as-needed设置,此后的–no-as-needed。
OUTPUT(filename)
OUTPUT命令为输出文件命名。使用脚本中的OUTPUT(filename)与命令行的’-o filename’类似(参见Command Line Options)。如果同时设置了,命令行的命令有效。
你可以使用OUTPUT命令定义一个默认的输出文件名来替代通常默认的名称a.out。
SEARCH_DIR(path)
SEARCH_DIR命令添加一个ld搜索库的路径。使用SEARCH_DIR(path)与命令行的’-L path’类似(参见Command Line Options)。如果都使用了,链接器将会搜索所有路径。命令行给出的路径会优先搜索。
STARTUP(filename)
STARTUP命令类似于INPUT命令,除了filename将作为首个被链接的输入文件处理,就像被在命令行第一个给出一样。在一些把第一个文件当作入口点的系统上这个命令非常有效。
3.4.3 Commands Dealing with Object File Formats
有一对处理目标文件格式的脚本命令。
**OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)**
The OUTPUT_FORMAT命令使用BFD格式的命名方式(参见BFD)。使用OUTPUT_FORMAT(bfdname)类似于命令行的’–oformat bfdname’(参考Command Line Options)。如果都使用了,以命令行为准。
可以使用三参数OUTPUT_FORMAT命令来使用不同的基于命令行’-EB’和’-EL’的格式。此命令允许链接脚本设置输出格式需要的大小端。
如果即没有’-EB’也没有’-EL’被使用,那么输出格式将会使用第一个参数。如果使用了’-EB’,输出格式将是第二个参数,大端。如果使用了’-EL’,输出格式将是第三个参数,小端。
例如MIPS ELF目标默认的链接脚本使用如下的命令:
OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)
这表示默认输出格式为’elf32-bigmips’,但如果在命令行输入了’-EL’命令,输出文件将以’elf32-littlemips’格式输出。
TARGET(bfdname)
TARGET命令设置读取输入文件时的BFD格式。这将影响后面的INPUT和GROUP命令。此命令类似使用命令行指令’-b bfdname’(参见Command Line Options)。如果使用了TARGET命令,但OUTPUT_FORMAT命令没使用,则最后的TARGET命令还被用来设置输出文件的格式。(参见BFD)
3.4.4 Assign alias names to memory regions
可以为MEMORY命令创建的内存区域提供别名。每个名字最多指代一个区域。
REGION_ALIAS(alias, region)
REGION_ALIAS函数为内存区域创建一个别名。这允许了输出段灵活的映射到内存区域。下面是一个例子:
假设有一个含有很多内存存储设备的嵌入式系统的应用。每个内存设备都有特殊的目的,易失内存RAM可以存放可执行代码或者数据。一些设备可能是只读的,非易失性内存ROM允许存储可执行代码和只读数据。最后的是一个只读的,非易失的内存ROM2,允许只读数据段读取,不允许指定代码段存储。现在有四个输出段:
- .text 程序代码
- .rodata 只读数据
- .data 可读写且需要初始化数据
- .bss 可读写的置零初始化数据
目标是提供一个链接命令文件含有系统无关的定义输出段的部分,以及系统相关的把输出段映射到系统有效内存区域的部分。我们的嵌入式系统含有三个不同的内存设置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);
}
}
注:目前分析,应该是AT命令把读写数据.data段即加载到ROM又在RAM分配了空间。
3.4.5 Other Linker Script Commands
这里有几个其它的链接脚本命令。
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会在此前被求值,因此他们可以被ASSERT。因此:
PROVIDE (__stack_size = 0x100);
.stack :
{
PROVIDE (__stack = .);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
将会工作。
EXTERN(symbol symbol …)
强制符号在输出文件中作为未定义符号。这样做了,可能会引发从标准库中连接一些额外的模块。你可以为每一个’EXTERN’列出几个符号,而且你可以多次使用’EXTERN’。 这个命令跟’-u’命令行选项具有相同的效果。
FORCE_COMMON_ALLOCATION
此命令类似于命令行命令’-d’:即便是使用了’-r’的重定位输出文件,也让ld为普通符号分配空间。
INHIBIT_COMMON_ALLOCATION
此命令与命令行命令’–no-define-common’效果类似:让ld不为普通符号分配空间,即便是一个非可重定位输出文件。
INSERT [ AFTER | BEFORE ] output_section
此命令在’-T’指定的脚本中典型应用是增强默认的SECTIONS,例如,重复占位程序段。它将把所有此前的链接脚本的声明插入output_section的后面(或者前面),并且使’-T’不要覆盖默认链接脚本。实际插入点类似于孤儿段。参见Location Counter。插入发生在链接器把输入段映射到输出段后。在插入前,因为’-T’的脚本在默认脚本之前被解析,在’-T’脚本中的声明会先于默认内部脚本的声明而执行。特别的,会先于默认脚本把输入段的声明被制成’-T’指定的输出段。下例为’-T’脚本使用INSERT可能的情况:
SECTIONS
{
OVERLAY :
{
.ov1 { ov1*(.text) }
.ov2 { ov2*(.text) }
}
}
INSERT AFTER .text;
NOCROSSREFS(section section …)
此命令可能被用来告诉ld,如果引用了section的参数就报错。
在特定的程序类型中,比如使用覆盖技术的嵌入式系统,当一个段被加载到内存中,另一个段不会被加载。任何两个段之间直接的引用都会带来错误。例如,如果一个段中的代码调用另一个段中的函数,将会产生错误。
NOCROSSREFS列出了一系列输出段的名字。如果ld检测到任何段间交叉引用,将会报告错误并返回非零退出码。注意NOCROSSREFS使用输出段名称,而不是输入段名称。
OUTPUT_ARCH(bfdarch)
指定一个特定的输出机器结构。参数为BFD库定义的名字之一(参考BFD)。可以使用objdump程序的’-f’指令查看一个目标文件的结构。
LD_FEATURE(string)
此命令用来控制ld行为。如果string是”SANE_EXPR”则脚本中的绝对符号和数字将被在任何地方当作数字对待。参考Expression Section。
3.5 Assigning Values to Symbols
在链接脚本中,可以为符号赋值。这将定义符号并将其放入全局的符号表内。
- Simple Assignments: 简单的赋值
- HIDDEN: HIDDEN
- PROVIDE: PROVIDE
- PROVIDE_HIDDEN: PROVIDE_HIDDEN
- Source Code Reference: 如何在源代码中使用一个链接脚本定义的符号。
3.5.1 Simple Assignments
可以使用任何C的赋值操作符号为符号赋值:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
第一种情况将会把表达式的值赋给符号。其他情况里,符号必须已经定义过,此后符号的值会被相应调整。
特殊符号’.’代表位置计数器。你可以在SECTIONS命令中使用它,参考Location Counter。
表达式后面的分号不能省略。
后面会有表达式的定义,参考Expressions。
你在写表达式赋值的时候,可以把它们作为单独的部分,也可以作为’SECTIONS’命令中的一个语句,或者作为’SECTIONS’命令中输出段描述的一个部分。
符号的有效区域由表达式所在的段决定,更多信息参考Expression Section。
下面是表示三种不同的使用符号赋值的地方:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
在这个例子里,’floating_point’将被设置为0。符号’_etext’被设置为紧随’.text’最后一个输入段后面的地址。符号’_bdata’将被定义为在’.text’输出段后面的一个4字节向上对齐的地址。
3.5.2 HIDDEN
为ELF目标的端口定义一个符号,符号将被隐藏并且不会被导出。语法是HIDDEN(symbol = expression)。
这是上面简单赋值的例子,使用HIDDEN重写:
HIDDEN(floating_point = 0);
SECTIONS
{
.text :
{
*(.text)
HIDDEN(_etext = .);
}
HIDDEN(_bdata = (. + 3) & ~ 3);
.data : { *(.data) }
}
这里此三个符号出了这个模块就不可见了。
3.5.3 PROVIDE
有些情况下,仅当一个符号被引用了却没有定义在任何链接目标中,才需要为链接脚本定义一个符号。例如,传统链接器定义符号’etext’。但是,ANSI C需要用户可以自由使用’etext’作为一个函数名称且不会引发错误。只有当符号被引用却没被定义的时候,PROVIDE关键字可以定义一个符号,比如’etext’。语法为PROVIDE(symbol = expression)。
下面是一个使用PROVIDE定义’etext’的例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在这个例子中,如果程序定义了’_etext’,链接器将给出重复定义错误。然而另一方面,如果程序定义了’etext’,链接器将会默认使用程序中的定义。如果程序已用了’etext’但没有定义它,链接器将使用链接脚本中的定义。
3.5.4 PROVIDE_HIDDEN
类似PROVIDE。对于ELF目标的端口,符号将被隐藏且不会被输出。
3.5.5 Source Code Reference
从源代码获得一个脚本定义的变量值不是直观的。特别是一个脚本符号与一个高级语言定义的变量声明不符的时候,将使用一个没有值的变量替代它。
在更深入前,需要注意的一点是,编译器常常把源码的名称转变为不同的名字再存入符号表中。例如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);
注意此时不需要操作符’&’了。
3.6 SECTIONS Command
SECTIONS命令告诉链接器如何将输入段映射到输出段,以及如何把输出段放入内存中。
SECTIONS命令的格式为:
SECTIONS
{
sections-command
sections-command
...
}
每个sections-command命令可能是下面之一:
- 一个入口命令(参考 Entry command)
- 一个符号定义 (参考 Assignments)
- 一个输出段的描述
- 一个重复描述
‘ENTRY’命令和符号赋值在’SECTIONS’命令中是允许的,这是为了方便在这些命令中使用位置计数器。这也可以让连接脚本更容易理解,因为你可以在更有意义的地方使用这些命令来控制输出文件的布局。
输出段描述和重叠将在后面分析。
如果你在链接脚本中不使用SECTIONS命令,链接器将会把所有输入段依照碰到的顺序分别放在一个独立名称的输出段中。例如,如果所有输入段出现在第一个文件中,输出文件的段的顺序将会与第一个输入文件保持一致。第一个段被放在地址0。
- Output Section Description: 输出段描述
- Output Section Name: 输出段名称
- Output Section Address: 输出段地址
- Input Section: 输入段描述
- Output Section Data: 输出段数据
- Output Section Keywords: 输出段关键字
- Output Section Discarding: 输出段抛弃内容
- Output Section Attributes: 输出段属性
- Overlay Description: 重叠描述
3.6.1 Output Section Description
完整的输出段描述看起来像下面这样(一个sections-command):
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] [,]
多数输出段不需要使用多数的可选段属性。
SECTION边上的空格是必须的,所以段名称是明确的。冒号跟花括号也是必须的。最后的逗号如果使用了fillexp,且下一个段命令看起来像是表达式的延续的时候可能会需要。断行和其他的空格是可选的。
每个output-section-command可能是下面之一:
- 一个符号定义(参考 Assignments)
- 一个输入段描述(参考 Input Section)
- 直接包含的数据值(参考 Output Section Data)
- 一个特殊输出段关键字(参考 Output Section Keywords)
3.6.2 Output Section Name
输出段的名字是section。section必须符合你的输出格式的规定。在仅支持一个有限段数目的格式中,例如a.out,名字必须为该格式支持的段名称之一(例如a.out格式仅允许’.text’,’.data’,’.bss’)。如果输出格式支持任意数量的段,但名称仅能为编号(例如Oasys),名字应该以双引号包裹的数字字串形式提供。一个段名字可能由任何字符组成,但一个含有许多特殊字符例如逗号等的名字需要被双引号括起来。
输出段名称’/DISCARD/’有特殊含义; 参考Output Section Discarding.
3.6.3 Output Section Address
地址(address)是一个输出段VMA(虚地址)的表达式。此地址为可选参数,但如果给出了地址,则输出地址就会被精确的设置到给定值。
如果输出的地址没有给定,则依照下面的尝试选择一个地址。此地址将会被调整到符合输出端要求的对齐地址。输出段的对齐要求是所有输入节中含有的对齐要求中最严格的一个。
输出段地址探索如下:
- 如果为段设置了内存区域,则段被放如该区域,并且段地址为区域中的下一个空闲位置。
- 如果使用MEMORY命令创建了一个内存区域列表,此时第一个属性匹配段的区域被选择来加载段,段地址为区域中的下一个空闲位置。参见MEMORY。
- 如果没有指定的内存区域,或者没有匹配段的,则输出地址将会基于当前位置计数器的值。
例如:
.text . : { *(.text) }
以及
.text : { *(.text) }
有着精细的差别。第一个将会把’.text’的地址设置为位置计数器。第二个将会依照所有’.text’输入段中最严格的对齐要求,设置地址为当前位置计数器对齐的值。(第一个有address,第二个没有,启动了探索机制)
地址可以是任何表达式;参考Expressions。例如,如果你想在0x10字节边界上对齐段,因此段地址最低的四个位(原文是bit,不是byte,有的翻译有错误)为0,你可类似如下面这么做:
.text ALIGN(0x10) : { *(.text) }
此代码可以工作,因为ALIGN将会返回位置计数器依照参数向上对齐的值。
为段指定地址将会改变位置计数器的值,如果该段不是空段的话。(空段被忽略)
3.6.4 Input Section Description
最常见的输出段命令(output-section-command)是输入段描述。
输入段描述是最基本的链接脚本操作。你使用输出段告诉链接器如何把程序放到内存中。你使用输入段描述告诉链接器如何把输入文件映射到你的内存布局。
- Input Section Basics: 基本的输入段
- Input Section Wildcards: 输入段通配符模板
- Input Section Common: 普通符号的输入段
- Input Section Keep: 输入段与垃圾回收
- Input Section Example: 输入段例子
3.6.4.1 Input Section Basics
一个输入段描述由跟随在段名称后面括号包含的一个可选的文件名称列表构成。
文件名和段名称可以为通配符,我们将在后面解释(参考Input Section Wildcards)。
最普通的输入段描述为在一个特定输出段内包含所有输入段。例如,把所有输入段放入’.text’段,可以这么写:
*(.text)
此处的’*’是一个通配符,可以匹配任何文件名。如果想从通配符匹配的文件列表中排除一系列文件,可以使用EXCLUDE_FILE。例如:
*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)
会让除了以crtend.o和otherfile.o结尾的所有文件的.ctors段被包含。
有两种方法包含更多的段:
*(.text .rdata)
*(.text) *(.rdata)
两种方法的区别是输入段的’.text’和’.rata’段出现在输出中的顺序。第一个例子里,他们将被混合在一起,按照链接器找到它们的顺序存放。另一个例子中,所有’.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列表中,但不能出现在其他链接脚本内部。例如,你不能使用’archive:file’从INPUT命令中取出一个库相关的文件。
如果你使用一个文件名而不指出段列表,则所有的输入文件的段将被放入输出段。通常不会这么做,但有些场合比较有用,例如:
data.o
当你使用一个文件名且不是'archive:file’特殊命令,并且不含任何通配符,链接器将先查看你是否在命令行上或者在INPUT命令里指定了改文件。如果没有这么做,链接器将尝试将文件当作输入文件打开,就像文件出现在了命令行一样。注意与INPUT命令有区别,因为链接器不会在库文件路径搜索文件。
3.6.4.2 Input Section Wildcard Patterns
在一个输入段描述中,文件名和段名都可以使用通配符。
许多例子中的文件名’*’是一个最简单的文件名通配符。
通配符模板类似于Unix shell中使用的那样。
- ‘*’匹配任意数量字符
- ‘?’匹配任意单字
- ‘[chars]’匹配一个简单的所有chars包含的字符;’-‘字符可被用来指出一个字符的范围,例如’[a-z]’可以用来匹配所有小写字母
- ‘\’引用后面的字符
当一个文件名与通配符进行匹配,通配符不会匹配一个’/’字符(被Unix用来分隔目录)。一个仅含’*’的模板是例外,其将永远匹配任和文件名,无论其是否含有’/’。在段名称部分,通配符会匹配’/’字符。
文件名通配符仅对那些显示在命令行或者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数字属性值升序排列后放入输出文件。
SORT是SORT_BY_NAME的别名。
当链接脚本中有网状排序结构时,最多允许1级的网结构用作段排序命令。
- 1.SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern))。将会先把输入段按照名字排序,此后如果两个段名字相同按照对齐方式排序。
- SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern))。将会先把输入段按照对齐方式排序,此后如果两个段名字相同按照名字排序。
- SORT_BY_NAME (SORT_BY_NAME (wildcard section pattern))。被当作SORT_BY_NAME (wildcard section pattern)。
- SORT_BY_ALIGNMENT (SORT_BY_ALIGNMENT (wildcard section pattern))。被当作SORT_BY_ALIGNMENT (wildcard section pattern)。
- 其他所有网状段排序命令都为无效命令。
当命令行段排序选项和链接脚本段排序命令都被使用时,排序命令优先于命令行选项。
如果脚本中的段排序命令不是网状的,则命令行选项将使段排序被当作网状排序使用。
- SORT_BY_NAME (wildcard section pattern ) 与 –sort-sections alignment 等价于 SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern)).
- SORT_BY_ALIGNMENT (wildcard section pattern) 与 –sort-section name 等价于 SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern)).
如果脚本的排序命令已经是网状的,则命令行选项被忽略。
SORT_NONE 禁止段排序,忽略命令行的排序选项。
如果你对输入段被放置到何处感到困惑,使用’-M’链接选项来生成映射文件,映射文件详细的说明了输入段具体被映射到输出段的哪里。
这个例子显示了通配符如何被用来分隔文件。这个链接脚本指引链接器把所有’.text’段放在’.text’里,以及所有’.bss’放到’.bss’中。链接器将会把所有以大写字母开头的文件的’.data’段放入’.DATA’,其他文件的’.data’段放入’.data’。
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
3.6.4.3 Input Section for Common Symbols
普通符号需要一个特别的标记,因为很多目标文件格式中没有特定的普通符号输入段。链接器把普通符号当作位于一个名为’COMMON’的输入段内。
你可以使用文件名与’COMMON’段的组合就像使用其它文件名与段一样。你可以用这种方法把一个特定文件的普通符号放入一个段内,同时把其它输入文件的普通符号放入另一个段内。
大多数情况下,输入文件的普通符号会被放到输出文件的’.bss’段里面。例如:
.bss { *(.bss) *(COMMON) }
有些目标文件格式含有多种普通符号的类型。例如,MIPS ELF目标文件把标准普通符号和小普通符号区分开来。在这种情况下,链接器会为另一个类型的普通符号使用其它的特殊段名称。在MIPS ELF中,链接器为普通符号使用’COMMON’以及为小普通符号使用’.scommon’。这样就可以把不同类型的普通符号映射到内存中的不同位置。
有时在老的链接脚本中能看见’[COMMON]’。这个标记现在已废弃。它等价于’*(COMMON)’。
3.6.4.4 Input Section and Garbage Collection
当使能了链接时垃圾收集(‘–gc-sections’),把段标记为不应被消除非常常用。此功能通过把一个输入段的通配符入口使用KEEP()实现,类似于KEEP((.init))或KEEP(SORT_BY_NAME()(.ctors))。
3.6.4.5 Input Section Example
下面是一个完整的链接脚本的例子。它告诉链接器从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)
}
}
3.6.5 Output Section Data
你可以通过使用输出段命令BYTE, SHORT, LONG, QUAD, 或者 SQUAD在输出段显式的包含几个字节的数据。每个关键字后面跟着一个括号包裹的表达式指出需要存储的数值(参照Expressions)。表达式的值被存储在当前位置计数器值的地方。
BYTE, SHORT, LONG, QUAD命令分别存储1,2,4,8字节。在存储字节后,位置计数器会按照存储的字节数增加。
例如,下面将会存储一个单字节数据1,然后存储一个四字节数据,值为符号’addr’的值:
BYTE(1)
LONG(addr)
使用64位主机或目标时,QUAD和SQUAD是一样的,都是存储8个字节,或64位的值。当主机和目标都是32位时,表达式被当作32位计算。在这种情况下QUAD存储一个32位的值,并使用0扩展到64位,SQUAD保存32位值并使用符号位扩展到64位。
如果输出文件的目标文件格式有显示的大小端,在正常的情况下,值将按照大小端存储。当目标文件格式没有显示的大小端,确实有这种情况,例如,S-records,值将被按照第一个输入目标文件的大小端存储。
注意这些命令仅在段描述内部工作,因此下面的例子会使链接器产生错误:
SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
下面是正确的例子:
SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }
可以使用FILL命令来设置当前段的填充模板。该命令后面跟着一个括号包裹的表达式。所有其它没有被特别指定段的内存区域(例如因为对齐需要而留出来的缝隙)按照表达式的值填充,如果有必要可以重复填充。FILL声明仅会覆盖它本身在段定义中出现的位置后面的所有内存区域;通过使用不同的FILL声明,你可以在一个输出段中使用不同的填充模板。
这个例子显示了如何使用’0x90’填充未定义内存区域:
FILL(0x90909090)
FILL命令类似’=fillexp’输出段属性,但其仅影响FILL命令后面的段,而不是整个段。如果同时使用,FILL命令为高优先级。参考See Output Section Fill获取更多填充细节。
3.6.6 Output Section Keywords
有两个关键字可以作为输出段的命令。
CREATE_OBJECT_SYMBOLS
此命令告诉链接器为每个输入文件创建一个符号。每个符号的名字为对应输入文件的名字。每个符号出现的位置位于包含CREATE_OBJECT_SYMBOLS命令的输出段中。
这个命令一直是a.out目标文件格式特有的。 它一般不为其它的目标文件格式所使用。
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’运行析构函数,或者直接从函数’exit’中运行。
对于COFF或者ELF等支持任意段名字的目标文件格式,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__ = .;
如果你正在使用GUN C++支持的初始化优先级,初始化优先级提供了一些对全局构造函数运行顺序的控制,则你必须在链接时对构造函数排序以保证它们以正确的顺序执行。当你使用CONSTRUCTORS命令,使用’SORT_BY_NAME(CONSTRUCTORS)’替换它。当使用.ctors和.dtors段,使用’*(SORT_BY_NAME(.ctors))’和’*(SORT_BY_NAME(.dtors))’取代’*(.ctors)’和’*(.dtors)’。
通常编译器和链接器将会自动处理这些问题,并且你无需自己关注这些。但是,在你自己写链接脚本且正在使用C++的时候,你可能需要考虑这些。
3.6.7 Output Section Discarding
链接器通常不会创建没有内容的输出段。这是为了方便引用那些有可能出现或者不出现任何输入文件中的段。例如:
.foo : { *(.foo) }
将会仅当至少有一个输入文件含有’.foo’段且’.foo’段不为空的时候才会在输出文件创建一个’.foo’段。其它链接脚本指出在一个段中间分配空间也会创建输出段。此外也会为’.’分配,即便此分配没有空间,除了’. = 0’,;. = . + 0’,;. = sym’,’. = . + sym’,’. = ALIGN (. != 0, expr, 1)’其中’sym’是一个值为0的已定义绝对符号。因此你可以强制一个空的输出段使用’.=.’。
链接器会忽略在抛弃的输出段内的地址设置(参考Output Section Address),除非链接脚本在输出段内定义了符号。这种情况下链接器会遵守地址赋值,有可能更新’.’即便段被抛弃了。
特殊输出段名称’/DISCARD/’可能被用来抛弃输入段。一个被分派到名为’/DISCARD/’的输出段的输入段将不会被包含在输出文件中。
3.6.8 Output Section Attributes
此前我们显示了完整的输出段描述看起来像这样:
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align)]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]
我们已经解释了section, address,以及output-section-command。在这章里我们将解释剩下的段属性。
- Output Section Type: 输出段类型
- Output Section LMA: 输出段LMA(加载地址)
- Forced Output Alignment: 强制输出对齐
- Forced Input Alignment: 强制输入对齐
- Output Section Constraint: 输出段限制
- Output Section Region: 输出段区域
- Output Section Phdr: 输出段phdr
- Output Section Fill: 输出段填充
3.6.8.1 Output Section Type
每个输出段都可能有个类型。类型为括号中的一个关键字。下面是已定义的类型:
NOLOAD
该段被标记为不要加载,因此程序运行时其将不会被加载到内存中。
**DSECT
COPY
INFO
OVERLAY**
这些类型名为了向下兼容,很少被使用。他们都具有同样的效果:段应被标记为不可分配,因此程序运行时不会为此段分配内存。
链接器通常基于映射到输出段的输入段的属性设置属性。你可以使用段类型重载这个属性。例如,在下面的脚本例子里,’ROM’段被定位在地址0,且在程序运行时不会被加载。
SECTIONS {
ROM 0 (NOLOAD) : { ... }
...
}
3.6.8.2 Output Section LMA
每个段都有一个虚拟地址(VMA)以及一个加载地址(LMA);参考Basic Script Concepts。虚地址参见前面的Output Section Address。加载地址由AT或者AT>关键字设置。指出加载地址为可选的命令。
AT关键字把一个表达式当作自己的参数。这将指定段的实际加载地址。关键字AT>使用内存区域的名字作为参数。参考MEMORY。段的加载地址被设置为该区域的当前空闲位置,并且按照段对齐要求对齐。
如果没有为可分配段使用AT和AT>,链接器会使用下面的尝试方式来决定加载地址:
- 如果段有一个特定的VMA地址,则LMA也使用该地址。
- 如果段为不可分配的则LMA被设置为它的VMA。
- 否则如果可以找到符合当前段的一个内存区域,且此区域至少包含了一个段,则设置LMA在那里。如此VMA和LMA的区别类似于VMA和LMA在该区域的上一个段的区别。
- 如果没有声明内存区域且默认区域覆盖了整个地址空间,则采用前面的步骤。
- 如果找不到合适的区域或者没有前面存在的段,则LMA被设置为等于VMA。
这个特性被设计成方便创建一个ROM镜像。例如,下面的链接脚本创建了三个输出段:一个叫做’.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镜像复制到运行时地址。注意这些代码是如何利用好连接脚本定义的符号的。
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;
3.6.8.3 Forced Output Alignment
你可以使用ALIGN增加输出段的对齐。作为替换,你可以通过ALIGN_WITH_INPUT属性强制VMA与LMA自始至终保持它们之间的区别。
3.6.8.4 Forced Input Alignment
你可以使用SUBALIGN强制输入段依照输出段对齐。给出的值将会重载输入段的设置,无论比原来大还是小。
3.6.8.5 Output Section Constraint
你可以特定一个输出段只有在所有输入段都为只读的情况下才能生成,或者所有输入段都是可读写的,分别对应ONLY_IF_RO和ONLY_IF_RW。
3.6.8.6 Output Section Region
可以使用’>region’把一个段指定到此前设置的内存区域内。参见MEMORY。
下面是一个例子:
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }
3.6.8.7 Output Section Phdr
你可以使用’:phdr’把一个段指定到此前定义的程序段内。参考PHDRS。如果一个段被分派到一个或者更多的片断中,则所有的后续可分配段将被同样分配到这些地方,除非显式使用了:phdr修饰语。你可以使用:NONE告诉链接器不要把段放到任何片断中。
下面是一个例子:
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
3.6.8.8 Output Section Fill
你可以使用’=fillexp’为整个段设置填充模板。fillexp是一个表达式(参考Expressions)。任何其它的未被特殊指定的输出段的内存区域(例如,因为对其输入段产生的缝隙)将会被用fillexp的值填充,如果有需要可以重复填充。如果表达式是一个简单的hex数字,例如一个十六进制数字由’0x’开头且结尾没有’k’或’M’,则一个任意长的十六进制数字可以被用来给填充模板赋值,前面的0同样成为模板的一部分。在其它情况中,包含额外的括号或者一个一元+,填充模板为表达式值的最低4个有意义的字节。在所有情况中,数字总是大端的。
你也可以使用FILL命令设置填充值(参考Output Section Data)。
下面是一个例子:
SECTIONS { .text : { *(.text) } =0x90909090 }
3.6.9 Overlay Description
一个覆盖描述提供了一种简单的方法用于描述一个要被作为一个单独内存映像的一部分载入内存,但是却要在同一个内存地址运行的段。在运行时,一些种类的覆盖管理器将会根据需要把覆盖段复制进入或者移出运行时内存,可能仅是简单的处理内存位。这个功能可能很有用,例如,当某个内存区域比其它区域快的多。
覆盖描述使用OVERLAY命令。OVERLAY命令和SECTIONS命令一起使用,就像一个输出段描述符。完整的OVERLAY命令的语义如下:
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] [,]
所有的部分都是可选的,除了OVERLAY(关键字),以及每个段都必须有一个名字(上面的secname1和secname2)。使用OVERLAY结构定义的段类似于那些普通的SECTIONS中的结构(参考SECTIONS),除了OVERLAY中不能为段定义地址和内存区域。
结尾的逗号可能会被使用,如果使用了fill且下一个sections-command看起来像是表达式的延续。
所有的段都使用同样的开始地址定义。所有段的载入地址都被排布,使它们在内存中从整个’OVERLAY’的载入地址开始都是连续的(就像普通的段定义,载入地址是可选的,缺省的就是开始地址;开始地址也是可选的,缺省是当前的位置计数器的值)。
如果使用了关键字NOCROSSREFS,并且在任何段间有互相引用,链接器将会产生一个错误报告。因为所有的段运行在同样的地址,直接引用其它的段通常没有任何意义。参考NOCROSSREFS。
每个伴随OVERLAY的段,链接器自动提供两个符号。符号__load_start_secname被定义为段的起始地址。符号__load_stop_secname被定义为段结束地址。任何不符合C定义的伴随secname的字符都将被移除。C(或者汇编)代码可以使用这些符号在需要时搬移复盖代码。
覆盖之后,位置计数器的值设置为覆盖的起始值加上最大段的长度。
下面是例子,请记住这应该放在SECTIONS结构内。
OVERLAY 0x1000 : AT (0x4000)
{
.text0 { o1/*.o(.text) }
.text1 { o2/*.o(.text) }
}
这将把’.text0’和’.text1’的起始地址设置为地址0x1000。’.text0’的加载地址为0x4000,’.text1’会加载到’.text0’后面。下面的符号如果被引用则会被定义: __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));
3.7 MEMORY Command
链接器默认的设置允许分配所有可用的内存。你通过MEMORY命令可以重载这些。
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’开始有256k字节,另一个从’0x40000000’开始,由4兆字节。链接器把所有没有显式映射到一个内存区域的段放到’rom’内存区域内,段可以是只读的或者可执行的。链接器将把其它没显式指定内存区域映射的段放到’ram’内存区域。
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
一旦你定义了一个内存区域,你可以使用’>region’输出段属性指引链接器把特殊输出段放到该内存区域。例如,如果你拥有一个内存区域名为’mem’,你可以在输出段定义中使用’>mem’。参考Output Section Region。如果没有给输出段指出地址,链接器将会把地址放到最先符合要求的内存区域中的可用地址。如果指引给一个内存区域的组合输出段比区域还大,链接器将会提交错误。
可以通过ORIGIN(memory)和LENGTH(memory)函数获得内存区域的起始地址以及长度:
_fstack = ORIGIN(ram) + LENGTH(ram) - 4;
3.8 PHDRS Command
ELF格式的目标文件使用程序头,类似于段.程序头描述程序应当如何加载到内存中。你可以使用objdum程序的’-p’选项将其显示出来。
在运行本地ELF程序的时候,加载系统读取程序的头部来找出如何加载。只有当程序头设置正确时程序才能工作。本手册不讲述系统如何通过解释头部加载程序,更多的信息,参考ELF ABI。
默认的链接器将会创建合适的程序头部。但是,有些情况下,你可能需要更加精确的方法来指出程序头。可以使用PHDRS命令达到此目的。如果连接器发现命令脚本中有PHDRS命令,就只会创建命令指定的程序头。
链接器仅在创建ELF输出文件时才会关注PHDRS命令。其他情况下链接器将会忽视PHDRS。
下面是PHDRS的语法。PHDRS, FILEHDR, AT, FLAGS都是关键字。
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS ( flags ) ] ;
}
name仅用来参照链接脚本中的SECTIONS命令。将不会被放入输出文件中。程序头名字用空格分隔存储,不会与符号名,文件名,或者段名产生冲突。每个程序头的名字必须互相可以区分。头按照顺序执行,且通常将它们以上升的加载顺序映射到段。
具体的程序头types描述系统加载器将会从文件中加载的头部段。在链接脚本内,可以通过放置可再分配输出段在头部段内来指定头部段的内容。使用’:phdr’输出段属性可以把一个段放入一个特定的头部段。参考See Output Section Phdr。
通常把一个段放入多于一个的头部段。这仅仅暗示一个头部段包含了其他的。可以为每个应当包含段的头部段重复使用’:phdr’命令。
如果使用’:phdr’把一个段放入了一个或者多个头部段,则链接器将会把所有后续的没使用’:phdr’的可再分配段都放入该头部段。这么做是为了方便,因为通常一整个连续段的集合会放在一个头部段内。可以使用:NONE来覆盖默认头部段并告诉连链接器不要将段放在任何头部段。
可以使用FILEHDR和PHDRS关键字在程序头类型(type)后面,用来进一步形容头部段的内容。FILEHDR告诉头部段需要被包含在ELF文件头内。PHDRS表示头部段应当自身包含程序头。如果应用了可加载头部段(PT_LOAD),所有此前的可加载程序段必须有一个前面的关键字。
类型可能是下面的值,数字表示关键字的值。
- PT_NULL (0)表示未使用的程序头。
- PT_LOAD (1)表示此程序头描述了一个从文家中加载的头部段。
- PT_DYNAMIC (2)表示一个可以找到动态链接信息的头部段。
- PT_INTERP (3)表示包含程序解释器的名字的头部段。
- PT_NOTE (4)表示一个含有note信息的头部段。
- PT_SHLIB (5)保留,未使用在ELF ABI。
- PT_PHDR (6)表示可以找到程序头的头部段。
- expression 给出程序头数字类型的一个表达式。可以被上面没有定义的类型使用。
你可以使用AT表达式指定一个头部段加载到一个特定的内存位置。这与AT作为输出段使用属性时的方法一样(参考Output Section LMA)。程序头的AT命令会覆盖输出段属性。
链接器通常根据组成头部段的段标志设置头部段。可以用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
...
}
3.9 VERSION Command
使用ELF时,链接器支持符号版本。符号版本仅在使用共享库的时候有意义。在一个程序可能会使用一个早期共享库版本时,动态链接器可以使用符号版本来选择函数的一个特殊版本。
可以在主链接脚本中直接包含一个版本脚本,或者以一个隐式连接脚本的形式提供这个版本脚本。也可以使用’–version-script’链接器选项。
VERSION命令语法很简单:
VERSION { version-script-commands }
版本脚本命令与Solaris2.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::*;
"f(int, double)";
};
} VERS_1.2;
这个版本脚本的例子定义了3个版本节点。第一个版本节点定义是’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中做检查时,会有更加精心考虑的功能。基本问题是,在被寻址时,通常会有一个外部函数的引用被一个as-needed基础绑定,但应用启动时不是所有都绑定了。如果一个共享库过期了,一个需要的接口可能会丢失;当应用尝试使用该接口,可能会突然发生未预期的失败。使用了符号版本,如果应用使用的库版本太老用户会在启动程序时获得一个警告。
GNU对Sun的版本确认办法有一些扩展。首先就是能在符号定义的源文件中把一个符号绑定到一个版本节点而不是在一个版本脚本中。这主要是为了减轻库维护的工作量。你把类似下面的代码:
__asm__(".symver original_foo,foo@VERS_1.1");
放到C源文件里。这将把函数’original_foo’改为绑定在版本节点’VERS_1.1’的’foo’的别名。’local:’指令可以用来防止符号’original_foo’被导出。一个’.symver’指令优先于版本脚本。
第二个GUN扩展是允许共享库中的同一个函数拥有多个版本。如此你可以实施一个接口无关的修改而无需增加标准库的主版本号,同时仍然允许链接到老版本的应用继续使用旧的接口。
如果要这么做,你必须在源文件使用多个’.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’绑定到没有指定基础版本的符号版本。源文件包含此例子将会定义四个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’中的模板匹配前分解。默认是C。
The supported lang's are
C’, C++', and
Java’. The linker will iterate over the list of symbols at the link time and demangle them according to lang' before matching them to the patterns specified in
version-script-commands’. The default lang' is
C’.
被分解的名字可能含有空格以及其他特殊字符。按照上面说的,可以使用正则表达式模板匹配分解的名字,或者可以使用双引号包裹的字符串来精确匹配字符串。在后一种情况中,注意位于版本脚本和分解输出间一个小的不同(比如空格)将会引起不匹配。分解器创建的字符串在未来可能会改变,即便将被重新组合的名字本身没变,在升级版本时你需要检查所有的版本指令是否都按照你期待的那样工作。
3.10 Expressions in Linker Scripts
链接脚本中的表达式语法与C中的类似。所有的表达式都按照整型计算。所有表达式的结果字节数一致,对于主机和目标都是32位的情况为32位,其他情况是64位。
可以在表达式内使用及设置符号的值。
链接器定义了几个特别使用目的的内建函数可以用在表达式内。
- Constants: 常数
- Symbolic Constants: 符号常树
- Symbols: 符号名称
- Orphan Sections: 孤儿段
- ocation Counter: 位置计数器
- Operators: 运算符号
- Evaluation: 求值
- Expression Section: 一个表达式的段
- Builtin Functions: 内建函数
3.10.1 Constants
所有的常数都是整型的。
类似C语言,链接器将’0’开头的整数当作8进制,以’0x’或’0X’开头的整数被当作16进制。注意链接器接受16进制的’h’或者’H’后缀,8进制的’o”O’,二进制的’b”B’,十进制的’d”D’。一个没有前缀或者后缀的整数被当作十进制。
此外,可以使用K和M后缀来缩放一个常数1024或者1024*1024倍。例如,下面的所有表达式的值相同:
_fourk_1 = 4K;
_fourk_2 = 4096;
_fourk_3 = 0x1000;
_fourk_4 = 10000o;
注意,K和M后缀不能与前面的其他系数同时使用。
3.10.2 Symbolic Constants
可以通过使用CONSTANT(name)操作符引用一个目标特定的常数,name为下面之一:
- MAXPAGESIZE:目标的最大页大小。
- COMMONPAGESIZE:目标的默认页大小。
因此例如:
.text ALIGN (CONSTANT (MAXPAGESIZE)) : { *(.text) }
将会创建一个对齐到目标支持的最大页边界的代码段。
3.10.3 Symbol Names
除非加了引号,符号名都是以一个字母,下划线或者点号开始,可以包含字母,数字,下划线,点点和连接号。没加引号的符号名不能与任何关键字重复。你可以使用引号让一个符号有两个字或者与关键字同名:
"SECTION" = 9;
"with a space" = "also with a space" + 10;
鉴于符号可以包含很多非字母的字符,使用空格分隔符号是安全的做法。例如’A-B’是一个符号,而’A - B’是一个减法表达式。
3.10.4 Orphan Sections
孤儿段是出现在输入文件中,且链接脚本没有显示指定需要放到输出文件什么位置的输入段。链接器仍将把这些段复制到输出文件,但必须猜测需要放到哪。链接器使用一个简单的尝试做这个。它尝试把孤儿段放到同属性的非孤儿段后面,例如code vs data,loadable vs non-loadable。如果没有足够的空间存放,则把其存放到文件尾部。
对于ELF目标,段属性包含了段类型及段标志。
命令行选项’–orphan-handling’和’–unique’ (参考Command Line Options) 可以控制孤儿段放入哪个输出段。
如果孤儿段的名字类似于C语言的定义,则链接器会自动PROVIDE(参考PROVIDE)两个符号:__start_SECNAME和__stop_SECNAME,SECNAME是段的名字。这表示了孤儿段的起始和结束地址。注意:很多段名称并不类似于C语言因为他们含有’.’字符。
3.10.5 The Location Counter
特殊链接符号’.’通常包含当前输出位置计数。因为’.’经常当作一个输出段的地址使用,因此它只能位于SECTIONS命令中以一个表达式形式出现。任何普通符号可以出现在表达式中的位置都可以使用’.’。
为’.’赋值将会使得位置计数器移动。这可以用来在输出段中创建空洞。位置计数器不能在一个输出段内向回移动,也不能在段外回退,如果这么做了将会创建重叠的LMA。
Assigning a value to . will cause the location counter to be moved. This may be used to create holes in the output section. The location counter may not be moved backwards inside an output section, and may not be moved backwards outside of an output section if so doing creates areas with overlapping LMAs.
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0x12345678;
}
在前面的例子里,文件file1的’text’段位于输出段output的起始位置。其后有个1000字节的缝隙。此后file2的’.text’段出现在输出段内,其后也有1000字节的缝隙,最后是file3的’.text’段。标记’=0x12345678’指定了应当向缝隙中填充的内容(参考Output Section Fill)。
注意:’.’实际上引用了当前容器目标开头的第一个字节地址。通常为SECTIONS声明,起始地址为0,因此’.’可以被当作一个绝对地址使用。但是如果’.’被在段描述符内使用,它表示从该段开始的偏移地址,不是一个绝对地址。因此在下面脚本中:
SECTIONS
{
. = 0x100
.text: {
*(.text)
. = 0x200
}
. = 0x500
.data: {
*(.data)
. += 0x600
}
}
‘.text’段将会被安排到起始地址0x100,实际大小为0x200字节,即便’.text’输入段没有足够的数据填充该区域(反之如果数据过多,将会产生一个错误,因为将会尝试向前回退’.’)。段’.data’将会从0x500开始,并且输出段会有额外的0x600字节空余空间在输入段’.text’。
如果链接器需要放置孤儿段,则在输出段声明外把位置计数器的值赋给符号可能会带来与预期的值不同的结果。例如下面的例子:
SECTIONS
{
start_of_text = . ;
.text: { *(.text) }
end_of_text = . ;
start_of_data = . ;
.data: { *(.data) }
end_of_data = . ;
}
如果链接器需要放置一些输入段,例如’.rodata’没有在脚本中提及,可能会被选择放到’.text’和’.data’段中间。你可能会觉得链接器应该把’.rodata’放在上面脚本的空行处,但空行对于链接器来说没有任何实际意义。同样的,链接器也不会把符号名与段联系起来。实际上,它假设所有定义或者其他声明属于前面的输出段,除了特殊情况设定’.’。例如,链接器将会类似于下面的脚本放置孤儿段:
SECTIONS
{
start_of_text = . ;
.text: { *(.text) }
end_of_text = . ;
start_of_data = . ;
.rodata: { *(.rodata) }
.data: { *(.data) }
end_of_data = . ;
}
注:链接器认为start_of_data是上一个段的符号,并将’.rodata’放入’.text’和
这能符合或者不符合脚本作者对于start_of_data的设置意图。一种影响孤儿段放置的办法是为位置计数器指定自身的值,链接器会认为一个’.’的设置是设定一个后面段的起始地址,因此该段应为一个组。因此可以这么写:
SECTIONS
{
start_of_text = . ;
.text: { *(.text) }
end_of_text = . ;
. = . ;
start_of_data = . ;
.data: { *(.data) }
end_of_data = . ;
}
如此,孤儿段’.rodata’将会被放置在end_of_text和start_of_data之间。
3.10.6 Operators
链接器可以识别所有的标准C语言的数学运算符,以及他们的标准绑定和优先级:
precedence associativity Operators Notes
(highest)
1 left ! - ~ (1)
2 left * / %
3 left + -
4 left >> <<
5 left == != > < <= >=
6 left &
7 left |
8 left &&
9 left ||
10 right ? :
11 right &= += -= *= /= (2)
(lowest)
Notes: (1) 前缀操作符 (2) 参考Assignments.
3.10.7 Evaluation
链接器采用懒惰策略计算表达式的值。仅当需要的时候才会计算一个表达式的结果。
链接器需要一些信息,例如第一个段的起始地址,以及内存区域的起始地址和长度等,才能够完成所有的链接工作。这些值会在链接器读链接脚本的时候立即计算。
但是其他的值(例如符号值)在存储分配之后才能知道或者需要。这种值将会推迟计算,直到符号赋值表达式的其他信息(例如输出段的大小)都可获得后。
直到分配后才能知道段的大小。因此依赖它的赋值都将在分配后才会执行。
一些表达式,例如那些依赖位置计数器’.’的,必须在段分配间计算。
如果需要一个表达式的结果,但其还未有有效值,则会产生一个错误,例如,下面的脚本:
SECTIONS
{
.text 9+this_isnt_constant :
{ *(.text) }
}
将会出发错误信息’non constant expression for initial address’。
3.10.8 The Section of an Expression
地址和符号可以是段相关或者绝对的。一个段相关符号是可重分配的。如果你使用’-r’命令要求一个可重分配输出,后面的链接操作可能会改变段相关符号的值。而绝对符号将会在任何链接操作中保持一致的值。
一些链接器表达式的形式可能为地址。这种情况确实存在于段相关符号以及可以返回地址的内建函数中,函数例如ADDR, LOADADDR, ORIGIN,以及 SEGMENT_START。其他形式就是简单的数字,或者其他的不返回地址的内建函数,例如LENGTH。一个复杂的情况是设置了LD_FEATURE (“SANE_EXPR”)(参考Miscellaneous Commands),数字和绝对符号取决于他们的位置被区别对待,为了兼容旧的ld版本。在输出段定义外面出现的所有表达式将所有数字当作绝对地址。在输出段定义内部出现的表达式将绝对符号当作数字。如果使用了LD_FEATURE (“SANE_EXPR”),则任何位置的绝对符号和数字都被简单的当作数字。
在下面的简单例子里,
SECTIONS
{
. = 0x100;
__executable_start = 0x100;
.data :
{
. = 0x10;
__data_start = 0x10;
*(.data)
}
...
}
在前两个赋值中,’.’和’__executable_start’都被设置为绝对地址0x100,在后两个赋值中,’.’和’__data_start’被设置为相对于’.data’的0x10。
对于包含了数字,相对地址,以及绝对地址的表达式,ld依照下面的规则计算形式:
- 操作绝对地址或数字的一元运算符,以及操作两个绝对地址或者两个数字的二元运算符,或者介于绝对地址和数字间的二元运算符,在数值上应用运算符。
- 操作相对地址的一元运算符,以及操作两个位于一个段内相对地址的二元运算符,或者一个是相对地址一个是数字的二元运算符,使用偏移地址来使用运算符。
- 其他二元运算符,例如,操作两个不在一个段内的相对地址或者一个相对地址一个绝对地址,首先将非绝对地址的转化为绝对地址形式,再应用运算符。
每个子表达式的段结果如下:
- 只有数字参与的运算符结果为数字。
- 比较运算’&&’和’||’的结果也是数字。
- 其他二元数学和逻辑运算符操作两个同一个段的相对地址或者两个绝对地址(在上面的转换后)结果也是数字。
- 其他运算符操作相对地址或者一个相对地址和一个数字的结果, 在同一个段内被当作相对操作数使用。
- 其他操作绝对地址的运算符(转换后)的结果是绝对地址。
可以使用内建函数ABSOLUTE来强制一个本来是相对地址的表达式变为绝对地址。例如,创建一个绝对地址符号并设置为输出段’.data’的结束地址:
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
如果不使用’ABSOLUTE’,’_edata’将会为’.data’段的相对地址。
使用LOADADDR也会强制一个表达式变为绝对地址,因为此特殊内建函数返回一个绝对地址。
3.10.9 Builtin Functions
链接脚本语言有一系列内建函数可以在链接脚本表达式内使用。
ABSOLUTE(exp)
返回表达式exp的绝对(非可重分配的,而不是非负)值。主要用来在段定义内为符号分配一个绝对值,通常段定义内的符号值都是相对段地址的。参考Expression Section。
ADDR(section)
返回名为’section’的段的地址(VMA)。你的脚本必须事先未该段定义了位置。在下面的例子里,start_of_output_1, symbol_1, symbol_2分配了同样的值,除了symbol_1为与段.output1相关的值而其他两个为绝对值:
SECTIONS { ...
.output1 :
{
start_of_output_1 = ABSOLUTE(.);
...
}
.output :
{
symbol_1 = ADDR(.output1);
symbol_2 = start_of_output_1;
}
... }
**ALIGN(align)
ALIGN(exp,align)**
返回位置计数器’.’或者任意表达式对齐到下一个align指定边界的值。单操作数ALIGN不改变位置计数器的值————它仅进行数学运算。双操作数ALIGN允许向上对齐一个任意表达式(ALIGN(align)等价于ALIGN(ABSOLUTE(.), align))。下面是一个例子,将把输出.data段对齐到前面段后面的0x2000字节边界,并且在段中设置一个下一个0x8000边界对齐位置的变量variable:
SECTIONS { ...
.data ALIGN(0x2000): {
*(.data)
variable = ALIGN(0x8000);
}
... }
例子中使用的第一个ALIGN指定了段的位置,ALIGN被当作了段定义的地址属性(参考Output Section Address)。第二个ALIGN用来定义符号的值。
内建函数NEXT与ALIGN关系非常紧密。
ALIGNOF(section)
如果section已分配,返回名为section的对齐字节。如果段还没被分配,链接器会报错。下面的例子里,.output段的对齐存储在该段的第一个值里。
SECTIONS{ ...
.output {
LONG (ALIGNOF (.output))
...
}
... }
BLOCK(exp)
这是’ALIGN’的同义词,是为了与其它的链接器保持兼容。这在设置输出段的地址时非常有用。
DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)
这等价于
(ALIGN(maxpagesize) + (. & (maxpagesize - 1)))
或者
(ALIGN(maxpagesize)
+ ((. + commonpagesize - 1) & (maxpagesize - commonpagesize)))
取决于后面数据段(位于此表达式结果之后以及DATA_SEGMENT_END之间)是否使用比前面更小的commonpagesize大小的页。如果后面的形式被使用了,表示着保存commonpagesize字节的运行时内存时,花费的代价最多浪费commonpagesize大小的磁盘空间。
此表达式仅能直接使用在SECTIONS命令中,不能再任何输出段描述里,且只能在链接脚本内出现一次。commonpagesize应当小于或者等于maxpagesize且应当为目标希望的最合适的系统页面大小(虽然仍然工作在系统页大小为maxpagesize的情况)。
例:
. = DATA_SEGMENT_ALIGN(0x10000, 0x2000);
DATA_SEGMENT_END(exp)
此命令为DATA_SEGMENT_ALIGN运算定义了数据段的结尾。
. = DATA_SEGMENT_END(.);
DATA_SEGMENT_RELRO_END(offset, exp)
此命令为使用’-z relro’命令的情况定义了PT_GNU_RELRO段的结尾。若没有使用’-z relro’,DATA_SEGMENT_RELRO_END不做任何事情,否则DATA_SEGMENT_RELRO_END将被填充,因此exp + offset被对齐到某个目标最常用的页边界。如果出现在链接脚本内,其通常位于DATA_SEGMENT_ALIGN和DATA_SEGMENT_END之间。第二个参数加上任何PT_GNU_RELRO段需要的填充都会导致段对齐。
. = DATA_SEGMENT_RELRO_END(24, .);
DEFINED(symbol)
如果在DEFINED命令出现在脚本前,symbol已经是链接器的全局符号则返回1,否则返回0。你可以使用此函数为符号提供默认的值。例如,下面的脚本片断显示了如果设置一个全局符号’begin’到’.text’段的开头位置————但如果一个叫做’begin’的符号已经存在了,则值被保留。
SECTIONS { ...
.text : {
begin = DEFINED(begin) ? begin : . ;
...
}
...
}
LENGTH(memory)
返回名为memory的内存的长度。
LOADADDR(section)
返回名为section的段的LMA绝对地址。(参考Output Section LMA)。
LOG2CEIL(exp)
返回exp的二进制对数的边界。
MAX(exp1, exp2)
返回exp1和exp2最大的。
MIN(exp1, exp2)
返回exp1和exp2最小的。
NEXT(exp)
返回exp的倍数的下一个未分配地址。此函数与ALIGN(exp)紧密相关;除非你使用MEMORY命令为输出文件定义不连续的内存,否则此两个函数等价。
ORIGIN(memory)
返回名为memory的内存区域的起始地址。
SEGMENT_START(segment, default)
返回名为segment的段的基地址。如果为段显式指定了值(使用命令行命令’-T’)(注:原文这里可能有误,’-T’应当是指定链接脚本)则值将会被返回否则值会使用默认值。在现在,’-T’命令行选项只能被用来设置“text”, “data”, 和“bss”段的基地址,但你可以使用SEGMENT_START搭配任何段名字。
SIZEOF(section)
返回名为section段的字节数。如果段还没被分配就是用函数求值,将会产生错误。下面是一个例子,symbol_1和symbol_2的值相同:
SECTIONS{ ...
.output {
.start = . ;
...
.end = . ;
}
symbol_1 = .end - .start ;
symbol_2 = SIZEOF(.output);
... }
**SIZEOF_HEADERS
sizeof_headers**
返回输出文件头的字节数。这是一个会出现在输出文件的起始位置的信息。你可以使用此数字来设置第一个段的起始地址,如果你想这么做来使分页更加便利的话。
当生成一个ELF输出文件,如果链接脚本使用了SIZEOF_HEADERS内建函数,链接器必须在决定所有段的地址和大小前计算程序头部的数量。如果链接器接下来发现需要额外的程序头,将会产生一个’not enough room for program headers’错误。为了避免这个错误,你必须避免使用SIZEOF_HEADERS函数,或者重新编写链接脚本来避免出现额外的程序头,或者使用PHDRS命令(参见PHDRS)自己定义程序头。
3.11 Implicit Linker Scripts
如果你指定了一个链接输入文件,而链接器无法将其识别为一个目标文件或者库文件,链接器将会尝试将其当作一个链接脚本文件。如果该文件不能被解释为一个链接脚本,链接器将会报错。
隐式链接脚本不会替代默认的链接脚本。
典型的一个隐式链接脚本仅会包含符号赋值,或者INPUT, GROUP, VERSION 命令。
隐式链接脚本会在命令行出现的位置被读取,这会影响库的搜索。