gcc链接脚本

摘自:http://blog.csdn.net/chenliujiang1989/article/details/7626833

对于.lds文件,它定义了整个程序编译之后的连接过程,决定了一个可执行程序的各个段的存储位置。虽然现在我还没怎么用它,但感觉还是挺重要的,有必要了解一下。

先看一下GNU官方网站上对.lds文件形式的完整描述:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}


secname和contents是必须的,其他的都是可选的。下面挑几个常用的看看:
1、secname:段名

2、contents:决定哪些内容放在本段,可以是整个目标文件,也可以是目标文件中的某段(代码段、数据段等)

3、start:本段连接(运行)的地址,如果没有使用AT(ldadr),本段存储的地址也是start。GNU网站上说start可以用任意一种描述地址的符号来描述。

4、AT(ldadr):定义本段存储(加载)的地址。

看一个简单的例子:(摘自《2410完全开发》)

/* nand.lds */
SECTIONS { 
firtst 0x00000000 : { head.o init.o } 
second 0x30000000 : AT(4096) { main.o } 
}

    以上,head.o放在0x00000000地址开始处,init.o放在head.o后面,他们的运行地址也是0x00000000,即连接和存储地址相同(没有AT指定);main.o放在4096(0x1000,是AT指定的,存储地址)开始处,但是它的运行地址在0x30000000,运行之前需要从0x1000(加载处)复制到0x30000000(运行处),此过程也就用到了读取Nand flash。

这就是存储地址和连接(运行)地址的不同,称为加载时域和运行时域,可以在.lds连接脚本文件中分别指定。

编写好的.lds文件,在用arm-linux-ld连接命令时带-Tfilename来调用执行,如
arm-linux-ld –Tnand.lds x.o y.o –o xy.o。也用-Ttext参数直接指定连接地址,如
arm-linux-ld –Ttext 0x30000000 x.o y.o –o xy.o。

既然程序有了两种地址,就涉及到一些跳转指令的区别,这里正好写下来,以后万一忘记了也可查看,以前不少东西没记下来现在忘得差不多了。。。

ARM汇编中,常有两种跳转方法:b跳转指令、ldr指令向PC赋值。

我自己经过归纳如下:

(1)       b step1 :b跳转指令是相对跳转,依赖当前PC的值,偏移量是通过该指令本身的bit[23:0]算出来的,这使得使用b指令的程序不依赖于要跳到的代码的位置,只看指令本身。

(2)       ldr pc, =step1 :该指令是从内存中的某个位置(step1)读出数据并赋给PC,同样依赖当前PC的值,但是偏移量是那个位置(step1)的连接地址(运行时的地址),所以可以用它实现从Flash到RAM的程序跳转。

(3)       此外,有必要回味一下adr伪指令,U-boot中那段relocate代码就是通过adr实现当前程序是在RAM中还是flash中。仍然用我当时的注释:

relocate: /* 把U-Boot重新定位到RAM */
    adr r0, _start /* r0是代码的当前位置 */ 
/* adr伪指令,汇编器自动通过当前PC的值算出 如果执行到_start时PC的值,放到r0中:
当此段在flash中执行时r0 = _start = 0;当此段在RAM中执行时_start = _TEXT_BASE(在board/smdk2410/config.mk中指定的值为0x33F80000,即u-boot在把代码拷贝到RAM中去执行的代码段的开始) */
    ldr r1, _TEXT_BASE /* 测试判断是从Flash启动,还是RAM */ 
/* 此句执行的结果r1始终是0x33FF80000,因为此值是又编译器指定的(ads中设置,或-D设置编译器参数) */
    cmp r0, r1 /* 比较r0和r1,调试的时候不要执行重定位 */

    下面,结合u-boot.lds看看一个正式的连接脚本文件。这个文件的基本功能还能看明白,虽然上面分析了好多,但其中那些GNU风格的符号还是着实让我感到迷惑。。。

