链接脚本.lds(详细)总结附实例快速掌握

一、简介

链接器:把一个或多个输入文件合并成一个输出文件,输入文件是目标文件或者链接脚本文件,输出文件是目标文件(库文件)或者可执行文件,链接器从链接脚本读完一个 section 后,将定位器符号的值增加该 section 的大小。

链接脚本:链接脚本的一个主要目的是描述输入文件中的各个段(数据段,代码段,堆,栈,bss)如何被映射到输出文件中,并控制输出文件的各部分在程序地址空间内的布局,地址空间包括 ROM 和 RAM。
链接器总是使用链接脚本的,如果你不提供,则链接器会使用一个缺省的脚本,这个脚本是被编译进链接器可执行文件的。
在这里插入图片描述

二、常用关键字详解

2.1 ENTRY(进入)

ENTRY(main)
ENTRY(MultibootEntry)

ENTRY关键字用于定义应用程序的入口点,即输出文件中的第一条可执行指令。该关键字接受链接程序/内核入口点的符号名作为单个参数。所提供的符号名指向的代码将是。ELF和PE二进制文件中的文本部分。
ENTRY(SYMBOL) :将符号SYMBOL的值设置成入口地址。
入口地址(entry point)是指进程执行的第一条用户空间的指令在进程地址空间的地址
ld有多种方法设置进程入口地址, 按一下顺序: (编号越前, 优先级越高)
1、ld命令行的-e选项
2、链接脚本的ENTRY(SYMBOL)命令
3、如果定义了start符号, 使用start符号值
4、如果存在.text section, 使用.text section的第一字节的位置值
5、使用值0

2.2 OUTPUT_FORMAT(输出格式)

OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_FORMAT("pe-i386")

OUTPUT_FORMAT指令只接受一个参数。它指定可执行文件的输出格式。要了解系统binutils和GCC支持哪些输出格式,可以使用objdump-i命令。

2.3 STARTUP (启动)

STARTUP(Boot.o)
STARTUP(crt0.o)

启动需要一个参数。它是要链接到可执行文件开头的文件。对于userland项目,这通常是crt0。o或crtbegin。o、 对于内核,通常是包含程序集样板的文件启动堆栈,在某些情况下是GDT之类的,然后调用kmain()。

2.4 SEARCH_DIR(搜索目录)

SEARCH_DIR(Directory)

这将为您的库搜索目录添加路径。-nostlib标志将导致在该路径中找到的任何库被有效忽略。我不知道为什么,这似乎就是ld的工作原理。它将链接器脚本指定的搜索目录视为标准目录,因此会忽略它们,而不使用默认的libs和此类标志

2.5 INPUT(输入)

INPUT(File1.o File2.o File3.o ...)
INPUT
(
	File1.o
	File2.o
	File3.o
	...
)

INPUT是一个“链接器脚本中”替换项,用于将对象文件添加到命令行。您通常会指定类似于ld File1的内容。o文件2。o、 可以使用输入部分在链接器脚本中执行此操作。

2.6 OUTPUT (输出)

OUTPUT(Kernel.bin)

OUTPUT命令指定要生成的文件作为链接过程的输出。这是最终创建的二进制文件的名称。此命令的效果与-o filename命令行标志的效果相同,后者会覆盖它。

2.7 MEMORY (记忆存储)

MEMORY
{
    
    
    ROM (rx) : ORIGIN = 0, LENGTH = 256k
    RAM (wx) : org = 0x00100000, len = 1M
}

MEMORY声明一个或多个内存区域,其属性指定该区域是否可以写入、读取或执行。这主要用于不同地址空间区域可能包含不同访问权限的嵌入式系统。
上面的示例脚本告诉链接器有两个内存区域:
a) “ROM”从地址0x00000000开始,长度为256kB,可以读取和执行。
b) “RAM”从地址0x00100000开始,长度为1MB,可以写入、读取和执行。

2.8 SECTIONS命令

SECTIONS命令告诉ld如何把输入文件的sections映射到输出文件的各个section: 如何将输入section合为输出section; 如何把输出section放入程序地址空间(VMA)和进程地址空间(LMA).

该命令格式如下:

SECTIONS
{
    
    
SECTIONS-COMMAND

SECTIONS-COMMAND
…
}

SECTION-COMMAND有四种:
(1) ENTRY命令
(2) 符号赋值语句
(3) 一个输出section的描述(output section description)
(4) 一个section叠加描述(overlay description)

