链接脚本

连接脚本

连接脚本的一个主要目的是描述输入文件中的节如何被映射到输出文件中,并控制输出文件的内存排布. 几乎所有的连接脚本只做这两件事情. 但是,在需要的时候,连接器脚本还可以指示连接器执行很多其他的操作.这通过下面描述的命令实现.

连接器总是使用连接器脚本的.如果你自己不提供, 连接器会使用一个缺省的脚本,这个脚本是被编译进连接器可执行文件的. 你可以使用’–verbose’命令行选项来显示缺省的连接器脚本的内容. 某些命令行选项,比如

‘-r’或’-N’, 会影响缺省的连接脚本.

你可以过使用’-T’命令行选项来提供你自己的连接脚本. 当你这么做的时候, 你的连接脚本会替换缺省的连接脚本.

你也可以通过把连接脚本作为一个连接器的输入文件来隐式地使用它,就象它们是一个被连接的文件一样.

基本的连接脚本的概念

============================

我们需要定义一些基本的概念与词汇以描述连接脚本语言.

连接器把多个输入文件合并成单个输出文件. 输出文件和输入文件都以一种叫做’目标文件格式’的数据格式形式存在. 每一个文件被叫做’目标文件’. 输出文件经常被叫做’可执行文件’,但是由于需要,我们也把它叫做目标文件. 每一个目标文件中,在其它东西之间,有一个节列表.我们有时把输入文件的节叫做输入节; 相似的,输出文件中的一个节经常被叫做输出节.

一个目标文件中的每一个节都有一个名字和一个大小尺寸. 大多数节还有一个相关的数据块, 称为节内容. 某一个节可能被标式讵’loadable’,含义是在输出文件被执行时,这个节应当被载入到内存中去. 一个没有内容的节可能是’allocatable’, 含义是内存中必须为这个节开辟一块空间,但是没有实际的内容载入到这里(在某些情况下,这块内存必须被标式讵零). 一个既不是loadable也不是allocatable的节一般含有一些调试信息.

每一个loadable或allocatable的输出节有两个地址. 第一个是’VMA’或称为虚拟内存地址. 这是当输出文件运行时节所拥有的地址. 第二个是”LMA’, 或称为载入内存地址. 这个节即将要载入的内存地址. 这大多数情况下这两个地址是相同的. 它们两个有可能不同的一个例子是当一个数据节在ROM中时, 当程序启动时,被拷贝到RAM中(这个技术经常被用在基于ROM的系统中进行全局变量的初始化). 在这种情况下, ROM地址就是LMA, 而RAM地址就是VMA.

你可以通过使用带有’-h’选项的’objdump’来察看目标文件中的节.

每一个目标文件还有一个关于符号的列表, 被称为’符号表’. 一个符号可能是定义过了的,也可能是未定义的.

每一个符号有一个名字, 而且每一个定义的符号有一个地址. 如果你把一个C/C++程序编译为一个目标文件,对于每一个定义的函数和全局或静态变量,你为得到一个定义的符号. 每一个在输入文件中只是一个引用而未定义的函数或全局变量会变成一个未定义的符号.

你可以使用’nm’程序来看一个目标文件中的符号, 或者使用’objdump’程序带有’-t’选项.

连接脚本的格式

====================

连接脚本是文本文件.

你写了一系列的命令作为一个连接脚本. 每一个命令是一个带有参数的关键字,或者是一个对符号的赋值. 你可以用分号分隔命令. 空格一般被忽略.

文件名或格式名之类的字符串一般可以被直接键入. 如果文件名含有特殊字符,比如一般作为分隔文件名用的逗号, 你可以把文件名放到双引号中. 文件名中间无法使用双引号.

你可以象在C语言中一样,在连接脚本中使用注释, 用’/‘和’/’隔开. 就像在C中,注释在语法上等同于空格.

简单的连接脚本示例

============================

许多脚本是相当的简单的.

可能的最简单的脚本只含有一个命令: ‘SECTIONS’. 你可以使用’SECTIONS’来描述输出文件的内存布局.

‘SECTIONS’是一个功能很强大的命令. 这里这们会描述一个很简单的使用. 让我们假设你的程序只有代码节, 初始化过的数据节, 和未初始化过的数据节. 这些会存在于’.text’,’.data’和’.bss’节, 另外, 让我们进一步假设在你的输入文件中只有这些节.

