最后一小部分翻译自:Builtin Functions (LD)
加上了一些我自己的备注。
链接脚本控制每次链接。这样的脚本是用链接器命令语言编写的。链接脚本的主要目的是描述如何将输入文件中的各个section(节)映射到输出文件中,并控制输出文件的内存布局。然而,在必要时,链接脚本也可以使用链接器命令指示链接器执行许多其他操作。下面的文档将讨论如何使用链接脚本及其命令。
链接器总是使用链接脚本。如果你自己不提供,链接器将使用一个默认的链接脚本,这个脚本被编译进了链接器可执行文件中。你可以使用'--verbose '命令行选项来显示默认的链接脚本。某些命令行选项,如' -r '或' -N ',将影响默认链接脚本。你可以使用' -T '命令行选项提供自己的链接脚本。当这样做时,指定的链接脚本将替换默认的链接脚本。
还可以隐式地使用链接脚本,将它们命名为链接器的输入文件,就像它们是要链接的文件一样。如果链接器打开了一个它不能识别为目标文件或归档文件的文件,它将尝试将其作为链接脚本读取。如果文件不能被解析为链接脚本,链接器将报告一个错误。隐式链接脚本不会取代默认的链接脚本。通常,隐式链接脚本只包含' INPUT '、' GROUP '或' VERSION '命令。
链接脚本的基本概念
为了描述链接脚本语言,我们需要定义一些基本的概念和词汇。
链接器将输入文件组合成一个输出文件。输出文件和每个输入文件都采用一种特殊的数据格式,称为目标文件格式。每个文件称为一个目标文件。输出文件通常称为可执行文件,但出于我们的目的,我们也将其称为目标文件。每个对象文件都有一个节列表。我们有时将输入文件中的一个节称为输入节;类似地,输出文件中的一个节就是一个输出节。
目标文件中的每个节都有一个名称和大小。大多数节也有一个关联的数据块,称为节内容。一个节可以被标记为可加载的,这意味着当输出文件运行时,内容应该被加载到内存中。没有内容的节可能是可分配的(比如.bss节),这意味着内存中应该留出一个区域,但不应该加载任何特别的内容(在某些情况下,该内存必须被置零)。一个既不能加载也不能分配的部分,通常包含某种调试信息。
每个可加载或可分配的输出节都有两个地址。第一个是VMA,即虚拟内存地址。这是输出文件运行时该节将拥有的地址。第二个是LMA,即加载内存地址。这是加载节的地址。在大多数情况下,这两个地址是相同的。它们可能不同的一个例子是,一个数据段被加载到ROM中,然后在程序启动时被复制到RAM中(这种技术经常用于在基于ROM的系统中初始化全局变量)。在这种情况下,ROM地址将是LMA, RAM地址将是VMA。
(译注:比如在一个嵌入式系统中,一般有一个支持XIP (eXecute In Place)的flash作为启动代码的存放位置,对于指令来说它是只读的,所以CPU可以直接读取flash中的指令并执行。但对于非只读的数据来说,程序运行过程中会发生频繁的读写,而flash写入速度很慢,且有写入寿命的问题,所以需要在运行功能代码前需要将非只读数据拷贝到RAM中。)
你可以通过使用带有' -h '选项的' objdump '程序来查看object文件中的节。
译注:比如:
sonny|22:43:42: /mnt/e/test/qemu-arm-baremetal$ objdump -h main.elf
main.elf: file format elf32-little
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000a8 00000000 00000000 00010000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000010 a0000000 000000a8 00020000 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000040 a0000010 000000b8 00020010 2**2
ALLOC
3 .comment 00000031 00000000 00000000 00020010 2**0
CONTENTS, READONLY
4 .ARM.attributes 0000002e 00000000 00000000 00020041 2**0
CONTENTS, READONLY
每个目标文件还有一个符号列表,称为符号表。符号可以是已定义的,也可以是未定义的。每个符号都有一个名称,每个定义的符号都有一个地址,以及其他信息。如果你将一个C或c++程序编译成一个目标文件,你将为每个已定义的函数和全局或静态变量获得一个已定义的符号。输入文件中引用的每个未定义函数或全局变量都将成为一个未定义的符号。你可以使用' nm '程序或使用' objdump '程序和' -t '选项来查看对象文件中的符号。
(译注:比如:
sonny|22:43:51: /mnt/e/test/qemu-arm-baremetal$ nm main.elf
00000000 t $a
0000003c t $a
00000020 t $a
00000090 t $d
a0000000 d $d
a0000000 d arr
a0000010 B arr
00000040 A bss_size
00000050 t copy
00000010 t dabt
00000010 A data_size
a0000050 B ebss
0000001c t fiq
000000a8 T flash_sdata
00000060 t init_bss
00000084 t init_stack
00000018 t irq
00000020 T main
0000000c t pabt
a0000010 D ram_edata
a0000000 D ram_sdata
00000000 t reset
a0000010 D sbss
0000003c t start
0000008c t stop
00000008 t swi
00000004 t undef
00000078 t zero
)
链接脚本的格式
链接脚本是文本文件。你写一个链接脚本就是写一系列命令。每个命令要么是一个可能后面跟着参数的关键字,要么是对符号的赋值。命令可以用分号分隔。空格通常被忽略。
对于字符串,如文件或格式名称,通常可以直接放进去。如果文件名包含逗号等字符,由于这种符号会分隔文件名,为了避免被分隔,可以将文件名放在双引号中。不能在文件名中使用双引号字符。
你可以在链接脚本中包含注释,就像在C中一样,用 /* 和*/ 分隔。与C语言一样,注释在语法上等同于空格。
链接脚本的简单示例
许多链接脚本都相当简单。最简单的链接脚本只有一个命令:' SECTIONS '。可以使用' SECTIONS '命令来描述输出文件的内存布局。SECTIONS命令是一个功能强大的命令。这里我们将描述它的一个简单用法。让我们假设程序只由代码、初始化数据和未初始化数据组成。它们将分别在' .text '、' .data '和' .bss '区域中。让我们进一步假设这些节是在输入文件中出现的唯一的节,没有其他节。对于这个例子,假设代码应该在地址' 0x10000 '加载,并且数据应该从地址' 0x8000000 '开始。下面的链接脚本将实现该功能。
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
你把' SECTIONS '命令写成关键字' SECTIONS ',后面跟着一系列符号赋值和用花括号括起来的输出节描述。上面例子中的第一行设置了特殊符号 '.' ,也就是位置计数器。如果没有以其他方式指定输出节的地址(其他方式将在后面描述),地址将被设置为位置计数器的当前值。然后,位置计数器按输出节的大小递增。第二行定义了一个输出节'. text '。冒号是语法要求必需的,现在可以先不管它。在输出节名称后面的花括号中,列出了输入节的名称,这些名称应该放在这个输出节中。' * '是一个通配符,可以匹配任何文件名。表达式' *(.text)'表示所有输入文件中的' .text '输入节。(译注:即输出节.text由所有输入文件中的.text节组成)
因为当定义输出节'. text '时,位置计数器是' 0x10000 ',链接器将输出文件中'. text '节的地址设置为' 0x10000 '。其余的行定义了输出文件中的' .data '和' .bss '节。'. data '输出节将位于地址' 0x8000000 '。当定义了' .bss '输出节时,位置计数器的值将是' 0x8000000 '加上' .data '输出节的大小。其效果是,' .bss '输出节在内存中将紧跟着' .data '输出节。
就是这样 ! 这是一个简单而完整的链接脚本。
简单的链接脚本命令
在下面的文档中,讨论描述了简单的链接脚本命令。
设置入口点(entry)
程序执行的第一条指令称为入口点。你可以使用' ENTRY '链接脚本命令来设置入口点。参数是一个符号名:
ENTRY ( symbol )
有几种方法可以设置入口点。链接器将依次尝试以下方法来设置入口点,并在其中一个成功时停止:
- -e 入口命令选项(译注:-e是链接器的参数);
- 链接脚本中的ENTRY (symbol) 命令 ;
- 如果符号start定义了,那就是start符号的值;
- 如果存在.text节,那就是.text节的第一个字节的地址;
- 地址0
处理文件的命令
有几个链接脚本命令处理文件。See also Command line options for ld and BFD.
INCLUDE filename
在该位置包含文件名为filename的链接脚本。该文件将在当前目录以及使用' -L '选项指定的任何目录中搜索。你可以最多嵌套调用' INCLUDE '10层。
INPUT (file , file , ...)
INPUT (file file ...)
' INPUT '命令指示链接器在链接中包含命名文件,就像它们是在命令行中命名的一样。
例如,如果你在一个链接过程中随便哪个时候总是想要包含' subr.o',但你总不能麻烦地把它放在每个链接命令行,那么你可以在链接脚本中输入' INPUT (sub.o)'。实际上,如果你愿意,可以在链接脚本中列出所有输入文件,然后只使用' -T '选项调用链接器。链接器将首先尝试打开当前目录中的文件。如果没有找到,链接器将搜索归档库的搜索路径。参见' -L '的描述。如果你使用' INPUT (-l file)', ld将名称转换为' libfile.a ',就像命令行参数' -l '一样。当你在一个隐式链接脚本中使用' INPUT '命令时,在链接脚本文件被包含的地方,文件将被包含在链接中。这可能会影响归档搜索。
GROUP(FILE, FILE, ...)
“GROUP”命令类似于“INPUT”,不同的是命名的文件都应该是归档文件,并且它们会被重复搜索,直到没有新的未定义的引用被创建。
OUTPUT (filename)
' OUTPUT '命令给输出文件命名。使用“OUTPUT (filename)'就像在命令行中使用' -o filename '一样。如果两者都使用,命令行选项优先。
你可以使用' OUTPUT '命令为输出文件定义一个默认名称,而不是通常默认的' a.out '。
SEARCH_DIR (path)
' SEARCH_DIR '命令将路径添加到' ld '查找归档库的路径列表中。使用' SEARCH_DIR (path)'就像在命令行中使用' -L path '一样; see Command line options for ld.。如果两者都使用,那么链接器将搜索这两个路径,并且首先搜索使用命令行选项指定的路径。
STARTUP ( filename )
' STARTUP '命令就像' INPUT '命令一样,除了文件名将成为第一个被链接的输入文件,就像它是在命令行上首先指定的一样。当在一个系统中,使用入口点总是第一个文件的开始,这可能很有用。
处理目标文件格式的命令(Commands dealing with object file formats)
一些链接脚本命令处理目标文件格式。请参见 Command line options for ld and BFD.
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 '命令也将用于设置输出文件的格式。
其他链接脚本命令(Other linker script commands)
还有一些其他的链接脚本命令。请参见 Command line options for ld and BFD.
FORCE_COMMON_ALLOCATION
此命令与' -d '命令行选项具有相同的效果:即使指定了可重定位的输出文件(' -r '),也要让' ld '为common符号分配空间。
NOCROSSREFS( section section ...)
此命令可用于告诉' ld '报告某些节之间的任何引用的错误。
在某些类型的程序中,特别是在嵌入式系统中使用overlays时,当一个节被加载到内存中,另一个节将不会被加载。任何两个部分之间的直接引用都是错误的。例如,如果一个节中的代码调用了另一个节中定义的函数,则会出现错误。' NOCROSSREFS '命令接受一个输出节名称列表。如果' ld '检测到sections之间的交叉引用,它将报告一个错误并返回一个非零的退出状态。记住' NOCROSSREFS '命令使用的是输出节名,而不是输入节名。
OUTPUT_ARCH( bfdarch )
指定一个特定的输出机器架构,bfdarch。该参数是BFD库使用的名称之一。通过使用带有' -f '选项的' objdump '程序,你可以看到一个对象文件的架构。
给符号赋值(Assigning values to symbols)
你可以在链接脚本中给一个符号赋值,这将把这个符号定义一个全局符号。
简单的赋值
你可以使用C的任何赋值操作符给一个符号赋值:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
- 第一种情况将“symbol”定义为“expression”的值。在后面的各个情况中,' symbol '必须已经定义,值将相应地调整。
- 特殊符号名称 '.' (译注:句点)表示位置计数器。你只能在' SECTIONS '命令中使用它。
- expression后面的分号是必需的;
- See Expressions in linker scripts.
- 你可以将符号赋值作为命令本身,或作为' SECTIONS '命令中的语句,或作为' SECTIONS '命令的输出节描述的一部分。
- 符号symbol所在的节是从expression的节设置的。for more information, see Expressions in linker scripts.
- 下面是一个例子显示了三个符号赋值可能被使用的地方:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 4; /*等价于 ALIGN(4) */
.data : { *(.data) }
}
在前面的例子中,' floating_point '符号将被定义为零。' _etext '符号将被定义为最后一个' .text '输入节后面的地址。符号' _bdata '将被定义为' .text '输出节后面的地址,该地址向上对齐到4字节的边界。
PROVIDE命令
在某些情况下,只有当符号被引用并且没有被链接中包含的任何对象定义时,链接脚本才需要定义符号。例如,传统的链接器定义了符号“etext”。然而,ANSI C要求用户能够使用“etext”作为函数名而不会遇到错误。“PROVIDE”关键字可以用来定义一个符号,例如“etext”,只有当它被引用但没有定义的时候。语法是:
PROVIDE( symbol = expression )
这是一个用PROVIDE命令定义符号etext的示例:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在前面的例子中,如果在程序定义了' _etext ',链接器将给出一个多重定义错误。另一方面,如果程序定义了'etext ',链接器将静默地在程序中使用该定义。如果程序引用了' etext '但没有定义它,链接器将使用链接脚本中的定义。(译注:PROVIDE命令类似于GCC中的 "attribute((weak))" 属性)。
SECTION命令
SECTION命令告诉链接器如何将输入节映射到输出节,以及如何将输出节放在内存中。' SECTIONS '命令的格式是:
SECTIONS
{
sections - command
sections - command
...
}
每个“sections - command”可以是以下其中之一:
- 一个 ` ENTRY ' 命令(see Setting the entry point)
- 一个符号赋值(see Simple assignments)
- 一个输出节描述 (see Output section description)
- 一个 overlay 描述 (see Overlay description)
SECTIONS 命令中允许“ENTRY”命令和符号赋值,以方便在这些命令中使用位置计数器。这还可以使链接脚本更容易理解,因为你可以在输出文件布局中有意义的地方使用这些命令。See Output section description and Overlay description.如果你在链接脚本中没有使用' SECTIONS '命令,链接器将把每个输入节放在一个命名相同的输出节中,顺序是这些节在输入文件中第一次遇到的顺序。如果所有的输入节都出现在第一个文件中,例如输出文件中各个节的顺序将与第一个输入文件中的顺序匹配。第一个节将位于地址0。
输出节描述(Output section description)
输出节的完整描述如下所示:
SECTION [ address ] [( type )] : [AT( LMA )]
{
output-sections-command
output-sections-command
...
} [> region ] [: phdr : phdr ...] [=fillexp]
多数输出节不使用大部分可选的节属性。“SECTION”周围的空格是必需的,这样SECTION名是明确的。冒号和花括号也是必需的。换行符和其他空白是可选的。
每个output-sections-command 命令可以是如下:
- 一个符号赋值 (see Simple assignments)
- 一个输入节描述(see Input section description)
- 要直接包含的数据值(see Output section data)
- 一个特殊的输出部分关键字(see Output section keywords)
输出节名称(Output section name)
输出节的名称是' section '。' section '必须满足输出格式的约束。在只支持有限数量的节的输出文件格式中,例如' a.out ',名称必须是该格式支持的名称之一(例如' a.out ',只允许' .text ', ' .data '或' .bss ')。如果输出格式支持任意数量的节,但是使用数字而不是名称(就像Oasys的情况一样),则名称应该作为引用的数字字符串提供。section名称可以由任何字符序列组成,但是包含逗号等任何不寻常字符的名称必须用引号括起来。输出节名' /DISCARD/ '是特殊的 See Output section discarding.
输出节地址 Output section address
“address”是输出节的VMA(虚拟内存地址)的表达式。如果你没有提供' address ',链接器将基于' region '(如果存在)设置它,或者以其他方式基于位置计数器的当前值。
如果你提供了' address ',输出节的地址将被精确地设置为该规范。如果你既不提供' address '也不提供' region ',那么输出节的地址将被设置为符合输出节的对齐要求的位置计数器的当前值。
输出节的对齐要求是输出节中包含的所有输入节中最严格的对齐方式。(译注:比如,一个输出节包括2个输入节,一个输入节是2字节对齐,一个输入节是4字节对齐,那么这个输出节就按照最严格的4字节对齐)
例如:
.text . : { *(.text) }
和
.text : { *(.text) }
有微妙的不同。第一个将把' .text '输出节的地址设置为位置计数器的当前值。第二个将其设置为位置计数器以' .text '输入节的最严格对齐方式对齐后的当前值。
“address”可以是任意的表达式。See Expressions in linker scripts. 例如,如果你想在0x10字节的边界上对齐section,以便section地址的最低4位为0,你可以做如下声明:
.text ALIGN(0x10) : { *(.text) }
这是因为' ALIGN '返回向上对齐到指定值的当前位置计数器。
为一个节指定' address '将改变位置计数器的值。
输入节描述 Input section description
最常见的输出节命令是输入节描述,输入节描述是最基本的链接脚本操作。使用输出节告诉链接如何在内存中布局程序。使用输入节描述来告诉链接器如何将输入文件映射到内存布局中。
输入节基础 Input section basics
输入节描述由一个文件名(可选)和一个用圆括号括起来的节名称列表组成。文件名和节名可以是通配符模式,这是我们要介绍的;see Input section wildcard patterns. 最常见的输入节描述是在输出节中包含具有特定名称的所有输入节。例如,要包含所有输入' .text '部分,你可以这样写:
*(.text)
这里的' * '是一个通配符,可以匹配任何文件名。
有两种方法可以包含多个节:
*(.text .rdata)
*(.text) *(.rdata)
它们之间的区别在于' .text '和' .rdata '输入节出现在输出节的顺序。在第一个例子中,它们将混合在一起。在第二个例子中,所有的' .text '输入部分将出现在前面,然后是所有的' .rdata '输入部分。
你可以指定文件名来包含来自特定文件的节。如果一个或多个文件包含需要位于内存中特定位置的特殊数据,就可以这样做。例如:
data.o(.data)
如果你使用的文件名没有包含一个节列表,那么输入文件中的所有节都将包含在输出节中。这种做法并不常见,但有时可能是有用的。例如:
data.o
当你使用不包含任何通配符的文件名时,链接器将首先查看你是在链接命令行上还是在' INPUT '命令中指定了文件名。如果没有,链接器将尝试将该文件作为输入文件打开,就像它出现在命令行上一样。注意,这与' INPUT '命令不同,因为链接器不会在存档搜索路径中搜索文件。
输入节通配符模式 Input section wildcard patterns
在输入区段描述中,文件名或节名或两者都可以是通配符模式。在许多示例中看到的 ' * ' 文件名是一个简单的文件名通配符模式。通配符模式类似于Unix shell使用的那些模式。
` * '
匹配任意数量的字符。
` ? '
匹配任意单个字符。
'[chars]'
匹配任何字符的单个实例;字符' - '可用于指定字符范围,如' [a-z] '以匹配任何小写字母。
` \ '
引用以下字符。
当文件名与通配符匹配时,通配符字符将不匹配' / '字符(在Unix上用于分隔目录名)。由一个' * '字符组成的模式是一个例外;它总是匹配任何文件名,无论它是否包含' / '。在节名中,通配符将匹配' / '字符。
文件名通配符模式只匹配在命令行或' INPUT '命令中显式指定的文件。链接器不搜索目录以展开通配符。
如果一个文件名匹配多个通配符模式,或者如果一个文件名显式出现并且也由通配符模式匹配,链接器将使用链接脚本中的第1个匹配。例如,输入部分描述的序列可能是错误的,因为' data.o '规则将不会被使用(译注:也就是第2个规则被第1个短路了):
.data : { *(.data) }
.data1 : { data.o(.data) }
如果你曾经对输入节的去向感到困惑,可以使用' -M '链接器选项来生成一个map文件。map文件精确地显示了输入节如何映射到输出节。
这个例子展示了如何使用通配符模式对文件进行分区。这个链接脚本指示链接器将所有的' .text '节放在'.text '中,将所有输入的'. bss '节放到输出的'. bss '节。链接器会将所有以大写字母开头的文件中的' .data '部分放在' .DATA '中;对于所有其他文件,链接器将在'. data '放置在'. data '节。
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
common符号的输入节 Input section for common symbols
common符号需要特殊的表示法,因为在许多目标文件格式中,common符号没有特定的输入节。
链接器将common符号视为在一个名为“COMMON”的输入节中。
你可以像使用其他输入节一样,对“COMMON”区段使用文件名。你可以使用它将来自特定输入文件的common符号放在一个节中,而来自其他输入文件的common符号放在另一个节中。
在大多数情况下,输入文件中的common符号将被放置在输出文件的' .bss '节。例如:
.bss { *(.bss) *(COMMON) }
(译注:参考https://www.cnblogs.com/l2017/p/11706414.html 关于COMMON符号的总结
)
一些目标文件格式有一种以上的common符号类型。例如,MIPS ELF对象文件格式区分标准common符号和小common符号。在这种情况下,链接器将为其他类型的common符号使用不同的特殊节名。在MIPS ELF的情况下,连接器使用' COMMON '表示标准的common符号,' .scommon '表示小的common符号。这允许你将不同类型的通用符号映射到不同位置的内存位置中。
你有时会在旧的链接脚本中看到' [COMMON] '。这种符号现在被认为是过时的。它相当于' *(COMMON)'.
输入节示例 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)
}
}
输出节数据 Output section data
你可以使用' BYTE ', ' SHORT ', ' LONG ', ' QUAD '或' SQUAD '作为输出节命令,在输出节中包含显式的若干字节的数据。每个关键字后面都有一个括号中的表达式,提供要存储的值;see Expressions in linker scripts.表达式的值存储在位置计数器的当前值处。
' 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),则该值将按照第一个输入对象文件的字节序存储。
可以使用' FILL '命令设置当前节的填充模式,它后面是一个用括号括起来的表达式。section内任何其他未指定的内存区域(例如,由于输入section需要对齐而留下的空白)将由表达式的两个最低有效字节填充,并在必要时重复。' FILL '语句涵盖了它在节定义中出现的位置之后的内存位置;通过包含多个' FILL '语句,可以在输出部分的不同部分使用不同的填充模式。
这个例子展示了如何用值' 0x9090 '填充未指定的内存区域:
FILL(0x9090)
' FILL '命令类似于' = fillexp '输出节属性(see Output section fill);但是它只影响节中' FILL '命令之后的部分,而不是整个节。如果两者都使用,' FILL '命令优先。
输出节关键字 Output section keywords
有两个关键字,它们可以作为输出节命令出现。
CREATE_OBJECT_SYMBOLS
该命令告诉链接器为每个输入文件创建一个符号。每个符号的名称将是对应的输入文件的名称。每个符号的节将是' CREATE_OBJECT_SYMBOLS '命令出现的输出节。
这是' a.out '目标文件格式的惯例。它通常不用于任何其他目标文件格式。
CONSTRUCTORS
当使用' a.out '目标文件格式进行链接时,链接器使用一个不寻常的set构造来支持C++全局构造函数和析构函数。当链接不支持任意节(如' ECOFF '和' XCOFF ')的目标文件格式时,链接器将根据名称自动识别C++全局构造函数和析构函数。对于这些目标文件格式,' CONSTRUCTORS '命令告诉链接器将构造函数信息放在' CONSTRUCTORS '命令出现的输出节。对于其他的目标文件格式,' CONSTRUCTORS '命令会被忽略。
符号' CTOR_LIST '标记全局构造函数的开始,符号' __DTOR_LIST '标记全局构造函数的结束。列表中的第一个Word是条目数,后面是每个构造函数或析构函数的地址,后面是一个零Word。
编译器必须进行整理以实际运行代码。对于这些目标文件格式,GNU C++通常从子例程' main '调用构造函数; 对' main '的调用是自动插入' main '启动代码中的。GNU C++通常通过' atexit '或直接从' exit '函数中运行析构函数。
对于诸如' COFF '或' ELF '这样支持任意节名的目标文件格式,GNU C++通常会将全局构造函数和析构函数的地址放在' .ctors '和' .dtors '节中。将以下序列放入链接脚本将构建GNU C++运行时代码期望看到的那种表。
__CTOR_LIST__ = .;
LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2) /*条目数量, -2表示此word和零word*/
*(.ctors) /*构造函数地址*/
LONG(0) /*一个word的零*/
__CTOR_END__ = .;
__DTOR_LIST__ = .;
LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
*(.dtors)
LONG(0)
__DTOR_END__ = .;
通常编译器和链接器会自动处理这些问题,你不需要关心它们。但是,如果你正在使用C++并编写自己的链接脚本,则可能需要考虑这种情况。
输出节废弃 Output section discarding
链接器不会创建没有任何内容的输出节。这是为了方便引用可能出现在或不出现在任何输入文件中的输入节。例如:
.foo { *(.foo) }
只有在至少一个输入文件中有' .foo '节时,才会在输出文件中创建' .foo '节。
如果使用输入节描述以外的任何东西作为输出节命令,例如符号赋值,那么输出节将始终被创建,即使没有匹配的输入节。特殊的输出节名' /DISCARD/ '可用于丢弃输入节。任何被分配给名为' /DISCARD/ '的输出节的输入节都不会包含在输出文件中。
输出节属性 Output section attributes
我们在上面展示了输出节的完整描述:
SECTION [ address ] [( type )] : [AT( LMA )]
{
output-sections-command
output-sections-command
...
} [> region ] [: phdr : phdr ...] [=fillexp]
我们已经介绍了 section ',
address ', 和 ` output-sections-command '. 在接下来的讨论中,我们将描述剩下的节属性。
输出节类型 Output section type
每个输出节可以有一个类型。类型是括号中的关键字。定义了以下类型:
NOLOAD
这个section应该被标记为不可加载的,这样当程序运行时它就不会被加载到内存中。
DSECT
COPY
INFO
OVERLAY
为了向后兼容,支持这些类型名,但很少使用。它们都具有相同的效果:section应该被标记为不可分配的,这样当程序运行时,就不会为section分配内存。
链接器通常根据映射到输出节的输入节设置输出节的属性。你可以通过使用节类型来覆盖它。例如,在下面的脚本示例中,' ROM '节被寻址在内存位置' 0 ',当程序运行时不需要加载。“ROM”部分的内容将照例出现在链接输出文件中(译注:因该ROM支持XIP,可以不用加载到内存。但也可以将ROM节加载到内存中,不过要修改ROM节的运行地址)。
SECTIONS {
ROM 0 (NOLOAD) : { ... }
...
}
输出节加载地址 Output section LMA
每个节都有一个虚拟地址(VMA)和加载地址(LMA)。see Basic linker script concepts. 可能出现在输出节描述中的地址表达式设置VMA。链接器通常将LMA设置为' VMA '。你可以通过使用' AT '关键字来改变LMA。' AT '关键字后面的LMA表达式指定该节的加载地址。设计这个特性是为了使构建ROM镜像变得容易。例如,下面的链接脚本创建了三个输出节:一个名为' .text ',开始于' 0x1000 ',一个名为' .mdata ',在' .text'的末尾加载,即使它的VMA是' 0x2000 '。还有一个节叫'.bss ',它在地址' 0x3000 '保存未初始化的数据。
SECTIONS
{
.text 0x1000 : {
*(.text) _etext = . ;
}
.mdata 0x2000 : AT(ADDR(.text) + SIZEOF(.text))
{
_data = . ;
*(.data);
_edata = . ;
}
.bss 0x3000 :
{
_bstart = . ;
*(.bss) *(COMMON) ;
_bend = . ;
}
}
符号' _data '用值' 0x2000 '定义,这表明位置计数器保存的是VMA值,而不是LMA值。
使用这个链接脚本生成的程序的运行时初始化代码将包括类似下面的内容:将初始化的数据从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;
( 译注:注意这里是取符号_etext的地址,不是符号本身的值,与链接脚本中的'_etext = .'直观上不一致。我们在可以这样理解 ‘_etext = .’ :符号_etext所在的地址是当前的位置计数器(详见简单的赋值)。而_etext的值就是它所在地址上存的内容。我们要的就是该符号所在的地址。)
输出节区域 Output section region
可以使用' > region '将一个section分配给前面定义的内存区域。这里有一个简单的例子:
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }
输出节程序头 (Output section phdr)
可以使用':phdr '将一个区段赋值给前面定义的程序段(segment)。如果一个节被分配给一个或多个segment,那么所有后续分配的section也将被分配给这些segment,除非它们显式地使用':phdr '修饰符。为了防止一个section被分配给一个segment,而它通常默认为一个,使用':NONE '。See PHDRS command.这是一个简单例子:
PHDRS { text PT_LOAD; }
SECTIONS { .text : { *(.text) } :text }
输出节填充 Output section fill
可以通过 =fillexp 来设置整个节的填充模式。 fillexp 是一个表达式; see Expressions in linker scripts.
输出节中任何未指定的内存区域 (例如,由于输入节需要对齐而留下的空白) 将用值的两个最低有效字节填充,并在必要时重复。
还可以在输出节命令中使用' FILL () '命令更改填充值。See Output section data. 这是一个简单例子:
SECTIONS { .text : { *(.text) } =0x9090 }
覆盖描述 Overlay description
覆盖描述提供了一种简单的方式来描述节,这些节将作为单个内存镜像的一部分加载,但将在相同的内存地址运行。在运行时,某种类型的覆盖管理器将根据需要将覆盖的节复制进运行时内存地址或复制出运行时内存地址,可能只需简单地操作寻址位。这种方法可能很有用,例如,当某个内存区域比另一个内存区域更快时。OVERLAY使用' 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 '构造内的section定义与一般的' SECTIONS '构造内的section定义是相同的,除了在' OVERLAY '构造内没有地址和内存区域可以被定义。See SECTIONS command.
这些节都定义了相同的起始地址。节的加载地址被安排,以便它们在内存中是连续的,从' OVERLAY '使用的加载地址作为一个整体开始(与正常的分段定义一样。加载地址是可选的,默认为起始地址。起始地址也是可选的,默认为位置计数器的当前值)。
如果使用了' NOCROSSREFS '关键字,并且在节之间有任何引用,链接器将报告一个错误。由于这些节都在同一个地址运行,所以一个节直接引用另一个节通常没有意义。
对于“OVERLAY”中的每个部分,链接器会自动定义两个符号。符号' load_start_ secname '被定义为该section的起始加载地址。符号' load_stop_secname '被定义为该section的最终加载地址。' secname '中的任何在C标识符中不合法的字符将被删除。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 '。
将覆盖' .text1 '复制到覆盖区域的C代码可能如下所示。
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) }
__load_start_text0 = LOADADDR (.text0);
__load_stop_text0 = LOADADDR (.text0) + SIZEOF (.text0);
.text1 0x1000 : AT (0x4000 + SIZEOF (.text0)) { o2/*.o(.text) }
__load_start_text1 = LOADADDR (.text1);
__load_stop_text1 = LOADADDR (.text1) + SIZEOF (.text1);
. = 0x1000 + MAX (SIZEOF (.text0), SIZEOF (.text1));
MEMORY命令 (MEMORY command)
链接器的默认配置允许分配所有可用内存。你可以使用' MEMORY '命令来重写它。
' MEMORY '命令描述了目标中的内存块的位置和大小。你可以使用它来描述链接器可以使用哪些内存区域,以及它必须避免哪些内存区域。然后可以将节分配给特定的内存区域。
链接器将基于内存区域设置节地址,并在区域变得太满时发出警告。链接器将不会随机移动节以适应可用区域。链接脚本最多可以包含一个' MEMORY '命令的使用。但是,你可以在其中定义任意数量的内存块。的语法是:
MEMORY
{
name [( attr )] : ORIGIN = origin , LENGTH = len
...
}
name”是链接脚本中用于引用该区域的名称。区域名称在链接脚本之外没有任何意义。区域名存储在单独的名称空间中,不会与符号名、文件名或节名冲突。每个内存区域必须有一个不同的名称。
' attr '字符串是一个可选的属性列表,它指定是否为输入节使用特定的内存区域,该区域没有在链接脚本中显式映射。如果没有为某个输入节指定输出节,链接器将创建一个与输入节同名的输出部分。如果你定义了区域属性,链接器将使用它们来为它创建的输出节选择内存区域。See SECTIONS command.
字符串' 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与列出的任何属性不匹配时,它才会被放置在内存区域中。
“ORIGIN”是内存区域的起始地址表达式。在执行内存分配之前,表达式必须计算为一个常数,这意味着你不能使用任何相对符号。关键字“ORIGIN”可以缩写为“org”或“o”(但不能缩写为“ORG”)。' len '是一个表示内存区域大小(以字节为单位)的表达式。与“origin”表达式一样,在执行内存分配之前,表达式必须计算为一个常数。关键字' LENGTH '可以缩写为' len '或' l '。
下面的示例中,我们指定有两个内存区域可供分配:一个从' 0 '开始的内存为256千字节,另一个从' 0x40000000 '开始的内存为4兆字节。链接器将把每个section放到rom内存区域,该section没有显式地映射到内存区域,并且是只读或可执行的。链接器将把其他没有显式映射到内存区域的部分放到“ram”内存区域。
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
如果你定义了一个名为“mem”的内存区域,你可以通过使用“> region”输出节属性引导链接器将特定的输出节放置到该内存区域。如果没有为输出节指定地址,链接器将把地址设置为所放置的内存区域内的下一个可用地址。如果指向某个内存区域的组合输出部分对该区域来说太大,链接器将发出错误消息。
PHDRS命令 PHDRS command
(译注:可执行文件或共享目标文件的程序头表是一个结构的数组,数组中的每项描述一个段或其他系统需要为程序执行准备的信息。一个目标文件段包含一个或多个节。程序头只对可执行文件和共享文件有意义。see elf(5) - Linux manual page)
ELF目标文件格式使用程序头,也称为段。程序头描述了程序应该如何被加载到内存中。你可以使用带有' -p '选项的' objdump '程序将它们打印出来。当你在本机ELF系统上运行ELF程序时,系统加载程序读取程序头,以便弄清楚如何加载程序。这只会在程序头被正确设置的情况下工作。本文档没有描述系统加载器如何解释程序头的细节;有关更多信息,请参见 elf(5) - Linux manual page。
默认情况下,链接器将创建合理的程序头。然而,在某些情况下,你可能需要更精确地指定程序头。你可以使用' PHDRS '命令来实现此目的。当链接器在链接脚本中看到' PHDRS '命令时,它将不会创建除指定的程序头之外的任何程序头。
链接器只在生成ELF输出文件时注意' PHDRS '命令。在其他情况下,链接器将简单地忽略' PHDRS '。这是' PHDRS '命令的语法。“PHDRS”、“FILEHDR”、“AT”和“FLAGS”是关键字。
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS ( flags ) ] ;
}
'name' 只用于链接脚本的' SECTIONS '命令中的引用。它不会被放到输出文件中。程序头名称存储在单独的名称空间中,不会与符号名、文件名或节名冲突。每个程序头必须有一个不同的名称。
这些程序头类型描述内存段,系统加载器将从文件中加载这些内存段。在链接脚本中,通过在段中放置可分配的输出节来指定这些段的内容。可以使用':phdr '输出section属性将一个section放在一个特定的段中。See Output section phdr.
将某些节放在多个分段中是正常的。这仅仅意味着一段内存包含另一段内存。你可以重复':phdr ',在每个包含section的片段中使用一次。如果你使用':phdr '将一个section放在一个或多个分段中,那么链接器将把所有没有指定':phdr '的后续可分配section放在相同的段中。这是为了方便起见,因为通常情况下,一组连续的节(section)将被放置在一个单一的段中。为了防止一个section被分配给一个segment而是采用它默认的那个,那就使用':NONE '。
你可以使用' FILEHDR '和' PHDRS '关键字出现在程序头类型之后来进一步描述该段的内容。' FILEHDR '关键字意味着该段应该包含ELF文件头。' PHDRS '关键字意味着段应该包括ELF程序头本身。
“type”可能是以下之一。数字表示关键字的值。
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 '表达式指定一个段应该在内存中的特定地址加载。这与作为输出section属性使用的' AT '命令相同。程序头的' AT '命令覆盖了输出section属性(译注:如果同时设置了程序头AT和数据section属性,那依前者)。 See Output section LMA.。链接器通常会根据组成段的section设置段标志。你可以使用' FLAGS '关键字来显式指定段标志。flags的值必须为整数。它用于设置程序头的' p_flags '字段。
(译注:
p_flags
This member holds a bit mask of flags relevant to the
segment:
PF_X An executable segment.
PF_W A writable segment.
PF_R A readable segment.
代码段通常有flags PF_X and PF_R.
数据段通常有PF_W and PF_R.
)
下面是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命令 VERSION command
当使用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;
} VERS_1.2;
这个版本脚本示例定义了三个版本节点。定义的第一个版本节点是' VERS_1.1 ';它没有其他依赖项。该脚本将符号' foo1'绑定到' VERS_1.1 '。它将大量符号收缩到局部作用域,以便它们在共享库之外不可见。接下来,版本脚本定义节点' 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”之间。然而,这将是一种令人困惑的版本脚本编写方式。
当你将应用程序链接到具有版本控制符号的共享库时,应用程序本身就知道它需要的每个符号的哪个版本,它还知道它需要链接到的每个共享库的哪个版本节点。因此,在运行时,动态加载器可以进行快速检查,以确保你所链接的库实际上提供了应用程序解析所有动态符号所需的所有版本节点。通过这种方式,动态链接器可以确定地知道它需要的所有外部符号都是可解析的,而不必搜索每个符号引用。
符号版本控制实际上是一种更复杂的方法,可以执行SunOS所做的小版本检查。这里要解决的基本问题是,对外部函数的引用通常是根据需要绑定的,而不是在应用程序启动时全部绑定。如果共享库过期,可能会缺少所需的接口;当应用程序尝试使用该接口时,可能会突然意外地失败。使用符号版本控制,当用户启动他们的程序时,如果应用程序使用的库太旧,将会得到一个警告。
Sun的版本控制方法有几个GNU扩展。第一个功能是将符号绑定到定义符号的源文件(而不是版本控制脚本)中的版本节点。这样做主要是为了减轻库维护人员的负担。你可以通过在C源文件中放入这样的内容来做到这一点:
__asm__(".symver original_foo,foo@VERS_1.1");
这将函数' original_foo '重命名为绑定到版本节点' VERS_1.1 '的' foo '的别名。' local: '指令可以用来防止符号' original_foo '被导出。
第二个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 '绑定到该符号未指定的基本版本。包含此示例的源文件将定义四个C函数:' original_foo '、' old_foo '、' old_foo1 '和' new_foo '。
当你有一个给定符号的多个定义时,需要某种方法来指定一个默认版本,该符号的外部引用将绑定到该版本。你可以使用' .symver '指令的' foo@@VERS_2.0 '类型来实现这一点。你只能以这种方式声明一个符号的一个版本为默认值;否则,你将有效地拥有同一个符号的多个定义。
如果你希望将引用绑定到共享库中符号的特定版本,可以方便地使用别名(即' old_foo '),或者你可以使用' .symver '指令专门绑定到有问题的函数的外部版本。
链接脚本中的表达式 Expressions in linker scripts
链接脚本语言中表达式的语法与C表达式相同。所有表达式都以整数计算。所有表达式都以相同的大小计算,如果主机和目标都是32位,则为32位,否则为64位。你可以在表达式中使用和设置符号值。链接器定义了在表达式中使用的几个特殊用途的内置函数。
常量 Constants
所有的常量都是整数。与C中一样,连接器认为以' 0 '开头的整数是八进制,以' 0x '或' 0x '开头的整数是十六进制。
链接器将其他整数视为十进制。
此外,你可以使用后缀“K”和“M”分别将一个常数乘以“1024”或“1024*1024”。例如,下面这些都是指相同的数量:
_fourk_1 = 4K;
_fourk_2 = 4096;
_fourk_3 = 0x1000;
符号名称 Symbol names
符号名称以字母、下划线或句号开头,可以包括字母、数字、下划线、句号和连字符。未加引号的符号名称不能与任何关键字冲突。你可以指定一个符号,它包含奇异的字符(译注:比如空格)或具有与关键字相同的名称,这个时候需要将符号名称放在双引号中:
"SECTION" = 9; /*译注:SECTION 是关键字,放到双引号中可以定义一个符号*/
"with a space" = "also with a space" + 10; /*译注:包括了空格,需要用双引号*/
因为符号可以包含许多非字母字符,所以用空格分隔符号是最安全的。例如,' A-B '是一个符号,而' A - B '是一个包含了减法的表达式。
位置计数器 The location counter
特殊链接器点 '.' 变量始终包含当前输出位置计数器。因为' . ' 总是指向输出节中的一个位置,它只出现在' SECTIONS '命令中的表达式中。' . ' 符号可以出现在表达式中任何允许普通符号出现的地方。
赋值给 '.’ 将导致位置计数器被移动。这可以用于在输出节创建洞。位置计数器可能永远不会向后移动(译注:向后即减小)。
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0x1234;
}
在前面的例子中,' file1 '中的'. text '区段位于输出节' output'的开头。它后面有一个1000字节的间隙。然后' file2 '中的' .text '节出现,' .text '后面也有一个1000字节的间隔。接着是file3文件的 .text 节。标记' = 0x1234 '指定在间隔中写入的数据。
操作符 Operators
链接器识别具有标准绑定和优先级的标准C算术操作符集;see Table 1: Arithmetic operators with precedence levels and bindings associations.
Table 1: Arithmetic operators with precedence levels and bindings associations
Precedence Association Operators Notes (highest) 1left ! - ~ 1 2left * / % 3left + - 4left >> << 5left == != > < <= >= 6left & 7left | 8left && 9left || 2 10right ? : 11right &= += -= *= /= (lowest)
\1. 前缀操作符
\2. See Assigning values to symbols.
求值 Evaluation
链接器对表达式的计算是惰性的。它只在绝对必要的时候计算表达式的值。
链接器需要一些信息,比如第1节的起始地址的值,以及存储区域的起始地址和长度,以便进行任何链接。当链接器读取链接脚本时,会尽快计算这些值。
但是,其他值(如符号值)直到存储分配完成后才知道或需要。稍后,当其他信息(如输出节的大小)在符号赋值表达式中可用时,这些值将被计算。
section的大小在分配之后才能知道,因此依赖于它们的赋值直到分配之后才会执行。
一些表达式,比如那些依赖于位置计数器 ‘.’ 的表达式。必须在节分配时求值。
如果需要表达式的结果,但该值不可用,则会产生错误。例如,像下面这样的脚本会导致错误消息,'非常量表达式的初始地址':
SECTIONS
{
.text 9+this_isnt_constant : /*this_isnt_constan非常量*/
{ *(.text) }
}
一个表达式的节 The section of an expression TODO
当链接器计算一个表达式时,结果要么是绝对的,要么是相对于某个节的。相对表达式表示为从节的基值开始的固定偏移量。
表达式在链接脚本中的位置决定了它是绝对的还是相对的。出现在输出节定义中的表达式是相对于输出部分的基址的。出现在其他地方的表达式将是绝对的。
如果使用' -r '选项请求可重定位输出,则设置为相对表达式的符号将是可重定位的。这意味着进一步的链接操作可能会改变该符号的值。符号的节将是相对表达式的节。
如果符号被设置为绝对表达式,则在任何进一步的链接操作中都将保持相同的值。符号将是绝对的,不会有任何特定的关联节。
你可以使用内置函数' ABSOLUTE '强制一个表达式成为绝对表达式,而不是相对表达式。例如,要创建一个绝对符号,将其设置为输出区段末尾的地址' .data ':
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
如果没有使用' ABSOLUTE ', ' _edata '将相对于' .data '节。
内置函数 Builtin functions
链接脚本语言包括许多内置函数,用于链接脚本表达式。
ABSOLUTE( exp )
返回' exp '表达式的绝对值(不可重定位,相对于非负值)。主要用于在节定义中为符号赋一个绝对值,而符号值通常是相对于节的。 See Expressions in linker scripts.
ADDR( section )
返回指定节的绝对地址(VMA)。你的脚本必须预先定义了该节的位置。在下面的例子中,symbol_1
和symbol_2
被赋了相同的值:
SECTIONS { ...
.output1 :
{
start_of_output_1 = ABSOLUTE(.);
...
}
.output :
{
symbol_1 = ADDR(.output1);
symbol_2 = start_of_output_1;
}
...
}
ALIGN( exp )
返回位置计数器('.')对齐到下一个' exp '边界。“exp”必须是一个值是2的幂的表达式。这相当于:
(. + exp - 1) & ~( exp - 1)
' ALIGN '不会改变位置计数器的值,它只是对其进行算术运算。下面是一个例子,它将输出节 ' .data '对齐到前一个节后的下一个' 0x2000 '字节边界,并将该节内的一个变量设置为输入节后的下一个' 0x8000 '边界:
SECTIONS { ...
.data ALIGN(0x2000): {
*(.data)
variable = ALIGN(0x8000);
}
...
}
本例中' ALIGN '的第一个用法指定了一个节的位置,因为它被用作一个节定义的可选' ADDRESS '属性。' ALIGN '的第二种用法是定义符号的值。内置函数' NEXT '与' ALIGN '密切相关。See Output section address.
BLOCK( exp )
这是' ALIGN '的同义词,以兼容较旧的链接脚本。这在设置输出节的地址时最常见。
DEFINED( symbol )
如果' symbol '在链接器全局符号表中并被定义,则返回' 1 ',否则返回' 0 '。你可以使用此函数为符号提供默认值。例如,下面的脚本片段展示了如何将一个全局符号' begin '设置为' .text ' 节的第一个位置,但是如果一个名为' begin '的符号已经存在,它的值将被保留:
SECTIONS { ...
.text : {
begin = DEFINED(begin) ? begin : . ;
...
}
...
}
LOADADDR( section )
返回名为' section '的绝对' LMA '。这通常与' ADDR '相同,但如果在输出部分定义中使用了' AT '属性,则可能不同。
MAX( exp1 , exp2 )
返回exp1和exp2的最大值。
MIN( exp1 , exp2 )
返回exp1和exp2的最小值。
NEXT( exp )
返回下一个未分配的地址,它是' exp '的倍数。这个函数与' ALIGN(exp) '密切相关。”;除非你使用' MEMORY '命令为输出文件定义不连续内存,否则这两个函数是等价的。
SIZEOF( section )
如果已分配section,则返回section的大小(以字节为单位)。如果在计算时没有分配section,链接器将报告一个错误。See PHDRS command. 在下面的例子中,symbol_1
和symbol_2
被赋了相同的值:(译注:原来的网页不完整,下面的是另外的网页:Builtin Functions (LD))
SECTIONS{ …
.output {
.start = . ;
…
.end = . ;
}
symbol_1 = .end - .start ;
symbol_2 = SIZEOF(.output);
… }
SIZEOF_HEADERS
sizeof_headers
返回输出文件头的大小(以字节为单位)。这是出现在输出文件开头的信息。如果你愿意,可以在设置第一个节的起始地址时使用该数字,以便分页。
在生成ELF输出文件时,如果链接脚本使用SIZEOF_HEADERS内置函数,则链接器必须在确定所有的section地址和大小之前计算程序头的数量。如果链接器后来发现它需要额外的程序头,它将报告一个错误“没有足够的空间来存放程序头”。要避免此错误,你必须避免使用SIZEOF_HEADERS函数,或者你必须重写你的链接脚本,以避免强制链接器使用额外的程序头,或者你必须使用PHDRS命令自己定义程序头(see PHDRS)。