·将目标文件或库中的段简单的堆砌在一起
·重定位
重定位的概念:
链接而生成的可执行程序虽然是放在文件中的,但当程序运行时需要加载到内存中。各段应放在内存空间的什么位置是由可执行程序文件内的头部信息指定的。
一个程序一旦被加载带内存中,就意味着不论是函数还是变量,它们都会在内存中占据一定的内存空间,而这关系到内存地址。假设foo()函数在加载带内存中后其地址刚好位于0x10000处。从处理器的角度来看,当我们在C程序中写下一行调用foo()函数的语句时,意味着在调用foo()函数时需要跳转到0x10000的内存地址处,那如何知道调用foo()函数时应当跳转到0x10000地址处呢?这就是链接器的需要完成的工作了。
当一个源文件被编译成目标文件时,此时目标文件中的各段并没有具体的地址,在目标文件中只记录了程序中的符号和个符号在段中的相对位置。当链接器将所有的目标文件整合成一个可执行程序文件时,各目标文件中的各段将会真正获得在内存中的具体地址。链接生成的可执行程序时,需要根据每一个符号所对应的真实地址而更新相应的指令,从而实现真正的函数调用和变量引用功能,这一动作就是重定位。
链接器如何知道每一个目标文件中的各个段应当放在哪一地址处呢?这需要通过链接脚本指定。
链接脚本:
在此先了解脚本中的三个概念:段,符号,存储区域 掌握这三个概念后,很多的链接脚本就可以读懂了。
段:
位置指针--》链接的最终目的,是要将各个目标文件中的段合在一起,而合起来时各部分放在内存的什么位置就有了地址的概念。通过使用和操控位置指针就能实现对各个段的地址空间的安排,位置指针的值其实代表的就是处理器的地址空间。
SECTIONS
{
. = 0x1000000;
.text :
{
*(.text)
}
.data 0x80000000 :
{
*(.data)
}
.bss :
{
*(.bss)
}
}
SECTIONS命令描述的是可执行程序各段在内存的布局。默认情况一进入SECTIONS命令的区间,位置指针的值就是0。
“.”就表示位置指针。像C语言中的指针一样,可以改变它,也可以获取它的值。
后面的.text段将从地址0x1000000处开始存放。
.text :
{
*(.text)
}
就是一个输出段描述语句块,它表示生成的可执行文件中将有一个.text段,因此当程序加载时,位于可执行程序中的.text段的内存将被拷贝到0x1000000内存地址开始处。其中的.text段的来源-->由输入段描述指定*(.text),表示所有目标文件中的.text段都将放入可执行程序的.text段。
.data 0x80000000 :指定了.data段的地址。如果不指定地址,则位置完全取决于程序的链接时的位置指针的值,就想.bss段为0x8000000加上.data段的大小.
当链接器生成可执行程序时,会将每一个段在内存中的开始地址及段大小信息放在可执行程序文件的头部,而这些信息是为程序加载器加载程序所准备的。
符号:
.bss :
{
__bss_start__ = .;
*(.bss)
__bss_end__ = .;
}
其中定义了两个符号,这些符号是通过位置指针加以指定的,__bss_start__代表.bss段的开始地址。符号一旦在链接脚本中定义后,就可以在程序中使用它们(进行外部声明后,可直接使用)。
存储区域:
在默认情形下,ld认为整个可执行程序都是放入同一个存储空间的。如果一个嵌入式系统中存在多块不同的存储空间,可以使用MEMORY来进行存储区域定义。
MEMORY
{
RAM0 (WX) : ORIGIN = 0x40000000, LENGTH = 256K
RAM1 (WX) : ORIGIN = 0, LENGTH = 2M
}
SECTIONS
{
.text :
{
. += 0x10000;
*(.text)
} > RAM1
.data :
{
*(.data)
} > RAM1
.bss
{
*(.bss)
} > RAM0
}
RAM0和RAM1都被定义为可以读写和执行的存储区域。在SECTIONS,命令中,将.bss段放入RAM0存储区域中,而将.text和.data段放入了RAM1存储区域中。
使用存储区域时,如果链接器碰到存放段大于存储区域的容量时就会发出告警。我们可以利用链接器的这一特性,通过定义多个(连续的)存储区域的形式监视各段是否超出规定的大小。
接下来介绍些常用命令和选项,更多查看ld官方手册《The GNU Linker》
ALIGN和BLOCK命令
格式:
ALIGN (_align)
ALIGN (_exp, _align)
第一个ALIGN命令将返回位置指针之后的第一个满足边界对齐字节数_alignde的地址值。注意ALIGN并不改变位置指针的值。
第二个ALIGN命令返回_exp表达式值之后的,满足边界对齐字节数_align的地址值。显然,ALIGN(_align)等价于ALIGN(., _align).
BLOCK命令与只有一个参数的ALIGN命令作用是一样的,它的存在是为了兼容老的语法。
BYTE,SHORT,LONG和QUAD命令
格式:
BYTE (_value)
SHORT (_value)
LONG (_value)
QUAD (_value)
这4个命令依次表示在输出的可执行程序文件中放置所占存储空间为1,2,4和8字节的值,值由_value参数指定。
SECTIONS
{
.text :
{
*(.text)
}
LONG(1)
.data :
{
*(.data)
}
}
上面的脚本将使得.text段与.data段之间存在4字节的”空隙“,且它的值为1.
ENTRY命令
格式:
ENTRY (_symbol)
这个命令用于指定可执行程序的入口点。入口点是指程序被加载到内存后,运行第一条指令所在的内存地址。
MEMORY命令
格式:
MEMORY
{
name [(_attr)] : org = _origin, LENGTH = _len
...
}
或
MEMORY
{
name [(_attr)] : org = _origin, l = _len
...
}
_attr可取值如下,注意是大小写敏感__________________________________
R 只读段
W 可读写段
X 可执行段
A 可分配段
I(大写i) 已初始化段
L 与大i一样
! 反转以上任一选项的含义
_________________________________
当各段没有想前面使用”>“指定它的存储区域的位置时,ld会自动根据各段的属性放入不同的区域。
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
rom (!x) : org = 0x40000000, l = 4M
}
像上面这样,ld会自动将.text段放入rom中,因为rom具有只读和可执行属性,另外,会将.data和.bss段放入ram中,因为它具有除可执行外的其他所有属性。
ORIGIN是指存储区域的开始地址,而LENGTH是指存储区域的字节大小。
PROVIDE 命令
格式:
PROVIDE (_symbol = _expression)
在某些情形下,我们希望脚本中定义的符号只有当它被引用时才出现。这可以通过PROVIDE命令实现。有些gcc对于C语言中所出现的符号表中会在它的前面加上一个下划线,但有的不会。可以通过使用PROVIDE保证无论gcc行为如何都能被成功编译
.bss :
{
PROVIDE( __bss_start__ = .);
PROVIDE( _bss_start__ = .);
*(.bss)
PROVIDE(__bss_end__ = .);
PROVIDE(_bss_end__ = .);
}
SECTIONS命令
格式:
SECTIONS
{
sections-command
sections-command
...
}
sections-command可以是:
(1)ENTRY命令,注意ENTRY命令既可以放在SECTIONS命令体内,也可以放在体外。
(2)符号定义与赋值
(3)输出段描述
如果脚本中不使用SECTIONS命令,ld将在可执行程序中创建与所有目标文件中同名的段并将同名的段合在一起。各段在可执行程序中的顺序与ld首次遇到它的顺序一致的。另外,第一个段的地址会是0.
输出段描述的格式如下:
section [-address] [(_type)]:
[AT (_lma)]
[ALIGN (_section_align)]
[SUBALIGN (_subsection_align)]
[_constraint]
{
output-section-command
output-section-command
...
} [> _region] [AT> _lma_region] [:_phdr :_phdr ...] [= _fillexp]
_section指的是像.text .data .bss这样的出现在可执行程序中的段名。”/DISCARD/“(不包括引号)是一个特殊的段名,它表示段内output-section-command所描述的个目标文件的段需要被丢弃,而不输出到可执行程序文件中。
_address指的是程序运行时段在内存中的虚拟地址。可以结合ALIGN等命令,使得段的开始地址满足MMU的页边界对齐等要求。
_type可以是以下任一值
___________________________________________________________________________
NOLOAD 当程序被运行时,段不被加载到内存中
DESCT COPY INFO OVERLAY 很少被使用,他们是为了兼容老的语法格式。
它们都是用于表示程序加载时段所需要的内存
是不能通过分配的形式获得
___________________________________________________________________________
_lma指的是程序加载时段的加载地址,如果不指定则认为_lam与_address是一样的。_section_align指示段在内存中存放的开始位置所应满足的边界对齐字节数。_subsection_align则表示段中的子段所应满足的边界字节对齐数。_constraint的值可以是ONLY _IF_RO和ONLY _IF_RW,这两个值分别表示所有的输入段是只读和所有的输入段是可读写时才创建输出段。
输出段中描述中output-sectioncommand可以是:
(1)符号定义和赋值
(2)输入段描述
(3)值定义
_region和_lma_region分别指使用MEMORY命令定义的存储区域的名称。一个是针对VMA的,另一个是针对LMA的,它们都用于指明可执行程序运行时段应放入的存储区域。对于可执行程序中的每一个段,ld都会子啊程序文件中为之创建一个段头,以便程序加载器在加载段时使用。_phdr的值将被用于设置段头。_fillexp指示当段与下一个段之间存在”空隙“时,对”空隙“中的内存空间所需填入的值。
SECTIOS
{
.text :
{
*(.text)
} = 0x90909090
}
表示将空隙中的各字节填充为0x90.段与段之间的空隙通常是因为各段的开始地址需要满足一定的边界对齐要求而造成的。
我们使用输出段描述来告诉ld如何将程序布置到内存中,而输入段描述用来告诉ld如何将所有的目标文件中的段映射到内存中。不难发现,链接脚本中的内容大部分是输入段的描述。以下是输入段常见的格式:
1. *(.text)
2. data.o(.data)
3. *(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)
4. libcore.a:task.o(.text)
5. libcore.a:(.text)
6. :task.o(.text)
第一种格式的意思是指所有目标文件中的.text段。目标文件可能来自于一个库,也可能是一个独立的文件。这种格式使用了通配符,可以在链接脚本中使用的通配符
——————————————————————————————————————————————————
* 匹配任意个数的字符
? 匹配任意一字符
[_chars] 匹配_chars所指定范围中的任一字符
\ 脱字符。器功能类似C语言中的”\n“中的反斜杠
______________________________________________________________________________________
第二种格式是指data.o目标文件中的.data段。
第三种格式是指除了目标文件名满足EXCLUDE_FILE命令中的格式外所有的目标文件中的.ctors段。
第四种格式是指libcore.a库中task.o目标文件中的.text段。
第五种格式libcore.a库中所有(目标文件)的.text段。
第六种格式是指不在任一个库文件中的task.o目标文件中的.text段。
看的出,括号内包含的是段名,而括号之前的描述目标文件的名字或通配符。注意,括号内的段名可以有多个
*(.text .rdata)
*(.text) *(.rdata)
上面两种并不等价。第一种每个目标文件中的.text和.rdata段是交替放在一起的;而第二种所生成的结果是各个目标文件中的.text段放在一起,各个目标文件中的.rdata段也是放在一起的,且整个.rdata段放在整个.text段的后面。
常用选项:
-e 指定程序的入口地地址,与之前的ENTRY命令功能相同。
-r 进行不完整的链接(部分链接,即部分重定位),且所生成的文件可以进一步被ld使用以便生成最终的可执行程序。-r选项的用途是,有时我们为了在自己的程序中生成所有程序的符号表数组,那么可以通过两次链接做到。第一次使用-r将所有目标文件和库进行链接,生成的输出文件通过nm等工具及另外的脚本生成符号表数组。最后对数据表数组进行编译以生成一个目标文件,并将之与第一次链接生成的文件再做一次链接以获得最终的可执行程序。