如果整个连接脚本内没有SECTIONS命令, 那么ld将所有同名输入section合成为一个输出section内, 各输入section的顺序为它们被连接器发现的顺序.如果某输入section没有在SECTIONS命令中提到, 那么该section将被直接拷贝成输出section。

2.8.1 KEEP (保持)

链接器脚本中的KEEP语句将指示链接器保留指定的节,即使其中没有引用任何符号。此语句在链接器脚本的节中使用。当在链接时执行垃圾收集时,这一点就变得很重要,在链接命令行内使用了选项 -gc-sections 后,链接器可能将某些它认为没用的 section 过滤掉,此时就有必要强制让链接器保留一些特定的 section,可用 KEEP() 关键字达此目的。说的通俗易懂就是:防止被优化。
该语句常见于针对ARM体系结构的链接器脚本中,用于将中断向量表放置在偏移量0x00000000处。如果没有这个指令,代码中可能不会显式引用的表将被删除。

SECTIONS
{
    
    
	.text :
	{
    
    
		KEEP(*(.text.ivt))
		*(.text.boot)
		*(.text*)
	} > ROM

	/** ... **/
}

2.8.2 PROVIDE

为在任何链接目标中没有定义但是被引用的一个符号,而在链接脚本定义一个符号。 PROVIDE(symbol = expression)。提供定义‘_exfun’的例子:

SECTIONS
{
    
    
  .text :
    {
    
    
      *(.text)
      _exfun = .;
      PROVIDE(_exfun = .);
    }
}

如果程序定义了’ _exfun ‘(带有前导下划线),链接器将给出重复定义错误。另一方面,如果程序定义了’ exfun‘(没有前导下划线),链接器会默认使用程序中的定义。如果程序引用了’ exfun '但没有定义它,链接器将使用链接器脚本中的定义。
PROVIDE指令将考虑定义一个普通符号,即使这样的符号可以与PROVIDE将创建的符号组合在一起。当考虑构造函数和析构函数列表符号时,这一点尤其重要,因为它们通常被定义为普通符号。

2.8.3 TYPE

每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。它可以为以下五种值

NOLOAD 该section在程序运行时,不被载入内存。
DSECT,COPY,INFO,OVERLAY :这些类型很少被使用,为了向后兼容才被保留下来。这种类型的section必须被标记为“不可加载的”,以便在程序运行不为它们分配内存。

2.8.4 AT( LAM_ADDR )

section包含两个地址:VMA(virtual memory address虚拟内存地址或程序地址空间地址)和LMA(load memory address加载内存地址或进程地址空间地址)。默认情况下 LMA 等于 VMA,但可以通过关键字 AT() 指定 LMA。

用关键字 AT()指定,括号内包含表达式,表达式的值用于设置LMA。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围。这个属性主要用于构件ROM境象。

一般而言, 某section的VMA == LMA. 但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况: 比如将输出文件加载到开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定)。
例子,

SECTIONS
{
    
    
.text 0×1000 : {
    
    _etext = . ;*(.text);  }
.mdata 0×2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{
    
     _data = . ; *(.data); _edata = . ; }
.bss 0×3000 :

{
    
     _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}

c程序如下:

extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;
while (dst rom }

2.8.5 REGION

这个region就是前面说的MEMORY命令定义的位置信息。

2.8.6 ALIGN 关键字

表示字节对齐, 如 “ . = ALIGN(4);”表示从该地址开始后面的存储进行4字节对齐。

2.8.7 输入section描述

输入section描述基本语法:

FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...)

FILENAME文件名,可以是一个特定的文件的名字,也可以是一个字符串模式。
SECTION名字,可以是一个特定的section名字,也可以是一个字符串模式。
具体示例解析:
1、*(.text) :表示所有输入文件的.text section

