每一个链接器都需要链接脚本来将不同的对象文件链接成最终的可执行文件。链接脚本的主要目的是指定如何将输入文件的不同节区(section)映射到输出文件中去,同时还可以控制输出文件最终在物理内存中的布局。
如果不特别指定的话,链接器程序本身自带了一个默认的链接脚本,可以通过一下命令获得这个默认的脚本:
ld --verbose
一般情况来说,默认的链接脚本就够用了,大多数人也不会自己专门编写链接脚本。
不过,如果你是编写内核或者固件程序的话,默认的链接脚本有时候就会显得不够用了,需要自己编写。可以用-T参数来告知连接器,使用你自己编写的链接脚本:
ld -T <YOUR LINKER SCRIPT>
链接脚本主要由命令和赋值语句组成,可以使用分号(;)将不同的命令或赋值语句隔开。
最常用的命令有以下几个:
1)ENTRY
使用ENTRY命令来指定输出的可执行文件的入口函数,也就是程序加载完成后要执行的第一条指令的位置。该命令的参数是一个符号名,有可能是入口函数的函数名,也有可能是节区的名字。
ENTRY(symbol)
2)SECTIONS
这个命令是链接脚本最重要的部分,真正定义了如何将输入文件的不同节区映射到输出文件的对应节区中。其格式定义如下:
SECTIONS
{
sections-command
sections-command
...
}
首先要有一个“SECTIONS”,接着一对大括号,里面包含了多条具体控制节区的命令,一般有如下几种:
- 可以是一个ENTRY命令,如前一节所述;
- 可以对一个符号进行赋值;
- 可以是一个对输出节区的描述;
对符号的赋值其实就是定义一个全局变量,可以在后面的语句中使用。其赋值格式和C语言很像,有如下几种:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
第一种情况定义符号,并对其赋值;后面的所有情况都必须要求符号首先被定义。注意,不要忘了像C语言一样,在最后加上“;”。
这其中有一个默认常用符号,句号(.)代表位置计数器。如果不特别指定的话,每个节区的数据都将会放到当前位置计数器所在的地方,然后位置计数器会相应加上节区的大小。
如果是一个对输出节区的描述,那情况要复杂很多,可能由很多部分组成,其定义如下:
section [address] [(type)] : [AT(lma)]
{
input-section-command
input-section-command
...
} [>region] [:phdr :phdr ...] [=fillexp]
“section”表示输出节区的名字,这个命令中指定的所有输入的节区都将映射到输出文件以这个名字命名的节区中。这其中有一个特例,输出节区名可以指定为“/DISCARD/”,如果这样的话,所有这条命令中指定的输入文件的节区都会被丢弃掉,不会出现在输出文件中。
“address”表示输出节区的起始虚拟地址(Virtual Address),这个参数不是必须的,如果没有指定的话(后面region也不指定),那么就会将该输出节区放置到当前位置计数器指定的位置上。
“(type)”表示输出节区的类型,这个参数也是可选的,而且一般不常用,如果用的话不要忘记加括号。最常碰到的是“NOLOAD”,如果设置成这个值,那么输出的节区将来执行的时候不会被加载进内存。
“AT(lma)”表示输出节区的加载地址(Load Address),同样也是可选的。前面提到的“address”指定的是虚拟地址,而这里指定的是实际加载的地址。如果不指定的话,那么加载地址被默认设置成虚拟地址。
“>region”用来指定将这个节区放置在某个内存区域(由MEMORY命令定义)之内。
“=fillexp”是一个表达式,表示输出节区所有未指定的区域都使用这个表达式的值来填充。某个节区和下一个节区开始之间可能有一些区域是空着的(有可能是因为要对齐造成的),对于这些区域就会用指定的值填充。
“input-section-command”指明哪些输入文件的哪些节区要放置到当前输出节区中,格式是:
input-file(section)
输入文件名接着一个括号指定输入节区名。这两个参数都可以使用通配符,主要有以下几种:
- “*”:匹配任何数量的任何字符;
- “?”:匹配一个任意字符;
- “[chars]”:匹配指定的字符,如果是一定范围内的连续字符可以用“-”指定,如“[a-z]”表示所有的小写字符。
如果文件名加节区的组合有多条规则可以匹配上,只会用最先匹配上的第一条,后面的匹配规则自动失效。
如果要将输入文件的多个节区映射到当前输出文件节区内,可以有两种做法。第一种是:
input-file(section1 section2)
而第二种是:
input-file(section1)
input-file(section2)
这两种写法都可以满足要求,但还是有一些细微的差别。第一种写法,输入文件中的两个节区会“交织”着写入输出文件节区中;而第二种写法,会先将输入文件的“section1”节区写入后再写入“section2”节区。
3)MEMORY
链接器默认会认为可以使用系统中的所有内存,但现实情况往往可能不是这样的,地址空间中的某些区域会被固定占用,不能被程序映射。可以使用MEMORY命令指定出一些内存区域,指示链接器将输出节区放置进去。该命令格式如下:
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
...
}
“name”是你给这块内存区域起的名字,只在链接脚本内有意义。在“sections-command”中可以用“>name”指定将输出节区放置在这个定义的内存区域中。
“(attr)”是这块内存区域的属性(不要忘记括号),该参数不是必须的,共有以下几种定义:
- “R”:表示内存区域只读;
- “W”:表示内存区域可读写;
- “X”:表示内存区域可执行;
- “A”:表示内存区域可分配;
- “I”或“L”:表示内存区域是需要初始化的;
- “!”:将该符号后面所有属性取反。
“ORIGIN”指出该内存区域的起始地址,后面的表达式必须是一个可以在链接时确定的固定数值,不能是相对于某一个节区的相对数值。“ORIGIN”可以缩写成“org”或“o”。
“LENGTH”指出该内存区域的长度大小,单位是字节。同样,后面的表达式必须是一个可以在链接时确定的固定数值。“LENGTH”可以缩写成“len”或“l”。
4)ASSERT
如果满足条件就继续执行,否则就退出,其命令格式如下:
ASSERT(exp, message)
确保表达式的值非0。否则,如果等于0的话,则链接退出,并且打印指定信息。
5)OUTPUT_ARCH
指明输出文件支持的指令集,该命令格式如下:
OUTPUT_ARCH(bfdarch)
参数就是指令集,例如对于Arm64来说,设置成“aarch64”。
最后,链接脚本还可以使用不少非常有用的内置函数:
1)ABSOLUTE(exp):返回表达式的绝对值,使用了之后这个值将不能被重定位,通常会在节区内使用。
2)ADDR(section):返回指定节区的虚拟地址,这个节区必须已经被定义过。
3)ALIGN(exp)或BLOCK(exp):根据位置计数器,计算满足表达式对齐要求的最近一个位置,表达式的值必须是2的幂次方。需要注意的是,这个函数并不会修改位置计数器。
4)SIZEOF(section):返回指定节区的字节大小,这个节区必须已经被定义过,否则会报错。
5)SIZEOF_HEADERS:返回输出文件头的大小。