对于这个例子, 我们说代码应当被载入到地址’0x10000’处, 而数据应当从0x8000000处开始. 下面是一个实现这个功能的脚本:

SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

你使用关键字’SECTIONS’写了这个SECTIONS命令, 后面跟有一串放在花括号中的符号赋值和输出节描述的内容.

上例中, 在’SECTIONS’命令中的第一行是对一个特殊的符号’.’赋值, 这是一个定位计数器. 如果你没有以其它的方式指定输出节的地址(其他方式在后面会描述), 那地址值就会被设为定位计数器的现有值. 定位计数器然后被加上输出节的尺寸. 在’SECTIONS’命令的开始处, 定位计数器拥有值’0’.

第二行定义一个输出节,’.text’. 冒号是语法需要,现在可以被忽略. 节名后面的花括号中,你列出所有应当被放入到这个输出节中的输入节的名字. ‘‘是一个通配符,匹配任何文件名. 表达式’(.text)’意思是所有的输入文件中的’.text’输入节.

因为当输出节’.text’定义的时候, 定位计数器的值是’0x10000’,连接器会把输出文件中的’.text’节的地址设为’0x10000’.

余下的内容定义了输出文件中的’.data’节和’.bss’节. 连接器会把’.data’输出节放到地址’0x8000000’处. 连接器放好’.data’输出节之后, 定位计数器的值是’0x8000000’加上’.data’输出节的长度. 得到的结果是连接器会把’.bss’输出节放到紧接’.data’节后面的位置.

连接器会通过在必要时增加定位计数器的值来保证每一个输出节具有它所需的对齐. 在这个例子中, 为’.text’和’.data’节指定的地址会满足对齐约束, 但是连接器可能会需要在’.data’和’.bss’节之间创建一个小的缺口。

就这样,这是一个简单但完整的连接脚本。

每个连接都被一个’连接脚本’所控制. 这个脚本是用连接命令语言书写的。

MEMORY命令

连接器在缺省状态下被配置为允许分配所有可用的内存块。你可以使用‘MEMORY’命令重新配置这个设置。

‘MEMORY’命令描述目标平台上内存块的位置与长度。你可以用它来描述哪些内存区域可以被连接器使用,
哪些内存区域是要避免使用的。然后你就可以把节分配到特定的内存区域中。连接器会基于内存区域设置节
的地址,对于太满的区域,会提示警告信息。连接器不会为了适应可用的区域而搅乱节。

一个连接脚本最多可以包含一次MEMORY命令。但是,你可以在命令中随心所欲定义任意多的内存块,语法
如下:

MEMORY
{
NAME [(ATTR)] : ORIGIN = ORIGIN, LENGTH = LEN
...
}

NAME是用在连接脚本中引用内存区域的名字。出了连接脚本,区域名就没有任何实际意义。区域名存储在一个
单独的名字空间中,它不会和符号名,文件名,节名产生冲突,每一块内存区域必须有一个唯一的名字。

ATTR字符串是一个可选的属性列表,它指出是否为一个没有在连接脚本中进行显式映射地输入段使用一个特定
的内存区域。如果你没有为某些输入段指定一个输出段,连接器会创建一个跟输入段同名的输出段。如果你定
义了区域属性,连接器会使用它们来为它创建的输出段选择内存区域。

ATTR字符串必须包含下面字符中的一个,且必须只包含一个:
R
只读节。
W
可读写节。
X
可执行节。
A
可分配节。
I
已初始化节。
L
同‘I’
!
对前一个属性值取反。

如果一个未映射节匹配了上面除’!’之外的一个属性,它就会被放入该内存区域。’!’属性对该测试取反,所以
只有当它不匹配上面列出的行何属性时,一个未映射节才会被放入到内存区域。

ORIGIN是一个关于内存区域地始地址的表达式。在内存分配执行之前,这个表达式必须被求值产生一个常数,
这意味着你不可以使用任何节相关的符号。关键字’ORIGIN’可以被缩写为’org’或’o’(但是,不可以写为,比
如‘ORG’)

LEN是一个关于内存区域长充(以字节为单位)的表达式。就像ORIGIN表达式,这个表达式在分配执行前也
必须被求得为一个常数值。关键字’LENGTH’可以被简写为‘len’或’l’。

