ld 链接器的功能是将一个可执行程序所需的目标文件和库最终整合为一体。一个程序通常包含传统的三个段:.text、 .data和 .bss 段。实际上,在目标文件和库被整合成一个可执行文件之前,通常各目标文件和库中也包含这三个段。不难想象链接器的功能就是将这些段进行合并。这之间有个非常重要的工作——重定位。
1、重定位
当一个源文件被编译为目标文件时,目标文件只记录了程序中的符号和各符号在段中的相对位置,而符号和段的具体地址并没有指定。当链接器将所有的目标文件整合成一个可执行程序时,各目标文件中的各段将会获得内存中的具体地址,各个符号的具体地址也会确定,相应的指令也会根据真实地址进行调整,从而实现函数调用和变量的引用,这一动作就是重定位。链接器如何知道每一个目标文件中的各个段应当放在哪一个地址呢?这需要链接脚本来指定。
2、链接脚本
链接脚本的功能是告诉链接器,如何将各个不同的目标文件(包括库)中的段合并在一起并最终生成一个可执行程序。从链接脚本的角度看,一方面他需要描述输出,即最终输出到可执行程序文件中的段;另一方面又要描述输入,即来自各个目标文件中的段。默认的链接脚本可以用 ld 的 --verbose 选项到处:
$ ld --verbose > ldscript
其大体结构如下:
SECTIONS
{
.../*省略*/
. = SIZEOF_HEADERS;
. = ALIGN(__section_alignment__);
.text xxx :
{
*(.init)
*(.text)
...
}
.data BLOCK(__section_alignment__) :
{
__data_start__ = . ;
*(.data)
...
*(.rdata)
...
}
...
.bss BLOCK(__section_alignment__) :
{
__bss_start__ = . ;
*(.bss)
*(COMMON)
__bss_end__ = . ;
}
...
}
2.1 SECTIONS 命令描述的是可执行程序各段在内存中的布局;
2.2 第 4 行,"." 表示 位置指针。一进入 SECTIONS 命令的区间,位置指针的值就为0,这里将位置指针的值设置为SIZEOF_HEADERS;后面的 .text 从 SIZEOF_HEADERS 处开始存放(姑且这么认为,事实上还跟下一条语句有关,SIZEOF_HEADERS 表示文件的文件头大小)。位置指针和 C 语言的指针概念相似;
2.3 第 5 行, ALIGN 和后面出现的 BLOCK 为链接脚本中常用的命令。
这里的 ALIGN 命令将返回位置指针之后的第一个满足边界对其字节数 __section_alignment__ 的地址值。ALIGN 命令还有种格式为: ALIGN(_exp, _align),它表示返回 _exp 表达式值之后的,满足边界对齐字节数 _align 的地址值。第5行的 ALIGN 就相当于是:ALIGN(., __section_alignment__)
BLOCK 命令和 ALIGN 命令的作用一样,它没有第二种格式,它的存在是为了兼容老的语法。
2.4 第 6~11 行是一个输出段描述语句块,它表示生成的可执行程序中将有一个 .text 段。当程序被加载时, .text 段的内容将被拷贝到 SIZEOF_HEADERS 处;
2.5 第 8~10 行表示可执行程序中 .text 的组成(也就是它的输入)。依次是所有目标文件的 .init 段和 .text 段。
2.6 第 12~19 行是 .data 段 输出语句块, .data 之后也加入了对齐(具体值取决于现在的位置指针的值,也就是之前 .text 段的大小),事实上这里可以直接指定一个具体的地址值(比如 0x80000000 或其它)。
2.7 第 14、23、26 行分别定义了三个符号。__data_start__ 符号代表了 .data 段的开始地址,__bss_start__ 符号代表了 .bss 段的开始地址,__bss_end__ 符号代表了 .bss 段的结束地址(准确的讲,__bss_end__ 符号所代表的地址并不属于 .bss 段,__bss_end__ - 1 才是)。
符号一旦在链接脚本中定义后,就可以在程序使用它们,假设现在有测试程序 main.c 如下:
--main.c--
#include <stdio.h>
extern char _data_start__[];
extern char _bss_start__[];
extern char _bss_end__[];
int main()
{
printf("__data_start__ = 0x%p.\n", _data_start__);
printf("__bss_start__ = 0x%p.\n", _bss_start__);
printf("__bss_end__ = 0x%p.\n", _bss_end__);
return 0;
}
在这个测试程序中,声明的变量和链接脚本中定义的符号相差一个下划线前缀。这是因为 Mingw 会在 C 语言中的符号之前加上一个下划线。
$ gcc -Wl,-Map=main.map main.c -o test
[ -Map 选项可以让 ld 生成整个程序的映射文件]
$ test.exe
$$ __data_start__ = 0x00404000.
$$ __bss_start__ = 0x00407000.
$$ __bss_end__ = 0x0040707C.
3、其它命令
3.1 MEMORY在默认情况下,ld 认为整个程序都是放入同一个存储空间的。如果一个嵌入式系统中存在多块不同的存储空间,就得使用MEMORY 命令进行存储区域定义。示例如下:
--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 都被定义为可读可写可执行的存储区域。.bss 存储于 RAM0, .text 和 .data 存储于 RAM1;
使用存储区域时,如果链接器碰到存放段大于存储区域的容量时就会报警,比如假设 .bss 段大于 256K,链接器将会告警,可以利用这一特性监视各个段是否超出规定的大小。
3.2 BYTE、SHORT、LONG、QUAD
这些命令的格式为:
BYTE (_value)
SHORT (_value)
LONG (_value)
QUAD (_value)
这四个命令依次表示在输出的可执行程序文件中放置所占存储空间为1、2、4、8字节的值 _value。
3.3 ENTRY
ENTRY命令的格式为:
ENTRY (_symbol)
指定可执行程序的入口点。入口点表示程序被加载到内存后,运行的第一条指令所在的内存地址。
3.4 PROVIDE
PROVIDE 命令的格式为:
PROVIDE (_symbol = _exp)
在某些情况下,我们希望脚本中定义的符号只有当它被引用时才出现,这可以通过 PROVIDE 命令实现。比如前文的测试程序 main.c,符号 "_bss_end__" 可能在不同的编译环境下表现不同(有的不能成功编译),但借助 PROVIDE 命令可以保证任意环境都可以:
PROVIDE (_bss_end__ = .)
PROVIDE (__bss_end__ = .)
当编译器会自动添加下划线前缀时, _bss_end__ 被引用,否则 __bss_end__ 被引用。
4、常用选项
-Map 选项可以让ld生成整个程序的映射文件;-e 选项可以指定程序的入口点,它的功能与链接脚本中的 ENTRY 命令一样;
-r 选项可以将多个目标文件或库进行不完整的链接,且生成的文件可以进一步被ld使用以便生成最终的可执行程序;
-T 指定自己编写的链接的脚本。