ld 链接脚本 和 链接脚本中 地址段相关语句介绍

ld 链接脚本 和 链接脚本中 地址段相关语句介绍

概述

在C语言编程中,LD文件通常指链接脚本(Linker Script)。链接脚本由链接器(链接程序,如 GNU 的 ld)用于控制链接过程,最终生成ELF文件。它可以确定在最终生成的可执行文件或者库文件中,各个段(如文本段 .text、数据段 .data)的布局和对齐。连接器脚本对于嵌入式系统的程序开发尤其重要,因为在这类系统中,开发者需要精确控制代码和数据在内存中的位置,而不是由操作系统选择加载地址。

我们在执行编译链接过程中,一般没有指定链接脚本,ld 命令会使用默认的链接脚本。
通过 $(ld) –verbose可以看到默认的使用的链接脚本,如下截图显示了部分内容

在这里插入图片描述

简单示例

以下是一个简单的ld脚本的示例

/* 示例 LD 链接脚本 */
ENTRY(main)
SECTIONS
{
    . = 0x10000;
    .text ALIGN (4) :
    {
        *(.text)
    }
    .data ALIGN (4) :
    {
        *(.data)
    }
    .bss ALIGN (4) :
    {
        *(.bss)
    }
}

控制链接过程的本质是控制输入段如何变成输出段,这个流程主要在section命令中实现。
这里包含了两个命令,ENTRY命令和SECTIONS命令,ENTRY命令指定程序的入口符合为main,SECTIONS函数定义输入段如何变成输出段,比如 . = 0x10000; 为赋值语句,表示当前的虚拟地址是 0x10000,随后的内容称为段转换规则,段转换规则是ld脚本中的最重要的部分,输出段中的 .text段的起始地址就是 0x10000,.text 四字节对齐,并且包含了输入文件中的所有 .text段。.text、.data和.bss段都被对齐到4字节边界

    . = 0x10000;
    .text ALIGN (4) :
    {
        *(.text)
    }

等价于

	.text  0x10000  ALIGN (4) :
    {
        *(.text)
    }

链接脚本的常见元素

  1. ENTRY(_start): 它定义了程序的入口点。在这个例子中,入口点被定义为_start
  2. SECTIONS: 它用于开始定义各个段(section)的布局。
  3. { . = ALIGN(4); }: 这行将当前位置对齐至4字节边界。
  4. KEEP(*(.text )): KEEP指令用于确保链接器不会丢弃指定的段。在这个例子中,链接器被告知保留所有的.text段的内容。
  5. AT(ALIGN(LOADADDR (.interp) + SIZEOF (.interp), ALIGNOF(.hash))): 这行指定了.interp段结束后的对齐方式,以满足.hash段的对齐要求。
  6. LOADADDR(segment): LOADADDR是一个内置函数,用于获取指定段的加载地址。
  7. SIZEOF(segment): SIZEOF是一个内置函数,用于获取指定段的大小。
  8. ALIGNOF(segment): ALIGNOF是一个内置函数,用于获取指定段的对齐要求。
  9. MEMORY:描述目标系统的内存布局。

关于ld脚本中的地址

如下几个是LD中的与地址相关的内置函数, 放在一起描述一下差异

ABSOLUTE(exp) 返回绝对地址;

ADDR(section) 返回对应段的绝对地址,在定义之前,对应的段的绝对地址必须要已经被定义;

LOADADDR(section) 返回段的绝对地址,通常和 ADDR一致,但是如果在引用的段的定义中,使用了 AT 关键字,则对应于AT关键字加载的地址。

ALIGN(exp) 返回当前的地址计数边界对齐的结果。

AT关键字指定了段的加载地址(即LMA),与虚拟地址(VMA)不同。在执行前,需要将数据从LMA地址,拷贝到VMA位置。当LMA和VMA不一样时,处理器并不会自动处理这个差异,而是依赖于启动代码来将程序从LMA复制到VMA。这部分需要在初始化代码中实现,否则无法正常执行。

.text : AT(0x10000) { *(.text) }

在这行代码中,AT(0x10000)指定了.text段的加载地址为0x10000。

LOADADDR关键字:LOADADDR是一个内置函数,用于获取指定段的加载地址。这个函数非常有用,特别是在需要计算段的大小或者在段之间需要保持某种特定关系的时候。与AT不同的是,LOADADDR不用于指定加载地址,而是获取已经设置的加载地址。

__data_end = LOADADDR (.data) + SIZEOF (.data);

在这行代码中,LOADADDR (.data)获取了.data段的加载地址,然后通过加上.data段的大小,计算得到.data段的结束地址。
总结一下,AT是用来设置加载地址的,而LOADADDR是用来获取加载地址的。

gcc调用方式

gcc -T选项用于指定链接脚本。链接脚本可以控制输入文件(通常为.o对象文件)的链接过程

	gcc -Ttest.ld $^ -o $@

加载区 LMA 和 执行区 VMA 在实际执行时的过程

有操作系统的情况

在Linux系统中,加载器的功能是由动态链接器实现的,其源代码位于glibc库中。glibc是GNU C库,为Linux和其他Unix-like系统提供了系统调用和基本功能。