在下面的例子中,我们指定两个可用于分配的内存区域:一个从0开始,有256kb长度,另一个从0x4000000
开始,有4mb长度。连接器会把那些没有进行显式映射且是只读或可执行的节放到’rom’内存区域。并会把另
外的没有被显式映射地节放入到’ram’内存区域。

MEMORY
{
rom (rx)  : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}

一旦你定义了一个内存区域,你也可以指示连接器把指定的输出段放入到这个内存区域中,这可以通过使用
‘>REGION’输出段属性。比如,如果你有一个名为’mem’的内存区域,你可以在输出段定义中使用’>mem’。如
果没有为输出段指定地址,连接器就会把地址设置为内存区域中的下一个可用的地址。如果总共的映射到一
个内存区域的输出段对于区域来说太大了,连接器会提示一条错误信息。

输入section和垃圾回收

在连接命令行内使用了选项–gc-sections后,连接器可能将某些它认为没用的section过滤掉,此时就有必要强制连接器保留一些特定的 section,可用KEEP()关键字达此目的。如KEEP((.text))或KEEP(SORT()(.text))

一个完整的lds文件示例

/*
GNU linker script for STM32F405
*/
/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 0x100000 /* entire flash, 1 MiB */
FLASH_ISR (rx)  : ORIGIN = 0x08000000, LENGTH = 0x004000 /* sector 0, 16 KiB */
FLASH_TEXT (rx) : ORIGIN = 0x08020000, LENGTH = 0x080000 /* sectors 5,6,7,8, 4*128KiB = 512 KiB (could increase it more) */
CCMRAM (xrw)    : ORIGIN = 0x10000000, LENGTH = 0x010000 /* 64 KiB */
RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 0x020000 /* 128 KiB */
}
/* top end of the stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* RAM extents for the garbage collector */
_ram_end = ORIGIN(RAM) + LENGTH(RAM);
_heap_end = 0x2001c000; /* tunable */
/* define output sections */
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } >FLASH_ISR /* The program code and other data goes into FLASH */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ *(.text*)          /* .text* sections (code) */
*(.rodata)         /* .rodata sections (constants, strings, etc.) */
*(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
/*  *(.glue_7)   */    /* glue arm to thumb code */
/*  *(.glue_7t)  */    /* glue thumb to arm code */
. = ALIGN(4);
_etext = .;        /* define a global symbol at end of code */
_sidata = _etext;  /* This is used by the startup in order to initialize the .data secion */
} >FLASH_TEXT
/*
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} >FLASH
.ARM :
{
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} >FLASH
*/
/* This is the initialized data section
The program executes knowing that the data is in the RAM
but the loader puts the initial values in the FLASH (inidata).
It is one task of the startup to copy the initial values from FLASH to RAM. */
.data : AT ( _sidata )
{
. = ALIGN(4);
_sdata = .;        /* create a global symbol at data start; used by startup code in order to initialise the .data section in RAM */
_ram_start = .;    /* create a global symbol at ram start for garbage collector */
*(.data)           /* .data sections */
*(.data*)          /* .data* sections */
. = ALIGN(4);
_edata = .;        /* define a global symbol at data end; used by startup code in order to initialise the .data section in RAM */
} >RAM
/* Uninitialized data section */
.bss :
{
. = ALIGN(4);
_sbss = .;         /* define a global symbol at bss start; used by startup code */
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;         /* define a global symbol at bss end; used by startup code */
} >RAM
/* this is to define the start of the heap, and make sure we have a minimum size */
.heap :
{
. = ALIGN(4);
_heap_start = .;    /* define a global symbol at heap start */
} >RAM
/* this just checks there is enough RAM for the stack */
.stack :
{
. = ALIGN(4);
} >RAM
/* Remove information from the standard libraries */
/*
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
*/
.ARM.attributes 0 : { *(.ARM.attributes) }
}

遇到疑问可以查参考文献[2],介绍的比较详细。

参考文献

[1] ld链接脚本文件语法解析之一, http://www.cnblogs.com/tureno/articles/3741143.html
[2] Linux下的lds链接脚本基础, http://www.cnblogs.com/hfww/archive/2011/05/27/2223374.html

版权声明:4c25b034b0faa408 发表于 2021-12-07 9:43:46。
转载请注明:链接脚本 | 链游|区块链游戏