
[001] [RISC-V] Linker Script 链接脚本说明
每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。一个可执行文件中的所有符号都有自己的地址,并保存在「全局符号表」中,但此时「全局符号表」中的地址还都是原来在各个目标文件中的地址,即相对于零地址的偏移。表示当前地址,它是一个变量,总是代表输出文件中的一个地址(根据输入文件section的大小不
1 基础概念
目标文件:程序源文件在经过编译器/汇编器 编译后会生成.o
格式的文件,一般分为3种:
- 可重定位的目标文件(relocatable files):汇编器生成,是不可执行的。
- 可执行的目标文件(executable files):经过链接器的链接、重定位后生成的可执行目标文件。
- 可被共享的目标文件(shared object files):一般以共享库的形式存在,在程序运行时需要动态加载到内存,跟应用程序一起运行。
链接器:多个目标文件.o
和库文件.a
输入文件链接成一个可执行输出文件.elf
,链接器从链接脚本读完一个 section 后,将重定位符号的值增加该 section 的大小。
section
:一个可执行文件通常由不同的section(段)构成:text代码段、data数据段、bss段、rodata只读数据段等。每个section用一个section header
来描述,包括段名、段的类型、段的起始地址、段的偏移和段的大小等。将这些section headers集中放到一起即为section header table
(节头表)。
符号表:在「汇编阶段」,汇编器会分析汇编语言中各个section
的信息,收集各种符号,生成符号表,将各个符号在section
内的偏移地址、类型、占用空间的大小也填充到符号表内。(符号表本身也以section
的形式添加到每一个可重定位目标文件中)
一个可执行文件中的所有符号都有自己的地址,并保存在「全局符号表」中,但此时「全局符号表」中的地址还都是原来在各个目标文件中的地址,即相对于零地址的偏移。
「Q」
链接生成的可执行文件最终是要被加载到内存中执行的,那么要加载到内存中的什么地方呢?
「A」
程序在链接程序时需要指定一个链接起始地址,链接开始地址一般也就是程序要加载到内存中的地址,通过链接脚本指定程序的链接地址和各个段的组装顺序。
链接脚本:主要用于规定各输入文件中的程序、数据等内容段在输出文件中的空间和地址如何分配。通俗的讲,链接脚本用于描述输入文件中的段,将其映射到输出文件中,并指定输出文件中的内存分配。
链接器就是根据链接脚本定义的规则来组装可执行文件的,并最终将这些信息以section
的形式保存到可执行文件的ELF Header
中。完整的ELF文件组织结构如下图所示:
2 常用语法
2.1 定位符 .
定位符 .
表示当前地址,它是一个变量,总是代表输出文件中的一个地址(根据输入文件section的大小不断增加,不能倒退,且只用于SECTIONS
指令中)。对定位符 .
赋值可指定其后内容的存储位置,如果没有以其它的方式指定输出节的地址,则地址值就会被设为定位计数器的当前值,下面举例说明:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
使用SECTIONS
来描述输出文件各段的内存布局,在SECTIONS
命令的开始处, 定位计数器当前值为0
。
.= 0x10000
:定位器当前值赋为0x10000
.text
即定义text代码段,且其定义时的地址即为定位器的当前值0x10000
,通配符*
代表所有的输入文件,即代表所有参与链接文件中的.text
段(*main.o(.text)
代表main.o文件中所有.text段)- 同理,
.data
即定义数据段,其地址为定时器当前值0x8000000
,*(.data)
代表所有参与链接文件中的.data
段;(*(.data.*)
则表示所有参与链接文件的data段中的全部数据) - 紧跟
data
段后的即为bss
段,其首地址为0x8000000 + .data section length
。
下图为各文件 .text section .data section .bss section链接分配的示意图:
注意:链接脚本从上往下,如果输入文件 A 已经被取出 .text section,此后输入文件 A 就没有 .text section,不能再被获取。
2.2 入口地址
ENTRY(SYMBOL):将符号 SYMBOL 的值设置为入口地址,入口地址是程序执行的第一条指令在程序地址空间的地址(如 ENTRY(Reset_Handler)
表示程序最开始从复位中断服务函数处执行)
有多种方法设置进程入口地址,以下编号越小,优先级越高:
1、ld 命令行的 -e 选项
2、链接脚本的 ENTRY(SYMBOL) 命令(如ENTRY( _start )
)
3、在汇编程序中定义了 start 符号,使用 start 符号值(如.global _start
)
4、如果存在 .text section,使用 .text section 首地址的值
5、使用地址 0 的值
声明了程序入口地址为_start
后,在启动文件中会让其跳转到复位向量表中:
.global _start
.align 1
_start:
j handle_reset
2.3 MEMORY
MEMORY
{
NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2
NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2
}
MEMORY
命令定义了存储空间。
NAME
:内存区域的名字,每一块内存区域必须有一个唯一的名字ATTR
:定义该存储区域的属性。ATTR属性内可以出现以下7 个字符:R
只读sectionW
读/写sectionX
可执行sectionA
可分配的sectionI
初始化了的sectionL
同I
!
反转以上任何属性的意义
ORIGIN
:地址空间的起始地址,可缩写为org
或o
(但不能写成ORG)LENGTH
:地址空间的长度,可缩写为len
或l
可单独使用ORIGIN(memory)
和LENGTH(memory)
命令获取内存区域的起始地址以及长度。
使用示例:
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K
RAM (xrw) : o = 0x20000000, l = 20K
}
FLASH
属性只读、可执行,起始地址为0x00000000,大小为64KRAM
属性读写、可执行,起始地址为0x20000000,大小为20K
2.4 PROVIDE
该关键字定义一个(输入文件内被引用但没定义)符号。相当于定义一个全局变量的符号表,其他C文件可以通过该符号来操作对应的存储内存。
.bss :
{
. = ALIGN(4);
PROVIDE( _sbss = .);
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss*)
*(.gnu.linkonce.b.*)
*(COMMON*)
. = ALIGN(4);
PROVIDE( _ebss = .);
} >RAM AT>FLASH
PROVIDE( _sbss = .)
定义了bss段的起始地址_sbss
,PROVIDE( _ebss = .)
定义的bss段的结束地址_ebss
。可在启动文件中调用该符号执行bss段清零操作:
/* clear bss section */
la a0, _sbss ; 将bss段起始地址_sbss加载到r0
la a1, _ebss ; 将bss段结束地址_ebss加载到r1
bgeu a0, a1, 2f ; 若a0 >= a1,则跳转到2处
1:
sw zero, (a0) ; sw即store word,以字为单位将a0地址中存储的值清零
addi a0, a0, 4 ; a0 += 4
bltu a0, a1, 1b ; 若a0 < a1,则跳转到1处
2:
其中,数字标签1:
用于本地引用。后缀为f
表示向前跳转;后缀为b
表示向后跳转。
注意:经过测试,实际上不加PROVIDE
关键字,在链接文件中定义的变量(符号)也可以在目标文件中直接使用。
2.5 HIDDEN
语法:HIDDEN (symbol = expression)
,对于ELF目标端口,符号将被隐藏且不被导出(输出文件中不可见),示例:
HIDDEN (private_symbol = .);
2.6 PROVIDE_HIDDEN
语法:PROVIDE_HIDDEN (symbol = expression)
,是PROVIDE 和HIDDEN的结合体,类似于局部变量(外部程序不能使用)。示例:
PROVIDE_HIDDEN (__preinit_array_start = .);
2.7 SECTIONS结构
SECTIONS
{
...
secname [start_ADDR] [(TYPE)] : [AT (LMA_ADDR)]
{
contents
} [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
...
}
[ ]
内的内容是可选项
-
secname
:表示输出文件的 section 名 -
contents
:描述输出文件的 section 内容是从哪些输入文件(目标文件.o
和库文件.a
)的哪些 section 里抽取而来 -
VMA
(virtual memory address):虚地址,即输出文件运行地址 -
LMA
(load memory address):加载地址,即数据实际存储的地址
数据段加载时会存至Flash
中(使用LMA
地址),一般需通过「重定位」将其搬运到RAM
(使用VMA
地址)。
-
start_addr
:表示将某个段强制链接到的地址(VMA
),start_addr会改变定位符.
的值。 -
TYPE
:每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。它可以为:NOLOAD
:该section在程序运行时,不被载入内存。DSECT,COPY,INFO,OVERLAY
:这些类型很少被使用,为了向后兼容才被保留下来。这种类型的section必须被标记为「不可加载的」,以便在程序运行不为它们分配内存。
-
AT( LAM_ADDR )
:输出 section 的 LMA(加载地址),默认情况下 LMA = VMA,但可以通过关键字AT()
指定 LMA。 -
REGION
:即前文所述用MEMORY
命令定义的存储区域。
示例:
__stack_size = 2048;
PROVIDE( _stack_size = __stack_size );
SECTIONS
{
...
.data :
{
main.o(.data)
*(.data)
} >RAM AT>FLASH
.bss :
{
. = ALIGN(4);
PROVIDE( _sbss = .);
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss*)
*(.gnu.linkonce.b.*)
*(COMMON*)
. = ALIGN(4);
PROVIDE( _ebss = .);
} >RAM AT>FLASH
PROVIDE( _end = _ebss);
PROVIDE( end = . ); /* 定义heap起始位置 */
.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
{
PROVIDE( _heap_end = . ); /* 定义heap结束位置,默认到栈底结束 */
. = ALIGN(4);
PROVIDE(_susrstack = . );
. = . + __stack_size;
PROVIDE( _eusrstack = .);
} >RAM
}
secname后至少要有1个空格。其中,名字前面的.
可有可无,一般都会加上。
*(.data)
含义先前已说明, 特别注意的是,main.o(.data)
先前已链接,此时就不会再链接,这样做的目的是可以将某些特殊的输入文件链接到地址前面。
>RAM AT>FLASH
:.data
段的内容存储至Flash中(AT>
指定),但运行时会加载至RAM中(通常为初始化全局变量),即**.data段的VMA为RAM,LMA为Flash**。
.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
:指定了栈底地址_susrstack
,即为RAM
的末尾地址 - 分配的栈大小,而_eusrstack
指定的栈顶地址。
由于使用的是满减栈,在启动文件中可以看到将栈顶地址_eusrstack
加载到了sp指针中:
la sp, _eusrstack
end
为堆的起始地址(紧跟bss
段之后),_heap_end
为堆的结束地址,等于栈低地址_susrstack
,各段存储示意图如下:
即除去程序用到的data
、bss
段,剩下RAM空间即为动态数据段,供堆的动态使用。
当然,也可以显示指定堆的大小,如:
PROVIDE( _end = _ebss);
PROVIDE( end = . ); /* 定义heap起始位置 */
PROVIDE( _heap_end = . + 0x400); /* 定义heap结束位置,长度为1KB */
.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
{
. = ALIGN(4);
PROVIDE(_susrstack = . );
/*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/
. = . + __stack_size;
PROVIDE( _eusrstack = .);
}
此外,链接脚本中定义了_end
为堆的起始地址,_heap_end
为堆的结束地址,因此我们需要在_sbrk
函数中进行指定,malloc
函数会调用_sbrk
函数获取当前堆的末端地址(入口参数incr
为需要申请内存堆的大小),若不指定则会始终返回-1
。
注意:_sbrk(0)
获取的才是当前堆的末端地址,而其他值表示获取的是调用之前堆的末端地址(此时新的堆末端地址为sbrk(incr) + incr
)
void *_sbrk(ptrdiff_t incr)
{
extern char _end[];
extern char _heap_end[];
static char *curbrk = _end;
if ((curbrk + incr < _end) || (curbrk + incr > _heap_end))
return NULL - 1;
curbrk += incr;
return curbrk - incr;
}
2.8 KEEP
当链接器使用--gc-sections
进行垃圾回收时,链接器可能将某些它认为没用的 section 过滤掉,此时就有必要强制让链接器保留一些特定的 section,KEEP()可以使得被标记section的内容不被清除(即防止被优化)。示例:
.fini :
{
KEEP(*(SORT_NONE(.fini)))
. = ALIGN(4);
} >FLASH AT>FLASH
2.9 ALIGN
表示字节对齐, 如 . = ALIGN(4)
表示从该地址开始后面的存储进行4字节对齐。
2.10 SORT_NONE
忽略 ld 命令行对满足字符串模式的所有名字进行递增排序的要求。e.g.三个源文件 DemoA.c,DemoB.c 和 DemoC.c,分别对其.text段使用SORT_NONE
与SORT
命令,即:
INPUT(DemoB.o)
INPUT(DemoA.o)
INPUT(DemoC.o)
SORT_NONE(*)(.text)
SORT(*)(.text)
可以看到,使用SORT_NONE
后按照我们导入目标文件的顺序进行链接,而使用SORT
后则按照字符递增顺序链接。
2.11 ASSERT
语法:ASSERT(exp, message)
,确保exp是非零值,如果为零,将以错误码的形式退出链接文件,并输出message。主要用于添加断言,定位问题。
/* The usage of ASSERT */
PROVIDE (__stack_size = 0x100);
.stack
{
PROVIDE (__stack = .);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
/* 当"__stack" 大于 "_end + __stack_size"时,在链接时,会出现错误,并提示"Error: No room left for the stack" */
2.12 EXCLUDE_FILE
语法:EXCLUDE_FILE(FILENAME1 FILENAME2)
剔除指定的输入文件,示例:
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
即去除crtend.o
与crtend?.o
目标文件的.dtors
段。
3 使用示例
以CH32V103
为例:
ENTRY( _start )
__stack_size = 2048;
PROVIDE( _stack_size = __stack_size );
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.init :
{
_sinit = .;
. = ALIGN(4);
KEEP(*(SORT_NONE(.init)))
. = ALIGN(4);
_einit = .;
} >FLASH AT>FLASH
.vector :
{
*(.vector);
. = ALIGN(64);
} >FLASH AT>FLASH
.text :
{
. = ALIGN(4);
*(.text)
*(.text.*)
*(.rodata)
*(.rodata*)
*(.glue_7)
*(.glue_7t)
*(.gnu.linkonce.t.*)
. = ALIGN(4);
PROVIDE(__ctors_start__ = .); /* C++构造函数初始化列表起始地址 */
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array))
PROVIDE(__ctors_end__ = .); /* C++构造函数初始化列表结束地址 */
. = ALIGN(4);
} >FLASH AT>FLASH
.fini :
{
KEEP(*(SORT_NONE(.fini)))
. = ALIGN(4);
} >FLASH AT>FLASH
PROVIDE( _etext = . );
PROVIDE( _eitcm = . );
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
} >FLASH AT>FLASH
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH AT>FLASH
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
PROVIDE_HIDDEN (__fini_array_end = .);
} >FLASH AT>FLASH
.ctors :
{
KEEP (*crtbegin.o(.ctors))
KEEP (*crtbegin?.o(.ctors))
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
} >FLASH AT>FLASH
.dtors :
{
KEEP (*crtbegin.o(.dtors))
KEEP (*crtbegin?.o(.dtors))
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
} >FLASH AT>FLASH
.dalign :
{
. = ALIGN(4);
PROVIDE(_data_vma = .); /* data段运行内存起始地址 */
} >RAM AT>FLASH
.dlalign :
{
. = ALIGN(4);
PROVIDE(_data_lma = .); /* data段加载内存起始地址 */
} >FLASH AT>FLASH
.data :
{
*(.gnu.linkonce.r.*)
*(.data .data.*) /* 等价于*(.data.*) */
*(.gnu.linkonce.d.*)
. = ALIGN(8);
PROVIDE( __global_pointer$ = . + 0x800 ); /* 定义全局指针gp地址「0x800 = 2K」*/
*(.sdata .sdata.*)
*(.sdata2.*)
*(.gnu.linkonce.s.*)
. = ALIGN(8);
*(.srodata.cst16)
*(.srodata.cst8)
*(.srodata.cst4)
*(.srodata.cst2)
*(.srodata .srodata.*)
. = ALIGN(4);
PROVIDE( _edata = .); /* data段结束地址 */
} >RAM AT>FLASH
.bss :
{
. = ALIGN(4);
PROVIDE( _sbss = .); /* bss段起始地址 */
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss*)
*(.gnu.linkonce.b.*)
*(COMMON*)
. = ALIGN(4);
PROVIDE( _ebss = .); /* bss段结束地址 */
} >RAM AT>FLASH
PROVIDE( _end = _ebss);
PROVIDE( end = . ); /* 定义heap起始位置 */
.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
{
PROVIDE( _heap_end = . ); /* 定义heap结束位置,默认到栈底结束 */
. = ALIGN(4);
PROVIDE(_susrstack = . ); /* 定义stack栈低地址*/
. = . + __stack_size;
PROVIDE( _eusrstack = .); /* 定义stack栈顶地址*/
} >RAM
}
这里主要说明下data
段及其重定位搬运操作,重点关注以下几个符号:
_data_vma
定义了data段运行内存起始地址(RAM)_data_lma
定义了data段加载内存起始地址(Flash)_edata
则为data段结束地址__global_pointer$
定义了全局指针寄存器gp的地址,通过gp指针,访问其值±2KB
,即4KB
范围内的全局变量,可以节约一条指令。
linker时使用__global_pointer$
来比较全局变量的地址,如果在范围内,就替换掉lui
或puipc
指令的绝对寻址或pc
相对寻址,变为gp
相对寻址,使得代码效率更高。该过程被称为linker relaxation
(链接器松弛),也可以使用-Wl,--no-relax
来关闭此功能。
4KB区域可以位于寻址内存中任意位置,但是为了使优化更有效率,最好覆盖最频繁使用的RAM区域。 .sdata
段与.sdata2
段使用“小数据”寻址,即使用较短的地址访问。因此,如果将经常使用的数据放入其中,代码大小与执行时间将会减少。所以,__global_pointer$
定义放在了 .sdata
段前。
注意:gp
寄存器在启动代码中加载为__global_pointer$
的地址,并且之后不能被改变。此外,有时候为了优化代码密度,可以根据实际情况修改gp
指针的位置,如工程中定义了大量的初始化为0或未初始化的全局数组作为缓冲区,可以将gp
指针的位置定义到bss
段。
ch32v103
启动文件中gp
指针地址加载与data
段搬运操作汇编代码如下:
handle_reset:
.option push
.option norelax
la gp, __global_pointer$
.option pop
la sp, _eusrstack
2:
/* Load data section from flash to RAM */
la a0, _data_lma ; data段加载内存起始地址 加载至a0
la a1, _data_vma ; data段运行内存起始地址 加载至a1
la a2, _edata ; data段结束地址 加载至a2
bgeu a1, a2, 2f ; 若a1 >= a2,则跳转到2处
1:
lw t0, (a0) ; 将a0中的数据 加载到 t0
sw t0, (a1) ; 将t0中的数据 加载到 a1
addi a0, a0, 4 ; a0 += 4
addi a1, a1, 4 ; a1 += 4
bltu a1, a2, 1b ; 若a1 < a2,则跳转到1处
2:
[...]
.option norelax
表示不支持链接器松弛,但仅在push与pop中间这一行,并不是全局的。因为.option push
的作用是将当前设置入栈,随后.option pop
又将入栈的设置弹了出来。松弛链接需要gp
寄存器,在代码刚启动时gp
寄存器还没有设置,因此在配置la gp, __global_pointer$
,需要暂时禁用。
若想全局禁用,可采用如下设置(但会导致代码空间变大,不推荐使用):
References:
- RISC-V MCU堆栈机制
- 浅谈RISC-V GCC之:链接脚本学习笔记(二)
- 链接脚本文件(.ld .lds)详解
- Executable and Linkable Format
- malloc实现中与sbrk函数的关系
- RISC-V MCU ld链接脚本说明 – 以CH32V103为例
- RISC-V gp全局指针寄存器说明
- The gp (Global Pointer) register
END
更多推荐
所有评论(0)