分类:
2011-03-18 09:48:56
原文地址:GCC-LD 连接脚本分析--uboot.lds 作者:Embedded_Li
1 Command Language
The command language provides explicit control over the link process allowing complete
specification of the mapping between the linkers input files and its output. It controls:
input files
file formats
output file layout
addresses of sections
placement of common blocks
1.1 input section and output section
所谓的输出段,是指生成的文件,例如 elf 中的每个段
所谓的输入段,是指连接的时候提供LD的所有目标文件(OBJ)中的段
1.2 ima and vma
lma = load memory address
vma = vitual memory address
1.3 relate with system and entry point
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
1.4 输出段的标准格式
section [address] [(type)] : [AT(lma)]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]
前面也说了,所谓的输出段是指最终生成的文件里面的段,所以一个输出段就可以理解为
最终文件里面的一个块,那么多个块合起来就是一个完成文件了。
section_name vma : AT(lma)
{
output-section-command
output-section-command
...
} [AT>lma_region]
section_name 根据ld手册说是有个确定的名字,其他没啥,自己添加一些新段也是可以的。
默认的4个段是必须有的:
.text 代码
.rodata 常量,例如字符串什么的
.data 初始化的全局变量
.bss 没有初始化的全局变量
其实没什么,可以说,都是固定的,所以一句话,照抄。段名字后面紧跟的是 vma ,
也就是这个段在程序运行的时候的地址,例如
.text 0x30000000 :
{
*(.text)
}
表示的是代码的运行时地址为 0x30000000 假如你的ROM在 0x0 地址,程序放在ROM中,
那个时候程序是不能正常运行的(位置无关代码除外),必须将代码COPY到VMA也就是
0x30000000 中才能正常运行。至于那个 AT(lma) 的关键字,只指示代码连接的时候应该
放在什么地方,注意好这个英文是 load memory address,是指程序应该装载在什么地方。
elf格式的文件里面不但包含了代码,还包含了各种各样的信息,例如上面说的每个段的lma
和vma,还有其他信息都包含在里面了。默认状态下,lma 是等于当前的vma的。
ecname定义了段名,其实最开始就忽略了一个重要的因素,arm-gcc-ld脚本需要描述
输入和输出,而表面上一看却看不出来什么是输入什么事输入,其实secname和contents
就是描述这两个信息的参数。secname是输出文件的段,即输出文件有哪些段,而contents就
是描述输出文件的这个段从哪些文件里抽取而来。明确这个了就不难理解为什么SECTIONS命令
什么都可以不要就是不能没有这两个参数了。secname:定义段,但是别以为定义的段一定要
是教科书上写的.data,.text这些科班的必须品,你甚至可以创建一个段来放一个的图片。
contents:它的语法开始复杂起来了,但是你可以简单的把输入文件写到代码中:
.data : { main.o led.o}
但是结果被列的目标文件中所有的代码都被链接到.data中去了,显然不大符合我们的要求啊。
那么还有一种写法:
.data : {
main.o(.data)
main.o(.text) // 也可以这样写 main.o(.data .text)或者main.o(.data , .text)
led.o(.data)
}
这个写法让只有被选中的文件的特殊段被链接到输出文件的.data段了。当然,我们似乎还有更好的写法:
.data : {
*(.data)
}
这样的话,所有目标文件的.data段都被连接到了输出文件中了(这似乎是最常用的方法)。核心的部分讲完了,开始回顾前面说到了的那些参数:
start:强制链接地址。也许没有讲清楚的是,在SECTIONS中,各个段是按次序排列的,前一个段用到什么地方下一个段接着用,而start就是强迫链接器将当前的段连接到指定的地址中。
.data 0x400000000 : { ..... }
BLOCK(align):在32位的ARM芯片上,指令都必须是align(4)对齐,不然无法执行。
AT(addr):实现存放地址和加载地址不一致的功能,AT表示在文件中存放的位置,即实时地址。表示指令或数据在二进制文件中的位置。
1、5 ENTRY(symbol)
这个symbol应该是某个函数,或者是汇编代码里的一个入口。然而,其实ARM-GCC-LD有很多种方式定义入口,所以当你看到你的脚本里没有这句话而板子运行的很正常的时候别大吃一斤。
1:在连接的时候使用-e参数。
2:在脚本里使用ENTRY
3:如果定义过start这个入口(如果你在汇编里如果本身就有这个名字叫start的入口,那么不用特别的声明也可以)
4:SECTION中.text的第一个入口函数
5:地址为0的指令其实看了这个我们可以理解是ARM对入口的一个选择优先级,1和2是一样的,显示的指明入口,这也是推荐的方法,没人会觉得程序员故弄玄虚是什么好事情。3是连接器的智能吧,而4和5就是无奈的选择了,程序员没干好的事情CPU只要猜着来处理了,有.text段的话就从它开始执行吧,连.text都没有的就从0x00000000开始执行。
关于定义变量,其实一般的脚本都会有的,目的只有一个,给汇编启动代码提地址信息。比如说,一段需要清零的区域在脚本里定义了,而脚本自己不是变形金刚,他不能主动给你清零的,需要你自己的启动代码来清零,清零的代码当然在汇编的启动代码里,它怎么知道需要清零的内存在什么地方?就靠脚本里定义的变量了。没错,事情就是这样的,那也就说一定会有一个提取地址的方法将地址赋给变量了哦,就是小小的一个点"."。
RAM_START = .;
定义了一个RAM_START变量,地址是当前的地址,什么是当前的地址啊?就是链接器在连接的时候根据前面的段排列后的当前位置,如:
. = 0x00000000
定义当前地址为0x0。
2、实时地址与虚拟地址引起的一个问题
2、1 分析下面连接代码
.text 0x30000000 :
{
*(.text)
*(.rodata)
}
.data 0x33ffff00 :
{
*(.data)
}
例如我们基本的两个段,我们指定了.text 和.data段的vma,但是没有指定lma,那么
lma到底应该是多少?很简单,ld认为当前的lma和vma是相同的,所以lma应该分别是0x30000000 0x33ffff00,编译生成的elf文件很小,但是objcopy出来的文件却非常
巨大,达到了60多MB,这是什么问题?elf文件很聪明,他只是保存了信息,.text段的lma
是0x30000000,那么elf就保存了知道本程序的代码应该加载到 0x30000000,然后又保存了
.data的lma,我们留意到中间有很多的地址空间是空的,并没有实际的代码,elf怎么处理?
elf只保存了两个地址和实际的代码,而对于其他空间里面的代码他并不处理,所以可以看出,
最后生成的elf文件并不大,也就100多KB而已,但是后来的OBJCOPY操作中,从elf文件中copy
出程序代码,这下就糟了,objcopy是从最开始的lma开始,这里是 0x30000000一直复制到最
尾段的lma,这里是 0x33ffff00,中间没有代码地方全部补零,那么60多MB的大bin文件就是
这样来的。
2、1 data段分离出来,连接到不同的vma运行时地址。
.text 0x30000000 :
{
*(.text)
*(.rodata)
}
.data 0x31000000 : AT(LOADADDR(.text) + SIZEOF(.text))
{
*(.data)
}
.bss :
{
*(.bss) *(.COMMON)
}
其实也不难解决,像上面的代码那样做就OK了,上面也分析了,如果vma不同的话,objcopy会一直复制,这样生成的bin文件会很大,怎么解决?很简单,手工指定 .data段的lma地址让 .data段的 lma 紧紧跟着 .text段的末尾,这样生成的 bin 文件就会很漂亮,跟第一种办法生成的bin文件结构一模一样!!
AT(LOADADDR(.text) + SIZEOF(.text))
AT 是指定lma 的,然后里面用了两个指令 LOADADDR ,和名字一样这个指令是用来求 lma 地址的! SIZEOF 也就是名字那样,求大小的LOADADDR(.text) 求出 .text 段的 lma,注意是开始地址
SIZEOF(.text) 求出 .text 段的大小AT(LOADADDR(.text) + SIZEOF(.text)) 的效果就是,指定 .data段的lma在 .text段lma的结尾处!这里补充一下,还有一个指令 ADDR(.text) 这个是求vma的,不是求lma。另外,注意一下 .bss段的lma和 .data段的 lma是一样的,这也反映了一个实质问题 .bss 段只分配运行地址 vma,并不实际占空间的。
3、下面分析下U-boot.lds连接脚本
本博另一文章中的连接脚本《11、串口实现printf()函数--s3c440》
参考 西比爱斯的 ARM-GCC-LD脚本而写成。