链接脚本的作用及格式

概念理解

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
2
3
/* 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));

注意其中的取地址符号 & 。C 代码中只能通过这种方式来使用 LS 中定义的变量. start_of_ROM 这个值本身是没有意义的,只有它的地址才有意义。因为它的值没有 初始化。地址就指向 .ROM 段的开头。说白了,LS 中定义的变量其实就是地址。

例 2:

1
2
3
4
5
6
7
8
9
/* main.c */
#include <stdio.h>

int a = 100;
int main(void)
{

printf("&a = %p", &a);
return 0;
}
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
2
3
4
5
6
7
8
9
SYMBOL = EXPRESSION ;
SYMBOL += EXPRESSION ;
SYMBOL -= EXPRESSION ;
SYMBOL *= EXPRESSION ;
SYMBOL /= EXPRESSION ;
SYMBOL <<= EXPRESSION ;
SYMBOL >>= EXPRESSION ;
SYMBOL &= EXPRESSION ;
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 命令中 的语句可以做以下三种事:

  1. 定义 entry point;
  2. 给符号(symbol)赋值;
  3. 描述怎样放置一个命名的 output section,以及其中放置哪些 input sections。

相应地 SECTIONS-COMMAND 有四种:

  1. ENTRY 命令
  2. 符号赋值语句
  3. 一个输出 section 的描述(output section description)
  4. 一个 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为以下四种之一,

  1. 符号赋值语句
  2. 一个输入section描述
  3. 直接包含的数据值
  4. 一个特殊的输出 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 的使用方法是一样的。有些目标文件格式把通用符号分成几类。

其他脚本命令

  1. ENTRY(SYMBOL): 将符号 symbol 设置成入口地址。

    entry point(入口地址):进程执行的第一条用户空间的指令在进程地址空间的地址。

    ld 有多种方法设置进程入口地址:(编号越前,优先级越高)

    1. ld 命令行的 -e 选项
    2. 链接脚本的 ENTRY(SYMBOL) 命令
    3. 如果定义了 start 符号,使用 start 符号值。
    4. 如果存在 .text section,使用 .text section 的第一字节的位置值。
    5. 使用值 0
  2. INCLUDE filename: 包含名为 filename 的链接脚本。

    相当于 c 程序里的 #include 宏指令,用以包含另一个链接脚本。脚本搜索路径由 -L 行期指定。 INCLUDE 指令可以嵌套使用,最大深度为 10.

  3. INPUT(files): 将括号内的文件作为链接过程的输入文件。

    ld命令首先在当前目录下寻找该文件,如果没有找到,则在由 -L 指定的搜索路径下搜 索。file 可以为 -lfile形式,就象命令行的 -l 选项一样。如果该命令出现在暗含的 脚本内,则该命令内的 file 在链接过程中的顺序由该暗含的脚本在命令行内的顺序决 定。

  4. GROUP(files): 指定需要重复搜索符号定义的多个输入文件

    file 必须是库文件,且 file 文件作为一组被 ld 重复扫描,直到不再有新的未定义的引用出现。

  5. OUTPUT(FILENAME): 定义输出文件的名字。

    同 ld 的 -o 选项,不过 -o 行期的优先级更高,所以它可以用来定义默认的输出文件名,如 a.out。

  6. SEARCH_DIR(PATH): 定义搜索路径。

    同 ld 的 -L 行期,不过由 -L 指定的路径要比它定义的优先被搜索。

  7. STARTUP(filename): 指定 filename 为第一个输入文件。

    在链接过程中,每个输入文件是有顺序的,该命令设置文件 filename 为第一个输入文件。

  8. 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 的帮助,里面也包括了对这些命令的介绍(但是我并没有搜到…)。

Last Updated 2018-05-27 Sun 17:57.
Render by hexo-renderer-org with Emacs 25.2.2 (Org mode 9.1.13)
0%