最近在看u-boot、osekOS的启动代码,其中涉及到lds文件,通过参考其他网友的文章,希望对lds文件有个明晰的认识,为了巩固及加深影响,特将相关博客内容重写一遍。
原始文章: http://linux.chinaunix.net/techdoc/beginner/2009/08/12/1129972.shtml。原始文章讲解更清楚,不懂的地方可以返回参考。
一、概念
1.1 lds文件
lds文件与scatter文件相似,定义了整个程序编译之后的连接过程,都是决定一个可执行程序的各个段的存储位置,以及入口地址,这也是链接定位的作用。
每一个链接过程都由链接脚本(linker script, 一般以lds作为文件的后缀名)控制。链接器通过链接脚本中指定的规则把一个或多个输入文件(可以是目标文件或链接脚本文件)的section合成一个输出文件(可以是目标文件或可执行文件)。
1.2 section
为了区分不同文件的section,目标文件中的每个section需要至少包含名字和大小,大部分还包含与其相关联的一块数据section contents。一个section可被标记为“loadable” (可加载的,在输出文件运行时,该section被载入进程地址空间)或“allocatable” (可分配的,输出文件运行时,在进程地址空间中预留section大小的部分)。
在目标文件中, loadable或allocatable的输出section有两种地址: VMA(virtual Memory Address,进程执行时使用的地址)和LMA(Load Memory Address,加载到进程地址空间的地址)。一般而言, 某section的VMA = LMA. 但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况: 比如将输出文件加载到开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定)。
1.3 符号
每个目标文件都有符号表(SYMBOL TABLE), 包含已定义的符号(对应全局变量和static变量和定义的函数的名字)和未定义符号(未定义的函数的名字和引用但没定义的符号)信息。每个符号对应一个地址, 即符号值(这与c程序内变量的值不一样, 某种情况下可以把它看成变量的地址)。
二、语法
2.1 脚本格式
链接脚本由一系列命令组成, 每个命令由一个关键字(一般在其后紧跟相关参数)或一条对符号的赋值语句组成。命令由分号‘;’分隔开。文件名或格式名内如果包含分号’;'或其他分隔符, 则要用引号‘”’将名字全称引用起来,无法处理含引号的文件名。
/* */之间的是注释。
GNU官方网站上对.lds文件形式的完整描述: 脚本命令; 脚本命令; … SECTIONS { ... secname start BLOCK(align) (TYPE) : AT ( ldadr ) { contents }[>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP] ... }
说明:secname和contents是必须的,前者用来命名这个段,后者用来确定代码中的什么部分放在这个段,以下是对这个描述中的一些关键字的解释。
1、脚本命令:参见2.3节,可以设置内存段、输出格式、入口地址等。
2、secname:输出section段名。
3、start BLOCK:是段的重定位地址,本段连接(运行)的地址,如果代码中有位置无关指令,程序运行时这个段必须放在这个地址上。start可以用任意一种描述地址的符号来描述。设置了输出section的VMA地址,其中align将定位符号的值调整到满足输出section对齐要求后的值。
4、TYPE:输出section类型,指明是否在程序运行时载入内存。
5、AT(ldadr):定义本段存储(加载)的地址,默认情况下,LMA等于VMA,但可以通过关键字AT()指定LMA。用关键字AT()指定,括号内包含表达式,表达式的值用于设置LMA。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围。通过这个选项可以控制各段分别保存于输出文件中不同的位置。
6、contents:决定哪些内容放在本段,可以是整个目标文件,也可以是目标文件中的某段(代码段、数据段等),详情参考2.4节。
7、region:如果start没有定义,则以region地址为VMA,如果region也没定义,以定位符号‘.’的值确定VMA。
8、phdr:没在具体的实例中看到过。
9、FILLEXP:指明空闲空间(如按align对齐后的空闲部分)的填充字段。
2.2 符号及表达式
2.2.1 符号
没有被引号“”包围的符号,以字母、下划线或‘.’开头,可包含字母、下划线、’.'和’-'。当符号名被引号包围时,符号名可以与关键字相同。
(1). 是一个特殊的符号,它是定位器,一个位置指针,指向程序地址空间内的某位置(或某section内的偏移,如果它在SECTIONS命令内的某section描述内),该符号只能在SECTIONS命令内使用。
(2)PROVIDE关键字:用于定义在目标文件内被引用,但没有在任何目标文件内被定义的符号。
2.2.2 表达式
(1)赋值
在目标文件内定义的符号可以在链接脚本内被赋值,但只有对全局变量赋值才有效,而且此处的赋值是更改这个符号对应的地址,而不是变量的值。赋值语句可以出现在连接脚本的三处地方:SECTIONS命令内,SECTIONS命令内的section描述内和全局位置;常用赋值操作有:
SYMBOL = EXPRESSION ; SYMBOL += EXPRESSION ; SYMBOL -= EXPRESSION ; SYMBOL *= EXPRESSION ; SYMBOL /= EXPRESSION ; SYMBOL >= EXPRESSION ; SYMBOL &= EXPRESSION ; SYMBOL |= EXPRESSION ;
注意:除了第一类表达式外, 使用其他表达式需要SYMBOL被定义于某目标文件。
(2)运算符优先级
优先级 结合顺序 操作符 1 left ! – ~ (1) 2 left * / % 3 left + - 4 left >> = 6 left & 7 left | 8 left && 9 left || 10 right ? : 11 right &= += -= *= /= (2)
(3)内建函数
ABSOLUTE(EXP) :转换成绝对值 ADDR(SECTION) :返回某section的VMA值。 ALIGN(EXP) :返回定位符’.'的修调值,对齐后的值,(. + EXP – 1) & ~(EXP – 1) BLOCK(EXP) :如同ALIGN(EXP),为了向前兼容。 DEFINED(SYMBOL) :如果符号SYMBOL在全局符号表内,且被定义了,那么返回1,否则返回0。 LOADADDR(SECTION) :返回三SECTION的LMA MAX(EXP1,EXP2) :返回大者 MIN(EXP1,EXP2) :返回小者 NEXT(EXP) :返回下一个能被使用的地址,该地址是EXP的倍数,类似于ALIGN(EXP)。除非使用了MEMORY命令定义了一些非连续的内存块,否则NEXT(EXP)与ALIGH(EXP)一定相同。 SIZEOF(SECTION) :返回SECTION的大小。当SECTION没有被分配时,即此时SECTION的大小还不能确定时,连接器会报错。 SIZEOF_HEADERS : sizeof_headers :返回输出文件的文件头大小(还是程序头大小),用以确定第一个section的开始地址(在文件内)。
2.3 脚本命令
2.3.1 简单命令
命令 |
说明 |
备注 |
ENTRY(SYMBOL) |
将符号SYMBOL的值设置成入口地址(进程执行的第一条用户空间的指令在进程地址空间的地址)。 |
进程入口地址优先级:ld命令行的-e选项>连接脚本的ENTRY(SYMBOL)命令>如果定义了start符号, 使用start符号值>如果存在.text section, 使用.text section的第一字节的位置值>使用值0。 |
INCLUDE filename |
包含其他名为filename的链接脚本 |
可以嵌套使用, 最大深度为10。 |
INPUT(files) |
将括号内的文件做为链接过程的输入文件。 |
ld首先在当前目录下寻找该文件, 如果没有, 则在由-L指定的搜索路径下搜索 |
GROUP(files) |
指定需要重复搜索符号定义的多个输入文件 |
file必须是库文件,被ld重复扫描,直到不在有新的未定义的引用出现。 |
OUTPUT(FILENAME) |
定义输出文件的名字 |
|
SEARCH_DIR(PATH) |
定义搜索路径 |
|
STARTUP(filename) |
指定filename为第一个输入文件 |
链接过程中, 每个输入文件是有顺序的,此命令设置filename为第一个输入文件。 |
TARGET(BFDNAME) |
设置输入文件的BFD格式。 |
|
OUTPUT_FORMAT(BFDNAME) |
设置输出文件使用的BFD格式。 |
|
OUTPUT_FORMAT(DEFAULT,BIG,LITTLE) |
定义三种输出文件的格式(大小端)。 |
若有命令行选项-EB, 则使用第2个BFD格式; -EL,则使用第3个BFD格式.否则默认第一个。 |
ASSERT(EXP, MESSAGE) |
如果EXP不为真,终止连接过程 |
|
EXTERN(SYMBOL SYMBOL …) |
在输出文件中增加未定义的符号 |
|
FORCE_COMMON_ALLOCATION |
为common symbol(通用符号)分配空间,即使用了-r连接选项也为其分配 |
|
NOCROSSREFS(SECTION SECTION …) |
检查列出的输出section,如果发现他们之间有相互引用,则报错。 |
对于某些系统,特别是内存较紧张的嵌入式系统,某些section是不能同时存在内存中的,所以他们之间不能相互引用。 |
OUTPUT_ARCH(BFDARCH) |
设置输出文件的machine architecture(体系结构) |
BFDARCH为被BFD库使用的名字之一。 |
man -S 1 ld |
查看ld的联机帮助, 里面也包括了对以上命令的介绍. |
2.3.2 其它命令
(1)内存区域命令
在默认情形下,连接器可以为section分配任意位置的存储区域。你也可以用MEMORY命令定义存储区域,并通过输出section描述的> REGION属性显示地将该输出section限定于某块存储区域,当存储区域大小不能满足要求时,连接器会报告该错误。
MEMORY命令的文法如下: MEMORY { NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2 NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2 … }
说明如下:
NAME :存储区域的名字,这个名字可以与符号名、文件名、section名重复,因为它处于一个独立的名字空间。
ATTR :定义该存储区域的属性,在讲述SECTIONS命令时提到,当某输入section没有在SECTIONS命令内引用时,连接器会把该输入 section直接拷贝成输出section,然后将该输出section放入内存区域内。如果设置了内存区域设置了ATTR属性,那么该区域只接受满足该属性的section(怎么判断该section是否满足?输出section描述内好象没有记录该section的读写执行属性)。
ATTR属性内可以出现以下7个字符:
R 只读section W 读/写section X 可执行section A ‘可分配的’section I 初始化了的section L 同I ! 不满足该字符之后的任何一个属性的section
ORIGIN :关键字,区域的开始地址,可简写成org或o
LENGTH :关键字,区域的大小,可简写成len或l
(2)PHDRS命令
在连接脚本内不指定PHDRS命令时,连接器能够很好的创建程序头,但是有时需要更精确的描述程序头,那么PAHDRS命令就派上用场了。
注意:一旦在连接脚本内使用了PHDRS命令,那么连接器**仅会**创建PHDRS命令指定的信息,所以使用时须谨慎。
PHDRS命令文法如下:
PHDRS
{
NAME TYPE [ FILEHDR ] [ PHDRS ] [ AT ( ADDRESS ) ]
[ FLAGS ( FLAGS ) ] ;
}
其中FILEHDR、PHDRS、AT、FLAGS为关键字。
NAME :为程序段名,此名字可以与符号名、section名、文件名重复,因为它在一个独立的名字空间内。此名字只能在SECTIONS命令内使用。
一个程序段可以由多个‘可加载’的section组成。通过输出section描述的属性:PHDRS可以将输出section加入一个程序段,: PHDRS中的PHDRS为程序段名。在一个输出section描述内可以多次使用:PHDRS命令,也即可以将一个section加入多个程序段。
如果在一个输出section描述内指定了:PHDRS属性,那么其后的输出section描述将默认使用该属性,除非它也定义了:PHDRS属性。显然当多个输出section属于同一程序段时可简化书写。
在TYPE属性后存在FILEHDR关键字,表示该段包含ELF文件头信息;存在PHDRS关键字,表示该段包含ELF程序头信息。
TYPE可以是以下八种形式: PT_NULL 0表示未被使用的程序段 PT_LOAD 1表示该程序段在程序运行时应该被加载 PT_DYNAMIC 2表示该程序段包含动态连接信息 PT_INTERP 3表示该程序段内包含程序加载器的名字,在linux下常见的程序加载器是ld-linux.so.2 PT_NOTE 4表示该程序段内包含程序的说明信息 PT_SHLIB 5一个保留的程序头类型,没有在ELF ABI文档内定义 PT_PHDR 6表示该程序段包含程序头信息。 EXPRESSION 表达式值
以上每个类型都对应一个数字,该表达式定义一个用户自定的程序头。
AT(ADDRESS)属性定义该程序段的加载位置(LMA),该属性将**覆盖**该程序段内的section的AT()属性。
默认情况下,连接器会根据该程序段包含的section的属性(什么属性?好象在输出section描述内没有看到)设置FLAGS标志,该标志用于设置程序段描述的p_flags域。
(3)版本号命令
请参看原文介绍
2.4 SECTIONS详解
SECTIONS命令告诉ld如何把输入文件的sections映射到输出文件的各个section: 如何将输入section合为输出section; 如何把输出section放入程序地址空间(VMA)和进程地址空间(LMA),格式如下:
SECTIONS { ENTRY命令/符号赋值语句/一个输出section的描述(output section description)/一个section叠加描述(overlay description) }
其中,一个输出section描述的格式为:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { 符号赋值语句/一个输入section描述/直接包含的数据值/一个特殊的输出section关键字 } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
说明如下:
(1)输出section名字(SECTION):
输出section名字必须符合输出文件格式要求,比如:a.out格式的文件只允许存在.text、.data和.bss section名。而有的格式只允许存在数字名字,那么此时应该用引号将所有名字内的数字组合在一起;另外,还有一些格式允许任何序列的字符存在于 section名字内,此时如果名字内包含特殊字符(比如空格、逗号等),那么需要用引号将其组合在一起。
(2)输出section地址(ADDRESS):
ADDRESS是一个表达式,它的值用于设置VMA。如果没有该选项且有REGION选项,那么连接器将根据REGION设置VMA;如果也没有 REGION选项,那么连接器将根据定位符号‘.’的值设置该section的VMA,将定位符号的值调整到满足输出section对齐要求后的值,输出 section的对齐要求为:该输出section描述内用到的所有输入section的对齐要求中最严格的。
注意:设置ADDRESS值,将更改定位符号的值。
(3)TYPE :
每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。它可以为以下五种值,
NOLOAD :该section在程序运行时,不被载入内存。
DSECT,COPY,INFO,OVERLAY :这些类型很少被使用,为了向后兼容才被保留下来。这种类型的section必须被标记为“不可加载的”,以便在程序运行不为它们分配内存。
(4)输出section的LMA :
默认情况下,LMA等于VMA,但可以通过关键字AT()指定LMA。
用关键字AT()指定,括号内包含表达式,表达式的值用于设置LMA。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围。
这个属性主要用于构件ROM境象。
(5)输入section描述:
最常见的输出section描述命令是输入section描述。输入section描述是最基本的连接脚本描述。
1)输入section描述基础:
基本语法:FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...)
FILENAME文件名,可以是一个特定的文件的名字,也可以是一个字符串模式。
SECTION名字,可以是一个特定的section名字,也可以是一个字符串模式
例如:
*(.text) :表示所有输入文件的.text section (*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有输入文件的.ctors section。 data.o(.data) :表示data.o文件的.data section data.o :表示data.o文件的所有section *(.text .data) :表示所有文件的.text section和.data section,顺序是:第一个文件的.text section,第一个文件的.data section,第二个文件的.text section,第二个文件的.data section,... *(.text) *(.data) :表示所有文件的.text section和.data section,顺序是:第一个文件的.text section,第二个文件的.text section,...,最后一个文件的.text section,第一个文件的.data section,第二个文件的.data section,...,最后一个文件的.data section
2)字符串模式内可存在以下通配符:
* :表示任意多个字符 ? :表示任意一个字符 [CHARS] :表示任意一个CHARS内的字符,可用-号表示范围,如:a-z :表示引用下一个紧跟的字符 SORT():对满足字符串模式的所有名字进行递增排序,如SORT(.text*)。
在文件名内,通配符不匹配文件夹分隔符/,但当字符串模式仅包含通配符*时除外。另外,任何一个文件的任意section只能在SECTIONS命令内出现一次。
3)通用符号(common symbol)的输入section
在许多目标文件格式中,通用符号并没有占用一个section。连接器认为:输入文件的所有通用符号在名为COMMON的section内。
4)输入section和垃圾回收
在连接命令行内使用了选项–gc-sections后,连接器可能将某些它认为没用的section过滤掉,此时就有必要强制连接器保留一些特定的 section,可用KEEP()关键字达此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text))。
5)在输出section存放数据命令
能够显示地在输出section内填入你想要填入的信息(这样是不是可以自己通过连接脚本写程序?当然是简单的程序)。
BYTE(EXPRESSION) 1 字节 SHORT(EXPRESSION) 2 字节 LOGN(EXPRESSION) 4 字节 QUAD(EXPRESSION) 8 字节 SQUAD(EXPRESSION) 64位处理器的代码时,8 字节 注意,这些命令只能放在输出section描述内,其他地方不行。 错误:SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } } 正确:SECTIONS { .text : { *(.text) LONG(1) } .data : { *(.data) } }
6)FILL(EXPRESSION)和=FILEEXP属性
在当前输出section内可能存在未描述的存储区域(比如由于对齐造成的空隙),可以用FILL(EXPRESSION)命令决定这些存储区域的内容, EXPRESSION的前两字节有效,这两字节在必要时可以重复被使用以填充这类存储区域。如FILE(0×9090)。在输出section描述中可以 有=FILEEXP属性,它的作用如同FILE()命令,但是FILE命令只作用于该FILE指令之后的section区域,而=FILEEXP属性作用 于整个输出section区域,且FILE命令的优先级更高!!!
7)输出section内命令的关键字
CREATE_OBJECT_SYMBOLS :为每个输入文件建立一个符号,符号名为输入文件的名字。每个符号所在的section是出现该关键字的section。
CONSTRUCTORS :与c++内的(全局对象的)构造函数和(全局对像的)析构函数相关。具体介绍可参看原文 http://linux.chinaunix.net/techdoc/beginner/2009/08/12/1129972.shtml。
8)输出section的丢弃
例子,.foo { *(.foo) },如果没有任何一个输入文件包含.foo section,那么连接器将不会创建.foo输出section。但是如果在这些输出section描述内包含了非输入section描述命令(如符号 赋值语句),那么连接器将总是创建该输出section。
有一个特殊的输出section,名为/DISCARD/,被该section引用的任何输入section将不会出现在输出文件内,
(6)覆盖图(overlay)描述
覆盖图描述使两个或多个不同的section占用同一块程序地址空间。覆盖图管理代码负责将section的拷入和拷出。文法如下,
SECTIONS { … 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] … }
由以上文法可以看出,同一覆盖图内的section具有相同的VMA。SECNAME2的LMA为SECTNAME1的LMA加上SECNAME1的大 小,同理计算SECNAME2,3,4…的LMA。SECNAME1的LMA由LDADDR决定,如果它没有被指定,那么由START决定,如果它也没有 被指定,那么由当前定位符号的值决定。
NOCROSSREFS关键字指定各section之间不能交叉引用,否则报错。
对于OVERLAY描述的每个section,连接器将定义两个符号__load_start_SECNAME和__load_stop_SECNAME,这两个符号的值分别代表SECNAME section的LMA地址的开始和结束。
连接器处理完OVERLAY描述语句后,将定位符号的值加上所有覆盖图内section大小的最大值。
2.5 暗含的链接脚本
输入文件可以是目标文件,也可以是连接脚本,此时的连接脚本被称为暗含的连接脚本。如果连接器不认识某个输入文件,那么该文件被当作连接脚本被解析。一个暗含的连接脚本不会替换默认的连接脚本,仅仅是增加新的连接而已。在连接命令行中,每个输入文件的顺序都被固定好了,暗含的连接脚本在连接命令行内占住一个位置,这个位置决定了由该连接脚本指定的输入文件在连接过程中的顺序。
三、实例
例1:
以下脚本将输出文件的text section定位在0×10000, data section定位在0×8000000:
SECTIONS { . = 0×10000; .text : { *(.text) } . = 0×8000000; .data : { *(.data) } .bss : { *(.bss) } }
解释一下上述的例子:
. = 0×10000 : 把定位器符号置为0×10000 (若不指定, 则该符号的初始值为0).
.text : { *(.text) } : 将所有(*符号代表任意输入文件)输入文件的.text section合并成一个.text section, 该section的地址由定位器符号的值指定, 即0×10000.
. = 0×8000000 :把定位器符号置为0×8000000
.data : { *(.data) } : 将所有输入文件的.data section合并成一个.data section, 该section的地址被置为0×8000000.
.bss : { *(.bss) } : 将所有输入文件的.bss section合并成一个.bss section,该section的地址被置为0×8000000+.data section的大小.
连接器每读完一个section描述后, 将定位器符号的值*增加*该section的大小. 注意: 此处没有考虑对齐约束。
例2
SECTIONS{ … OVERLAY 0×1000 : AT (0×4000) { .text0 { o1/*.o(.text) } .text1 { o2/*.o(.text) } } … }
.text0 section和.text1 section的VMA地址是0×1000,.text0 section加载于地址0×4000,.text1 section紧跟在其后。
程序代码,拷贝.text1 section代码:
extern char __load_start_text1, __load_stop_text1; memcpy ((char *) 0×1000, &__load_start_text1, &__load_stop_text1 – &__load_start_text1);
例3
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 */ … . = . + 0×1000; /* move to a new page in memory */ .data : { *(.data) } :data .dynamic : { *(.dynamic) } :data :dynamic … }
例4
MEMORY { rom (rx) : ORIGIN = 0, LENGTH = 256K ram (!rx) : org = 0×40000000, l = 4M }