【技术进阶】ld链接脚本干货,关键字并非全部! (附gcc demo示例)

   

目录

   

往期推荐

什么是链接脚本

链接脚本的工作原理

Pre-section

ENTRY

OUTPUT_FORMAT

OUTPUT_ARCH

符号定义

MEMORY 部分

注意:

SECTIONS 部分

示例

调用预处理器

生成汇编代码

汇编代码

符号表

链接

链接脚本

预处理部分 

MEMORY 部分 

SECTIONS 部分 


往期推荐

  1. ETAS工具链自动化实战指南<一>
  2. ETAS工具链自动化实战指南<二>
  3. ETAS工具链自动化实战指南<三>
  4. AUTOSAR工程师必读:Artop的核心功能
  5. Vector工具链自动化实战指南<一>
  6. isolar高手秘籍| ECU Configuration三分钟速成!
  7. 掌握核心步骤:RTA-BSW以太网配置全解析
  8. 一文详解TC399 CAN MCAL 配置
  9. LSL常见应用场景及示例<一>
  10. LSL常见应用场景及示例<二>
  11. LSL常见应用场景及示例<三>
  12. 为什么Autosar钟情arxml而非json?大揭秘!
  13. 深入浅出:SOME/IP-SD的工作原理与应用
  14. 【技术进阶】|一文掌握Autosar ComStack的精髓!
  15. Autosar培训笔记整理<一>
  16. 【AutoSAR进阶】|实战详解ETAS工具链UDS 0x2f服务核心配置!
  17. 实战详解ETAS工具链CanTp模块自动化配置
  18. 一文掌握5种常见的AUTOSAR 错误类型
  19. 【AUTOSAR工程师必备知识】一文搞懂AUTOSAR架构9种通信方式
  20. 实战干货|详解ETAS工具链之 intra-ECU通信的数据转换

什么是链接脚本

图片

链接脚本是一种使用特定脚本语言编写的文件,专门用于指导编译器将数据和代码映射到指定的内存位置。

这些代码和数据以名为“段”的独立块形式出现,段是由编译器或程序员定义的,用于逻辑分区程序的各个部分。程序员可以通过编译器指令生成非默认的段,例如:

图片

这样,之后声明的所有代码和变量都将位于该段内。

通过链接脚本,我们可以将这些输入段映射到新的输出段,这些输出段在链接脚本中定义,并最终被映射到内存区域中。

链接脚本的工作原理

链接脚本通常包含两个部分。第一个部分称为 MEMORY 部分,通常用于描述芯片和软件架构的内存布局。它通过为标识符分配地址来实现,这些标识符可以代表芯片的物理内存布局,或是为特定代码或数据集分配的内存区域的架构布局。

第二个部分称为 SECTIONS 部分,它将第一个部分中描述的内存区域与代码和数据对应起来,将这些代码和数据分配到指定的内存区域中。

我们可以看到链接脚本中的这两个部分,如下图所示:第一个部分从第8行开始,第二个部分从第15行开始。

其中:链接脚本也包含预处理宏或类似内容,如第 1 行到第 5 行所示。第 3 行到第 5 行实际上是符号定义,

图片

图一

Pre-section

如下图所示(图 2): 显示了链接脚本的开头。在这个位置,在任何段被写入之前,可以或必须声明一些内容。这部分称为Pre-section,以下将讨论在此处声明的内容。

图片

图2

ENTRY

在编写两个主要段之前,至少需要使用一个宏,即 `ENTRY` 宏。它告诉链接器程序的起始地址,这可能是硬件要求的起始地址,具体会在硬件编程手册中描述,或者是引导加载程序(bootloader)期望跳转的地址。

`ENTRY` 宏可以引用程序中的任何现有符号,包括 `main` 函数或其他启动代码的符号名。

在链接脚本中,可以看到 `ENTRY` 宏的使用方式如下:`ENTRY(_START)`。

OUTPUT_FORMAT

`OUTPUT_FORMAT` 宏用于指定最终可执行文件的链接文件格式。在上面的示例中,针对 TriCore 目标使用了 `elf32-tricore` 格式。这告诉编译器生成特定于 TriCore 目标的 32 位 ELF 文件。

在图 2 中,此格式写为:`OUTPUT_FORMAT("elf32-tricore")`。

OUTPUT_ARCH

`OUTPUT_ARCH` 宏用于指定编译和链接的目标架构。如果在调用编译器时已经通过参数指定了目标架构,可能不需要此宏;但是如果编译和链接是分开进行的,那么该宏是必要的。在图 2 中写为:`OUTPUT_ARCH(tricore)`。

符号定义

