你的位置:首页 > 操作系统

[操作系统]lds文件


最近在看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] [:PHDRHDR ...] [=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的LMAMAX(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 = LEN2NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2…}

  说明如下:

  NAME :存储区域的名字,这个名字可以与符号名、文件名、section名重复,因为它处于一个独立的名字空间。

  ATTR :定义该存储区域的属性,在讲述SECTIONS命令时提到,当某输入section没有在SECTIONS命令内引用时,连接器会把该输入 section直接拷贝成输出section,然后将该输出section放入内存区域内。如果设置了内存区域设置了ATTR属性,那么该区域只接受满足该属性的section(怎么判断该section是否满足?输出section描述内好象没有记录该section的读写执行属性)。

  ATTR属性内可以出现以下7个字符:

R 只读sectionW 读/写sectionX 可执行sectionA ‘可分配的’sectionI 初始化了的sectionL 同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.2PT_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] [:PHDRHDR ...] [=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 sectiondata.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-COMMANDOUTPUT-SECTION-COMMAND…} [:PHDR...] [=FILL]SECNAME2{OUTPUT-SECTION-COMMANDOUTPUT-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,这两个符号的值分别代表SECNAMEsection的LMA地址的开始和结束。

连接器处理完OVERLAY描述语句后,将定位符号的值加上所有覆盖图内section大小的最大值。

 

2.5 暗含的链接脚本

输入文件可以是目标文件,也可以是连接脚本,此时的连接脚本被称为暗含的连接脚本。如果连接器不认识某个输入文件,那么该文件被当作连接脚本被解析。一个暗含的连接脚本不会替换默认的连接脚本,仅仅是增加新的连接而已。在连接命令行中,每个输入文件的顺序都被固定好了,暗含的连接脚本在连接命令行内占住一个位置,这个位置决定了由该连接脚本指定的输入文件在连接过程中的顺序。

 

三、实例

例1:

         以下脚本将输出文件的textsection定位在0×10000, data section定位在0×8000000:

SECTIONS{. = 0×10000;.text : { *(.text) }. = 0×8000000;.data : { *(.data) }.bss : { *(.bss) }}

 

解释一下上述的例子:

. = 0×10000 : 把定位器符号置为0×10000(若不指定, 则该符号的初始值为0).

.text : { *(.text) } : 将所有(*符号代表任意输入文件)输入文件的.textsection合并成一个.text section, 该section的地址由定位器符号的值指定, 即0×10000.

. = 0×8000000 :把定位器符号置为0×8000000

.data : { *(.data) } : 将所有输入文件的.datasection合并成一个.data section, 该section的地址被置为0×8000000.

.bss : { *(.bss) } : 将所有输入文件的.bsssection合并成一个.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 = 256Kram (!rx) : org = 0×40000000, l = 4M}