ld链接脚本与booter
grep _ftext `find ./ -name *.S`查找,令我惊讶的是没有找到。既然源文件中都没有,那么又是什么神奇的力量让程序在链接的时候又能正常链接通过呢?
答案是这些变量定义在链接脚本中。链接脚本通常的名字叫做*.lds *.ld,或者是ld.* 。那么,如此我们就应该明白ld的链接过程的输入文件就是目标文件、库文件、链接脚本。当然输出就是各种格式的可执行文件了,linux最常见的就是elf格式和.out格式。要理解链接脚本,那么了解可执行文件的格式也是必要的,比如elf的section 、symbol table、file head 、program head等。
也许你会疑惑我些个最简单的hello,word,直接用最简单的gcc test.c -o test,没有见什么链接脚本啊。其实, 连接器有个默认的内置连接脚本, 可用ld --verbose查看. -T选项用以指定自己的链接脚本, 它将代替默认的连接脚本,于是我在编译链接内核的时候,看到这样的输出(我的board是MIPS架构):
mipsel-linux-ld -G 0 -static -n -T arch/mips/ld.script -Ttext 0x80001000
可以看到,-T 指定了arch/mips/ld.script为链接脚本。
输出文件中,往往存在多个section,我们最熟悉当然就是代码段、数据段和BSS。每个section具有不同的属性,比如我的内核通过mipsel-linux-objdump -h vmlinux 得到的大概就是这样的输出:
Idx Name Size VMA LMA File off Algn
0 .text 001f99c0 80001000 80001000 000000a0 2**5
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .fixup 00000fdc 801fa9c0 801fa9c0 001f9a60 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
...........................
13 .data 0003b000 80218000 80218000 00216000 2**12
CONTENTS, ALLOC, LOAD, DATA
14 .sbss 00000000 80253000 80253000 00251000 2**2
ALLOC
15 .bss 000b803c 80253000 80253000 00251000 2**5
ALLOC
......................................
CONTENTS, ALLOC, LOAD, READONLY, CODE这些都是section的属性,在目标文件中, loadable或allocatable的输出section有两种地址: VMA(virtual Memory Address)和LMA(Load Memory Address). VMA是执行输出文件时section所在的地址, 而LMA是加载输出文件时section所在的地址. 一般而言, 某section的VMA == LMA. 但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况,通常是booter代码, 比如将输出文件烧录在开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定).对于mips来说,启动地址是0xBFC00000,那么0xBFC00000,应该对应的就是flash的booter code(nor flash可以直接在flash中启动,如果是nand flash的话需要CPU支持,又是另外一番景象)。而运行的时候自然为了效率会把booter代码加载到内存里面去运行,至于加载到什么地方就可以通过VMA来指定了。比如我的booter代码用objdump出来就是(0xbfc开头的地址都位于flash,0x80000000到0x84000000为64M的直接映射cached内存地址):
Idx Name Size VMA LMA File off Algn
0 .text 0003d830 9fc00000 bfc00000 00001000 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00003134 80001000 bfc3d830 0003f000 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .sbss 000003a0 80004140 80004140 00000140 2**4
ALLOC
3 .bss 00000000 800044e0 800044e0 00000140 2**0
ALLOC
.............................
可以看到data段LMA=0xbfc3d830, 说明存储在flash空间,VMA=0x80001000 ,说明运行的时候要加载到0x80001000的内存中,至于为什么是0x80001000,则是由链接脚本指定了,低0x1000空间通常用于中断向量表。
而BSS段却是VMA=LMA,这很容易解释,因为BSS只在运行时站内存空间,他是不占用flash存储空间的。而代码段就有些奇怪了,他的VMA其实是等于LMA的。因为在MIPS地址映射中0x80000000~0x9fffffff 和0xa0000000~0xbfffffff实际上是映射同一块区域,只是前者是cached而后者是uncached。既然这样是不是意味booter就是在flash中跑呢?当然不是,通常在booter的启动代码中是不会管text的VMA的(想想booter最终可是elf转换而来的bin文件),booter的代码是PIC(postion independent code),也就是说可以加载到内存的任何地方都可以跑的位置无关代码,所以VMA对于他来说没有任何意义。
恩,讲了这么多该说说链接脚本了,主要说下section就ok了。下面的内容主要是来源于网络上别的牛人所写:
SECTIONS命令告诉ld如何把输入文件的sections映射到输出文件的各个section: 如何将输入section合为输出section; 如何把输出section放入程序地址空间(VMA)和进程地址空间(LMA).该命令格式如下:
SECTIONS
{
SECTIONS-COMMAND
SECTIONS-COMMAND
...
}
SECTION-COMMAND有四种:
(1) ENTRY命令
(2) 符号赋值语句
(3) 一个输出section的描述(output section description)
(4) 一个section叠加描述(overlay description)
如果整个连接脚本内没有SECTIONS命令, 那么ld将所有同名输入section合成为一个输出section内, 各输入section的顺序为它们被连接器发现的顺序.
如果某输入section没有在SECTIONS命令中提到, 那么该section将被直接拷贝成输出section。
输出section描述
输出section描述具有如下格式:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
[ ]内的内容为可选选项, 一般不需要.
SECTION:section名字
SECTION左右的空白、圆括号、冒号是必须的,换行符和其他空格是可选的。
每个OUTPUT-SECTION-COMMAND为以下四种之一,
符号赋值语句
一个输入section描述
直接包含的数据值
一个特殊的输出section关键字
输出section名字(SECTION):
输出section名字必须符合输出文件格式要求,比如:a.out格式的文件只允许存在.text、.data和.bss section名。而有的格式只允许存在数字名字,那么此时应该用引号将所有名字内的数字组合在一起;另外,还有一些格式允许任何序列的字符存在于 section名字内,此时如果名字内包含特殊字符(比如空格、逗号等),那么需要用引号将其组合在一起。
输出section地址(ADDRESS):
ADDRESS是一个表达式,它的值用于设置VMA。如果没有该选项且有REGION选项,那么连接器将根据REGION设置VMA;如果也没有 REGION选项,那么连接器将根据定位符号‘.’的值设置该section的VMA,将定位符号的值调整到满足输出section对齐要求后的值,输出 section的对齐要求为:该输出section描述内用到的所有输入section的对齐要求中最严格的。
例子:
.text . : { *(.text) }
和
.text : { *(.text) }
这两个描述是截然不同的,第一个将.text section的VMA设置为定位符号的值,而第二个则是设置成定位符号的修调值,满足对齐要求后的。
ADDRESS可以是一个任意表达式,比如ALIGN(0x10)这将把该section的VMA设置成定位符号的修调值,满足16字节对齐后的。
注意:设置ADDRESS值,将更改定位符号的值。
输入section描述:
最常见的输出section描述命令是输入section描述。
输入section描述是最基本的连接脚本描述。
输入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
下面看连接器是如何找到对应的文件的。
当FILENAME是一个特定的文件名时,连接器会查看它是否在连接命令行内出现或在INPUT命令中出现。
当FILENAME是一个字符串模式时,连接器仅仅只查看它是否在连接命令行内出现。
注意:如果连接器发现某文件在INPUT命令内出现,那么它会在-L指定的路径内搜寻该文件。
字符串模式内可存在以下通配符:
* :表示任意多个字符
? :表示任意一个字符
[CHARS] :表示任意一个CHARS内的字符,可用-号表示范围,如:a-z
:表示引用下一个紧跟的字符
在文件名内,通配符不匹配文件夹分隔符/,但当字符串模式仅包含通配符*时除外。
任何一个文件的任意section只能在SECTIONS命令内出现一次。看如下例子,
SECTIONS {
.data : { *(.data) }
.data1 : { data.o(.data) }
}
data.o文件的.data section在第一个OUTPUT-SECTION-COMMAND命令内被使用了,那么在第二个OUTPUT-SECTION-COMMAND命令内 将不会再被使用,也就是说即使连接器不报错,输出文件的.data1 section的内容也是空的。
再次强调:连接器依次扫描每个OUTPUT-SECTION-COMMAND命令内的文件名,任何一个文件的任何一个section都只能使用一次。
读者可以用-M连接命令选项来产生一个map文件,它包含了所有输入section到输出section的组合信息。
再看个例子,
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
这个例子中说明,所有文件的输入.text section组成输出.text section;所有以大写字母开头的文件的.data section组成输出.DATA section,其他文件的.data section组成输出.data section;所有文件的输入.bss section组成输出.bss section。
可以用SORT()关键字对满足字符串模式的所有名字进行递增排序,如SORT(.text*)。
通用符号(common symbol)的输入section:
在许多目标文件格式中,通用符号并没有占用一个section。连接器认为:输入文件的所有通用符号在名为COMMON的section内。
例子,
.bss { *(.bss) *(COMMON) }
这个例子中将所有输入文件的所有通用符号放入输出.bss section内。可以看到COMMOM section的使用方法跟其他section的使用方法是一样的。
有些目标文件格式把通用符号分成几类。例如,在MIPS elf目标文件格式中,把通用符号分成standard common symbols(标准通用符号)和small common symbols(微通用符号,不知道这么译对不对?),此时连接器认为所有standard common symbols在COMMON section内,而small common symbols在.scommon section内。
在一些以前的连接脚本内可以看见[COMMON],相当于*(COMMON),不建议继续使用这种陈旧的方式。
输入section和垃圾回收:
在连接命令行内使用了选项--gc-sections后,连接器可能将某些它认为没用的section过滤掉,此时就有必要强制连接器保留一些特定的 section,可用KEEP()关键字达此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text))
下面的例子非常的好:
例子,
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
程序如下,
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;
/* ROM has data at end of text; copy it. */
while (dst < &_edata) {
*dst++ = *src++;
}
/* Zero bss */
for (dst = &_bstart; dst< &_bend; dst++)
*dst = 0;
链接脚本中定义了 变量 _etext, _data, _edata, _bstart, _bend;_etext,就是end text,代码段的结束地址,其他的也就类推了。符号'.' 是链接程序的定位器,每增加一个段后定位器会做相应的增加。比如上面的 .text 0x1000 : { *(.text) _etext = . ; }表示首先将定位器指向代码段的开始地址0x1000,然后将所有的输入文件的代码段放到输出文件的代码中,变量_etext=0x1000+sizeof(.text),此程序的目的是将处于ROM内的已初始化数据拷贝到该数据应在的位置(VMA地址),并将为初始化数据置零。理解这个例子后就很容易理解booter的整个地址布局机制了。
转自:http://liyun1982-1982.blog.163.com/blog/static/10488328820081113422130/
转:
1. 哪些人可以不需要学GNU-ld链接脚本?
1)所有事务都交给编译器自动完成的,只需要写代码的
2)只使用商业性编译器
3)只使用avr,并且不需要实现复杂功能的
2. 哪些人可以考虑去学GNU-ld链接脚本?
1)希望比makefile更进一步控制程序的产生
2)希望在自己需要的存储地址上保存自己指定的数据
3)希望实现程序在存储空间中的模块化或特殊结构的
4)希望把不同格式的目标文件链接为一个格式
5)最重要的一点是:希望用gcc为其他架构的处理器编写程序(而不只是avr),让它在你手中真正成为通用编译器的必要的一步
3. 学习前提:
1)有winavr的使用经验
2)有x86汇编基础(要求会的指令不多,有记忆即可,忘了的随时上网搜)
3. 我推荐的学习顺序:
1)《程序的链接和装入及Linux下动态链接的实现》:http://www-128.ibm.com/developerworks/cn/linux/l-dynlink/
如果你对编译和链接过程有一定了解,也有反编译库文件、目标文件的经验可以跳过这篇文章
2)《GNU-ld链接脚本浅析》:http://blog.chinaunix.net/u/13991/showart_177822.html
建议可以结合winavr的链接脚本来学习,在<winavr安装目录>\avr\lib\ldscripts下,后缀为“.x”的是对应不同架构avr使用的脚本
也可以看winavr的默认脚本,在命令行下输入“avr-ld --verbose”即可看到。
3)《Using ld The GNU linker ld version 2》:http://www.gnu.org/software/binutils/manual/ld-2.9.1/html_mono/ld.html
这是官方的手册,参考。
有兴趣还可以看看这篇《UNIX/LINUX 平台可执行文件格式分析》(http://www-128.ibm.com/developerworks/cn/linux/l-excutff/)
如果学过arm开发工具ads的网友大概知道“分散加载文件”,现在看来其实它就是一种链接脚本。对于存储地址分配各不相同的arm实现,这是很重要的文件。
我学习gnu的开发软件时间也不长,以上有不当之处,希望大家指出。
另外,感谢IBM、北航的这些文章。