原文
注:文中大部分文章参考引用都是自身的引用,为了不产生混淆,各个章节标题使用英文原称,同时参考引用也用英文原称。
每个链接都由一个链接脚本控制。这个脚本由链接命令语言编写。
链接脚本的主要目的是描述输入文件中的段应当如何映射到输出文件中,并控制输出文件的内存布局。多数链接脚本都执行类似功能。但是,如果需要,链接脚本也可以使用下面所描述的命令指挥链接器进行很多其他操作。
链接器通常使用一个链接脚本。如果没有为其提供一个,链接器将会使用默认的编译在链接器执行文件内部的脚本。可以使用命令’–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");
}
<