OUTPUT_FORMAT("elf32­littlearm", "elf32­littlearm", "elf32­littlearm")
;指定输出可执行文件是elf格式,32位ARM指令,小端
OUTPUT_ARCH(arm)
;指定输出可执行文件的平台为ARM
ENTRY(_start)
;指定输出可执行文件的起始代码段为_start.
SECTIONS
{
        . = 0x00000000 ; 从0x0位置开始
        . = ALIGN(4) ; 代码以4字节对齐
        .text : ;指定代码段
        {
          cpu/arm920t/start.o (.text) ; 代码的第一个代码部分
          *(.text) ;其它代码部分
        }
        . = ALIGN(4) 
        .rodata : { *(.rodata) } ;指定只读数据段
        . = ALIGN(4);
        .data : { *(.data) } ;指定读/写数据段
        . = ALIGN(4);
        .got : { *(.got) } ;指定got段, got段式是uboot自定义的一个段, 非标准段
        __u_boot_cmd_start = . ;把__u_boot_cmd_start赋值为当前位置, 即起始位置
        .u_boot_cmd : { *(.u_boot_cmd) } ;指定u_boot_cmd段, uboot把所有的uboot命令放在该段.
        __u_boot_cmd_end = .;把__u_boot_cmd_end赋值为当前位置,即结束位置
        . = ALIGN(4);
        __bss_start = .; 把__bss_start赋值为当前位置,即bss段的开始位置
        .bss : { *(.bss) }; 指定bss段
        _end = .; 把_end赋值为当前位置,即bss段的结束位置


摘自:http://linux.chinaunix.net/techdoc/develop/2009/04/29/1109360.shtml

连接脚本将我整整蒙了1天零一个上午,做了很多实验,看了人家不少例子代码
勉强能驾驭了,让linker按照我想要的来处理,做个笔记。
1,什么叫输入段,什么叫输出段
不知道怎么回事,我对GCC系列的输入和输出两个单词总是进入思维死角,很简单
就是 input section 和 output section,这里不是说翻译的问题,我觉得是一种
思考的方式的问题。
我的问题就是:既然叫输入端,那输入什么?同理,输出的是什么?不知道其他人
不会不理解这个问题,我自己的话是理解了不少时间了 -v-
所谓的输出段,是指生成的文件,例如 elf 中的每个段
所谓的输入段,是指连接的时候提供LD的所有目标文件(OBJ)中的段。
2,lma 和 vma
lma =  load memory address
vma =  vitual memory address
如果有研究过ADS的估计有印象,那里有个 RO BASE 和 RW BASE 和 ZI BASE,也
就是说,lma 是装载地址,vma 是运行地址,想搞清楚这两个问题,可以阅读一下
《ARM学习报告(杜云海)》作者写的很好,将这个问题分析的很透澈。lma 和vma
只是GCC的叫法而已,其实原理是一样的。

3,两个基本架构
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
           "elf32-littlearm")
OUTPUT_ARCH(arm)
一句话,照抄......因为我们没有修改的余地,都是系统默认的关键字。第一句
指示系统可以有生成两种格式,默认是 elf32-arm,端格式是 little endian

4,ENTRY(__ENTRY)
指定入口点,LD的手册说,ENTRY POINT 就是程序第一条执行的指令,但是,说老
实话,我并不理解,因为这里跟我的理解矛盾了,首先,通常情况,系统需要一个
初始化的 STARTUP.S文件来初始化硬件,也就是 bootloader的第一阶段了。那么
很自然,入口点需要设置在这段代码的第一条指令中,那么正常运行的时候从第一
条指令开始运行。所以这里设置了__ENTRY为入口点,这个在汇编代码中必须得先
声明一下为全局,才能用,否则系统找不到。例如:
.global __ENTRY
但是问题是,如果我用同样的办法,设置另外一个不是第一条指令的入口点,LD并
没有报错,但是问题来了,生成的文件和刚才设置入口点为 __ENTRY 的时候一模一
样,这就蒙了,到底这个入口点是怎么回事?
记得以前ADS的时候也碰到过 entry point的问题,下载仿真的时候确实是自动跳转
到 entry point中运行。
我想到的可能的原因,第一,生成 elf 文件并不是能直接用在嵌入式平台上面裸跑
的,因为我们并没有操作系统,我们不需要elf文件头的那些指示信息提供给操作
系统,指示系统怎么去加载文件,在嵌入式上面的完全没有那个必要,只需要将实
际的代码提取出来,直接运行就OK,也就是 objcopy的操作,所以我觉得,在裸奔
的嵌入式系统上面,entry point是没有意义的,只需要指向整个代码最开始的指
令就OK了。
暂时我还是不能清晰的理解这个东西。先放下。以后碰到问题再分析。

5,一个输出段的标准格式
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,是指程序应该装载在什么地方,而不是指这个段
应该在最后生成的bin文件的位置!!!这个东西蒙骗了我,让我郁闷了1天。elf格
式的文件里面不但包含了代码,还包含了各种各样的信息,例如上面说的每个段的
lma 和vma,还有其他信息都包含在里面了。
默认状态下,lma 是等于当前的vma的,例如
    .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文件就是这样来的。
可以验证一下,如果手动指定开始的lma为0的话
    .text 0x30000000 : AT(0)

  *(.text) 
  *(.rodata)
}
    .data 0x33ffff00 :  

  *(.data) 
}
其中.text段的lma被AT强制指定为0,那么objcop出来的bin文件相当的华丽,达到了
700多MB,为什么?都说了,从0开始到 0x33ffff00,中间补零,字节数相当的可观呢。
一般我们常用的做法是:
1,.data段的 lma 和 vma 都是紧跟着 .text 的,或者用ARM的说法就是 RW段紧跟着
RO段,这样的做法非常简单
    .text 0x30000000 : 

  *(.text) 
  *(.rodata)
}
    .data :  

  *(.data) 
}
    .bss  :
        {
                *(.bss) *(.COMMON)
        }
