本文对链接脚本(Linker Scripts)
常用知识进行总结,详细介绍可以通过查看GNU Linker手册了解。
文章目录
基本概念
链接器的作用是将多个输入的目标文件(object file)
,一般后缀为.o
,链接成一个可执行文件(executable file)
,在linux下为elf文件
,而链接脚本的作用是告诉链接器如何将每个.o文件
中的内容整合在一起,并给一些符号(symbol)
赋值,用于系统的初始化等。
链接器以段(section)
为单位对.o文件
进行整合,一般来说,段中会存储程序执行必须的内容,如指令(.text段)
、数据(.data段)
等,但是也有例外,所以依据段中的内容,段主要分为三类:
- 可装载(loadable):段中的内容需要装载(load)到内存。
- 可分配(allocatable):段中没有内容需要装载到内存中,但是在内存中需要预留该段的空间,通常这一部分空间被赋值为0。
- 其他:除以上两种段以外的其他段,段中包含debug信息。
对于可装载段和可分配段,会存在两个地址,分别是虚拟地址(Virtual Memory Address,VMA)
和装载地址(Load Memory Address,LMA)
,虚拟地址是各段实际运行时的地址,而装载地址是各段装载到内存中的地址,一般来说,段的虚拟地址会用符号(symbol)进行记录,在程序中会依据这些符号进行段的移动等操作,段的装载地址即段在elf文件中的地址。例如,一个数据段会装载到ROM中,当程序启动时,需要将ROM中的数据段复制到RAM中,该数据段在ROM中的地址为装载地址,而RAM中的地址为虚拟地址,且该虚拟地址会使用对应的符号(symbol)记录,在启动程序中,依据该符号进行数据的复制。
对于目标文件(.o文件)
可以使用objdump
程序的-h
选项查看文件中的段(section)
,-t
选项查看文件中的符号(symbol)
。
常用命令(command)
用户在链接脚本(Linker Script)
中通过命令(command)
对链接器进行控制。下面对常用的命令进行介绍。
ENTRY
ENTRY
命令描述elf文件
开始执行的位置。例如,ENTRY(_start)
,表示从符号(symbol)_start
开始执行。另外可以通过命令选项-e
指定开始执行的位置。
MEMORY
MEMORY
命令描述elf文件
中内存空间的分配。MEMORY
命令的格式如下,
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
...
}
name
表示内存空间的名字,(attr)
表示内存空间的读写执行属性,ORIGIN
表示内存空间的起始地址,LENGTH
表示内存空间的大小。具体例子如下所示,
MEMORY
{
flash (wxa!ri) : ORIGIN = 0x00000000, LENGTH = 16K
ram (wxa!ri) : ORIGIN = 0x10000000, LENGTH = 16K
}
表示elf文件
中对应两块内存空间,分别为flash
、ram
,其中flash
的起始地址为0x00000000
,ram
的起始地址为0x10000000
,并且flash
和ram
的大小均为16KB
。下面详细介绍(attr)
的含义。
当需要链接的.o文件
中的段,在链接脚本中,没有被显示安排在elf文件
的某一段,那么.o文件
中这些段将会按照.o文件
中的段名单独成为elf文件
中的一段,这些段所处的内存空间会按照原.o文件
中这些段的属性和各个内存空间的属性进行匹配,匹配成功则会将该段放在对应的内存空间。其中MEMORY
中的(attr)
有以下几种,r
表示只读,w
表示可读可写,x
表示可执行,a
表示可分配,i
表示初始化,!
表示该段不具有后续的属性。上述例子中,(wxa!ri)
的含义为,该内存空间中可以放置段属性为,可读写、可执行或可分配,且段属性不是只读和已经初始化的段。
SECTIONS
SECTIONS
链接脚本中最重要的命令,用户可以用SECTIONS
命令描述最后生成的elf文件
的内存布局(memory layout)。下面通过例子介绍SECTIONS
命令的简单用法。
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
SECTIONS
命令由关键字SECTIONS
和一对大括号{}
组成,大括号内部由描述内存布局的一系列符号(symbol)
组成。
在上述例子中,SECTIONS
中首先设置特殊符号.
的值,.
表示位置计数器(location counter)
,位置计数器在初始值为0,可以通过设置位置计数器来指定后续内容在输出的elf文件
中地址,上例中. = 0x10000
表示位置计数器的值设置为0x10000
,即后续的内容从elf文件
的地址0x10000
开始排列。
SECTIONS
第二个符号.text
定义了输出的elf文件
中的.text段
,而在输出elf文件
中会存在符号.text
,该符号的值为.text段
的起始地址,在此例中为0x10000
。.text
后的大括号{}
中的内容是为了说明,所有需要链接的.o文件
中的哪些段,需要被放置到输出elf文件
的.text段
中,上例中,*(.text)
的通配符(wildcard)*
,表示所有.o文件
中的.text段
,都需要被放置到elf文件
的.text段
内。
后续. = 0x8000000
将位置计数器设置为0x8000000
,所以elf文件
的.data段
的起始地址为0x8000000
,在.data段
链接完成后,位置计数器的值为0x8000000
加上.data段
的大小,而当前的位置计数器的值也就是elf文件
中.bss段
的起始地址。
通过上述例子,可以总结在SECTIONS命令中可以包含的内容
- 符号的赋值,例如,
. = 0x10000
、. = 0x8000000
,给特殊符号.赋值。 - 输出
elf文件
中段的描述,例如,.text : { ... }
,.data : { ... }
,.bss = { ... }
。
现在对上述两类内容进行详细介绍。
符号的赋值
在SECTIONS
命令中,不仅可以给特殊符号.
赋值,还可以给定义的任意符号赋值,一般形式为symbol_name = xxx
,表示symbol_name
的值为xxx
。较为常用的是symbol_name = .
,表示symbol_name
为当前位置计数器的值,即当前的虚拟地址(VMA)
的值,也即位置计数器.
前一个段链接完成后,对应的虚拟地址的值,关于位置计数器值的问题,会在段的描述章节中详细介绍。下面介绍符号赋值时常用的命令。
PROVIDE
PROVIDE
用来定义在编程中可以使用符号。例如,PROVIDE( etext = . )
,使用户编程时可以使用etext
变量,而且变量etext
的值为位置计数器.
对应的地址。
PROVIDE_HIDDEN
和PROVIDE
相反,PROVIDE_HIDDEN
用来隐藏符号。例如,PROVIDE( etext = . )
,表示etext
在编程时不可见。
段的描述
在SECTIONS
命令中,可以对链接完成后,输出的elf文件
的段进行描述。具体格式如下,
SECTIONS
{
...
section : {
...
output-section-command
output-section-command
...
} [>region] [AT>lma_region]
...
}
下面对SECTIONS
命令中的段描述各部分进行说明。
段的名字
上述例子中,section
为输出elf文件
中段的名字,段名会记录在符号表(symbol table)
中,并且段名对应符号的值为该段的起始虚拟地址。一般elf文件
中常见的段有,.text
、.data
、.bss
等。
输入的段
上述例子中,output-section-command
可以用来说明输出elf文件
中的段,由被链接的.o文件
中的哪些段构成。例如,output-section-command
对应的命令为*( .text )
,表示所有被链接的.o文件
的.text段
,都会链接到elf文件
的当前段中。另外,如果在链接时对链接器使用--gc-sections
命令(该命令可以让链接器去除没有被使用到的段,以减小最后elf文件
的大小),那么可以使用KEEP
命令,即KEEP(*( .text ))
,表示所有输入.o文件
中的.text段
均不会被优化掉。
符号赋值
上述例子中,output-section-command
可以对符号进行赋值,具体的规则和段的描述之外的符号赋值相同。值得注意的是,段的描述内的位置计数器.
的值,均对应该段的虚拟地址。
[>region]
[>region]
用来说明段的虚拟地址位于哪块内存空间,例如,>ram
表示当前段的虚拟地址在内存空间ram
中。段的虚拟地址所处的内存空间,对该段附近的位置计数器的值也存在影响。如下例所示,
SECTIONS
{
section1 : {
section1_start = .;
} >ram AT>flash
section1_end = .;
section2 : {
section2_start = .;
} >flash AT>flash
section2_end = .;
}
section1
的虚拟地址在内存空间ram
中,section1
对应的大括号{}
内以及大括号{}
后的位置计数器也对应内存空间ram
中的地址,所以符号section1_start
和section2_end
对应内存空间ram
中的地址。section2
的虚拟地址在内存空间flash
中,section2
对应的大括号{}
内以及大括号{}
的位置计数器对应内存空间flash
中的地址,所以符号section2_start
和section2_end
对应内存空间flash
中的地址。
[AT>lma_region]
[AT>lma_region]
用来说明段的装载地址位于哪块内存空间,例如,AT>flash
表示当前段的装载地址在内存空间flash
中。装载地址对应该段在elf文件
中的地址。