概念理解
Linker script 是用来控制链接过程的脚本。它的作用主要是规定如何把输入文件内的 section 放入输出文件内,并控制输入文件内各部分在程序地址空间内的布局。
链接器有个默认的内置链接脚本,可用 ld --verbose
查看。链接选项 -r 和 -N
可以影响默认的链接脚本。 -T 选项用以指定链接脚本。
链接器把一个或多个输入文件合成一个输出文件。有时把输入文件内的 section 称为
输入section
(input section),把输出文件内的 section 称为
输出section
(output section)。
目标文件的每个 section 至少包含两个信息:名字和大小。段内部还可能包含一些数据, 被称作段内容(section contents)。一个 section 可被标记为 loadable 或 allocatable。例如 text 段的标志为 loadable,表示该段的段内容在运行的时候需要 加载到内在中;还有一些段(比如 bss 段)没有段内容,那么这些段标志为 allocatable,即需要分配一些内存。
每个 loadable 或 allocatable 输出 section 通常包含两个地址:VMA(virtual memory address) 和 LMA(load memory address)。通常,VMA 和 LMA 是相同的。但在 嵌入式系统中,经常存在加载地址和执行地址不同的情况:比如将输出文件加载到开发 板的 flash 中(由 LMA)指定,而在运行时将位于 flash 中的输出文件复制到 SDRAM中 (由 VMA 指定)。
符号(symbol):每个目标文件都有符号表(symbol table),包含已定义的符号和未定义 的符号。(分别是什么?)
符号值:每个符号对应一个地址,即符号值(可理解为地址)。可用 nm 命令查看它们。
脚本格式
链接脚本也是脚本,由一系列命令组成,每个命令由一个关键字(一般其后紧跟相关参
数)或一条对符号的赋值语句组成。命令由 ;
分隔开。文件名或格式名内如果包含分
号 ;
或其他分隔符,则要用双引号将名字全称引用起来。无法处理含有双引号的文件
名。注释写在 /* */
之间。
定义变量和符号赋值
定义变量
Linker script 中也可以定义变量,这时只会生成一个 symbol 项,并不会分配内存。 在目标文件内定义的符号也可以在链接脚本内被赋值。(注意和 C 语言中赋值的不同!) 此时该符号被定义为全局的。每个符号都对应了一个地址,此处的赋值是更改这个符号 对应的地址。
提示: Linux kernel 里有时会出现来源不明的变量,在 linker script 里定义赋值是 可能之一。
例 1:
1 | /* linker script */ start_of_ROM = .ROM; end_of_ROM = .ROM + sizeof(.ROM) - 1; start_of_FLASH = .FLASH; |
上面三个变量是在 linker script 中定义的,分别指向 .ROM 段的开始的结尾,以及 .FLASH 段的开始。现在在 C 代码中想把 .ROM 段的内容拷贝到 .FLASH 段中:
1 | /* c */ |
注意其中的取地址符号 &
。C 代码中只能通过这种方式来使用 LS 中定义的变量.
start_of_ROM
这个值本身是没有意义的,只有它的地址才有意义。因为它的值没有
初始化。地址就指向 .ROM 段的开头。说白了,LS 中定义的变量其实就是地址。
例 2:
1 | /* main.c */ |
1 | /* link.lds */ a = 3; |
1 | $ gcc -Wall -o a_without_lds main.c $ gcc -Wall -o a_with_lds main.c link.lds $ ./a_without_lds &a = 0x601040 $ ./a_with_lds &a = 0x3 |
注意:对符号的赋值只对全局变量起作用
符号赋值
symbol 还可以使用 C 语言中赋值的操作:
1 | SYMBOL = EXPRESSION ; |
除了第一类表达式外,其他表达式需要 SYMBOL 被定义于某目标文件。
.
是一个特殊符号,是定位器,是一个位置指针,指向程序地址空间的某位置(或某
section 内的特殊偏移,如果它在 SECTIONS
命令的某 section 描述内的话),该
符号只能在 SECTIONS
命令内使用。
注意:赋值语句包含 4 个语法元素:符号名、操作符、表达式、分号,一个也不能少。
赋值语句可以出现在链接脚本的三个位置:SECTIONS 命令内,SECTIONS 命令内的 section 描述符内和全局位置。如:
1 | floating_point = 0; /* 全局位置 */ SECTIONS { .text : { *(.text) _etext = .; /* section描述内 */ } _bdata = (. + 3) & ~ 4; /* SECTIONS命令内 */ .data : { *(.data) } } |
PROVIDE
关键字
该关键字用于定义这类符号:在目标文件内被引用,但没有在任何目标文件内被定义的 符号。如:
1 | SECTIONS { .text : { *(.text) _etext = .; PROVIDE(etext = .); } } |
当目标文件内引用了 etext 符号,却没有定义它时,etext 符号对应的地址被定义
为 .text
section 之后的第一个字节的地址。
SECTIONS 命令
SECTIONS
命令是 linker script 里最重要的命令,精确地控制着 input sections
在 output sections 中怎样被放置。包括放置顺序,放置在哪个 output sections 中等。
一个脚本里最多只能有一个 SECTIONS 命令,但其中的语句数量不限。SECTIONS 命令中 的语句可以做以下三种事:
- 定义 entry point;
- 给符号(symbol)赋值;
- 描述怎样放置一个命名的 output section,以及其中放置哪些 input sections。
相应地 SECTIONS-COMMAND 有四种:
- ENTRY 命令
- 符号赋值语句
- 一个输出 section 的描述(output section description)
- 一个 section 叠加描述(overlay description)
该命令格式如下:
1 | SECTIONS { SECTIONS-COMMAND SECTIONS-COMMAND /* ... */ } |
如果整个链接脚本内没有 SECTIONS
命令,那么 ld 将所有同名输入 section 合成为
一个输出 section内,各输入 section 的顺序为它们被链接器发现的顺序。如果某输入
section 没有在 SECTIONS
命令中提到,那么该 section 将被直接拷贝成输出
section。
例子:
1 | SECTIONS { . = 0x10000; .text : { *(.text) } . = 0x80000000; .data : { file1(.data) . += 1000 file2(.data) } = 0x1234 .bss : { *(.bss) } } |
解释:
. = 0x10000
: 把定位器符号(Location Counter(LC))置为 0x10000 (若不指定,默认值为 0).
LC 总是包含当前的输出位置(即地址),因为 LC 总是指向 output section 的一个地 址,它必须总是在 SECTIONS 命令中的某个表达式里。因此改变 LC 的值就会影响其后 section 的内存位置。
LC 的值只能增加不能减小。 LC 的地址应该是指 LMA。
.text : { \*(.text) }
: 将所有(*表示任意输入文件)输入文件的 .text section 合
并成一个 .text section,该 section 的地址由定位器符号的值指定,即 0x10000.
.data 描述中 LC 的值在 file1 之后增加了 1000, 之后才是 file2. 右大括号后面的
=0x1234
表示其中的空白由值 0x1234 填充。
链接器每读完一个 section 描述后,将定位器符号的值增加该 section 的大小。 注意:此处没有考虑对齐约束。
输出 section 描述 (output section description)
输出 section 描述具有如下格式:
1 | SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND /* ... */ } [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP] |
[]内的内容为可选,一般不需要。
SECTION: section 名字
SECTION 左右的空白、圆括号、冒号是必须的,换行符和其他空格是可选的。每个 OUTPUT-SECTION-COMMAND为以下四种之一,
- 符号赋值语句
- 一个输入section描述
- 直接包含的数据值
- 一个特殊的输出 section 关键字
输出 section 名字 (SECTION):
名字必须符合输出文件格式要求,比如:a.out 格式的文件只允许存在 .text
.data
和 .bss
section名。而有的格式只允许存在数字名字,那么此时应该用引
号将所有名字的数字组合在一起;另外,还有一些格式允许任何序列的字符存在于
section 名字内,此时如果名字内包含特殊字符(比如空格逗号等),那么需要用引号
将其组合在一起。
输出 section 地址 (ADDRESS):
ADDRESS 是一个表达式,它的值用于设置 VMA。如果没有该选项且有 REGION 选项,那
么链接器将根据定位符号 .
的值设置该 section 的 VMA,将定位符号的值高速到满
足输出 section 对齐要求后的值,输出 section 的对齐要求为:该输出 section 描
述内用到的所有输入 section 的对齐要求中最严格的。
例子:
1 | .text . : { *(.text)} |
和
1 | .text : { *(.text)} |
这两个描述是截然不同的,第一个将 .text
section 的 VMA 设置为定位符号的值,
而第二个则是设置成满足对齐要求后的定位符号的修调值。
ADDRESS 可以是一个任意表达式,比如 ALIGN(0x10)
将该 section 的 VMA 设置成
定位符号满足16 字节后的修调值。
注意:设置 ADDRESS 的值将会改变定位符号的值。
More on ALIGN(exp)
ALIGN(exp)
根据 exp 对齐后的位置返回当前 LC(定位符)。exp 必须是 2 的指数。 ALIGN 等价于:
(. + exp - 1) & ~(exp - 1)
ALIGN(exp) 本身并不改变 LC 的值,只是根据它的值进行算术运算。用法参考以下例子。
1 | SECTIONS { /* ... */ .data ALIGN(0x2000): { *(.data) variable = ALIGN(0x8000); } /* ... */ } |
有一个特殊的 output section 是 DISCARD
, 用来丢弃 input sections,所有指定
到 DISCARD
的 input section 都不会被包含在输出文件里。
输入 section 描述 (input section description)
最常见的输出 section 描述命令是输入 section 描述。输入 section 描述是最基本的链接脚本描述, 其基本用法如下:
基本语法: // TODO syntax is not clear
1 | FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...)] SECTION1 SECTION2 ...) |
FILENAME 文件名,可以是一个特定的文件的名字,也可以是一个字符串模式。
SECTION 名字,可以是一个特定的 section 名字,也可以是一个字符串模式。
例子:
*(.text)
: 表示所有输入文件的 .text section
(*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors))
: 表示除 crtend.o、
otherfile.o 文件外的所有输入文件的 .ctors section。
data.o(.data)
: 表示 data.o 文件的 .data section
data.o
: 表示 data.o 文件的所有 section
*(.text .data)
: 表示所有输入文件的 .text section 和 .data section,顺序是:
第一个文件的 .text section,第一个文件的 .data section;
第二个文件的 .text section,第二个文件的 .data section;
…
链接器是如何找到对应的文件的
当 FILENAME 是一个特定的文件名时,链接器会查看它是否在链接命令行内出现或在 INPUT 命令中出现。
当 FILENAME 是一个字符串模式时,链接器仅仅查看它是否在链接命令内出现。
注意:如果链接器发现某文件在 INPUT 命令内出现,那么它会在 -L 指定的路径内搜索该文件。
字符串模式内可以存在以下通配符1(4.6.4.2. Input Section Wildcard Patterns):
*
: 表示任意多个字符?
: 表示任意一个字符[CHARS]
: 表示任意一个 CHARS 内的字符,可用 - 号表示范围,如 [a-z]\
: 表示引用下一个紧跟的字符
在文件名内,通配符不匹配文件夹分隔符 /
,但当字符串模式仅包含 *
时除外。
任何一个文件的任意 section 只能在 SECTIONS 命令内出现一次。例如:
1 | SECTIONS { .data : { *(.data) } .data1 : { data.o(.data) } } |
data.o 文件的 .data
section 在第一个 OUTPUT-SECTION-COMMAND
命令内被使用了,那么在第二个
命令内将不会再被使用,也就是说即使链接器不报错,输出文件的 .data1 section 的内容也是空的。
注意:链接器依次扫描每个 OUTPUT-SECTION-COMMAND
命令内的文件名,任何一个文件的任何一个
section 都只能使用一次。
读者可以和 -M
连接命令选项来产生一个 map 文件,它包含了所有输入 section 到输出 section 的组合信息。
再看一个例子:
1 | SECTIONS { .text : { *(.text) } .DATA : { [A-Z]*(.data) } .data : { *(.data) } .bss : { *(.bss) } } |
这个例子中,所有文件的输入 .text
section 组成输出 .text
section;所有以大写字母开头的文件的
.data
section 组成输出 .DATA
section,其他文件的 .data
section 组成输出 .data
section;
所有文件的输入 .bss
section 组成输出 .bss
section。
可以用 SORT() 关键字对满足字符串模式的所有名字进行递增排序,如 SORT(.text*)。
通用符号(common symbol)的输入 section: 在许多目标文件格式中,通用符号并没有占用一个 section。链接器认为:输入文件的所有通用符号在名为 COMMON 的 section 内。
例子:
1 | .bss { *(.bss) *(COMMON) } |
这个例子中将所有输入文件的所有通用符号放入输出 .bss
section 内。可以看到 COMMON section
的使用方法跟其他 section 的使用方法是一样的。有些目标文件格式把通用符号分成几类。
其他脚本命令
ENTRY(SYMBOL)
: 将符号 symbol 设置成入口地址。entry point(入口地址):进程执行的第一条用户空间的指令在进程地址空间的地址。
ld 有多种方法设置进程入口地址:(编号越前,优先级越高)
- ld 命令行的 -e 选项
- 链接脚本的 ENTRY(SYMBOL) 命令
- 如果定义了 start 符号,使用 start 符号值。
- 如果存在 .text section,使用 .text section 的第一字节的位置值。
- 使用值 0
INCLUDE filename
: 包含名为 filename 的链接脚本。相当于 c 程序里的 #include 宏指令,用以包含另一个链接脚本。脚本搜索路径由 -L 行期指定。 INCLUDE 指令可以嵌套使用,最大深度为 10.
INPUT(files)
: 将括号内的文件作为链接过程的输入文件。ld命令首先在当前目录下寻找该文件,如果没有找到,则在由 -L 指定的搜索路径下搜 索。file 可以为 -lfile形式,就象命令行的 -l 选项一样。如果该命令出现在暗含的 脚本内,则该命令内的 file 在链接过程中的顺序由该暗含的脚本在命令行内的顺序决 定。
GROUP(files)
: 指定需要重复搜索符号定义的多个输入文件file 必须是库文件,且 file 文件作为一组被 ld 重复扫描,直到不再有新的未定义的引用出现。
OUTPUT(FILENAME)
: 定义输出文件的名字。同 ld 的 -o 选项,不过 -o 行期的优先级更高,所以它可以用来定义默认的输出文件名,如 a.out。
SEARCH_DIR(PATH)
: 定义搜索路径。同 ld 的 -L 行期,不过由 -L 指定的路径要比它定义的优先被搜索。
STARTUP(filename)
: 指定 filename 为第一个输入文件。在链接过程中,每个输入文件是有顺序的,该命令设置文件 filename 为第一个输入文件。
OUTPUT_FORMAT(DEFAULT,BIG,LITTLE)
: 定义三种输出文件的格式(大小端)若有命令行选项 -EB,则使用第 2 个 BFD 格式;若有命令行选项 -EL,则使用第 3 个 BFD 格式。否则选择第一个 BFD 格式。
更多:
ASSERT(EXP,MESSAGE)
: 如果 EXP 不为真,终止链接过程。
EXTERN(SYMBOL SYMBOL ...)
: 在输出文件中啬未定义的符号,如同连接器选项 -u
FORCE\_COMMON\_ALLOCATION
: 为 common symbol 分配空间,即使用了 -r 链接行期也为其分配。
NOCROSSREFS(SECTION SECTION ...)
: 检查列出的输出 section ,如果发现他们之间有相互引用则
报错。对于某些系统,特别是内在较紧张的嵌入式系统,某些 section 是不能同时存在内在中的,所以他们
之间不能相互引用。
OUTPUT\_ARCH(BFDARCH)
: 设置输出文件的 machine architecture(体系结构),BFDARCH 为被
BFD 库使用的名字安安静静。可以用命令 objdump -f 查看。
tips: 可通过 `man ld` 查看 ld 的帮助,里面也包括了对这些命令的介绍(但是我并没有搜到…)。