只指定RO BASE,然后所有代码都是跟着RO BASE分配,这样非常简单。
2,.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,如果我想自己添加一些段,应该怎么去实现?
例如我要添加一个 .vector 的段,里面放的是一些数据,怎么实现?
(1)如果在汇编代码里面添加,那么可以新启动一个段
例如在 2440init.S 中添加 .vector 段
.section .text
....
....     
(其他代码)
.section   .vector     @ 在这里声明一个段,并且放连个数据
.word  0x55
.word  0xaa 
汇编代码段的开始由  .section 声明,接着后面的都属于这个段,直到第二个 .section 声明
为止。
我这个 .vector段是需要连接到 0x33ffff00 的,非常的特殊,那么按照前面的办法
    .text 0x30000000 : 

  *(.text) 
  *(.rodata)
}
    .data 0x31000000 : AT(LOADADDR(.text) + SIZEOF(.text))  

  *(.data) 
}
    .bss  :
        {
                *(.bss) *(.COMMON)
        }
    .vector 0x33ffff00 : AT(LOADADDR(.data) + SIZEOF(.data))
        {
                *(.vector)
        }
可以看出,形式其实是一样,不过看一下,添加的段的lma放在 .data 段的lma的后面,前面也说
看 .bss 和 .data的lma是一样的,所以其实无视掉 .bss段就OK了。
(2)在C语言中怎么添加一个变量指定放到 .vector段
很简单,用GNU扩展语法(注意了,是GNU系列工具通用而已,例如gcc,这个并不是C的标准)
格式如下
unsigned int __attribute__((section(".vector"))) vec=0x9988;
定义一个 vec 变量,值为 0x9988,分配在 .vector 段,编译后用 objdump 一下查看汇编代码
可以发现到
Disassembly of section .vector:
33ffff00 :
33ffff00: 00009988  .word 0x00009988
33ffff04: 00000055  .word 0x00000055
33ffff08: 000000aa  .word 0x000000aa
看到没有?刚才说的在汇编代码和C代码里面定义的数值都被连接进去了 .vector段了,vma也正确
最后还可以看看生成的 bin 文件,看看最后的几个数据是不是就是 0x9988 0x55 0xaa ?这样应该
就理解了整个连接的过程了吧?
4,MEMORY 命令在指定lma中的使用
每个段都要用 AT 来指定具体的位置,其实挺烦的,我们有更加简单的办法,我们定义一个内存区域
让,然后将所有的段都扔进去。
MEMORY
{
rom (rx)     : ORIGIN = 0x30000000, LENGTH = 1M 
}
注意,我们现在要实现的是lma,并不是vma,也就是说在最后生成的 bin文件中怎么将所有段合在一
起。定义一个开始地址为 0x30000000 ,也就是lma,对应上面的 .text段的lma,长度自己设,我设置
为 1M ,其实溢出会提示的,随便设就OK了。
    .text 0x30000000 : 

  *(.text) 
  *(.rodata)
} AT>rom
    .data 0x31000000 : 

  *(.data) 
} AT>rom
    .bss  :
        {
                *(.bss) *(.COMMON)
        } 
    .vector 0x33ffff00 :
        {
                *(.vector)
        } AT>rom
看到每个输出段的末尾都有个 AT>rom 的操作吧?应该大概猜到,通俗一点说就是:"将这个输出段扔到rom
指定的那个内存区域!!" ,rom是上面已经定义了,那么这些操作之后,.text .data .vector 都乖乖的
扔进 rom 指向的那个区域,注意了,我们说的是lma,所以不要在意那个开始地址,刚才不是说了吗?那个
objcopy是从最开始的lma开始copy而已,这样出来的效果和第三点中生成的bin文件其实是一模一样的!!
不信的话用UE查看一下 bin 文件的16进制代码,或者查看连接生成的 map文件。这样做方便很多。
既然 lma 是包含在 elf文件当中,那这个地址到底有什么用?这个我也不知道了,我猜测,首先,elf文件
是linux下面的可执行文件格式,跟windows上面的 .exe文件其实一样的,看过window的可执行文件的PE结构
的应该知道,真正的代码前面是有一堆标志啊,地址啊,什么的,操作系统就是通过读取这部分信息,就知道
应该怎么将这个可执行文件加载进去。同理,elf文件头也有一堆有用的信息。不过对于我们的嵌入式系统
我估计应该是用不上了(我说的是裸奔),基本上都是通过 objcopy 将真正的代码弄出来烧些到 flash里面
跑的,所以在嵌入式系统上面,这个 lma我觉得应该是没有用处的。
另外,如果用工具调试的时候,例如我用的是 openocd,如果加载 elf文件,并不需要指定地址,openocd会
自动的加载,为什么这个神奇?我觉得应该是elf文件里面包含了 lma 的作用吧,呵呵,其实挺方便的。
结束语:
被这个小东西虐待了整整一天半,疯狂找资料,啃ld as等的英文资料手册,算是实验了一点成果出来,上面
说的技术对于我暂时的应用来说已经足够了,也足够看懂很多例子里面的 ld script了,网上的资料基本都是
在翻译 ld 的英文手册 ...... 唉 ......


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值