gcc之ld链接脚本
gcc之ld链接脚本
这一篇准备谈谈链接的一些基础知识以及gcc ld链接脚本等知识。文中提到的内容都是基于linux系统。
1 为什么要链接?
假如我们将所有代码写到一个文件中(且不需要其它库支持)时,就不需要链接器了。很显然,如果代码开发规模很大,放到一个文件里缺点太多,如下:
-
代码阅读维护太困难;
-
每次有一点改动需要全部编译一遍,太耗时。如果分多个文件就可以采用make的增量编译(只编译有改动的部分),大大减少再次编译的耗时;
-
等等
基于以上缺点,所以我们需要将一个工程分为多个源文件,分别进行编译,然后通过链接器组合到一起,形成最终的单一的可执行程序。
链接器就是将各个目标文件(.o文件),静态库(.a),动态库(.so)链接成最终的可执行文件。
2 ld链接脚本
ld链接脚本语法继承自AT&T链接器命令语言的语法,风格有点像C语言。链接脚本由一系列语句组成,语句分为两种,一种是命令语句,一种是赋值语句。原则上语句之间要以分号;作为分隔符,但是对于命令语句也可以使用换行来结束改语句,对于赋值语句来说必须要以分号;结束。
2.1 ld链接脚本实例
如下:以一个链接脚本来解释ld的语法(来自于参考1,有删减)。
OUTPUT_FORMAT("elf32-little") /* 定义输出格式 */
OUTPUT_ARCH("riscv") /* 定义输出架构 */
ENTRY( _start ) /* 程序入口为_start函数,嵌入式工程中常定义在start.s启动文件中 */
/* MEMORY 用来定义内存分布 */
/* 如下:定义了三块地址区间,分别名为flash,ilm和ram,ORIGIN 与 LENGTH 指明区域的起始地址与长度
* 如:flash (rxai!w) : ORIGIN = 0x00020000, LENGTH = 4M 表示内存块的名字为flash(可任意取,只在链接脚本内有效),
* 其属性为rxai!w, 起始地址为0x00020000,长度为4M Byte,MEMORY语法中可以使用如K、M和G这样的内存单位。
*/
MEMORY
{
flash (rxai!w) : ORIGIN = 0x00020000, LENGTH = 4M
ilm (rxai!w) : ORIGIN = 0x00080000, LENGTH = 64K
ram (wxa!ri) : ORIGIN = 0x00090000, LENGTH = 64K
}
/* 可以对内存块的名字起别名,方便使用 */
REGION_ALIAS("ROM", flash)
REGION_ALIAS("RAM", ram)
SECTIONS
{
.init : /* .init 为输出段的段名,段名后面必须加一个空格,这样使得输出段名不会有歧义 */
{
KEEP (*(SORT_NONE(.init))) /* 链接时使用了选项–gc-sections后,链接器可能将某些它认为没用的section过滤掉,
* KEEP命令可以强制链接器保留一些特定的section
*/
} >ROM AT>flash
.text : /*.text 为输出段的段名,段名后面必须加一个空格,这样使得输出段名不会有歧义 */
{
*(.text .text.*) /* *(.text) 表示所有输入文件中名字为.text的文件。*是通配符,链接脚本支持正则表达式中的?、[]等规则
* 如:[a-z]*(.text*[A-Z])表示所有输入文件为小写字母a-z开头,所有段名以.text开头,以大写字母A-Z结尾
* 的段
*/
*(.gnu.linkonce.t.*)
} >ilm AT>flash
/* 注意:上述语法中AT前的一个ilm表示该段的运行地址,AT后的flash表示该段的加载地址,意思是让程序段存储在Flash之中,而装载到ILM中运行,
* 加载地址是该程序的存储地址(调试器下载程序之时会下载到此地址),运行地址却是指程序真正运行起来后所处于的地址。
*/
.data :
{
*(.rdata)
*(.rodata .rodata.*)
*(.data .data.*)
*(.gnu.linkonce.d.*)
. = ALIGN(8); /* ALIGN 表示字节对齐,需要2的幂 */
PROVIDE( __global_pointer$ = . + 0x800 ); /* PROVIDE在链接脚本中定义一个标签,相当于全局变量,
* 该符号可以在程序中引用,后续会举例说到
*/
*(.sdata .sdata.* .sdata*)
*(.gnu.linkonce.s.* )
. = ALIGN(8); /* ‘.’ 表示当前地址,可以作为左值也可作为右值 */
*(.srodata .srodata.*)
} >RAM AT>ROM
PROVIDE( __bss_start = . );
.bss (NOLOAD) : ALIGN(8) /* NOLOAD 表示该section在程序运行时不被载入内存
* 另外,还有四种类型DSECT,COPY,INFO,OVERLAY,很少被使用
*/
{
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss .bss.*)
*(.gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(4);
} >RAM AT>RAM
PROVIDE( _end = . );
PROVIDE( end = . );
}
2.2 一些补充
上例中的注释已经将链接脚本的常用的规则展示的较清楚了,这里做一些补充。
SECTIONS语法:
section其完整语法如下:
section [address] [(type)] : [AT(lma)]
{
input-section-command
input-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]
section 表示输出段的名字,段名后需要接至少一个空格,这样使得输出段名不会有歧义;
address 表示这个段的起始地址(VMA),这个参数不是必须的,如果没有指定(同时>region也没有指定),那么就会将该段放置到当前位置计数器指定的位置上;
(type) 表示段的类型,这个参数也是可选的,常见的NOLOAD 表示该section在程序运行时不被载入内存,另外,还有四种类型 DSECT、COPY、INFO、OVERLAY,很少被使用;
AT(lma) 表示段的加载地址,同样也是可选的。前面提到的“address”指定的是虚拟地址,而这里指定的是实际加载的地址。如果不指定的话,那么加载地址被默认设置成虚拟地址;
>region表示将这个段的运行地址(VMA);
AT>lma_region表示这个段的加载地址(LMA);
=fillexp是一个表达式,表示输出节区所有未指定的区域都使用这个表达式的值来填充。
input-section-command指明哪些输入文件的哪些段要合并输出到当前段中。格式为:
input-file(section)
输入文件名接着一个括号指定输入段名,可以使用通配符如:“*”, "?", "[chars]"。
LMA与VMA:
VMA: virtual memory address. This is the address the section will have when the output file is run
LMA: load memory address. This is the address at which the section will be loaded
大部分时候LMA(加载地址)与VMA(运行地址)都是相同的,程序都是先加载,后运行,但有些嵌入式系统中先都将代码和数据加载到了ROM中,此时的地址就是LMA,当开始运行之后,需要将数据部分拷贝到RAM中,此时数据的地址就是VMA,这块工作也常在start.s 中完成搬运工作。
.bss段:
.bss段是.data段的一个优化。.bss段通常是指用来存放程序中未初始化的或初始化为0的全局变量和静态变量。
有一个说法是:.bss段不占据实际的磁盘空间,这样说易引起误解,正确的说法是:bss段不占用存放空间,但占据运行空间。
比如:常在启动文件start.s 的在加载过程中对bss段进行清0操作。
/* Clear bss section */
la a0, __bss_start # __bss_start 与 _end 在链接脚本中已经定义
la a1, _end
bgeu a0, a1, 2f
1:
sw zero, (a0)
addi a0, a0, 4
bltu a0, a1, 1b
2:
参考:
2 《程序员的自我修养—链接、装载与库》
3 链接脚本解析