连接脚本将我整整蒙了1天零一个上午,做了很多实验,看了人家不少例子代码
勉强能驾驭了,让linker按照我想要的来处理,做个笔记。
勉强能驾驭了,让linker按照我想要的来处理,做个笔记。
1,什么叫输入段,什么叫输出段
不知道怎么回事,我对GCC系列的输入和输出两个单词总是进入思维死角,很简单
就是 input section 和 output section,这里不是说翻译的问题,我觉得是一种
思考的方式的问题。
我的问题就是:既然叫输入端,那输入什么?同理,输出的是什么?不知道其他人
不会不理解这个问题,我自己的话是理解了不少时间了 -v-
不知道怎么回事,我对GCC系列的输入和输出两个单词总是进入思维死角,很简单
就是 input section 和 output section,这里不是说翻译的问题,我觉得是一种
思考的方式的问题。
我的问题就是:既然叫输入端,那输入什么?同理,输出的是什么?不知道其他人
不会不理解这个问题,我自己的话是理解了不少时间了 -v-
所谓的输出段,是指生成的文件,例如 elf 中的每个段
所谓的输入段,是指连接的时候提供LD的所有目标文件(OBJ)中的段。
所谓的输入段,是指连接的时候提供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)
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
"elf32-littlearm")
OUTPUT_ARCH(arm)
一句话,照抄......因为我们没有修改的余地,都是系统默认的关键字。第一句
指示系统可以有生成两种格式,默认是 elf32-arm,端格式是 little endian
指示系统可以有生成两种格式,默认是 elf32-arm,端格式是 little endian
4,ENTRY(__ENTRY)
指定入口点,LD的手册说,ENTRY POINT 就是程序第一条执行的指令,但是,说老
实话,我并不理解,因为这里跟我的理解矛盾了,首先,通常情况,系统需要一个
初始化的 STARTUP.S文件来初始化硬件,也就是 bootloader的第一阶段了。那么
很自然,入口点需要设置在这段代码的第一条指令中,那么正常运行的时候从第一
条指令开始运行。所以这里设置了__ENTRY为入口点,这个在汇编代码中必须得先
声明一下为全局,才能用,否则系统找不到。例如:
.global __ENTRY
指定入口点,LD的手册说,ENTRY POINT 就是程序第一条执行的指令,但是,说老
实话,我并不理解,因为这里跟我的理解矛盾了,首先,通常情况,系统需要一个
初始化的 STARTUP.S文件来初始化硬件,也就是 bootloader的第一阶段了。那么
很自然,入口点需要设置在这段代码的第一条指令中,那么正常运行的时候从第一
条指令开始运行。所以这里设置了__ENTRY为入口点,这个在汇编代码中必须得先
声明一下为全局,才能用,否则系统找不到。例如:
.global __ENTRY
但是问题是,如果我用同样的办法,设置另外一个不是第一条指令的入口点,LD并
没有报错,但是问题来了,生成的文件和刚才设置入口点为 __ENTRY 的时候一模一
样,这就蒙了,到底这个入口点是怎么回事?
记得以前ADS的时候也碰到过 entry point的问题,下载仿真的时候确实是自动跳转
到 entry point中运行。
我想到的可能的原因,第一,生成 elf 文件并不是能直接用在嵌入式平台上面裸跑
的,因为我们并没有操作系统,我们不需要elf文件头的那些指示信息提供给操作
系统,指示系统怎么去加载文件,在嵌入式上面的完全没有那个必要,只需要将实
际的代码提取出来,直接运行就OK,也就是 objcopy的操作,所以我觉得,在裸奔
的嵌入式系统上面,entry point是没有意义的,只需要指向整个代码最开始的指
令就OK了。
暂时我还是不能清晰的理解这个东西。先放下。以后碰到问题再分析。
没有报错,但是问题来了,生成的文件和刚才设置入口点为 __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 vma : AT(lma)
{
output-section-command
output-section-command
...
} [AT>lma_region]
section_name 根据ld手册说是有个确定的名字,其他没啥,自己添加一些新段也是
可以的。
默认的4个段是必须有的
.text 代码
.rodata 常量,例如字符串什么的
.data 初始化的全局变量
.bss 没有初始化的全局变量
其实没什么,可以说,都是固定的,所以一句话,照抄。
可以的。
默认的4个段是必须有的
.text 代码
.rodata 常量,例如字符串什么的
.data 初始化的全局变量
.bss 没有初始化的全局变量
其实没什么,可以说,都是固定的,所以一句话,照抄。
段名字后面紧跟的是 vma ,也就是这个段在程序运行的时候的地址,例如
.text 0x30000000 :
{
*(.text)
}
表示的是代码的运行时地址为 0x30000000 假如你的ROM在 0x0 地址,程序放在ROM
中,那个时候程序是不能正常运行的(位置无关代码除外),必须将代码COPY到VMA
也就是 0x30000000 中才能正常运行。
.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,中间补零,字节数相当的可观呢。
这个英文是 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分配,这样非常简单。
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。
.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,并不实际占空间的。
只分配运行地址 vma,并不实际占空间的。
3,如果我想自己添加一些段,应该怎么去实现?
例如我要添加一个 .vector 的段,里面放的是一些数据,怎么实现?
(1)如果在汇编代码里面添加,那么可以新启动一个段
例如在 2440init.S 中添加 .vector 段
.section .text
....
....
(其他代码)
例如我要添加一个 .vector 的段,里面放的是一些数据,怎么实现?
(1)如果在汇编代码里面添加,那么可以新启动一个段
例如在 2440init.S 中添加 .vector 段
.section .text
....
....
(其他代码)
.section .vector @ 在这里声明一个段,并且放连个数据
.word 0x55
.word 0xaa
.word 0x55
.word 0xaa
汇编代码段的开始由 .section 声明,接着后面的都属于这个段,直到第二个 .section 声明
为止。
我这个 .vector段是需要连接到 0x33ffff00 的,非常的特殊,那么按照前面的办法
为止。
我这个 .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)
}
{
*(.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了。
看 .bss 和 .data的lma是一样的,所以其实无视掉 .bss段就OK了。
(2)在C语言中怎么添加一个变量指定放到 .vector段
很简单,用GNU扩展语法(注意了,是GNU系列工具通用而已,例如gcc,这个并不是C的标准)
格式如下
很简单,用GNU扩展语法(注意了,是GNU系列工具通用而已,例如gcc,这个并不是C的标准)
格式如下
unsigned int __attribute__((section(".vector"))) vec=0x9988;
定义一个 vec 变量,值为 0x9988,分配在 .vector 段,编译后用 objdump 一下查看汇编代码
可以发现到
可以发现到
Disassembly of section .vector:
33ffff00 <vec>:
33ffff00: 00009988 .word 0x00009988
33ffff00 <vec>:
33ffff00: 00009988 .word 0x00009988
33ffff04: 00000055 .word 0x00000055
33ffff08: 000000aa .word 0x000000aa
33ffff08: 000000aa .word 0x000000aa
看到没有?刚才说的在汇编代码和C代码里面定义的数值都被连接进去了 .vector段了,vma也正确
最后还可以看看生成的 bin 文件,看看最后的几个数据是不是就是 0x9988 0x55 0xaa ?这样应该
就理解了整个连接的过程了吧?
最后还可以看看生成的 bin 文件,看看最后的几个数据是不是就是 0x9988 0x55 0xaa ?这样应该
就理解了整个连接的过程了吧?
4,MEMORY 命令在指定lma中的使用
每个段都要用 AT 来指定具体的位置,其实挺烦的,我们有更加简单的办法,我们定义一个内存区域
让,然后将所有的段都扔进去。
MEMORY
{
rom (rx) : ORIGIN = 0x30000000, LENGTH = 1M
}
每个段都要用 AT 来指定具体的位置,其实挺烦的,我们有更加简单的办法,我们定义一个内存区域
让,然后将所有的段都扔进去。
MEMORY
{
rom (rx) : ORIGIN = 0x30000000, LENGTH = 1M
}
注意,我们现在要实现的是lma,并不是vma,也就是说在最后生成的 bin文件中怎么将所有段合在一
起。定义一个开始地址为 0x30000000 ,也就是lma,对应上面的 .text段的lma,长度自己设,我设置
为 1M ,其实溢出会提示的,随便设就OK了。
起。定义一个开始地址为 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
{
*(.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文件。这样做方便很多。
指定的那个内存区域!!" ,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 的作用吧,呵呵,其实挺方便的。
是linux下面的可执行文件格式,跟windows上面的 .exe文件其实一样的,看过window的可执行文件的PE结构
的应该知道,真正的代码前面是有一堆标志啊,地址啊,什么的,操作系统就是通过读取这部分信息,就知道
应该怎么将这个可执行文件加载进去。同理,elf文件头也有一堆有用的信息。不过对于我们的嵌入式系统
我估计应该是用不上了(我说的是裸奔),基本上都是通过 objcopy 将真正的代码弄出来烧些到 flash里面
跑的,所以在嵌入式系统上面,这个 lma我觉得应该是没有用处的。
另外,如果用工具调试的时候,例如我用的是 openocd,如果加载 elf文件,并不需要指定地址,openocd会
自动的加载,为什么这个神奇?我觉得应该是elf文件里面包含了 lma 的作用吧,呵呵,其实挺方便的。
结束语:
被这个小东西虐待了整整一天半,疯狂找资料,啃ld as等的英文资料手册,算是实验了一点成果出来,上面
说的技术对于我暂时的应用来说已经足够了,也足够看懂很多例子里面的 ld script了,网上的资料基本都是
在翻译 ld 的英文手册 ...... 唉 ......
被这个小东西虐待了整整一天半,疯狂找资料,啃ld as等的英文资料手册,算是实验了一点成果出来,上面
说的技术对于我暂时的应用来说已经足够了,也足够看懂很多例子里面的 ld script了,网上的资料基本都是
在翻译 ld 的英文手册 ...... 唉 ......