[原]ld gnu linker脚本

每个链接都被一个链接脚本控制。 这个脚本是用连接命令语言书写的。

链接脚本的主要目的是描述输入文件中的域应该如何映射到输出文件中, 并且控制输出文件的内存布局。 大多数链接脚本除此之外什么也不做。 然而, 当有必要时,链接脚本也可以指导链接器去产生其他更多的操作, 使用下面的命令。

链接器总是使用一个链接脚本。 如果你没有自己提供一个,链接器会使用默认脚本,被编译为链接器可执行的.

你也可以隐式使用链接脚本,通过指出他们的名字作为输入文件.

基本的链接脚本概念:
链接器将输入文件组装为单一的输出文件. 输出文件和每个输入文件在一种特定的数据格式, 被称为目标文件. 每个文件成为目标文件. 输出文件经常称为可执行的, 但是对我们目的来讲, 我们也称之为目标文件. 每个目标文件有一系列的区域, 在其他的东西中. 我们有时候引用一个输入文件的区域称为输入区; 类似的, 输出文件中的区域称为输出区.

每个目标文件的区域都有一个名字, 和大小. 绝大多数区域同样也有一个相联系的数据块, 被称为区域内容. 一个区域被标记为可加载的, 意味着内容将被加载到内存中, 当输出文件被执行时. 一个区域没有内容, 被称为可分配的, 意味着那个区域在内存中应该被分配, 但是不应加载任何东西(在某些情况下, 这片内存必须被清零). 一个区域既不是可加载的, 也不是可分配的, 包含某些调试信息.

每个可加载或者可分配的输出区域都有两个地址. 第一个被称为VMA(虚拟内存地址). 这个地址用于区域当输出文件运行时. 另一个是LMA(加载内存地址). 这个地址是区域被加载的地址. 在大多数情况下, 两个地址是相同的. 一个例子指出什么时候不同, 当数据区被加载到ROM中, 接着被拷贝到RAM中, 当程序开始(这个技术经常用于初始化全局变量在ROM中的基于系统的). 在这种情况下, ROM的地址将是LMA, 而RAM的地址将是VMA.

你可以查看obj文件中的区域, 通过objdump命令, 加上-h选项.

每个目标文件同样也含有一个符号列表, 称为符号表. 一个符号可能被定义, 或者未定义. 每个符号都有名字, 每个定义的符号都有地址, 在其他信息中. 如果你编译c程序到obj文件, 你可以得到一个定义的符号关于每一个定义的函数, 全局变量,  静态变量. 每个未定义的函数或者全局变量在输入文件中引用的将会称为未定义的符号.

你可以通过nm查看obj文件的符号, 或者通过objdump -t.

链接脚本的格式:

文本格式脚本:
你可以写一个命令串的脚本. 每个命令可以是关键字, 后接参数, 或者一个对符号的赋值语句. 你可以采用分号分离命令. 空格通常被忽略.

文件或者格式名称字符串可以直接输入. 如果文件名包含逗号等类似字符, 作为文件名分隔符, 你应该将文件名放到双引号中. 不能在文件名中使用双引号.

你可以包含注释, 采用/**/.

最简单的链接脚本仅含一条命令:
`SECTION. 你可以使用`SECTION命令描述输出文件的内存布局.

