• GNU链接脚本详解


    0. 前言

    每一个链接都是由链接脚本控制的,链接脚本是用链接命令语言编写的脚本。链接都会用到一个链接脚本,如果你没有指定自己的脚本,就会使用默认的链接脚本。可以用 "--verbose" 命令行选项显示默认的连接脚本。指定命令行参数,比如 '-r'、'-N'都会影响默认的链接脚本。也可以用 '-T' 来指定自己的链接脚本,也可以隐式地把自己的连接脚本当成链接输入文件,就像普通的链接文件一
    样,参见链接文件说明。

    如上图,链接器是将多个对象文件链接成可执行程序。

    链接器输入文件:目标文件或链接脚本文件;

    链接器输出文件:可执行文件;

    目标文件(包括可执行文件) 具有固定的格式,在 Unix 或GNU/linux 平台下,一般为 ELF 格式。

    1. 链接器脚本

    链接器是由链接器脚本控制,该链接器脚本控制输入文件的链接方式。该脚本以连接器命令语言编写,控制一下链接属性:

    • 输入文件中的部分如何映射到输出文件;
    • 输出文件的文件格式和内存布局;
    • 已创建段的运行时加载属性;
    • 代码执行入口点;
    • 共享库版本;

    链接器有一个内置脚本,它用作将代码和数据分配到内存的默认链接器脚本。用户不能修改默认脚本,但是,它可以通过两种方式进行更改:

    • 它可以完全由用户定义的脚本文件替换。在这种情况下,文件名在链接器命令行上指定为链接器选项 -T 的参数;
    • 可以通过将链接器命令上的用户定义脚本文件指定为普通链接器输入文件来扩充它;

    指定为普通链接器输入文件的脚本称为隐式脚本。因为它们扩充了默认链接描述文件,所以隐式脚本通常只包含 symbol assignments(符号分配) 或INPUT、GROUP、VERSION 命令。

    2. 基础知识 

    2.1 分号

    在连接器脚本中,分号通常仅出于美观用作分隔符,否则将被忽略。但一下地方是必需的:

    • 分号必需出现在 symbol assignments 的末尾;
    • 分号必需出现在 PHDRS 命令的末尾;

    2.2 注释

    可以使用标准 C 分隔符将注释包含在链接描述文件中:

    /*   ...    */

    2.3 字符串

    文件或格式名称等字符串通常可以直接输入,无需分隔符。

    如果文件名包含逗号等字符,否则该字符将用于分隔文件名,则文件名可以用双引号引起来。 文件名中不能使用双引号字符。

    2.4 表达式

    许多命令参数接受算术表达式。 表达式的语法与 C 中的表达式语法相同,具有以下特点:

    • 所有表达式都被计算为 long 或 unsigned long 类型的整数
    • 所有常量都是整数
    • 提供所有 C 算术运算符
    • 可以定义、创建和引用全局变量
    • 支持多种预定义功能

    2.5 位置计数器

    链接器中有个特殊变量:点号 ' . '

    点号始终包含当前输出位置计数器,由于 点号 始终引入输出节中的位置,它必须始终出现在 SECTIONS 命令中的表达式中。

    点号 可以出现在表达式中允许使用普通符号的任何位置,但它的赋值有副作用。

    3. 脚本命令

    3.1 指定入口地址命令

    程序运行的第一条指令就是调用入口地址,可以使用 ENTRY 链接命令来指定程序的入口地址。

    入口地址的指定方式有:

    • 通过命令行指定:gcc -e symbol;
    • 链接脚本指定:ENTRY(symbol)

    4. 文件处理命令

    4.1 INCLUDE

    使用 INCLUDE 将其他链接脚本包含到当前脚本,链接器会在当前目录和用 -L 参数指定过的目录下查找被包含的文件。

    链接脚本可以嵌套包含,最多层数为 10 层。

    INCLUDE 既可以放在链接脚本的开始,也可以放在 MEMROY 或 SECTIONS 命令里面,或放在输出节的描述里面。

    格式:INCLUDE filename
    功能:包含其他脚本文件。
    搜索路径:当前目录、-L添加的目录。
    放置位置:链接脚本开始、MEMORY或SECTIONS中、输出节描述中。

    4.2 INPUT

    INPUT 命令直接链接指定的文件名,就像从命令行输入一样。

    例如如果要包含subr.o,
    但是又不想在每一条链接命令中都写上的话,就可以在链接脚本中使用 ‘INPUT(subr.o)’。实际上,还可以把所有输入文件 (*.o) 都写在链接脚本里面,然后只要用 ' -T ' 指定一下链接脚本就好了。

    为了防止设置了根目录,文件名要用'/'开始,这样连接脚本就会从根目录开始检索文件,否则,链接器就会在当前目录下查找文件,如果找不到文件,链接器就会在所有归档库里面检索。根目录也可以在文件名一开始的时候用 ‘=’ 来强制指定,或者在文件名前面加上 $SYSROOT。

    如果使用 ‘INPUT (-lfile)’,链接器会自动翻译成libfile.a,就好像使用命令行参数 "-l" 一样。如果使用INPUT命令在链接脚本中包含文件的话,文件会从链接脚本所在的目录开始检索。这会影响到归档文件的检索。

    格式:
    INPUT(file, file, …)
    INPUT(file file …)
    功能:指定要链接的输入文件(.o,.a)。 搜索路径:$SYSROOT、当前目录。
    subr.o包含:INPUT(subr.o)
    libfile.a包含:INPUT(-lfile)

    4.3 GROUP

    格式:

    GROUP(file, file, …)

    GROUP(file file …)

    GROUP命令语法跟 INPUT 差不多,但是专门用来指定归档文件 (*.a),这个会不断的检索直到发现一个新的未定义的符号引用。参见命令行参数里面关于 '-(' 的描述。

    4.4 OUTPUT

    OUTPUT用来设置输出文件的名称,等价的命令行参数为-o filename。默认输出文件名称为a.out。

    格式:OUTPUT(filename)
    功能:设置输出文件名称。 等价命令行参数:-o filename

    4.5 SEARCH_DIR

    SEARCH_DIR命令用来添加链接器的搜索路径,等价的命令行参数是 -L path

    如果即用了 -L 也用了SEARCH_DIR,那么链接器会优先使用 -L 设置的路径。

    格式:SEARCH_DIR(path)
    功能:添加链接器的搜索路径。
    等价命令行参数:-L path

    4.6 STARTUP

    STARTUP命令用来指定第一个被链接的输入文件,等价于命令行中第一个输入文件,当目标操作系统的要求程序入口地址必须位于第一个输入文件的时候使用。

    格式:STARTUP(filename)
    功能:指定第一个被链接的输入文件。
    使用场景:目标操作系统的要求程序入口地址必须位于第一个输入文件的时候。

    5. 输出文件格式命令

    5.1 OUTPUT_FORMAT

    OUTPUT_FORMAT命令用来设置输出文件使用的BFD格式。等价的命令行参数为 --oformat bfdname。命令行参数优先。

    OUTPUT_FORMAT可以设置三个格式,当命令行没有 -EB 和 -EL的时候,使用第一个格式,当有-EB的时候使用第二个参数,当有 -EL 的时候,使用第三个参数。

    格式:
    OUTPUT_FORMAT(bfdname)
    OUTPUT_FORMAT(default, big, little)
    功能:设置输出文件使用的BFD格式。 等价命令行:--oformat bfdname

    5.2 TARGET

    TARGET命令用来设置链接器读取输入文件的时候使用的BFD格式。等价命令行参数 -b bfdname。

    格式:
    TARGET(bfdname)

    6. 其他命令

    7. 符号赋值

    7.1 像C 一样简单的赋值

    1. symbol = expression ;
    2. symbol += expression ;
    3. symbol -= expression ;
    4. symbol *= expression ;
    5. symbol /= expression ;
    6. symbol <<= expression ;
    7. symbol >>= expression ;
    8. symbol &= expression ;
    9. symbol |= expression ;

    符号被定义为在脚本中具有全局范围;

    符号赋值语句在两方面与链接器脚本表达式中使用运算符不同:

    • 赋值只能在表达式的根部进行,例如, a = b+3; 是允许的,但 a+b=3; 是一个错误;
    • 赋值语句必须以尾部分号结束

    赋值语句可以出现在下面位置:

    • 作为链接器脚本中的独立命令;
    • 作为 SECTIONS 命令中的独立语句;
    • 作为 SECTIONS 命令中某一节定义内容的一部分;

    7.2 HIDDEN

    格式:

    HIDDEN(symbol = expression)


    定义成 HIDDEN 的符号不会被输出到目标文件,如:

    1. HIDDEN(floating_point = 0);
    2. SECTIONS
    3. {
    4. .text :
    5. {
    6. *(.text)
    7. HIDDEN(_etext = .);
    8. }
    9. HIDDEN(_bdata = (. + 3) & ~ 3);
    10. .data : { *(.data) }
    11. }

    7.3 PROVIDE

    格式:

    PROVIDE(symbol = expression)


    定义一个输入文件里面引用但未定义的符号,如:

    1. SECTIONS
    2. {
    3. .text :
    4. {
    5. *(.text)
    6. _etext = .;
    7. PROVIDE(etext = .);
    8. }
    9. }

    _etext 可以在输入文件中引用,如果输入文件中也定义了 _etext,那么优先使用输入文件中的,但是如果输入文件中定义了_etext,链接的时候就会报多重定义的错误。

    7.4 PROVIDE_HIDDEN

    跟 PROVIDE 功能类似,但不会输出到目标文件中。

    8. SECTIONS 命令

    1. SECTIONS
    2. {
    3. sections-command
    4. sections-command
    5. }

    在一个脚本文件中只能声明一条 SECTIONS 命令,但是该命令可以包含任意数量的语句来指定必要的映射和放置信息。

    每一个SECTIONS 的组成都有:

    • ENTRY 命令;
    • 符号排列;
    • 输出section 的描述;
    • 覆盖描述;

    如果链接文件中没有定义SECTIONS,那么输入文件中的节就会原封不动的输出到目标文件中。

    8.1 输出section 描述

    SECTIONS命令中最常用的语句是输出部分描述,它指定了输出部分的属性:它的位置、对齐方式、内容、填充模式和目标内存区域。

    格式:

    1. section [address] [(type)] :
    2. [AT(lma)]
    3. [ALIGN(section_align) | ALIGN_WITH_INPUT]
    4. [SUBALIGN(subsection_align)]
    5. [constraint]
    6. {
    7. output-section-command
    8. output-section-command
    9. } [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp] [,]
    • 一个输出section 的名称可以由任何字符序列组成;
    • section 名称声明周围需要留白,以确保名称的明确性;
    • 输出section 名称后面,冒号 ' : ' 和  ' { } ' 是必需的; 
    • 每个输出section 都有一个虚拟地址和加载地址,虚拟地址 VMA就是 section 加载到内存中的地址,加载地址 LMA 就是在目标文件中的地址。LMA 可以通过 AT(lma) 来设置;
    • ALIGN 强制输出section地址对齐,SUBALIGN 强制输入section 地址对齐;
    • 一个输出section 的描述,由一个或多个语句组成,包括:
      • 符号分配;
      • 输入section 描述;
      • 直接包含是输出section 的数据;
      • 特殊的输出section 关键字;

    8.2 输入 section 描述

    输入section 的描述指定了被映射到输出section 的输入部分。

    *(.init.rodata .init.rodata.*)

    这里使用了通配符,表示输入部分为:

    • * 代表所有目标文件;
    • 后面括号,是指.init.rodata 段 和 .init.rodata.* 段;

    这句话意思是:将所有目标文件中的 .init.rodata 段和 .init.rodata.* 段都包含到输出 section 中。

    8.2.1 输入 section 描述的通配符

    * 号:任意数量的字符;

    ? 号:任何单个字符;

    [CHARS] 号:匹配任意一个CHARS内的单个字符,可用 ' - ' 号表示范围。例如,[ A-Z ],表示匹配 A~Z之间的单个字符;

    8.2.2 KEEP

    KEEP(*(.initcallearly.init))

    当链接命令行使用选项 --gc-secionts 后,链接器可能将某些它认为没用的 section 过滤掉。

    KEEP 用来强制链接器保留一些特定的 section。

    例如上面代码,其实可以看成:

    *(.initcallearly.init)

    加上KEEP,则要求链接器不能优化掉这个section,哪怕是没用。

    参考:

    https://blog.csdn.net/shenjin_s/article/details/88712249

    https://zhuanlan.zhihu.com/p/516338675

  • 相关阅读:
    【Java 基础篇】Java类型通配符:解密泛型的神秘面纱
    2022年软件评测师考试大纲
    Vue_Bug VUE-ELEMENT-ADMIN默认是英文模式
    面试阿里技术专家岗,对答如流,这些面试题你能答出多少
    Spring Boot之Spring MVC基于注解的控制器(RequestMapping注解类型 重定向与转发 依赖注入)
    linux常用命令之设定acl相关命令 setfacl/getfacl/chattr
    高阶python | 装饰器
    Nacos基本学习
    Servlet生命周期
    Servlet知识点总结-DX的笔记
  • 原文地址:https://blog.csdn.net/jingerppp/article/details/134267583