在 `MEMORY` 部分编写之前,也可以在此定义符号。在链接脚本的pre-section段中定义符号,允许 `MEMORY` 部分引用这些符号,以定义内存中区域的地址和大小。正如图 1 所示,符号 `_MainAddress`、`_OtherStuff` 和 `_OtherStuffLen` 就被定义在此部分。请注意,符号定义以分号(;)结束。

注意:所有符号和数值都可以在算术表达式中使用,以基于先前定义的符号计算新值,包括三元运算符,正如图 2 所示。这可以在定义符号值时进行,也可以在使用符号时进行,通常用于计算内存中的偏移量。

MEMORY 部分

图片

图3

链接脚本的 MEMORY 部分如前所述,用于定义内存区域。它以关键字 MEMORY 开头,后面跟一对大括号 {},在其中描述所有指定的内存区域。这些内存区域可以表示物理硬件(如图 3 所示,其中 PMU_PFLASH0 区域直接映射到 TriCore 的 PFLASH 0 的地址),也可以表示逻辑上的代码或数据区域(如图 1 所示,为 main 函数创建了特定的内存区域,其他内容分配到另一个区域中)。

内存区域的定义格式如下:name (attribute) : org = origin, len = length

  • 首先,写出符号或区域名称,通常用大写字母并以下划线分隔单词,遵循 C 预处理器符号定义的约定。

  • 随后是区域属性,告诉编译器该内存区域允许执行的操作,属性包括:

    • r - 只读

    • w - 读写

    • x - 包含可执行代码的区域

    • a - 已分配的区域

    • i - 初始化区域

    • l - 等同于 i

    • ! - 反转后续属性的行为

注意:属性对大小写不敏感,并且是可选的。

  • 接着指定内存区域的起始地址(origin),使用关键字 org,后跟一个地址(可以是明文地址、符号名称地址或算术计算的地址)。

  • 最后用关键字 len 指定内存区域的长度,同样可以是明文地址、符号名称地址或算术计算的地址。

注意

  • 链接脚本对标点敏感,例如属性后需使用冒号(:),org 规范后需使用逗号(,)。

  • 可以使用十进制或十六进制数,十六进制数需加 0x 前缀,否则视为十进制。

  • 数值后缀 km 和 g 表示容量单位(千字节、兆字节、吉字节),不区分大小写。

  • 链接脚本支持 C 风格的多行注释,或使用 # 进行单行注释。

SECTIONS 部分

图片

图4

SECTIONS 部分以关键字 SECTIONS 开头,后跟一对大括号 {},其中描述了所有输出段。

输出段的定义规则如下:

  • 输出段的名称用一个点(.)加上段名称表示,随后是一个冒号(:),如图 4 所示。

  • 接下来是一对大括号 {},其中将输入段的内容映射到输出段。

  • 最后,这个输出段映射到链接脚本 MEMORY 部分中先前描述的一个内存区域。

输入段与输出段的映射方式如下:使用星号(*)作为通配符,使我们无需关心输入段路径的特定部分。这是因为段可以包含子段,访问路径类似于:

图片

使用通配符可以忽略子段路径,例如:

图片

或者:

图片

所有输入段将按照它们在链接脚本中出现的顺序映射到该输出段中,这是因为链接脚本使用一个地址计数器变量,该变量在输入段依次映射时递增。该变量可以直接访问并用于脚本中,其名称为点号(.),如图 4 所示。在图 4 的段定义中,我们可以看到以下内容:

首先定义了一个符号 _startOfSillySection,该符号可以在 C 代码中使用 extern 关键字与符号名访问。该符号被赋予当时地址计数器的值,因为它是 mySillySection 段中的第一个元素,因此这个符号的地址就是 mySillySection 的起始地址。通过读取符号的地址,可以在运行时确定映射代码的位置,或者通过在生成的内存映射文件中找到该符号来查看位置。

注意:该符号没有值,不是变量,而是程序符号表中一个地址的名称,因此在 C 代码中读取此地址时需要对该符号进行解引用。

在 mySillySection 定义的末尾,可以看到定义了第二个符号,它记录了段的结束地址。通过这两个符号,可以在运行时计算输出段的大小。

还需注意,可以在链接脚本中为地址计数器赋值,这可用于在代码/数据映射之前强制内存对齐,这可以通过 ALIGN 宏实现,如下所示:

图片

在上方的截图中还可以看到 KEEP 关键字,它用于强制编译器保留符号,即使它们没有被显式引用。当我们需要跳转或读取/写入内存中我们已知的内容时(例如独立编译的其他人的代码,如固件,或者通过未命名内存地址访问的寄存器),可以使用 KEEP 关键字。