2、(*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有输入文件的.ctors section。

3、data.o(.data) :表示data.o文件的.data section

4、data.o :表示data.o文件的所有section

5、*(.text .data) :表示所有文件的.text section和.data section,顺序是:第一个文件的.text section,第一个文件的.data section,第二个文件的.text section,第二个文件的.data section,…

6、*(.text) *(.data) :表示所有文件的.text section和.data section,顺序是:第一个文件的.text section,第二个文件的.text section,…,最后一个文件的.text section,第一个文件的.data section,第二个文件的.data section,…,最后一个文件的.data section

下面看连接器是如何找到对应的文件的。
当FILENAME是一个特定的文件名时,连接器会查看它是否在连接命令行内出现或在INPUT命令中出现。
当FILENAME是一个字符串模式时,连接器仅仅只查看它是否在连接命令行内出现。
注意:如果连接器发现某文件在INPUT命令内出现,那么它会在-L指定的路径内搜寻该文件。

字符串模式内可存在以下通配符:

* :表示任意多个字符
? :表示任意一个字符
[CHARS] :表示任意一个CHARS内的字符,可用-号表示范围,如:a-z
:表示引用下一个紧跟的字符

在文件名内,通配符不匹配文件夹分隔符/,但当字符串模式仅包含通配符*时除外。
任何一个文件的任意section只能在SECTIONS命令内出现一次。
看如下例子

SECTIONS {
    
    

.data : {
    
     *(.data) }
.data1 : {
    
     data.o(.data) }
}

data.o文件的.data section在第一个OUTPUT-SECTION-COMMAND命令内被使用了,那么在第二个OUTPUT-SECTION-COMMAND命令内将不会再被使用,也就是说即使连接器不报错,输出文件的.data1 section的内容也是空的。

2.9 ASSERT(断言)

ASSERT(exp, message)

2.10 内建函数

lds中有以下一些内建函数:
ABSOLUTE(EXP) :转换成绝对值

ADDR(SECTION) :返回某section的VMA值。

ALIGN(EXP) :返回定位符’.'的按照EXP进行对齐后的修调值,对齐后的修调值算法为:(. + EXP – 1) & ~(EXP – 1)

BLOCK(EXP) :如同ALIGN(EXP),为了向前兼容。

DEFINED(SYMBOL) :如果符号SYMBOL在全局符号表内,且被定义了,那么返回1,否则返回0

LOADADDR(SECTION) :返回三SECTION的LMA

MAX(EXP1,EXP2) :返回大者

MIN(EXP1,EXP2) :返回小者

NEXT(EXP) :返回下一个能被使用的地址,该地址是EXP的倍数,类似于ALIGN(EXP)。除非使用了MEMORY命令定义了一些非连续的内存块,否则NEXT(EXP)与ALIGH(EXP)一定相同

SIZEOF(SECTION) :返回SECTION的大小。当SECTION没有被分配时,即此时SECTION的大小还不能确定时,连接器会报错

SIZEOF_HEADERS :返回输出文件头部的字节数。这些信息出现在输出文件的开始处。当设置第一个段的开始地址时,你可以使用这个数字。如果你选择了加速分页,当产生一个ELF输出文件时,如果链接器脚本使用SIZEOF_HEADERS内建函数,连接器必须在它算出所有段地址和长度之前计算程序头部的数值。如果连接器后来发现它需要附加程序头,它将报告一个“not enough room for program headers”错误。为了避免这样的错误,你必须避免使用SIZEOF_HEADERS函数,或者你必须修改你的连接器脚本去避免强制连接器去使用附加程序头,或者你必须使用PHDRS命令去定义你自己的程序头

2.11 Symbols (象征)

可以在链接器脚本中定义任意符号。这些符号被添加到程序的符号表中。表中的每个符号都有一个名称和一个关联的地址。链接器脚本中已赋值的符号将被赋予外部链接,并可在程序代码中作为指针访问。

floating_point = 0;
SECTIONS
{
    
    
  .text :
    {
    
    
      *(.text)
      _etext = .;
    }
  _bdata = (. + 3) & ~ 3;
  .data : {
    
     *(.data) }
}

在上面的示例中,符号浮点被定义为零。符号_etext被定义为最后一个字符后面的地址。文本输入部分。符号_bdata被定义为以下地址:。文本输出部分向上对齐到4字节边界。

三、实例解析

3.1 常用实例详解1

 SECTIONS
 {
    
    
   .= 0x10000; //把定位器符号置为 0x10000(若不指定,则该符号的初始值为0)
   .text : {
    
     *(.text) } //*符号代表所有的输入文件,此句表示获取所有输入文件的 .text section放在一块连续的地址空间,首地址由上一句的定位器符号确定,即 0x10000
   .= 0×8000000;  //把定位器符号置为 0x8000000
   .data : {
    
     *(.data) } //获取所有输入文件的 .data section 放在一块连续的地址空间,该 section 的首地址为 0x8000000
   .bss : {
    
     *(.bss) }  //获取所有输入文件的 .bss section 放在一块连续的地址空间,该 section 的首地址为 0x8000000 + .data section 的大小
 }
 
SECTIONS {
    
    
    . = 0x30000000;             //指定当前的链接地址=0x30000000

.text          :   {
    
    
head.o(.text)    //添加第一个目标文件,里面会调用这些函数
init.o(.text)      //添加第二个目标文件,里面存放关看门狗,初始化SDRAM等函数
nand.o(.text)   //添加第三个目标文件,里面存放初始化nand函数
*(.text)    // *(.text) 表示添加剩下的全部文件的.text代码段
}

.rodata ALIGN(4) : {
    
    *(.rodata)}       //指定只读数据段

.data ALIGN(4) : {
    
     *(.data) }     //指定读写数据段,     *(data):添加所有文件的数据段

__bss_start = .;     //把__bss_start赋值为当前地址位置,即bss段的开始位置

.bss ALIGN(4)  : {
    
     *(.bss)  *(COMMON) }     //指定bss段,里面存放未被使用的变量

__bss_end = .;        //把_end赋值为当前地址位置,即bss段的结束位置

}

上面的链接地址=0x30000000,表示程序运行的地方应该位于0x30000000处,0x30000000就是我们的SDRAM基地址,而一上电后,nand的前4k地址会被2440自动装载到内部ram中,所以我们初始化了sdram和nand后,就需要把程序所有内容都复制到链接地址0x30000000上才行。

3.2 uboot下lds实例分析2

OUTPUT_ARCH(arm)                                //设置输出文件的体系架构。
ENTRY(_start)                                   //将_start这个全局符号设置成入口地址。
SECTIONS                                        //输出文件内容布局
{
    
    
    . = 0x00000000;                            //指定地址0x00000000
 
    . = ALIGN(4);                             //代码以4字节对齐
    .text      :                                //指定.text section段(位于0x00000000)   
    {
    
    
      cpu/arm920t/start.o   (.text)          //添加第一个目标文件: cpu/arm920t/start.o里面的.text代码段
          board/100ask24x0/boot_init.o (.text)   //添加第二个目标文件: board/100ask24x0/boot_init.o里面的.text代码段
      *(.text)                               // *(.text) 表示添加剩下的全部文件的.text代码段
    }
 
    . = ALIGN(4);
    .rodata : {
    
     *(.rodata) }        //指定.rodata section段(位于0x00000000+.text section),将所有的.rodata只读数据段合并成一个.rodata只读数据段 
 
    . = ALIGN(4);
    .data : {
    
     *(.data) }            //指定读写数据段,     *(data):添加所有文件的数据段
 
    . = ALIGN(4);
    .got : {
    
     *(.got) }              //指定got段,got段是uboot自定义的一个段
 
    . = .;
    __u_boot_cmd_start = .;            //把__u_boot_cmd_start赋值为当前位置, 即起始位置
    .u_boot_cmd : {
    
     *(.u_boot_cmd) }   // u_boot_cmd段,所有的u-boot命令相关的定义都放在这个位置
    __u_boot_cmd_end = .;              //  u_boot_cmd段结束位置
 
    . = ALIGN(4);
    __bss_start = .;                   //把__bss_start赋值为当前位置,即bss段的开始位置
    .bss : {
    
     *(.bss) }                 //指定bss段,这里NOLOAD的意思是这段不需装载,仅在执行域中才会有这段
    _end = .;                          //把_end赋值为当前位置,即bss段的结束位置
}

四、lds的其他用法

4.1 提供全局变量

//a.lds文件
a = 3;
//编译命令:
gcc -Wall -o a-without-lds.exe a.c
运行结果:
&a = 0×601020
//编译命令:
gcc -Wall -o a-with-lds.exe a.c a.lds
//运行结果:
&a = 0×3
//注意: 对符号的赋值只对全局变量起作用!

4.2 调用lds地址变量

SECTIONS
{
    
    
       .....
        . = ALIGN(4);
        .rodata : {
    
     *(.rodata) }

        . = ALIGN(4);
        .data : {
    
     *(.data) }

        . = ALIGN(4);
        .got : {
    
     *(.got) }

        . = ALIGN(4);
        __bss_start = .;
        .bss : {
    
     *(.bss) }
        _end = .;
}
void clean_bss(void)
{
    
    
    extern int __bss_start, _end;
    int *p = &__bss_start;
        for (; p < &_end; p++)
        *p = 0;
}

五、其他相关知识链接

1、Makefile语法详细总结

2、gcc编译流程、参数实例总结

3、Linux下gcc交叉编译工具链总结

4、交叉编译linux内核实例讲解

猜你喜欢

转载自blog.csdn.net/Luckiers/article/details/127346876