GNU C 链接脚本学习笔记

基本概念

链接脚本是用于控制连接过程的文件 。它通过定义程序各个段在内存中的布局和属性,指导链接器(如 GNU 链接器 ld )如何将输入文件(目标文件和库文件,比如.o文件)组合成一个最终的可执行文件或库。链接脚本在嵌入式系统开发中尤为重要,因为它允许开发者精确控制代码和数据在内存中的布局,以满足特定的硬件和性能要求。

重要概念:

  1. 输入文件:输入文件是指链接器在链接过程中需要处理的目标文件或库文件。这些文件通常是通过编译器生成的,包含了编译后生成的代码和数据。
    • 目标文件 (Object Files) :编译器生成的中间文件,通常扩展名为 .o。这些文件包含了机器代码和符号信息。
    • 库文件 (Library Files) :可以是静态库(扩展名为 .a)或动态库(扩展名为 .so.dll),这些库包含了一组可重用的目标文件。
  2. 输入段:输入段是指输入文件中的特定段(section),这些段包含了特定类型的数据和代码。常见的输入段包括:
    • .text:代码段,包含程序的可执行指令。
    • .data:数据段,包含已初始化的全局变量和静态变量。
    • .bss:未初始化数据段,包含未初始化的全局变量和静态变量。
    • .rodata :只读数据段,通常包含字符串常量和其他只读数据。(有时也放在.text中)
  3. 输出文件:输出文件是指链接器在链接过程中生成的最终文件。这个文件可以是可执行文件、共享库或静态库,具体取决于链接的目标。输出文件包含了所有合并后的代码和数据,并具有适当的格式,以便可以在目标平台上运行或使用。
  4. 输出段:输出段是链接器在输出文件中定义的段。表示各文件输入段合并后在输出文件中的位置和内容。输出段的定义在链接脚本的SECTIONS 中进行。
  5. 内存区域:链接脚本允许定义内存区域,这些区域可以表示实际的物理内存逻辑地址空间。每个内存区域可以有一个名称、起始地址和大小。
  6. 位置计数器 :位置计数器(通常用 . 表示)是一个特殊符号,它是一个定位器,一个位置指针,指向程序地址空间某位置(或某section内的偏移,如果它在SECTIONS命令内的某section描述内),该符号只能在SECTIONS命令内使用。
  7. 入口点:入口点是程序开始执行 的地址,通常由链接脚本指定。默认入口点是 _start,但可以通过链接脚本的 ENTRY 命令进行修改。
  8. 符号:符号是应用程序中的标识符,可以表示变量、函数或地址。链接脚本可以定义和操作符号,以便在连接过程中使用。
  9. 加载地址 LMA:一个输出段初始化时段内容的获取来源。
  10. 虚拟地址 VMA:一个输出段初始化时段内容的存放位置。

语法

ld 脚本是语句的集合,大部分是设置特定选项的简单关键字,其中SECTIONSMEMORY是有对链接过程具有基本和普遍的影响。

ENTRY

在应用程序中执行的第一条指令称为入口点。默认入口点是 _start,但可以通过链接脚本的 ENTRY 命令进行修改。

定义语句:

c 复制代码
ENTRY(symbol)

有几种设置入口点的方法。 链接器将通过依次尝试以下每种方法来设置入口点,并在其中一种成功后停止:

  • **-e**输入命令行选项,在调用命令行链接命令时指定;
  • 链接描脚本中的 ENTRY(symbol) 命令;
  • 目标默认符号值(如果已定义); 对于许多目标来说是 _start 符号。
  • .text 输出段的第一个字节的地址(如果存在);
  • 地址0。

SECTIONS

SECTIONS 命令常用于段(section)定义,段是目标文件中的基本单位,一个段在内存中就是一块位置,这块位置放什么就取决于我们编写的 SECTIONS 语句。

定义语句:

c 复制代码
SECTIONS{
    输出段1 : { filename(输入段1, 输入段2, ...)}
    输出段2 : { filename(输入段1, 输入段2, ...)}
    ...
}
  • filename 可以使用 * 等正则表达式,比如 *.text 表示 所有输入文件中的 .text
  • 输出段x 之后,: 之前,可选增加 EXPRESSION 语句,用于设置输出段的虚拟地址
  • 输出段x 之后,: 之前,可选增加 AT (EXPRESSION) 语句,用于设置输出段的加载地址
  • 输出段定义的最末端,可选增加 > 内存空间 语句 ,用于定义输出段所处的内存具体位置(即虚拟地址VMA)

示例:

