基本概念
链接脚本是用于控制连接过程的文件。它通过定义程序各个段在内存中的布局和属性,指导链接器(如 GNU 链接器 ld )如何将输入文件(目标文件和库文件,比如.o文件)组合成一个最终的可执行文件或库。链接脚本在嵌入式系统开发中尤为重要,因为它允许开发者精确控制代码和数据在内存中的布局,以满足特定的硬件和性能要求。
重要概念:
- 输入文件:输入文件是指链接器在链接过程中需要处理的目标文件或库文件。这些文件通常是通过编译器生成的,包含了编译后生成的代码和数据。
- 目标文件 (Object Files):编译器生成的中间文件,通常扩展名为
.o
。这些文件包含了机器代码和符号信息。 - 库文件 (Library Files):可以是静态库(扩展名为
.a
)或动态库(扩展名为.so
或.dll
),这些库包含了一组可重用的目标文件。
- 目标文件 (Object Files):编译器生成的中间文件,通常扩展名为
- 输入段:输入段是指输入文件中的特定段(section),这些段包含了特定类型的数据和代码。常见的输入段包括:
.text
:代码段,包含程序的可执行指令。.data
:数据段,包含已初始化的全局变量和静态变量。.bss
:未初始化数据段,包含未初始化的全局变量和静态变量。.rodata
:只读数据段,通常包含字符串常量和其他只读数据。(有时也放在.text
中)
- 输出文件:输出文件是指链接器在链接过程中生成的最终文件。这个文件可以是可执行文件、共享库或静态库,具体取决于链接的目标。输出文件包含了所有合并后的代码和数据,并具有适当的格式,以便可以在目标平台上运行或使用。
- 输出段:输出段是链接器在输出文件中定义的段。表示各文件输入段合并后在输出文件中的位置和内容。输出段的定义在链接脚本的
SECTIONS
中进行。 - 内存区域:链接脚本允许定义内存区域,这些区域可以表示实际的物理内存或逻辑地址空间。每个内存区域可以有一个名称、起始地址和大小。
- 位置计数器:位置计数器(通常用
.
表示)是一个特殊符号,它是一个定位器,一个位置指针,指向程序地址空间某位置(或某section内的偏移,如果它在SECTIONS命令内的某section描述内),该符号只能在SECTIONS命令内使用。 - 入口点:入口点是程序开始执行的地址,通常由链接脚本指定。默认入口点是
_start
,但可以通过链接脚本的ENTRY
命令进行修改。 - 符号:符号是应用程序中的标识符,可以表示变量、函数或地址。链接脚本可以定义和操作符号,以便在连接过程中使用。
- 加载地址 LMA:一个输出段初始化时段内容的获取来源。
- 虚拟地址 VMA:一个输出段初始化时段内容的存放位置。
语法
ld 脚本是语句的集合,大部分是设置特定选项的简单关键字,其中SECTIONS
和MEMORY
是有对链接过程具有基本和普遍的影响。
ENTRY
在应用程序中执行的第一条指令称为入口点。默认入口点是 _start
,但可以通过链接脚本的 ENTRY
命令进行修改。
定义语句:
ENTRY(symbol)
有几种设置入口点的方法。 链接器将通过依次尝试以下每种方法来设置入口点,并在其中一种成功后停止:
- **
-e
**输入命令行选项,在调用命令行链接命令时指定; - 链接描脚本中的
ENTRY(symbol)
命令; - 目标默认符号值(如果已定义); 对于许多目标来说是
_start
符号。 .text
输出段的第一个字节的地址(如果存在);- 地址0。
SECTIONS
SECTIONS
命令常用于段(section)定义,段是目标文件中的基本单位,一个段在内存中就是一块位置,这块位置放什么就取决于我们编写的 SECTIONS
语句。
定义语句:
SECTIONS{
输出段1 : { filename(输入段1, 输入段2, ...)}
输出段2 : { filename(输入段1, 输入段2, ...)}
...
}
- filename 可以使用
*
等正则表达式,比如*.text
表示 所有输入文件中的.text
段 输出段x
之后,:
之前,可选增加EXPRESSION
语句,用于设置输出段的虚拟地址输出段x
之后,:
之前,可选增加AT (EXPRESSION)
语句,用于设置输出段的加载地址- 输出段定义的最末端,可选增加
> 内存空间
语句 ,用于定义输出段所处的内存具体位置(即虚拟地址VMA)
示例:
/* 在下面的示例中,命令脚本将输出文件排列成三个连续的部分,分别命名为.aaa、.bbb、 .ccc,并从 所有输入文件 中相应命名的部分获取每个部分的输入 */
/* 对于所有以大写字符开头的文件,.ccc段被放在.CCC,对于小写开头,.ccc段被放置在.ccc输出段中。*/
SECTIONS {
.aaa 0x400 : { *(.aaa) } /* “0x400” 指定 .aaa 段的虚拟地址绝对路径 */
.bbb : { *(.bbb) } > ram /* “> ram” 指定 .bbb 段的虚拟地址放在ram中 */
_sss = 0x100;
.ccc AT(_sss) : { [a-z]*(.ccc) } /* “AT(_sss)” 指定 .ccc 段的加载地址为0x100 */
.CCC : { [A-Z]*(.ccc) } > ram AT> rom /* “AT> rom” 指定 .ccc 段的加载地址在rom中 */
}
符号&表达式
表达式语法和C语言的表达式语法一样,表达式的值都是整形,如果ld的运行主机和生成文件的目标机都是32位,则表达式是32位数据否则是64位数据,表达式的格式如下:
SYMBOL = EXPRESSION;
注意:赋值语句包含4个语法元素:符号名,操作符,表达式,分号,缺一不可。赋值语句可以出现在链接脚本的三个地方
- SECTIONS 命令内
- SECTIONS 命令内的 section 描述内
- 全局位置
符号的有效作用区域由表达式所在的段决定。
特殊符号 .
:
.
是位置计数器,如果没有以某种其他的方式指定输出段(output section)的地址,那么下一个定义的输出段地址就会是位置计数器中的当前值。- 设置完一个输出段后,位置计数器就会以输出段 的大小增加其值。在
SECTIONS
命令的开始,位置计数器是0。. = 0x100
表示把位置计数器设为 0x100 - 位置计数器在 SECTIONS 命令内表示绝对内存位置
- 位置计数器在 SECTIONS 命令的 section 描述内表示相对于当前 section 的相对位置
示例:
i_am_global = 0; /* i_am_global 在全局位置 */
SECTIONS
{
. = 0x100; /* 设置位置计数器为 0x100 */
__executable_start = .; /* __executable_start 在 SECTIONS 命令内,赋值为 0x100*/
.text :
{
. = 0x10; /* 设置 . 为 0x10,这是相对 .text 起始地址的路径 */
*(.text)
_etext = .; /* _etext 在 section 描述内 */
}
}
并且以上可以在C语言中代码中使用,但请注意,链接文件中的数值都是表示地址,所以C语言中也要把这些数值当成地址来操作
extern char i_am_global[];
extern char _begin[];
extern char _etext[];
// 但请注意,以上数组的地址上没有存储任何特殊内容。这意味着您无法访问链接程序脚本定义的符号的值——它没有值。您所能做的就是访问链接器脚本定义符号的地址。
PROVIDE
PROVIDE
和C语言中的 weak
关键字功能相似。
PROVIDE
用于在链接脚本中定义一个符号,并为其赋予一个默认值。如果在输入文件中没有其他地方定义该符号,那么链接器会使用 PROVIDE
定义的值。如果输入文件中已经定义了相同的符号,PROVIDE
定义的符号将被忽略,从而避免重复定义错误。
示例:
SECTIONS
{
.text :
{
PROVIDE(_stext = .); /* 定义 _stext 为 .text 输出段的起始地址,如果应用程序定义了 _stext ,那么连接器不会使用这里的 _stext */
*(.text)
_etext = .; /* _etext 如果在应用程序中重复定义,那么链接过程会报错 */
}
}
MEMORY
MEMORY
命令描述目标机器中各内存块的位置和大小。链接器将请求的分段移动到正确的内存块中,并在内存块太满时发出错误。
链接器的默认MEMORY
配置允许分配所有可用内存。我们可以使用MEMORY
命令覆盖此配置。
一个链接脚本最多只能有一条MEMORY
命令,但可以在一个MEMORY
命令中定义多个内存块。
定义语句:
MEMORY
{
名称 (属性列表) : ORIGIN = 起始地址, LENGTH = 块长度
...
}
-
名称:是链接器内部用来引用区域的名称。可以使用任何符号名。不可省。
-
属性列表:内存块属性的可选列表。有效的属性列表必须由字符“ALIRWX”组成,以匹配部分属性。可省,如果你省略了属性列表,你也可以省略它周围的括号。目前支持的属性有:
R: 只读 W: 读写 X: 可执行 A: 已分配区域 Allocated sections. I: 已初始化区域 Initialized sections. L: Same as I. !: 反转后面紧跟的属性的含义 Invert the sense of any of the following attributes.
-
ORIGIN:定义起始地址关键字,不可省,可缩写成
org
或o
。 -
起始地址:物理内存中区域的起始地址。它是一个表达式,在执行内存分配之前必须求值为一个常量。不可省。
-
LENGTH:定义内存块长度关键字,单位是字节,不可省,可缩写成
len
或l
。 -
块长度:物理内存中区域的长度。它是一个表达式,在执行内存分配之前必须求值为一个常量。不可省。
一旦定义了一个名为 mem
的内存区域,我们就可以在sections命令中使用以 > mem
结尾的命令,将特定的输出部分定向到那里。如果指向某个地区的组合输出部分对于该地区来说太大,则链接器将发出错误消息。
示例:
/*
以下示例定义了
ROM 内存区域,起始地址为 0,长度 3MB,只读;
RAM 内存区域,起始地址为 0x10000000,长度 1MB,可读写。
*/
MEMORY
{
ROM (R!W): ORIGIN = 0, LENGTH = 3M
RAM (RW): ORIGIN = 0x10000000, LENGTH = 1M
}
REGION_ALIAS
REGION_ALIAS
用来为 MEMORY
命令创建的内存区域提供别名。 每个名称最多对应一个存储区域。
定义语句:
REGION_ALIAS(别名, 内存区域)
- 别名内不能有空格
示例:
MEMORY
{
ROM (R!W): ORIGIN = 0, LENGTH = 3M
RAM (RW): ORIGIN = 0x10000000, LENGTH = 1M
}
/* 定义别名 */
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
/* 以下段的内存映射就可以使用以上定义的别名 */
SECTIONS
{
.text : { (.text) } > REGION_TEXT
.rodata : { (.rodata) } > REGION_RODATA
.data : { (.data) } > REGION_DATA
.bss : { (.bss) } > REGION_BSS
}