在输入段映射到输出段后,需要指定该输出段所属的内存区域,该区域在闭合大括号后指定,示例如下:

图片

图片

通常只需使用一个大于号(>)指定一个预先声明的内存区域。然而,有时需要指定两个区域,一个区域用于指定可执行文件中的代码/数据位置,另一个区域用于指定芯片上代码/数据的实际位置。

这是因为我们可能会将数据写入 Flash,然后需要将其复制到 RAM 进行修改,或者代码需要复制到 RAM 执行。为此,我们使用大于号(>)后跟内存区域,称为虚拟内存地址(VMA)。VMA 表示程序执行期间可见的内存区域,但不指明在 Flash 文件中数据/代码的实际位置。

在描述 VMA 后,使用关键字 AT,随后再用一个大于号(>)指定加载内存地址(LMA)。LMA 是在 Flash 文件中代码/数据的实际位置。需要两个地址的原因是,有时代码/数据需要从一个位置复制到另一个位置才能运行。例如,将配置数据放置在特定的硬件相关地址,或将代码/数据移动到 RAM 或缓存以提高性能,或将代码/数据移动到调试器或闪存工具无法访问的内存(如 RAM 或缓存)。

示例

接下来,我们将通过一个简单示例介绍如何在 GCC 中使用链接脚本。该示例还解释了编译的各个阶段。下面我们将逐步在 GCC 中处理一个 C 文件,查看每个阶段的输出,以了解各段的形成及最终链接。

该示例下载路径: gccAndLinkerDemo

图片

下面是我们将要使用的 C 文件。可以看到,文件顶部包含了 stdio.h,然后声明了两个静态变量:一个是 4 字节的 int 类型并初始化为 10,另一个是未初始化的 double 类型,占用 8 字节。

接着,我们在预处理器中定义了 TRUE 和 FALSE,分别对应 1 和 0,然后实现 main 函数。

在 main 函数中有三个条件判断语句:

  • 第一个条件判断会始终执行,因为预处理器会将其解析为以下内容:

图片

  • 第二个条件判断不会执行,因为预处理器将其解析为以下内容:

图片

  • 最后一个条件判断在预处理阶段就会被移除。

此外,请注意预处理器会删除所有注释。

调用预处理器

要调用预处理器,请进入项目中对应操作系统的文件夹,运行脚本 preproc(.bat)。这将对 C 文件运行预处理器,并在项目根目录生成一个预处理后的输出文件 demo_preprocessed.i

在文件顶部可以看到,预处理器已经解析了包含 stdio.h 所需的所有包含文件;在文件底部可以看到以下内容:

图片

这表明预处理器已将 TRUE 和 FALSE 替换为声明的值,移除了注释并删除了第三个条件判断。这是将交由编译器处理的代码。

生成汇编代码

在预处理之后,代码文件会进入生成阶段,解析 C 代码并生成抽象语法树(AST)。AST 是表示代码的逻辑树结构,将根据语言规则进行解析,然后转为生成的汇编代码。可以通过运行脚本 gen(.bat) 来调用该阶段,此脚本会处理预处理后的文件并生成名为 demo_genned.s 的文件。在第 5 行和第 6 行,我们可以看到符号 `_x` 的定义,它被声明为一个 32 位长整型并赋值为 10。请注意,在 C 代码中我们将该变量命名为 `x`,但在生成的符号中被翻译为 `_x`。C 编译器在标识符前加上下划线作为一种名称重整(name mangling)方式。

图片

在第 5 行和第 6 行,我们可以看到符号 _x 的定义,它被声明为一个 32 位长整型并赋值为 10。请注意,在 C 代码中我们将该变量命名为 x,但在生成的符号中被翻译为 _x。C 编译器在标识符前加上下划线作为一种名称重整(name mangling)方式。

图片

在第 7 行,我们可以看到变量 y 被定义为 _y,使用了 .lcomm 指令,这会在名为 .bss 的段中分配内存。我们稍后会讨论该段。分配规则如下:

图片

正如我们所见,_y 被分配为一个 8 字节对齐的块,占用 8 字节,适合 double 类型。

在第 15 行,我们可以看到 main 函数被定义为 _main

接下来在第 11 行,可以看到在第一个条件判断中使用的字符串使用 .ascii 指令声明。请注意,其他字符串没有出现,因为在之前的阶段中,它们被优化移除,识别到这些代码永远不会被执行。

首先设置堆栈,然后在第 26 行可以看到,指向 LC0(包含字符串)的指针被复制到堆栈,然后调用 _printf,最后通过将 0 移动到寄存器 A 并调用 return 来完成 _main