c 复制代码
/* 在下面的示例中,命令脚本将输出文件排列成三个连续的部分,分别命名为.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位数据,表达式的格式如下:

c 复制代码
SYMBOL = EXPRESSION;

注意:赋值语句包含4个语法元素:符号名,操作符,表达式,分号,缺一不可。赋值语句可以出现在链接脚本的三个地方

  1. SECTIONS 命令内
  2. SECTIONS 命令内的 section 描述内
  3. 全局位置

符号的有效作用区域由表达式所在的段决定。

特殊符号 .

  • .位置计数器,如果没有以某种其他的方式指定输出段(output section)的地址,那么下一个定义的输出段地址就会是位置计数器中的当前值。
  • 设置完一个输出段后,位置计数器就会以输出段 的大小增加其值。在SECTIONS命令的开始,位置计数器是0。. = 0x100表示把位置计数器设为 0x100
  • 位置计数器在 SECTIONS 命令内表示绝对内存位置
  • 位置计数器在 SECTIONS 命令的 section 描述内表示相对于当前 section 的相对位置

示例:

c 复制代码
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语言中也要把这些数值当成地址来操作

c 复制代码
extern char i_am_global[];
extern char _begin[];
extern char _etext[];
// 但请注意,以上数组的地址上没有存储任何特殊内容。这意味着您无法访问链接程序脚本定义的符号的值------它没有值。您所能做的就是访问链接器脚本定义符号的地址。

PROVIDE

PROVIDE 和C语言中的 weak 关键字功能相似。

PROVIDE 用于在链接脚本中定义一个符号,并为其赋予一个默认值 。如果在输入文件中没有其他地方定义该符号,那么链接器会使用 PROVIDE 定义的值。如果输入文件中已经定义了相同的符号,PROVIDE 定义的符号将被忽略,从而避免重复定义错误。

示例:

c 复制代码
SECTIONS
{
    .text :
    {
        PROVIDE(_stext = .); /* 定义 _stext 为 .text 输出段的起始地址,如果应用程序定义了 _stext ,那么连接器不会使用这里的 _stext */
        *(.text)
        _etext = .; /* _etext 如果在应用程序中重复定义,那么链接过程会报错 */
    }
}

MEMORY

MEMORY命令描述目标机器中各内存块的位置和大小。链接器将请求的分段移动到正确的内存块中,并在内存块太满时发出错误。

链接器的默认MEMORY配置允许分配所有可用内存。我们可以使用MEMORY命令覆盖此配置。

一个链接脚本最多只能有一条MEMORY命令,但可以在一个MEMORY命令中定义多个内存块。

定义语句:

c 复制代码
MEMORY 
  {
    名称 (属性列表) : ORIGIN = 起始地址, LENGTH = 块长度
    ...
  }
  • 名称:是链接器内部用来引用区域的名称。可以使用任何符号名。不可省

  • 属性列表:内存块属性的可选列表。有效的属性列表必须由字符"ALIRWX"组成,以匹配部分属性。可省,如果你省略了属性列表,你也可以省略它周围的括号。目前支持的属性有:

    tex 复制代码
    R: 只读
    W: 读写
    X: 可执行
    A: 已分配区域   Allocated sections.
    I: 已初始化区域 Initialized sections.
    L: Same as I.
    !: 反转后面紧跟的属性的含义 Invert the sense of any of the following attributes.
  • ORIGIN:定义起始地址关键字,不可省,可缩写成 orgo

  • 起始地址:物理内存中区域的起始地址。它是一个表达式,在执行内存分配之前必须求值为一个常量。不可省

  • LENGTH:定义内存块长度关键字,单位是字节,不可省,可缩写成 lenl

  • 块长度:物理内存中区域的长度。它是一个表达式,在执行内存分配之前必须求值为一个常量。不可省

一旦定义了一个名为 mem 的内存区域,我们就可以在sections命令中使用以 > mem 结尾的命令,将特定的输出部分定向到那里。如果指向某个地区的组合输出部分对于该地区来说太大,则链接器将发出错误消息。

示例:

c 复制代码
/* 
以下示例定义了
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 命令创建的内存区域提供别名。 每个名称最多对应一个存储区域。

定义语句:

c 复制代码
REGION_ALIAS(别名, 内存区域)
  • 别名内不能有空格

示例:

c 复制代码
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
}
相关推荐
心怀梦想的咸鱼25 分钟前
UE5 第一人称射击项目学习(四)
学习·ue5
AI完全体28 分钟前
【AI日记】24.11.22 学习谷歌数据分析初级课程-第2/3课
学习·数据分析
Mephisto.java1 小时前
【大数据学习 | Spark-Core】Spark提交及运行流程
大数据·学习·spark
PandaCave1 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
yuwinter2 小时前
鸿蒙HarmonyOS学习笔记(2)
笔记·学习·harmonyos
red_redemption2 小时前
自由学习记录(23)
学习·unity·lua·ab包
幽兰的天空2 小时前
默语博主的推荐:探索技术世界的旅程
学习·程序人生·生活·美食·交友·美女·帅哥
珹洺3 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
沐泽Mu3 小时前
嵌入式学习-C嘎嘎-Day05
开发语言·c++·学习
你可以叫我仔哥呀3 小时前
ElasticSearch学习笔记三:基础操作(一)
笔记·学习·elasticsearch