在操作系统之上编程时我们是不需要关注这部分内容的,因为按照ld默认的行为链接即可,然后所有的事情都交给作系统处理完成。但是在嵌入式开发过程中,特别是移植Bootloader和内核的时候,链接脚本就显得格外的重要了。因为这个时候程序的运行环境都是裸机环境,没有任何的内存管理功能,代码操作的是物理地址,所以就要求我们对程序二进制镜像在链接和运行时的内存布局进行理安排。下面介绍的链接脚本就是干这件事的。
详细的链接脚本语法可以参考官方文档《Using as》。
链接脚本格式
首先,链接脚本就是文本文件。其中包含了一系列的命令,每一个命令是一个带有参数的关键字,或者是一个对符号的赋值。 你可以用分号分隔命令。在连接脚本中使用注释,用'/*'和'*/'隔开,就像在C中一样。下面就介绍一些常用的命令。
常用命令
ENTRY(SYMBOL)
程序中被执行的第一条指令称为"入口点",ENTRY()命令用来设置入口点。参数通常是一个标签名。ld会按如下顺序尝试确定程序的入口点, 如果成功了,就会停止继续尝试:
- ld的命令行选项-e指定了程序的入口点;
- 链接脚本中的ENTRY()命令;
- 程序中定义的start符号;
- 程序中.text段的首地址;
- 地址`0';
推荐使用上述前三种中的某一种式来明确指定程序的入口点。
INCLUDE FILENAME
在命令执行处包含链接脚本文件FILENAME。 ld会在当前路径下或-L选项指定的路径下搜索该文件,INCLUDE命令可以嵌套最多10层。
INPUT(FILE, FILE, ...)
INPUT(FILE FILE ...)
指示ld将这些文件一起链接, 效果和在命令行指定它们一样。例如,如果你在连接的时候总是要包含文件'subr.o',但是又不想每次在命令行上输入它,就可以在你的连接脚本中使用命令INPUT (subr.o)。
然而,Makefil或一些自动化脚本都可以达到该效果,所有有必要将链接脚本弄的很复杂。
INPUT (-lFILE)
ld会把FILE转换为'libFILE.a',就像命令行参数-l一样。
OUTPUT(FILENAME)
相当于在命令行中使用-o FILENAME选项。
SEARCH_DIR(PATH)
和命令行-L选项相同,如果两个都使用了,那ld两个路径都会搜索。用命令行选项指定的路径首先被搜索。
OUTPUT_FORMAT(BFDNAME)
指定输出文件的BFD格式。OUTPUT_formAT(BFDNAME)和命令行选项-oformat BFDNAME等效,如果两个都使用了,命令行选项优先。
OUTPUT_FORMAT(DEFAULT, BIG, LITTLE)
配合命令行选项-EB和-EL来指定输出文件的BFD格式。如果命令行既没有-EB,也没有-EL,那么输出文件格式为DEFAULT参数指定的BFD格式;如果命令行为-EB,那么输出文件格式为BIG参数指定的BFD格式;反之,就是LITTLE参数指定的BFD格式。
OUTPUT_ARCH(BFDARCH)
指定输出文件所属的处理器架构,参数是BFD库中使用的一个名字。可以通过使用带有-f选项的'objdump'程序来查看一个目标文件所属的体系结构。
符号赋值
在链接脚本中,可以定义新的符号,此时这些符号代表的是地址。当然也可以对输入文件中的符号重新赋值(不过一般不这么干)。
链接脚本中对符号直接赋值就相当于定义该符号,语法支持所有的C语言风格的赋值语句:
SYMBOL = EXPRESSION ;
SYMBOL += EXPRESSION ;
SYMBOL -= EXPRESSION;
SYMBOL *= EXPRESSION;
SYMBOL /= EXPRESSION;
SYMBOL <<= EXPRESSION;
SYMBOL >>= EXPRESSION;
SYMBOL &= EXPRESSION;
SYMBOL |= EXPRESSION ;
第一个情况会把SYMBOL定义并赋值为EXPRESSION。其它情况下,SYMBOL必须是已经定义了的,而值会作出相应的调整。
注意:在符号赋值语句的末尾一定要有分号,但是其他类型的命令后面不需要分号(好奇怪的语法)。
SECTIONS
该命令描述了输出节的内容,ld会根据它的说明将输入文件中的节安排到指定的输出节中。如果链接脚本中未使用该命令,那么ld会按在输入文件中遇到的节的顺序把每一个输入节放到同名的输出节中。SECTIONS命令的基本格式如下:
SECTIONS {
SECTIONS-COMMAND
SECTIONS-COMMAND
...
}
每一个SECTIONS-COMMAND可能是下面任意一种内容:
- 一个ENTRY命令或一个符号赋值;
- 一个输出节描述;
- 一个重叠描述;
ENTRY命令和符号赋值在SECTIONS命令中是允许的,这是为了方便在这些命令中使用定位计数器,定位计数器用一个"."来表示,它代表了当前链接地址的值。
输出节描述
一个完整的输出节格式如下:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] {
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
方括号中的内容都是可选的,而且在实际使用中也确实很少用到,下面介绍一些常用的语法。
输出节名SECTION
输出节的名字必须满足输出文件的要求。在一个只支持限制数量的节的格式中,比如'a.out',这个名字必须是格式支持的节名中的一个(比如, 'a.out'只允许'.text','.data'或'.bss')。在ARM中该名字几乎没有限制。
ADDRESS
每个输出节最终都会有一个链接地址。1) 如果指定了ADDRESS,那么该输出节的链接地址就是ADDRESS指定的值;2)否则,如果指定了REGION,那么链接地址为REGION;3)否则链接地址为会被设为当前定位计数器向上对齐的值,输出节的对齐要求是其所有输入节中对齐要求中最严格的一个。
ADDRESS可以是任意表达式。比如,如果需要把节安排在0x10字节对齐处可以这样做:
.text ALIGN(0x10) : {
*(.text)
}
这个语句可以正常工作,因为ALIGN命令返回的是将定位计数器按照指定字节对齐后的值。注意:指定一个节的地址会改变定位计数器的值。
TYPE
TYPE指定输出节的类型,目前定义的一些属性几乎不会用到,暂时不关注。
LMA
每个输出节除了链接地址外,还有一个加载地址,默认情况下,ld会将该地址值设置为链接地址,即ADDRESS决定的地址。可以通过该关键字改变这种默认行为,指定一个特定的加载地址。
这个特性常用于u-boot和内核的链接脚本中。如果链接地址和加载地址不相等,那么在程序运行前,需要首先将其搬到VMA地址处,这种移动通常需要我们自己来实现。
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND的内容可以是下面任意一种:
- 一个符号赋值;
- 一个输入节描述;
- 输出节数据;
- FILL关键字;
输入节描述
输入节描述是最核心的链接脚本语法。使用输入节告诉ld如何把输入文件的内容映射到输出节中。
一个输入节描述的全部格式如下所示:
文件(节名...)
其中文件名可以是通配符,来匹配多个文件,通配符下面介绍。节名也是可选的,如果不指定节名,那么小括号也可以没有,此时会将文件中的所有节都包含到该输出节中。节名也可以使用通配符。
文件和节名部分的通配符和shell中的通配符类似,主要有下面几种:
- 星号*可以匹配任意数量的字符;
- 问号?可以匹配任意单个字符;
- 方括号[CHARS]可以匹配范围内的任意单个字符;
- 斜杠 \ 可以转义其后面的任意一个通配字符;
输出节数据
可以通过输出节命令BYTE、SHORT、LONG、QUAD、SQUAD在输出节中显式包含若干字节的数据,数据被保存在了当前定位计数器指示位置,填充完毕后,定位计数器向前移动指定字节。示例如下:
// 错误示例
SECTIONS {
.text : { *(.text) }
LONG(1)
.data : { *(.data) }
}
// 正确示例
SECTIONS {
.text : { *(.text) LONG(1) }
.data : { *(.data) }
}
注意, 这些命令只在一个节描述内部才有效,而不是在它们之间。
FILL关键字
输入节在安排过程中可能会出现裂缝(如特殊的对齐要求),如果有需要,可以使用FILL关键字指定这些裂缝的填充值。
SECTIONS {
.text : { *(.text) LONG(1) }
FILL(0x90)
. += 100;
.data : { *(.data) }
}
如上,FIILL会用0x90填充出现的裂缝。由于FILL只会对出现在其语句之后的裂缝进行填充,所以可以通过多个FILL关键字指定不同的填充效果。
FILL关键字的作用和输出节属性=FILLEXP类似,只不过后者影响的是整个节,而且如果同时出现,那么FILL优先级更高。
表达式
链接脚本中的表达式语法跟C是完全是相同的,所有的表达式都以整型值被求值,它们和系统的位数是相同的。
常数
常数都是整型值。就像在C中,ld把以0开头的视为八进制数,把以0x或0X开头的视为十六进制数,把其它的整型数视为十进制。
可以使用K和M后缀作为常数的度量单位,分别表示1024和1024*1024。
表达式的节
当ld在计算表达式的值时,得到的结果可能是一个绝对值,也可能跟某个节相关的偏移值。表达式出现的位置决定了它是绝对的或是节相关的:出现在输出节定义中的表达式是跟输出节的基地址相关的;其它地方的表达式的值是绝对的。
使用内建函数ABSOLUTE()可以将一个节相关的表达式的值转换为绝对的值。如下示例将_edata赋值为输出节.data的末尾地址的绝对符号:
SECTIONS {
.data : {
*(.data)
_edata = ABSOLUTE(.);
}
}
如果没有使用ABSOLUTE(),那么_edata的值是基于.data的偏移量。
内建函数
ABSOLUTE(EXP)
返回表达式EXP的绝对值(不可重定位)。主要在把一个绝对值赋给一个节定义内的符号时使用。
ADDR(SECTION)
返回节SECTION的链接地址。下面的例子中,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向上对齐后的值,它并不改变定位计数器的值,它只是进行了一次计算。
SECTIONS {
...
.data ALIGN(0x2000): {
*(.data)
variable = ALIGN(0x8000);
}
...
}
上述例子中,把输出节.data对齐到下一个0x2000字节的边界,并在输入节之后把节内的一个变量对齐到下一个0x8000字节的边界。