汇编代码

现在我们已经查看并理解了汇编代码的组成,接下来我们将对其进行汇编并查看输出。要执行此操作,可以通过运行脚本 assemble(.bat) 来调用汇编器。

这会生成一个目标文件,它不再是文本表示,我们需要一些工具进一步检查。幸运的是,GCC 已经提供了这些工具,我们将使用的第一个工具叫 objdump。在命令行中运行脚本 objdmp(.bat) 后,你将看到大量的文本输出。

Objdump 从目标文件中转储数据,例如程序的段和符号表,因此有必要解释这两者。

段是一个命名的数据或代码块,其中一些段因历史支持而具有固定的名称和用途,另一些则由程序员或编译器命名。有三个主要的段:

  • .text:代码通常位于此处,我们将会看到 main 函数位于这里。

  • .bss:用于存放所有未初始化的数据,记得 _y 被放入 .bss,因为它未初始化,而 _x 实际上放在名为 .data 的段中。

  • .data:存放已初始化的可读写数据。

  • rdata:仅存放只读数据并保护其免受写操作,包括默认情况下的字符串。我们将看到第一个条件判断中的字符串会放在这里。

图片

符号表

符号表是一种映射关系,将符号(如变量、函数或其他标识符的名称)映射到地址,这正是变量的由来。函数名称也会在符号表中找到,并指向 .text 段中的代码地址。在符号表中,名称会被加上下划线前缀,甚至段的名称也会出现在符号表中。

图片

在这里,我们可以看到变量 x 和 y 以及函数 main 和 printf 的符号/名称,甚至还有段名称。

现在,所有这些段及其内容都位于一个目标文件中,更准确地说是一个可重定位目标文件,因为文件中的所有内容都是相对于文件内的段来寻址的,这里没有绝对地址,因此不能直接执行。将目标文件固定到绝对地址的过程称为链接。

链接

现在我们来到了编译的最后阶段,目标文件(Object File)被链接在一起,输出一个可执行的二进制文件。要调用链接器,我们运行 link(.bat) 脚本,这将生成 ELF 文件或 EXE 文件,但在此之前,我们必须指示编译器如何排列输入段(来自目标文件的段)并将其内容重新安排到新的输出段中,这些输出段将在可执行文件中找到。为此,我们使用称为 **链接脚本**(linkerscript)的工具。

这是我们在演示中使用的链接脚本:

图片

链接脚本

预处理部分 

通过这个脚本,我们现在可以理解为什么 ENTRY 被赋予符号 _main,这是因为函数名经过名称修饰(name mangling)后在符号表中转换为符号。

我们还有一个符号叫做 _MainAddr,它被赋予地址 0xABBA,这是我们在内存中放置 main 函数的位置。接着我们有另一个符号 _OtherStuff,它被赋予地址 0xF00D,这是放置其他内容的位置。最后,对于符号,我们有一个叫做 _OtherStuffLen 的符号,它被赋予地址 0x4000。请注意,在这种情况下,这个符号指向一个地址,如果我们查看符号表,我们会看到一个名为 _OtherStuffLen 的符号指向那个地址,这个符号并不是用来指向内存的,而是纯粹作为一个命名的值。

MEMORY 部分 

在定义了符号后,我们进入脚本的 MEMORY 部分,在这里我们定义了一个名为 myMain 的内存区域,该区域可以读取和执行,但不能写入,它的起始地址是符号表中找到的 _MainAddr,长度为 10KB。

第二个区域叫做 otherStuff,是用来放置其他所有内容的,因此它具有读取、写入和执行的权限,因为数据和代码都放在这里。它的地址是符号表中找到的 _OtherStuff,长度是符号表中的 _OtherStuffLen。请注意,这里的值并不是符号表中的地址,而是符号表中的地址值。不要取消引用 _OtherStuffLen,因为它不是一个有效的地址。

SECTIONS 部分 

在 SECTIONS 部分,我们将从目标文件中提取所有输入段(如 .text.bss.rdata 等),并创建输出段供可执行文件使用,并将它们分配到内存区域。这里我们可以看到定义了两个输出段:.text 和 .otherThings

在 .text 段中,我们使用通配符(*)符号来搜索任何目标文件中的 .text 段或其他名为 .text 的段,并将其放入 myMain 区域。

在 .otherThings 段中,我们使用通配符(*)符号将所有未分配的内容放入 otherStuff 内存区域。

使用这个脚本调用链接器应该会生成一个包含两个段的可执行文件。

使用 objdump 对输出进行分析,像这样:

图片

     

获取gcc ld 示例工程: gccAndLinkerDemo!!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值