`SECTION命令功能强大. 假设你的程序由代码, 初始化数据, 未初始化数据组成. 放在.text, .data ,.bss段中. 让我们作更深的假设, 这些是仅有的区域.

作为这个例子, 让我们要求.code必须加载到地址0x10000, .data数据必须从0x8000000开始. 这是一个链接脚本.
SECTIONS
{
   .=0x10000;
   .text : {*(.text)};
   . = 0x8000000;
   .data : {*(.data)};
   .bss
}
你写SECTIONS命令, 后接一系列符号赋值, 和输出区域描述通过放到花括号中.

这第一行在SECTION命令中, 用来设置特殊符号.的值, 是定位计数. 如果你没有通过其他方式(后面指出方式)指定输出区域的地址, 地址将被设置为从当前定位计数器的值开始. 定位计数器接着增加输出区域的大小. 在SECTION命令开始, 定位计数器值为0.

第二行定义输出区域.text . 冒号当前可以被忽略. 在花括号中, 你可以列出输入区域的名字, 作为放置到输出的内容. *是一个通配符, 匹配任何文件名. 表达式*(.text)表示所有的.text输入区域.

因为当.text定义时, 定位计数器是0x10000, 所以链接器将会设置.text区域的在输出文件中, 作为0x10000

接下来的行定义了.data 和.bss区域, 在输出文件中. 链接器将会放置.data输出区域到地址0x8000000. 在链接器放置完.data后, 定位计数器的值为0x8000000 + 输出.data大小. 效果是链接器放置.bss紧跟这.data输出区域.

链接器将会保证每个输出区域有需要的对齐, 通过必要地增加定位计数器. 在这个例子中, 特定的地址.text和.data可能满足任何对齐约束, 但是链接器会作最小的空隙在.data和.bss之间.

入口:
程序中第一条被执行的语句称为入口. 你可以使用
ENTRY(SYMBOL)

有几种方式去设定入口. 链接器将会设置入口, 通过尝试每种方法:
-e选项
ENTRY命令
start符号
.text区域地址
0

与文件有关的命令:
INCLUDE FILENAME
INPUT(FILE, FILE)
INPUT (FILE FILE)

目标文件格式设置:
OUTPUT_FORMAT(BFDNAME)

符号赋值:
可以通过一下方式为符号赋值:
SYMBOL = EXCEPTION;
SYMBOL += EXCEPTION
SYMBOL ...
表达式之后需要分号
你可以书写符号赋值作为命令, 或者SECTIONS语句, 或者作为输出区域的描述.
符号的区域将会被设为表达式的区域.

这是个例子:
floating_point = 0;
SECTIONS
{
 .text :
{
  *(.text)
  _etext = .;
}
_bdata = (. + 3) & ~3;
.data : {*(.data)}
}
这个例子, 符号floating_point定以为0, 符号_etext定以为.text最后的地址. 符号_bdata将被定义为.text输出区域对齐上边界4字节地址.

PROVIDE命令:
在某些情况下, 希望能够定义一个符号没有在任何输入中定义, 只要有定义即可. 例如, 传统的链接器定义etext.
然而ANSI C需要用户能够是用etext作为函数名, 而不会出错. PROVIDE正是这样的关键字, 只是引用, 却不定义.语法是: PROVIDE(SYMBOL = EXPRESSION)
这是一个例子:
SECTION
{
 .text:
 {
   *(.text)
   _etext = .;
  PROVIDE(etext = .);
 }
}
在这个例子中, 如果程序定义_etext, 链接器给出多重定义错误. 如果, 定义etext, 链接器只会使用它. 如果程序引用etext, 但是没有定义, 链接器将会使用脚本里的定义.

源代码引用:
访问一个链接脚本, 定义一个变量从源代码中, 是不直观的. 脚本符号不等价于变量声明, 只是替换了一个符号没有值.

通常编译器将名字转换为不同的符号在符号表中. 因此, 可能有偏差在变量名和脚本符号间. 例如, 在c链接器脚本中, 变量可能引用 extern int foo;
但是在脚本中:
_foo = 1000;
一下例子假设没有名字转换.

当一个符号在c中声明, 两件事发生. 一编译器保留空间. 二编译器创建入口在符号表中. 例如, 符号表包含内存块地址, 包含变量值.
int foo = 1000;
建造入口, foo. 这个入口保存这int大小的内存块, 初始化数字1000存储在这里。

当一个程序引用符号, 编译器生成的代码首先访问符号表,查找符号在内存中的地址,接着编码读取内存块的值,例如:
foo = 1;
查找符号foo, 获得符号地址,想地址写入值1。
然而:
int *a = &foo;
查找符号表, 找到符号foo,获得符号地址 ,拷贝地址到a相关内存块。
连接器脚本符号声明,与上面相反,创建一个符号表入口,但是不给符号符任何内存。 因此, 他们只是地址,却没有值. 例如:
foo = 1000;
建立一个入口, 拥有地址1000, 但是没有指定存储任何东西. 这意味着, 你不能访问链结脚本变量的值, 唯一能做的只是访问符号.

因此, 当你在源代码使用链结脚本变量, 你应该总是获取符号地址, 不要试图使用值. 例如假设你拷贝内存区域的内容. 区域ROM称为.FLASH 并且链结脚本包含这些声明:
start_of_ROM = .ROM;
end_of_ROM = .ROM;
start_of_FLASH = .FLASH;
接着c源代码展示复制是:
extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy(&start_of_FLASH, &start_of_ROM, &end_of_ROM - &start_of_ROM);
使用&是正确的.

SECTIONS 命令:
他告诉连接器如何映射输入区域到输出区域中去, 如何放置输出区域:
SECTIONS
{
}
ENTRY 命令;
符号赋值;
输出区域描述;
覆盖描述;

entry和符号赋值允许SECTIONS内部命令定位计数器. 也可以是脚本更易懂.

输出区域描述和覆盖描述:

如果不使用SECTIONS, 链结器会放置输出区域到一个标识的输出区域中, 以在文件中遇到的区域的顺序来放置. 如果所有的输入区域都在第一个文件中出现. 第一个区域定位于地址0.

输出区域描述:
SECTION [ADDRESS] [(TYPE)] :
[AT(LMA)] [ALIGN(SECTION_ALIGN)] [SUBALIGN(SUBSECTION_ALIGN)]
{
}[>REGION] [AT>LMA_REGION] [:PHDR :PHDR] [=FILEXP]

输入区域基础:
包含文件名和之后的括号中的区域名列表.

输入区域通配符:
*任意字符;
?单个字符;
[CHARS]
范围;


PHDRS命令:
elf文件类型使用program header, 也称为segment. ph描述程序如何加载到内存中. 你可以通过objdump打印 -p选项.

当你读取elf在本地的elf系统上时, 系统加载器会读取程序头, 确定加载方式. 这只会在正确的头部时工作.

连接器会产生正确的程序头. 然而, 在某些情况下, 你可能需要去更准确确定头部. 你可以使用PHDRS确定.

链结器在产生elf文件时, 会考虑PHDRS.

语法:
PHDRS
{
NAME TYPE [FILEHDR] [PHDRS] [AT(ADDRESS)] [FLAGS(FLAGS)];
}
name只在section命令中引用, 不会输出. 程序头存储在隔离的名字空间, 不会与符号名, 文件名, 区域名冲突. 每个文件头有不同的名字.

特定的文件头描述内存段, 用来加载文件.


作者:liyonghelpme 发表于2010/6/10 18:24:00 原文链接
阅读:612 评论:0 查看评论
posted @ 2010-06-10 18:24  liyonghelpme  阅读(281)  评论(0编辑  收藏  举报