特别地,对于ELF可执行文件,负责加载的是ld-linux.so(例如ld-linux.so.2或ld-linux-x86-64.so.2),这是glibc提供的动态链接器。当你执行一个ELF可执行文件时,操作系统会在背后调用这个动态链接器来加载程序和所需的动态库,然后开始执行程序。

具体来说,动态链接器会读取ELF文件的头部信息,解析程序头表,然后把代码段和数据段等加载到内存中,解析和加载所需的动态库,处理重定位(加载器还需要处理段之间的依赖关系。例如,.data 或 .bss 段可能包含了对 .text 段中函数的引用,这些引用在链接过程中被替换为符号,所以加载器需要解析这些符号,将它们替换为正确的内存地址。),然后把控制权交给程序的入口点,开始执行程序。

需要注意的是,动态链接器本身也是一个ELF可执行文件,它在执行前也需要被加载。这个初级加载过程是由内核直接完成的,内核会映射动态链接器到内存,然后把控制权交给它,由它来完成剩下的加载过程。

无操作系统的情况

在一些没有操作系统的嵌入式系统中,加载器的功能可能会由引导加载器(bootloader)或者硬件直接实现。此外,一些嵌入式系统可能会使用特殊的二进制格式而不是通用的 ELF 格式。

程序通常会被存储在只读存储器 (ROM) 中,但在执行时,它可能需要被复制到随机访问存储器 (RAM) 中。这就是链接地址(Virtual Memory Address,VMA)和加载地址(Load Memory Address,LMA)可能不同的情况

GNU链接器的AT关键字来指定段的加载地址(Load Memory Address,简称 LMA)。默认的加载地址LMA和重定位地址也就是虚拟内存地址VMA是一致的。设计初衷是为了方便创建ROM镜像,即上面描述的场景。例如,在给出的SECTIONS定义中,创建了两个输出段:一个名为.text的段,起始地址是0x1000;另一个名为.mdata的段,其加载地址是.text段的结束地址,尽管其重定位地址是0x2000。符号_data被赋值为0x2000。对于按照这种方式生成的ROM,运行时需要有特定的初始化代码(这段来自gnu对AT的解释和定义)

SECTIONS
  {
  .text 0x1000 : { *(.text) _etext = . ; }
  .mdata 0x2000 : 
    AT ( ADDR(.text) + SIZEOF ( .text ) )
    { _data = . ; *(.data); _edata = . ;  }
  .bss 0x3000 :
    { _bstart = . ;  *(.bss) *(COMMON) ; _bend = . ;}
}

对于按照这种方式生成的ROM,其运行时初始化代码(对于C程序,通常是crt0)需要包含类似以下的代码,来将初始化的数据从ROM镜像复制到其运行时地址:

char *src = _etext;
char *dst = _data;

/* ROM has data at end of text; copy it. */
while (dst < _edata) {
  *dst++ = *src++;
}

/* Zero bss */
for (dst = _bstart; dst< _bend; dst++)
  *dst = 0;

在这段代码中,首先定义了两个指针,src指向.text段的结束地址,dst指向.data段的开始地址。然后,通过一个while循环,将ROM(src所指向的地址)中的数据复制到RAM(dst所指向的地址)。最后,通过一个for循环,将.bss段(从_bstart到_bend)的内容清零。

MEMORY 语句的作用

GNU链接器的MEMORY语句用于描述目标系统的内存布局。它定义了一组内存区域,每个区域都有一个名称、起始地址、长度和属性。链接器会根据这些信息,将各个段(section)放置在合适的内存区域中。

如下是一个使用MEMORY定义SECTIONS的链接脚本示例:

MEMORY
{
  ram (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
  flash (rx) : ORIGIN = 0x8000000, LENGTH = 256K
}

SECTIONS
{
  .text :
  {
    *(.text)
  } > flash

  .data :
  {
    *(.data)
  } > ram

  .bss :
  {
    *(.bss)
  } > ram
}

MEMORY部分定义了两个内存区域,ram和flash。ram区域的起始地址是0x20000000,长度是64K,属性是rwx,表示这个区域可读、可写、可执行。flash区域的起始地址是0x8000000,长度是256K,属性是rx,表示这个区域可读、可执行,但不可写。

在这个例子中,SECTIONS语句定义了段在输出文件中的布局。它首先指定了.text段(包含程序的机器代码)应该被放置在flash内存区域,然后.data.bss段(包含全局和静态变量)应该被放置在ram内存区域。

MEMORY 和 AT 之间的关系

MEMORY定义和AT指定加载地址的方式都可以用来指定段的位置,但是他们的用途和工作方式有所不同。

MEMORY定义在链接脚本中用来描述目标系统的物理内存布局,它定义了一组内存区域,每个区域都有一个名称、起始地址、长度和属性。链接器会根据这些信息,将各个段放置在合适的内存区域中。

AT指令用于指定段的加载地址。这在程序的运行地址(LMA,Load Memory Address)和链接地址(VMA,Virtual Memory Address)不同的情况下非常有用。例如,一个段可能在链接时被放置在RAM中,但在程序运行时需要被复制到RAM的其他位置。在这种情况下,可以使用AT指令指定加载地址。

总的来说,MEMORY定义主要用于描述整个系统的内存布局,而AT指令主要用于处理特定段的加载地址。

参考链接

https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#IDX231

(部分内容由notionAI生成)

  • 29
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值