LD文件详解
一. LD 文件的概念
ld
文件通常指的是链接脚本文件,主要用于控制链接器(如 GNU 链接器 ld
)的行为。链接器是将编译后的目标文件(object files)和库文件(libraries)结合起来生成可执行文件或共享库的工具。链接脚本允许开发者精确地控制链接过程,例如定义内存布局、设置节(section)的地址、指定符号的位置等。
基本概念
链接器将输入文件组合成一个输出文件。输出文件和每个输入文件都采用一种被称为目标文件格式的特殊数据格式。每个文件被称为目标文件。输出文件通常被称为可执行文件,但在这里我们也称其为目标文件。每个目标文件都有一个节列表。我们有时将输入文件中的节称为输入节;类似地,输出文件中的节称为输出节。
目标文件中的每个节都有一个名称和一个大小。大多数节还关联有一个数据块,称为节内容。一个节可能被标记为可加载,这意味着在运行输出文件时,其内容应该被加载到内存中。没有内容的节可能是可分配的,这意味着应该在内存中预留一个区域,但不需要加载任何特定内容(在某些情况下,这段内存必须被清零)。既不可加载也不可分配的节通常包含某种调试信息。
每个可加载或可分配的输出节都有两个地址。第一个是VMA
,即虚拟内存地址。这是节在运行输出文件时的地址。第二个是LMA
,即加载内存地址。这是节被加载时的地址。在大多数情况下,这两个地址是相同的。当数据节被加载到ROM中,然后在程序启动时被复制到RAM中时,这两个地址可能会不同(这种技术常用于在基于ROM的系统中初始化全局变量)。在这种情况下,ROM地址是LMA,而RAM地址是VMA。
你可以使用带有-h
选项的objdump程序查看目标文件中的节。
每个目标文件还有一个符号列表,称为符号表。符号可以是已定义的或未定义的。每个符号都有一个名称,每个已定义符号都有一个地址及其他信息。如果你将一个C或C++程序编译成目标文件,那么对于每个已定义的函数和全局或静态变量,你都会得到一个已定义符号。每个在输入文件中被引用的未定义函数或全局变量将成为一个未定义符号。
你可以使用nm程序或带有-t
选项的objdump程序查看目标文件中的符号。
ld 文件的作用
- 指定内存布局:定义程序在内存中的布局,包括代码段、数据段、堆栈等的位置和大小。
- 设置节地址:控制各个节(如
.text
、.data
、.bss
等)的起始地址和排列顺序。 - 定义符号:可以定义新的符号或重定义已有符号的地址。
- 合并和排列节:控制不同目标文件中的节如何合并和排列。
- 设置入口点:指定程序的入口点,即从哪里开始执行。
ld 文件的产生
手动编写
开发者可以手动编写 ld
链接脚本文件,根据需要定义链接过程的详细信息。以下是一个简单的 ld
文件示例:
SECTIONS
{
/* 定义内存布局 */
. = 0x1000; /* 设置起始地址 */
.text : { *(.text) } /* 将所有 .text 节合并到 .text 段 */
. = 0x8000;
.data : { *(.data) }
.bss : { *(.bss) }
}
自动生成
在一些复杂的项目中,链接脚本可能由构建系统(如 CMake)或构建工具链自动生成。这种情况下,开发者通常会编写模板或规则,然后由构建系统根据具体的构建配置生成最终的 ld
文件。
例如,在使用 CMake 时,开发者可以通过 CMakeLists.txt 文件配置链接器脚本的生成:
set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/linker.ld)
add_executable(my_program main.c)
target_link_libraries(my_program ${LINKER_SCRIPT})
使用 ld 文件
当编写或生成了 ld
文件后,链接器需要知道使用该文件进行链接。通常通过命令行参数指定链接脚本文件:
gcc -o my_program main.o -T linker.ld
在这条命令中,-T linker.ld
命令行参数告诉 GCC 使用 linker.ld
文件作为链接脚本。
基本表达式
-
常量表达式:
- 直接使用数值,例如:
0x1000
、4096
。
- 直接使用数值,例如:
-
符号:
- 符号可以是目标文件中的符号或在
ld
脚本中定义的符号。例如:_start
、_end
。
- 符号可以是目标文件中的符号或在
-
算术运算:
- 可以对数值和符号进行算术运算,例如:
_start + 0x1000
、SIZEOF(.text) + 4
。
- 可以对数值和符号进行算术运算,例如:
-
逻辑运算和比较:
- 可以进行逻辑运算和比较,例如:
(SIZEOF(.text) > 1024) ? 0x1000 : 0x2000
。
- 可以进行逻辑运算和比较,例如:
特殊函数和操作符
-
ALIGN:
- 用于对齐地址,例如:
ALIGN(0x1000)
表示将当前地址对齐到0x1000的倍数。
- 用于对齐地址,例如:
-
SIZEOF:
- 返回某个节的大小,例如:
SIZEOF(.text)
。
- 返回某个节的大小,例如:
-
ADDR:
- 返回某个节的起始地址,例如:
ADDR(.data)
。
- 返回某个节的起始地址,例如:
-
NEXT:
- 返回当前地址,并将其对齐到指定的边界,例如:
NEXT(0x100)
。
- 返回当前地址,并将其对齐到指定的边界,例如:
例子
以下是一个简单的ld
脚本示例,展示了如何使用这些表达式:
SECTIONS
{
. = 0x1000; /* 设置链接起始地址 */
.text : {
*(.text) /* 将所有输入文件中的.text节合并到输出文件的.text节中 */
}
. = ALIGN(0x1000); /* 对齐到0x1000字节边界 */
.data : {
*(.data) /* 将所有输入文件中的.data节合并到输出文件的.data节中 */
}
.bss : {
*(.bss) /* 将所有输入文件中的.bss节合并到输出文件的.bss节中 */
}
}
ENTRY(_start) /* 设置程序入口点 */
二. LD的命令
GNU 链接器 ld
支持多种命令和指令,这些命令和指令用于控制链接过程的各个方面,包括内存布局、节的排列、符号定义等。以下是一些常用的 ld
链接脚本命令和指令:
2.1 SECTIONS命令
在 GNU 链接器 (ld
) 中,SECTIONS
命令是链接脚本中最重要的部分之一。它定义了输出文件的内存布局,并指定各个输入文件的哪些部分应该放在哪里。通过 SECTIONS
命令,您可以精确控制程序的内存映射,这是嵌入式系统开发中必不可少的功能。
SECTIONS
基本语法
SECTIONS
{
...
}
在 SECTIONS
命令的花括号内,您可以定义多个段(sections),每个段描述了输出文件的一部分的布局和内容。
段定义
每个段的定义通常包括段名、段属性、段对齐方式、段内容和段放置规则。例如:
SECTIONS
{
.text : ALIGN(4)
{
*(.text)
} > FLASH
.data : ALIGN(4)
{
*(.data)
*(.rodata)
} > RAM AT > FLASH
.bss : ALIGN(4)
{
__bss_start__ = .;
*(.bss)
*(COMMON)
__bss_end__ = .;
} > RAM
}
详细解释
段名
.text :
.text
是段的名称。通常的段名有.text
(代码段)、.data
(初始化数据段)、.bss
(未初始化数据段)等。
段属性和对齐
ALIGN(4)
ALIGN(4)
指定段的开始地址需要对齐到 4 字节边界。
段内容
{
*(.text)
}
- 花括号内指定了段的内容。
*
表示匹配所有输入文件的.text
段,并将它们包含在这个输出文件的.text
段中。
存储器区域
> FLASH
> FLASH
指定把段放在FLASH
存储区域。存储区域通常在链接脚本的MEMORY
命令中定义。
AT 指令
AT > FLASH
AT
指令指定段的加载地址。例如,在RAM
中运行的.data
段可以从FLASH
中加载。
综合示例解释
SECTIONS
{
.text : ALIGN(4)
{
*(.text)
} > FLASH
.data : ALIGN(4)
{
*(.data)
*(.rodata)
} > RAM AT > FLASH
.bss : ALIGN(4)
{
__bss_start__ = .;
*(.bss)
*(COMMON)
__bss_end__ = .;
} > RAM
}
-
.text
段:- 名称:
.text
- 对齐:4 字节
- 内容:所有输入文件的
.text
段 - 存储器区域:
FLASH
- 名称:
-
.data
段:- 名称:
.data
- 对齐:4 字节
- 内容:所有输入文件的
.data
和.rodata
段 - 存储器区域:
RAM
- 加载地址:
FLASH
- 名称:
-
.bss
段:- 名称:
.bss
- 对齐:4 字节
- 内容:
__bss_start__
符号表示.bss
段开始地址- 所有输入文件的
.bss
段和COMMON
段 __bss_end__
符号表示.bss
段结束地址
- 存储器区域:
RAM
- 名称:
常用命令和表达式
符号定义
在段内可以定义符号,这些符号可以用于表示地址和大小。例如:
__start_text = .;
.
表示当前地址计数器。
SIZEOF
和 ADDR
SIZEOF(section)
:返回段的大小。ADDR(section)
:返回段的地址。
例如:
__data_size = SIZEOF(.data);
__data_start = ADDR(.data);
FILL
用于指定段的填充模式,例如:
.text : FILL(0x90)
{
*(.text)
} > FLASH
FILL(0x90)
将.text
段填充为0x90
。
2.2 赋值命令
好的,在 GNU 链接器 (ld
) 的链接脚本中,赋值命令用于定义和控制符号的值。下面分为几个部分详细介绍赋值语句、HIDDEN
、PROVIDE
和 PROVIDE_HIDDEN
。
1. 赋值语句
赋值语句是链接脚本中最基本的操作之一,用于给符号赋值。
语法
symbol = expression;
symbol
是符号的名称。expression
是要赋给符号的值,可以是常数、地址、当前地址计数器(.
),或其他符号的值。
示例
SECTIONS
{
.text : {
start_of_text = .;
*(.text)
end_of_text = .;
} > FLASH
}
在这个例子中:
start_of_text
被赋值为.text
段的开始地址。end_of_text
被赋值为.text
段的结束地址。
2. HIDDEN
HIDDEN
关键字用于将符号的可见性设置为隐藏,这样符号就只在本模块内可见,不会暴露给外部模块。这在控制符号范围和避免命名冲突时非常有用。
语法
HIDDEN(symbol = expression);
示例
SECTIONS
{
.text : {
HIDDEN(hidden_symbol = .);
*(.text)
HIDDEN(hidden_end = .);
} > FLASH
}
在这个例子中:
hidden_symbol
和hidden_end
是隐藏的符号,只在本模块内可见。
3. PROVIDE
PROVIDE
关键字用于有条件地定义符号。如果符号在其他地方已经定义,则 PROVIDE
不会重新定义它。
语法
PROVIDE(symbol = expression);
symbol
是符号的名称。expression
是要赋给符号的值。
示例
SECTIONS
{
.text : {
PROVIDE(__start_of_text = .);
*(.text)
PROVIDE(__end_of_text = .);
} > FLASH
}
在这个例子中:
__start_of_text
和__end_of_text
将在没有其他定义的情况下被赋值。
4. PROVIDE_HIDDEN
PROVIDE_HIDDEN
结合了 PROVIDE
和 HIDDEN
的功能,用于有条件地定义一个隐藏的符号。如果符号在其他地方已经定义,则不会重新定义它。
语法
PROVIDE_HIDDEN(symbol = expression);
symbol
是符号的名称。expression
是要赋给符号的值。
示例
SECTIONS
{
.text : {
PROVIDE_HIDDEN(__hidden_start = .);
*(.text)
PROVIDE_HIDDEN(__hidden_end = .);
} > FLASH
}
在这个例子中:
__hidden_start
和__hidden_end
将在没有其他定义的情况下被赋值,并且这些符号是隐藏的,只在本模块内可见。
2.3 MEMORY 命令
在 GNU 链接器 (ld
) 的链接脚本中,MEMORY
命令用于定义目标系统的内存布局。它能够指定不同内存区域的大小、起始地址和属性。这对于嵌入式系统等对内存布局有严格要求的应用非常重要。
MEMORY
命令的基本语法
MEMORY
{
name (attr) : ORIGIN = origin, LENGTH = length
...
}
name
是内存区域的名称。(attr)
是可选的内存区域属性,用于指定该区域的特性(如只读、只写等)。ORIGIN
是内存区域的起始地址。LENGTH
是内存区域的长度。
内存区域的属性
内存区域的属性用括号括起来,可以包含以下字符:
r
:表示该区域可读。w
:表示该区域可写。x
:表示该区域可执行。
示例
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
在这个示例中:
FLASH
区域从地址0x08000000
开始,长度为256K
,该区域是只读且可执行的(rx
)。RAM
区域从地址0x20000000
开始,长度为64K
,该区域是可读、可写和可执行的(rwx
)。
使用 MEMORY
定义的区域
定义了内存区域之后,可以在 SECTIONS
命令中使用这些区域来分配段。
示例
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.text : {
*(.text)
} > FLASH
.data : {
*(.data)
} > RAM
.bss : {
*(.bss)
} > RAM
}
在这个示例中:
.text
段被放置在FLASH
区域。.data
和.bss
段被放置在RAM
区域。
2.4 其它命令
节命令
-
. (dot)
- 当前地址位置,可以用来设置节的起始地址。
- 示例:
SECTIONS { . = 0x1000; .text : { *(.text) } }
-
KEEP
- 确保指定的节或符号不会被垃圾回收。
- 示例:
SECTIONS { .text : { KEEP(*(.init)) *(.text) } }
-
ALIGN
- 对齐当前地址。
- 示例:
. = ALIGN(4);
-
FILL
- 用于填充未定义的空间。
- 示例:
.text : { *(.text) } FILL(0x90)
符号命令
-
ASSERT
- 检查条件是否满足,如果不满足则报错。
- 示例:
ASSERT(_end <= 0x20008000, "Not enough RAM");
-
EXTERN
- 声明外部符号,确保链接器不会删除它们。
- 示例:
EXTERN(_start)
文件命令
-
INCLUDE
- 包含另一个链接脚本文件。
- 示例:
INCLUDE "common.ld"
-
SEARCH_DIR
- 添加库文件搜索路径。
- 示例:
SEARCH_DIR(/usr/local/lib)
其他命令
-
PHDRS
- 用于定义程序头表(Program Header Table),通常用于生成 ELF 文件。
- 示例:
PHDRS { text PT_LOAD FILEHDR PHDRS; data PT_LOAD; }
-
VERSION
- 用于定义版本脚本,管理符号的可见性。
- 示例:
VERSION { GLOBAL { main; foo; }; LOCAL { *; }; }
-
LOADADDR
- 用于获取特定节的加载地址。加载地址是指该节在执行文件中被加载到内存中的地址。
- 示例:
LOADADDR(section)
以下是一些更加高级和特定用途的命令和指令,继续补充前面的列表:
高级节命令
-
SORT
- 按名称或地址对节进行排序。
- 示例:
.text : { SORT(*)(.text) }
-
SORT_BY_NAME
- 按名称对节进行排序。
- 示例:
.text : { SORT_BY_NAME(*)(.text) }
-
SORT_BY_ALIGNMENT
- 按对齐方式对节进行排序。
- 示例:
.text : { SORT_BY_ALIGNMENT(*)(.text) }
-
BLOCK
- 用于对齐整个节组。
- 示例:
.text : { BLOCK(4) { *(.text) } }
高级符号命令
-
DEFINED
- 检查符号是否已定义。
- 示例:
SECTIONS { .text : { *(.text) } .data : { _data_start = .; *(.data) _data_end = .; } ASSERT(DEFINED(_data_start), "Data start not defined"); }
-
PROVIDE_HIDDEN
- 定义一个隐藏符号,仅在符号未被定义时生效。
- 示例:
PROVIDE_HIDDEN(__stack_size = 0x2000);
高级文件命令
-
GROUP
- 将多个文件视为一个文件进行处理。
- 示例:
GROUP(lib1.a lib2.a)
-
INPUT
- 指定输入文件。
- 示例:
INPUT(main.o utils.o)
-
OUTPUT
- 指定输出文件名。
- 示例:
OUTPUT("output.elf")
高级内存命令
- REGION_ALIAS
- 为内存区域创建别名。
- 示例:
MEMORY { RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K } REGION_ALIAS("RAM_ALIAS", RAM);
高级调试命令
- MAP
- 生成链接映射文件,显示链接过程的详细信息。
- 示例:
OUTPUT_FORMAT("elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } } MAP("output.map")
高级控制命令
-
FORCE_COMMON_ALLOCATION
- 强制分配 COMMON 符号。
- 示例:
FORCE_COMMON_ALLOCATION
-
NOLOAD
- 指定节不应加载到内存中。
- 示例:
SECTIONS { .bss (NOLOAD) : { *(.bss) } }
-
OVERLAY
- 定义重叠区域,用于共享内存。
- 示例:
SECTIONS { .overlay1 : { *(.text1) } > RAM .overlay2 : { *(.text2) } > RAM .overlay3 : { *(.text3) } > RAM }
动态链接命令
-
DYNAMIC
- 定义动态节。
- 示例:
DYNAMIC { .dynamic : { *(.dynamic) } }
-
VERSION
- 定义动态链接版本。
- 示例:
VERSION { v1.0 { global: foo; local: *; }; }
完整的 GNU ld
链接脚本命令和指令可以参考 GNU Binutils 文档。