目录
3.4.2.2 INPUT(file, file, ...)
3.4.2.3 GROUP(file, file, ...)
3.5.2 PROVIDE(symbol = expression)
3.6.1 Output Section Description
3.6.3 Output Section Description
3.6.4 INPUT Section Description
3.6.4.3 Input Section for Common Symbols
3.6.4.4 输入section和垃圾回收(Garbage Collection)机制
3.6.6 Output Section Attributes
1.前言
本文主要是Using ld这篇英文文档的阅读笔记,部分内容引用自CSAPP的链接章节,作为总结和备忘,也分享给需要的人。部分我拿捏不准的词汇,将保留原文内容,以防止曲解。
2. 概要
在学习ld文件之前,需要了解一些链接的基础知识。
2.1 目标文件
目标文件是源代码经过编译生成的可以被CPU直接识别的二进制代码,主要有以下几种形式:
①可重定位的目标文件:包含二进制代码(text)和数据(data),可以通过链接和其他可重定位文件合并起来,创建一个可执行目标文件;
②可执行目标文件:包含二进制代码(text)和数据(data),可以加载到内存执行;
③共享目标文件:一种特殊的可重定位目标文件,可以在加载或运行时被动态地加载并链接;
总的来说,编译器和汇编器生成可重定位的目标文件(包括共享目标文件),而链接器则在此基础上生成可执行目标文件,整个编译的过程如图1所示:
/************ hello.c ***********/
#include<stdio>
int a = 1;
int main(
{
if(1 == a)
printf("hello, world\n");
return 0;
}
图1 编译过程示意图(以printf("hello world"为例)
其中,预处理器生成中间文件Hello.i,经编译器翻译成Hello.s(一个ASCII汇编语言文件)。随后,汇编器进一步将Hello.s翻译成可重定位的目标文件(Hello.o),并在链接器的作用下和标准库的共享目标文件printf.o进行链接,生成最终的可执行文件Hello,整个过程链接器做着类似资源整合的事情。
而所谓可重定位目标文件,是编译器按照某种标准格式生成的,编译器会将其定义和引用的符号信息都记录在该目标文件对应的符号表中。比如,mian.o的符号表中会记录着全局变量a(位于section .data中)和函数main(位于section .text中)所在的section及对应的offset和size等信息。而这些信息,就是链接器在重定向的收所不可或缺的。
2.2 链接
链接(linking)是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存中执行。根据链接的时机不同,可以将之分为以下几种:
①执行于编译(compile time)时,即源代码被翻译成机器代码时;
②执行于加载时(load time),即程序被加载器(loader)加载到内存并执行时;
③执行于运行(run time)时,即由应用程序来执行;
链接使得分离编译成为可能,使得我们可以将软件代码模块化地分布到相对独立的文件中,在对其中的部分源文件进行修改时,只须对新增修改的源文件进行增量编译,而不必rebuild整个工程文件,这个理念在make的使用中得到了充分的体现。所以,理解链接的过程,是驾驭大型程序的必备前提。
做个不恰当的类比,就好像做西红柿炒鸡蛋时,会先将鸡蛋炒好,放在盘子1里(盘子的位置编号和大小会登记在符号表里,假设盘子装满)。然后,继续翻炒西红柿,炒完后装到盘子2中。以上这些步骤可以类比成编译器和汇编器所做的事情。此后,链接器出场,将从符号表中找到两个盘子的位置编号,一起入锅翻炒,并在快完成时加入葱花、盐等调料(类似共享库),最后出锅。这时候,链接器需要知道出锅后装盘的所需容量,就又从符号表中获取了盘子1和盘子2的大小,从而根据这两个盘子的大小决定该选用盘子3(可以装满锅里的菜),并高调摆盘,将西红柿和鸡蛋摆成了太极鱼图案,即西红柿和鸡蛋在盘子的不同分区内。至此,一道完整的菜得以上桌。这道菜,就是最终的可执行文件。
2.3 链接器和ld文件
链接通常是由链接器来处理的,而ld就是链接器程序。它将.o文件及一些必要的系统目标文件组合起来,创建一个可执行目标文件(executable object file)。
2.4. GNU linker 简介
ld 主要作用是对包括.obj(object files)和.a(archive files) 这些文件内数据进行重定向,并进行完整的打包。通常来说,编译的最后阶段即是运行ld,而ld有着一套标准的语法,可以清晰明确的控制整个链接过程。此版本的ld使用通用的BFD库对目标文件进行操作,以兼容更多不同的目标文件的读取和写入,例如COFF和a.out等。不同格式的数据得以链接在一起生成任意可用类型的目标文件。
此外,GNU linker在遇到错误时,但凡还有一丝希望,绝不会放弃治疗,会继续执行,以最大可能地延长链接过程,使得后续的错误也有机会一次性暴露出来(对程序猿很友好)。甚至,在某些情况下,即便有错误,最终也会输出文件。
GNU linker很好很强大,支持大量的命令行选项,但在实际应用中很少用到。比如,当在Unix上用ld链接一个标准的Unix目标文件,比如hello.o时,通常会这么写:
ld -o output /lib/crt0.o hello.o -lc
其中,该指令的主要作用是,告诉ld生成一个叫output的目标文件,而输入则是/lib/crt0.o和hello.o,当然还有位于标准路径下的静态库libc.a。
一些命令行选项可以在命令行中的任意位置指定。但是,引用文件的相关选项是位置敏感的,诸如‘-l’或'-T',意在该命令行选项出现的位置读取文件,有点类似cmd的感觉。而对于非文件引用的命令行选项,由于命令行的执行顺序是从左向右的,重复使用这些非文件引用的命令行选项,要么没有进一步的效果,要么会覆盖之前的选项内容。
非选项变量包括目标文件或 .a 文件等输入,这些变量可以在命令行参数前面、后面,也可以与命令行参数混合在一起。通常,至少需要一个输入,ld才可以正常工作。或者,可以使用 ‘-l’, '-R' 或脚本指令来指定其他形式的二进制输入文件。否则,链接器将不会产生任何输出,并报出“no input files"的错误。
如果链接器不能识别目标文件的格式,则会将之假定为一个链接器脚本。以这种方式指定的脚本可以为主链接脚本提供参数(默认链接脚本或 '-T' 指定的链接器脚本)。这个特性运行链接器链接到一些看起来像是 .a或 .o的目标文件,但其实只是定义了一些符号值,或是使用INPUT 或 GROUP来加载其他目标文件。
对于名称为单字母的选项,选项参数要么紧跟选项字母(不加空格)后面,要么作为单独的参数紧跟在需要它们的选项后面。
由多字母组成的选项,可以在选项名称前加一到两个破折号。例如,' -trace-symbol' 和 '--trace-symbol' 是等价的。注意,该规则也有例外,即以 “o”开头的多字母选项前面只支持有两个破折号,这是为了减少与 “-o”选项的混淆。 例如, '-omagic' 将文件输出文件名设置为 'magic', 而 '--omagic' 为在输出文件设置NMAGIC标志。
多字母选项的参数与选项名称之间必须用等号与选项名分开,或者作为单独的参数紧跟在对应的选项后面。例如,'--trace-symbol foo' 和 '--trace-symbol-foo' 是等价的。如果多字母选项的缩写是唯一的,这个缩写则是语法所允许的。注意,如果链接器是通过编译器驱动程序间接调用的(以gcc为例),则所有的链接器命令行选项都因该以 'Wl' 作为前缀,比如:
gcc -Wl,--startgroup foo.o bar.o -Wl,--endgroup
这一点很重要,否则,编译器驱动程序可能会默认删除这些链接选项,导致链接失败。
2.5 GNU支持的通用命令行开关
GNU支持的通用命令行开关非常多,下面主要摘要常见的几个:
-e entry --entry = entry | 应该显式地使用entry作为程序开始执行的入口标识,而不是使用默认的入口点。 |
-EB | 链接文件为大端模式 |
-EL | 链接文件为小端模式 |
-i | 执行增量链接(与选项 '-r' 相同) |
-larchive --library=archive | 将 archive文件加入到链接文件列表中,该选项可以多次使用 |
-Lsearchdir --library-path=searchdir | 将路径searchdir添加到搜索路径列表,ld会通过这个列表地毯式地从前到后搜索 archive 库和 ld 控制脚本。该选项可以多次使用,且该搜索路径列表比默认路径优先搜索。 此外,如果searchdir以 “=”开头,则“=”将被sysroot前缀(配置链接器时指定的路径)替换。 |
-n --nmagic | 关闭sections的页面对齐,如果可以,将输出标记为NMAGIC |
-N --omagic | 将文本和数据部分设置为可读和可写。此外,不要对数据段进行页面对齐,并禁用共享库的链接。 |
-no-omagic | 该选项忽略绝大部分 '-N'选项的影响,并将text段设置为只读,强制数据段按页对齐。注意,该选项不适用于链接共享库。 |
-o output --output=output | 使用output作为ld输出的可执行文件名;如果该选项没有指定,则默认使用名称 "a.out";脚本命令OUTPUT也可以指定输出文件名。 |
-o level | 如果level大于0,则优化输出(可能需要更长时间,因此适合对最终的二进制文件启用)。 |
-unique[=SECTION] | 为每个匹配SECTION的输入section分别创建一个输出section;如果缺省通配符SECTION,则为每个section创建要给单独的输出section;该选项主要是为了防止输入输出section的重复命名引起的冲突。 |
-v --version -V | 显示ld的版本号 |
-y symbol --trace-symbol=symbol | 打印所有包含symbol的链接文件的名称,该选项可多次使用。当链接中有一个未定义的符号,但不知道引用来自何处时,此选型即可派上用场。 |
-(archives-) --start-group archives --end-group | archives是一个 .a文件列表,列表中既可以是文件名称,或者是 '-l' 选项。列表中共的文件会被重复搜索,直到没有新的未定义引用产生。通常,一个 .a文件只会在命令行指定的地方被搜索一次。由于命令行是从左向右解析执行的,如果一个 .a中的符号被命令行后边解析的目标文件所引用,则链接器无法解析该引用。而通过个这些 .a文件进行分组,这些文件都会被反复搜索,直到引用解析全部完成。 该选项会使得开销巨大,非必要谢绝使用。 |
--fatal-warnings | 将所有警告示为错误 |
--warn-common | 当COMMON符号与其他的COMMON符号或强符号产生重复冲突时,会发出警告。此处,可以结合强符号和弱符号的定义来理解。 |
-z keyword | 可识别的keyword如下: ①combreloc: 组合多个reloc section并对其进行排序,使得动态符号查找缓存成为可能; ②def: 禁止在目标文件中使用未定义的符号,共享库中仍允许使用未定义的符号; ③initfirst: 此选项只有在构建共享目标文件时才有意义,即通过标记目标文件,使之在运行时相对于其他目标文件最先完成初始化;反之,在结束时,被标记的目标文件最后结束运行,既负责冲锋,也负责断后; ④interpose: 标记目标文件,使之符号表强势插入,除了主执行程序之外,其他人通通靠后,真正的一人之下; ⑤loadfltr: 标记目标文件,使之过滤器在运行时立即处理; ⑥muldefs: 允许多重定义; ⑦nodefaultlib: 标记目标文件,该对象的依赖项搜索将忽略任何默认搜索路径; ⑧nodelete: 标记对象不应再运行时卸载; ⑨origin: 标记目标文件可能含有$ORIGIN; |
-T scriptfile --script=scriptfile | 使用scriptfile作为链接器脚本(取代ld的默认链接器脚本,所以必须包含输出文件的所有依赖项) |
用户可以使用 '-T' 命令行选项来指定自定义的脚本。如果用户没有提供链接脚本,则链接器会启用一个默认脚本。
3. 链接脚本
链接脚本基于链接器命令行的语法来编写,用于控制整个链接过程。链接脚本主要描述了输入文件的各个section是如何映射到输出文件中的,并掌控着输出文件的内存布局。除此之外,链接器还支持一些额外的指令功能,这些就是后话了。
3.1 基本概念
链接器将(多个)输入文件整合成单一输出文件。输入和输出文件都采用一种称为目标文件格式的特殊数据格式,每个目标文件中都有一个section列表。其中,输出文件通常称为可执行目标文件。输入目标文件和输出目标文件中的section,也对应的被称为输入section和输出section。
目标文件中的每个section都有名字和大小属性。大部分section都有一个相关的数据块,被称为section contents. 根据section最终在内存中的表现,可以将之分为以下几类:
①loadable: 可加载的,即当输出的可执行文件运行时,section contents需要被加载到内存中去;
②allocatable: 可分配的,即需要在内存中分配一块区域,但不需要加载任何数据进去(在某些情况下,这片内存需要清零);
③既不可加载,也不可分配的section,通常包含某种调试信息;
每个可加载或可分配的输出section都坐拥两个地址,即 VMA 和 LMA:
①VMA: virtual memory address,输出的可执行文件运行的时候,该section所使用的地址, 即runtime时的地址;
②LMA: load memory address, 对应着section被加载到的地址;
多数情况下,两个地址是相同的。举个反例,当一个data section被加载到ROM,并在程序启动时拷贝到RAM中(通常用来初始化全局变量或基于ROM的系统)。在这种情况下,LMA为ROM中的地址,而VMA为RAM地址。
3.2 链接脚本格式
链接脚本是文本文件,可以看作是一些列命令的集合:
①每一个命令要么是一个可以带参数的关键字,要么是对符号的赋值。
②可以使用分号分隔命令,空格通常被忽略;
③通常可以直接输入文件名或格式名等字符串,如果文件中包含用来分隔文件名的逗号之列的字符,则可以将文件名放在双引号中(不可在文件名中使用双引号);
④可以用/* 和 */包含注释内容给,这与C语言一致;
3.3 链接脚本示例
最简单的链接脚本莫过于只包含一个 "SECTION" 命令(用来描述输出文件的 memory layout,后文称之为内存分布)的脚本了。本例中,假设程序中只包含代码,已初始化的数据和未初始化的数据,分别对应着 '.text', '.data' 和 '.bss' 段(section,有的文章翻译成“节”):
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
其中:
①SECTIONS为关键字,表示 SECNTIONS命令,后面大括号内是该命令的内容;
②第一行的 ‘.’ 是定位计数器(仅支持在SECTIONS命令内部使用),该行的含义是将当前地址设置成0x10000(在SECTIONS命令的起始处,定位计数为0),之后的地址会随着输出段的内容而递增;
②第二行定义了一个输出段 '.text'(在输出文件中,该段的起始地址被设置为0x10000),随后的冒号不可省略,而之后的大括号中,列出了所有应该放在这个数据段中输入段的名称( '*'是通配符,可以匹配任何文件名 ),例如hello.o中的main就在这个段中;
③同理,随后定义了输出文件中的 '.data' 和 '.bss'段;
④如有必要,链接器将通过增加定位计数器来确保每个输出段的内存对齐,例如,在本例中, '.data' 和 '.bss'段直接就有可能需要链接器在两者之间插入空字节以保证内存对齐。
3.4 链接器脚本命令
3.4.1 设置 Entry Point
程序执行的第一条指令被称为 "entry point"(入口点),可以用以下指令设置程序的入口点:
ENTRY(symbol)
有几种不同的方法可以设置程序入口点,链接器会逐一尝试以下各方式,直到设置成功:
① '-e' 入口命令行选项;
②链接脚本中的ENTRY(symbol);
③如果定义了 ''start",使用start的值;
④ '.text'段的第一个字节的地址;
⑤地址0;
3.4.2 文件处理相关命令
3.4.2.1 INCLUDE filename
include 链接器脚本文件,默认在当前路径和使用 '-L' 选项指定的路径下搜索该文件,支持嵌套调用深度为10层;
3.4.2.2 INPUT(file, file, ...)
也可以写作 INPUT(file file ...)。INPUT命令指示链接器在链接中需要包含的文件,就像在命令行中指示被引用的文件一样。例如,如果每次链接都要包含 'subr.o',为了避免每次都要在链接命令行中写入 'subr.o',则可以将 'INPUT(subr.o)' 写到链接脚本中。更进一步地,也可以将所有输入文件都写在链接脚本中,每次启动链接器时只要设置 '-T' 选项就可以了。
当在隐式链接器脚本中使用INPUT命令时,输入文件在链接被包含的位置取决于链接脚本被包含的位置,这会进一步影响archive文件的搜索。
3.4.2.3 GROUP(file, file, ...)
也可以写作 GROUP(file file ...)。GROUP命令类似INPUT命令,唯一的区别是适用对象是archive文件,且这些文件会被多次搜索,直至消灭所有未定义的引用。
3.4.2.4 OUTPUT(filename)
OUTPUT命令用来指定输出文件的名字,效果类似于在命令行中使用 '-o filename',但同时使用时,优先级低于命令行。如果两者都不使用,通常默认的输出文件为 'a.out'.
3.4.2.5 SEARCH_DIR(path)
该命令用于将path添加到ld查找archive文件的路径列表中,类似于在命令行中使用 '-L path'。如果两者都使用,两个路径都会被搜索,但优先搜索命令行选项指定的路径。
3.4.2.6 STARTUP(filename)
该命令类似于INPUT命令,区别在于filename会被作为第一个被链接的输入文件,效果等同于在命令行中指定。在一些系统上,程序入口点总是第一个文件的起点,那么这条命令就派上用场了。
3.5 给符号赋值
可以在链接脚本中给符号赋值,被赋值的符号具有全局属性。
3.5.1 类C赋值
可以使用C语言的方式对运算符进行赋值,例如:
symbol = expression;
symbol += expression;
symbol -= expression;
symbol *= expression;
symbol /= expression;
symbol <<= expression;
symbol >>= expression;
symbol &= expression;
symbol |= expression;
"symbol = expression;" 定义了symbol并初始化为expression。其他用例中,symbol必须是已定义的。此外,expression后的分号不可省略。以下为一个实际用例:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
-etext = 0;
}
_bdata = (. + 3) & ~3;
.data :
{
*(.data)
}
}
其中,_bdata定义在 '.text'段之后,这里'.text'段的边界会保持四字节对齐。
3.5.2 PROVIDE(symbol = expression)
当symbol被引用而程序没有定义symbol时,会启用链接器脚本中定义的symbol;反之,链接器仍会优先使用程序对symbol的定义。
3.6 SECTIONS COMMAND
SECTIONS命令用于告知链接器如何将输入section映射到输出section中,以及如何将输出section分布到内存中,其命令格式为:
SECTIONS
{
sections-command
sections-command
...
}
其中,sections-command通常为以下几种:
①ENTRY命令;
②符号赋值;
③输出段的描述;
④覆盖描述;
如果链接器脚本没有使用SECTIONS命令,链接器将按照输入文件中首次遇到的SECTIONS的顺序,将每个输入section放到同名的输出section中,且第一个section在地址0处。
3.6.1 Output Section Description
一个完整的输出section描述大概长这个样子:
section [address] [(type)]:
[AT(lma)] [SUBALIGN(subsection_align)]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_regiion] [:phdr :phdr ...] [=fillexp]
其中,output-section-command可以是下面的几种:
①符号赋值;
②输入section描述;
③直接包含的数据值;
④一个特殊的输出section关键字;
3.6.2 Output Section Name
输出section的名称是 "section",它必须符合输出格式。有些输出格式只支持有限的sections,比如 'a.out' 只支持 '.text','.data','.bss' 段。
3.6.3 Output Section Description
该地址指的是输出section的VMA的表示式。如果提供地址,输出section的地址将被精确设置为该地址;如果不提供地址,则链接器将根据所在的内存区域(region)设置地址,否则将根据定位计数器的当前值设置地址;如果既不提供地址,也不提供region,那么输出sectio的地址将被设置成与输出section的内存对齐要求的定位计数器的当前值。输出section的对齐要求是其包含的所有输入section中最严格的对齐。
① .text . : { *(.text) }
② .text : { *(.text) }
举个例子,①和②的唯一区别在于 '.text' 后是否使用了定位计数器,前者直接将输出section的地址设置成定位计数器的值;后者要考虑输入section的对齐要求,即将输出section的地址设置为定位计数器按对齐要求圆整后的值。
另外,也可以通过ALIGN命令更改输出文件的边界对齐方式,如更改为边界按16字节对齐:
.text ALING(0x10) : { *(.text) }
3.6.4 INPUT Section Description
3.6.4.1 Input Section Basis
最常见的输出section命令是输入section描述。输入section描述用来告知链接器如何将输入文件映射到内存分布中,而输出sections告知linker如何将整个程序如何映射到内存中。如 '*(.text)' 指的是所有输入文件中以 '.text' 为结尾的section内容,这里用到了通配符;也可以在此基础上排除部分输入文件的相应text段,如:
(EXCLUDE_FILE( *crtend.o *otherfile.o) .octors))
这条语句意在包含所有输入文件(除了 crtend.o 和 otherfile.o) 的 '.octors'段。
通常来说,主要有两种方式一次性地包含多个section:
① .text : { *(.text .rdata) }
② .text : { *(.text) *(.rdata) }
两者的区别在于两个section最后在输出文件中的排布顺序,前者按(.text .rdata)来搜索解析每个输入文件,所以两个section的排布式混合的;而后者则是所有 '.text'排布在一起,然后是所有 '.rdata' 排布在一起。
当然也可以指定从某一文件中包含特定的section,如:
.data: { data.o(.data) }
也可以不指定section列表,从而包含输入文件(data.o)中的所有section:
.data: { data.o }
3.6.4.2 输入section的通配符
在输入section描述中,文件名或section名都可以使用通配符,其使用方法类似于UNIX shell的使用方法。
* | 通配多个字符; |
? | 通配单个字符; |
[chars] | 通配单个 chars 实例,也可以用 '-' 指定字符范围,如 [a-z] 用于匹配所有小写字母; |
\ | 引用随后的字符 |
如果文件名匹配多个通配符,或者文件名显示出现同时也被通配符所匹配,则链接器将使用链接脚本中的第一个匹配项,例如:
.data :{ *(.data) }
.data1 : { data.o(.data) }
这个输入section描述会生成错误,因为 'data.o' 这一规则不会使用。
此外,可以用 SORT命令(如" SORT(.text*)" )让链接器按升序对输入文件或sections进行内存分布,而非按解析顺序。
下面的例子中,链接器会将所有文件名为大写的输入文件的 '.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符号没有特定的输入section,链接器将之视为位于名为 ''COMMON" 的输入据section中的符号。
多数情况下,Common符号会被放到输出文件的bss段,例如:
.bss :{ *(.bss) *(COMMON) }
3.6.4.4 输入section和垃圾回收(Garbage Collection)机制
当链接中垃圾回收机制开启(通过 ' --gc-sections')时, 经常很有必要标记那些不该被消除的sections,这主要通过KEEP命令实现,如:
KEEP(*(.init))
3.6.4.5 Input Section Example
SECTIONS
{
.outputa 0x10000 :
{
all.o
foo.o (.input1)
}
.outputb:
{
foo.o(.input2)
foo1.o(.input1)
}
.outputc :
{
*(.input1)
*(.input2)
}
}
其中,all.o中所有的sections和foo.o中的input1段都放到输出文件的outputa段中;foo.o中d input2和foo1.o中的input1段都放到输出文件的outputb段中;其余所有的输入文件中的input1和input2段都放到输出文件的outputc段中。
3.6.5 Output Section Data
可以使用关键字BYTE(单字节),SHORT(两字节),LONG(四字节),QUAD(八字节),SQUAD定义数据变量,每个关键字后跟一个带括号的表达式,该变量作用于输出section,表达式的值存储在定位计数器的当前值中,例如:
BYTE(1)
LONG(addr)
上述指令在一字节(占位)存储后,存储了一个名为 ''addr"的符号。
此外,当主机和目标机都是64位时,QUAD和SQUAD是相同的,都存储8字节的值;当主机和目标机都是32位时,表达式最大支持32位,AUAD存储一个32位的值(0拓展到64位),SQUAD存储一个32的值(按符号位拓展到64位)。
注意,这些命令只能在一个section描述的内部使用,而不能在各section的描述之间使用,例如下面的写法是错误的:
SECTIONS
{
.text :
{
*(.text)
}
LONG(1)
.data :
{
*(.data)
}
}
正确的写法应该是:
SECTIONS
{
.text :
{
*(.text);
LONG(1)
}
.data :
{
*(.data)
}
}
FILL命令后面跟着一个带括号的表达式,可以用于将section内部任何未指定的内存区域(如由于输入section需要对齐而留下的内存空白)都用表达式的值进行填充(必要时重复填充),如:
FILL(0x90909090)
FILL命令类似于 '=fillexp',但只影响FILL命令后面的部分,而不是整个section,如果两者同时使用,则FILL命令优先。
3.6.6 Output Section Attributes
section [address] [(type)]:
[AT(lma)] [SUBALIGN(subsection_align)]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_regiion] [:phdr :phdr ...] [=fillexp]
3.6.6.1 Output Section Type
每个输出section都类型,包括:
NOLOAD | 标记该section的类型为不加载,即程序运行时无须加载到内存; |
DSECT COPY INFO OVERLAY | 为了向后兼容才支持这些类型,因此很少用;它们都具有相同的效果:将该section标记为不可分配的(not allocatable),即程序运行时不会为该section分配内存; |
链接器通常根据映射到输出section的输入section来设置输出section的属性。当然,也可以使用section类型来覆盖输入section的类型。例如:
SECTIONS
{
.ROM 0 (NOLOAD):
{
...
}
...
}
上述命令表示ROM段的起始地址为0,且在程序运行时无须加载到内存中,但ROM段的内容仍会像往常一样出现在链接器的输出文件之中。
3.6.6.2 Output Section LMA
每个section都有一个VMA和LMA,链接器通常会默认两者相等,地址表达式可以用于在输出section中设置VMA ,而使用 'AT' 关键字可以更改LMA(使之不必与VMA保持一致)。以下举个例子来详细说明。
链接脚本:
SECTIONS
{
.text 0x1000:
{
*(.text);
_etext = .;
}
.mdata 0x2000:
AT ( ADDR( .text) + SIZEOF( .text) )
{
_data = . ;
*(.data) ;
_edata = . ;
}
.bss 0x3000:
{
_bstart = .;
*(.bss) *(COMMON); _bend = .;
}
}
其中,text段的起始地址为0x1000;mdata段的LMA(加载地址)被设置紧跟在text段之后,VMA设置为0x2000,符号_data的值为0x2000,等于mdata的VMA;bss段的起始地址为0x3000。
使用此链接脚本生成的程序运行时,初始化代码必然需要将用于初始化的数据从ROM映像复制到其运行地址(VMA),其中链接器脚本定义的符号使得数据搬运变得很容易实现:
extern char _etext, _data, _edata, _bstart, _bend;
char * src = &_etext;
char * dst = &_data;
/* ROM has data at end of text; copy it to VMA*/
while(dst < &_edata)
{
*dst++ = *src++;
}
/*Zero bss*/
for(dst = &_bstart; dst < &_bend; dst++)
{
*dst = 0;
}
3.6.6.3 强制对齐
可以使用SUBALIGN在输出section中强制输入section对齐,指定的值无论大小,都将覆盖输入section定义的对齐方式。
3.6.6.4 Output Section Region
可以使用 '>region' 将一个section赋值给已定义的内存区域(region),如:
MEMORY { rom: ORIGIN: 0x1000, LENGTH = 0x1000 }
SECTIONS
{
.ROM:
{
*(.text)
} > rom
}
3.6.6.5 Output Section Phdr
可以使用 ':phdr' 将一个section分配给已定义的程序段。如果一个section被分配给多个段,则后续分配的section也将分配给这些段,除非它们显示的使用phrd修饰符;也可以使用 ':NONE' 告知编译器不将该section放在任何段中。
PHDRS { text PT_LOAD }
SECTIONS
{
.text:
{
*(.text)
} : text
}
3.6.6.6 Output Section Phdr
可以使用 '=fillexp' 设置整个section的填充模式,fillexp是一个表达式,输出section中任何未指定的内存区域(例如,由于输入section需要对齐而留下的内存空白)都将被该值填充,必要时重复填充,填充值默认大端格式。
另外,还可以在输出section命令中使用FILL命令更改填充值。
例如:
SECTIONS
{
.text:
{
*(.text)
} =0x90909090
}
3.7 MEMORY COMMAND
链接器的默认配置允许分配所有可用内存,但可以通过MEMORY命令来覆盖此设置。MEMORY命令用于描述目标文件中内存块(blocks of memory)的位置和大小,指定了链接器可以使用的及不可使用的内存区域,并在内存溢出时发出警告。链接器不会打乱section的分布来适应可用的内存区域。MEMORY的语法如下:
MEMORY
{
regionname [(attr)] : ORIGIN = origin, LENGTH = len
...
}
regionname是链接器脚本中用于引用该内存区域(region)的名称,region的名称存储在单独的命名空间中,只在链接器脚本范围内有意义,且不会与符号名称、文件名或section名冲突,但每个region之间不可重名。定义了内存区域后,就可以在输出section属性中使用 '>regionname' 指示链接器将特定的输出section放入该内存区域。
attr(区域属性)字符串是一个可选的属性列表,它指定是否为没有在链接器脚本中显式映射的输入section使用特定的内存区域。如果没有为某个输入section指定输出section,链接器将创建一个与输入section同名的输出section。如果定义了区域属性,链接器将根据属性为输出section选择内存区域。
区域属性只能选择以下几种:
R | Read-only section |
W | Read/write section |
X | Executable section |
A | Allocatable section |
I | Initialized section |
L | Same as 'I' |
! | 对以上几种属性含义取反 |
一个未分配内存的section配置上面除 '!' 之外的任何属性,该section都会被放入此内存区域中。反之,一旦配置了 '!',只有不匹配其余任何属性,才可以放入对应内存区域中。
origin是内存区域的起始地址(一个常量表达式)。在执行内存分配前,表达式的值必须式常量,即不可以使用任何与secton相关的符号。此外,关键字 ORIGIN 可以缩写成小写的org或o。
len是一个以字节为单位的常量表达式,表示内存区域的大小。与origin一样,也是个常量表达式。关键字LENGTH可以缩写成小写的len或l。
例如:
MEMORY
{
rom [rx] : ORIGIN = 0, LENGTH = 256K
ram [!rx] : ORIGIN = 0X40000000, l= 4M
}
其中,链接器会将只读和可执行的section放入rom内存区域中,而其他的secton则会放入ram中。
3.7 PHDRS COMMAND
ELF目标文件格式使用程序头,也称为segment(段)。程序头描述了程序应如何加载到内存中,当elf程序运行时,系统加载程序会读取程序头文件,以便弄清楚如何加载程序。
默认情况下,链接器将创建合理的程序头。在某些情况下,如果需要更精确地指定程序头文件,可以使用PHDRS命令来实现。在生成ELF输出文件时,链接器只关注PHDRS命令;其他情况下,链接器将直接忽略PHDRS命令。
以下是PHDRS命令的语法,PHDRS、FILEHDR、AT 和FLAGS是关键字:
PHDRS
{
name type [FILEHDR] [PHDRS] [AT (address) ]
[FLAGS (flags)];
}
name只用于在链接器脚本的SECTONS命令中引用,而非放入放入输出文件中。程序头的名称存储在单独的命名空间中,不会与符号名、文件名或section名称冲突,但每个程序头之间不可重名。
程序头的类型(type)描述了系统加载程序将从文件中加载哪些内存段。在链接器脚本中,会(通过 output section Phdr)指定这些内存段中防止哪些输出section,类型的定义如下(括号内常数表示类型的值):
PT_NULL(0) | 指示未使用的程序头(program header) |
PT_LOAD(1) | 指示程序头描述的段将从文件加载 |
PT_DYNAMIC(2) | 指示段中包含动态链接信息 |
PT_INTERP(3) | 指示段中包含程序解释器(program interpreter)的名字 |
PT_NOTE(4) | 指示段中包含note信息 |
PT_SHLIB(5) | 未定义的类型 |
PT_PHDR(6) | 指示段中包含程序头 |
FILEHDR关键字表示程序段应包含ELF文件头(file header),PHDRS关键字表示这些段应包含ELF程序头(proram headers).
可以使用AT表达式指定应该在内存中的特定地址加载段,这与用作输出section属性的AT命令相同,程序头的AT命令覆盖输出section的属性。
链接器通常会根据组成段的section来设置段标志(flags),可以使用FLAGS关键字来指定段标志。flags必须为整数,用于指定程序头的p_flags字段。
还是来个例子吧。
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*/
.dynamic :
{
*(.dynamic)
} :data :dynamic
...
}
3.6.8 链接标本中的表达式
链接脚本中共表达式语法与C语言表达式语法相同,但所有表达式都计算为整数,且所占字节数相同。如果主机和目标及都是32位,则整数大小为32位,否则为64位。可以在表达式中使用和设置符号值。
3.6.8.1 常数
所有的常数都是整数,八进制和十六进制的表示方法与C语言相同。此外,可以使用后缀K和M分别将常数缩放1024或1024*1024,例如,以下都是指同样的数量:
_fourk_1 = 4K;
_fourk_1 = 4096;
_fourk_1 = 0x1000;
3.6.8.2 Symbol Names
除非加引号,否则符号名称以字母、下划线或句点开头,可以包含字母、下划线、句点和连字符。未加引号的符号名不可与任何关键字冲突,但一旦加了引号,就可以六亲不认无法无天,指定包含各种奇葩字符或与关键字相同的符号,例如:
"SECTION" = 9;
"fuck boring" = "also fuck boring" + 10;
由于符号可以包含很多非字母字符,因而用空格分隔符号是最安全的,例如:'A-B'是一个符号,而'A - B' 是一个包含减号的表达式。
3.6.8.3 The Location Counter
作为一个特殊的链接器变量,'.' 总是包含当前输出文件的定位计数器。因为总是指向输出section中的位置,它只能出现在SECTIONS命令的表达式中。但作为符号,'.'可以出现在表达式中允许普通符号的任何地方,例如:
SECTIONS
{
.output:
{
file1(.text)
. = . + 1000; /*a 1000 byte gap here*/
file2(.text)
. = . + 1000; /*a 1000 byte gap here*/
file3(.text)
} = 0x12345678; /*use FILL to specify what data to write in the gaps*/
}
此外, '.'实际上指的是从当前包含目标的起始位置的相对偏移量。通常来说,SECTIONS语句的起始地址是0,因此可以用作绝对地址。但是,当'.' 用在section的描述中,它指的是从该section起始位置的相对偏移量,而不是一个绝对地址,例如:
SECTIONS
{
. = 0x100;
.text :
{
*(.text)
. = 0x200
} /* .text will be assigned a starting address of 0x100 and a size of 200
bytes, that is range from absolute address 0 to 0x200*/
. = 0x500;
.data :
{
*(.data)
. += 0x600;
} /* .data has an extra 0x600 bytes at the end of the input section .data
and the end of itself*/
.bss :
{
*(.bss)
}
}
3.6.8.4 求值
链接器是拖延症晚期患者,非绝对必要绝不计算表达式的值。链接时链接器必须知道一些信息,比如第一个section的起始地址,以及region(内存区域)的起始地址和长度。因此,当链接器读取链接脚本时,不得不尽快计算这些值。
但是,其他值(如符号值)只需要在存储分配后才需要知道。当符号赋值表达式需要的其他信息(如输出section的大小)可以使用时,再对这些值进行计算。然而,输出section的大小只有再内存分配后才能知道。
有一些表达式,比如哪些依赖于定位计数器的表达式,在section分配的时候必须进行求值。此时,如果需要表达式的结果,而该值不可用(比如包含非常量内容),则会产生一个错误,例如:
SECTIONS
{
.text 9+this_isnt_constant:
{
*(.text)
}
}
3.6.8.5 表达式的Section
当链接器对表达式求值时,结果要么时绝对的,要么是相对于某个section的。相对表达式表示相对于section base的固定偏移量。
至于表达式到底是绝对的还是相对的,取决于它在链接器脚本中的位置。出现在输出section定义中的表达式时相对于该section的base的;在其他地方出现的表达式则是相对的。
如果使用 '-r' 选项请i去可重定位输出,则设置未相对表达式的符号将是可重定位的。这意味着进一步的链接操作可能会改变符号的值,符号的section将是相对表达式的section。
设置为绝对表达式的符号将在以后的任何链接操作中保持相同的值。符号将是绝对的,且不会有任何特定的关联部分。可以使用内置函数ABSOLUE强制表达式为绝对,否则它将是相对的,例如:
SECTIONS
{
.data:
{
*(.data) _edata = ABSOLUTE(.);
}
}
其中,_edata为标识data段末尾的常量(这有点像是C语言中const 变量的感觉),如果不使用ABSOLUTE,_edata相对于.data段是相对的。
3.6.8.6 Buildin Functions
链接器脚本语言拥有许多用于表达式的内置函数。部分内置函数如下:
ABSOLUTE(exp) | 返回表达式exp的绝对值(绝对值表示不可重定位,而不是非负值),主要用于在section定义中为符号赋绝对值,其中符号值通常是相对于section的; |
ADDR(section) | 返回指定section的绝对地址(VMA),前提是脚本必须先定义了secton的存储位置; |
ALIGN(exp) | 返回定位计数器按表达式exp边界对齐的值,ALIGN并不会改变定位计数器的值,只会利用其值进行算数运算; |
BLOCK(exp) | 同ALING(exp),主要为了兼容老版本的链接器脚本,常用于设置输出section的地址; |
DEFINED(symbol) | 当symbol存在于链接器的全局符号表(global symbol table)中,且在DEFINED调用前已被定义,则返回1;否则,返回0; |
LOADADDR(section) | 返回section的绝对LMA,通常与ADDR返回一样的值,除非设置了输出section的AT属性(单独设置LMA) |
MAX(exp1,exp2) | 返回最大值 |
MIN(exp1,exp2) | 返回最小值 |
NEXT(exp) | 返回下一个exp整数倍的未分配地址,效果等同于ALIGN(exp),除非使用了MEMORY命令给输出section定义了非连续的内存; |
SIZEOF(section) | 如果section的内存已分配,则按字节返回secton的大小;否则,链接器报错; |
举几个例子:
SECTIONS
{
.output1:
{
start_of_output1 = ABSOLUTE(.);
...
}
.output2:
{
symbol_1 = ADDR(.output1);
symbol_2 = start_of_output1;
}
...
}
其中,symbol_1和symbol_2的值显然相同。
又如:
SECTIONS
{
.output:
{
.start = . ;
...
.end = . ;
}
symbol_1 = .end - .start;
symbol_2 = SIZEOF(.output);
...
}
在整个例子中,symbol_1和symbol_2的值显然相同。