[001] [RISC-V] Linker Script 链接脚本说明

RISC-V
Contents
基础概念
常用语法
使用示例

1 基础概念

image-20220819172031382

▲ ARM工具链软件编译流程

目标文件:程序源文件在经过编译器/汇编器 编译后会生成.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(节头表)。

请添加图片描述

▲ Important Sections

详见:GNU ELF special sections

符号表:在「汇编阶段」,汇编器会分析汇编语言中各个section的信息,收集各种符号,生成符号表,将各个符号在section内的偏移地址、类型、占用空间的大小也填充到符号表内。(符号表本身也以section的形式添加到每一个可重定位目标文件中)

一个可执行文件中的所有符号都有自己的地址,并保存在「全局符号表」中,但此时「全局符号表」中的地址还都是原来在各个目标文件中的地址,即相对于零地址的偏移。

「Q」链接生成的可执行文件最终是要被加载到内存中执行的,那么要加载到内存中的什么地方呢?

「A」程序在链接程序时需要指定一个链接起始地址,链接开始地址一般也就是程序要加载到内存中的地址,通过链接脚本指定程序的链接地址和各个段的组装顺序。

链接脚本:主要用于规定各输入文件中的程序、数据等内容段在输出文件中的空间和地址如何分配。通俗的讲,链接脚本用于描述输入文件中的段,将其映射到输出文件中,并指定输出文件中的内存分配

链接器就是根据链接脚本定义的规则来组装可执行文件的,并最终将这些信息以section的形式保存到可执行文件的ELF Header中。完整的ELF文件组织结构如下图所示:

image-20220824141043249

▲ Executable and Linkable Format 文件组织结构

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链接分配的示意图:

image-20220822153815351

注意:链接脚本从上往下,如果输入文件 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 只读section
    • W 读/写section
    • X 可执行section
    • A 可分配的section
    • I 初始化了的section
    • LI
    • ! 反转以上任何属性的意义
  • ORIGIN:地址空间的起始地址,可缩写为orgo(但不能写成ORG)
  • LENGTH:地址空间的长度,可缩写为lenl

可单独使用ORIGIN(memory)LENGTH(memory)命令获取内存区域的起始地址以及长度。

使用示例:

MEMORY
{
    
    
	FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K
	RAM (xrw) : o = 0x20000000, l = 20K
}
  • FLASH属性只读、可执行,起始地址为0x00000000,大小为64K
  • RAM属性读写、可执行,起始地址为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段的起始地址_sbssPROVIDE( _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,各段存储示意图如下:

image-20220823114052481

即除去程序用到的databss段,剩下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_NONESORT命令,即:

INPUT(DemoB.o)
INPUT(DemoA.o)
INPUT(DemoC.o)
SORT_NONE(*)(.text) 
SORT(*)(.text) 

image-20220823102628944

可以看到,使用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.ocrtend?.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$来比较全局变量的地址,如果在范围内,就替换掉luipuipc指令的绝对寻址或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$,需要暂时禁用。

若想全局禁用,可采用如下设置(但会导致代码空间变大,不推荐使用):

img


References

END

猜你喜欢

转载自blog.csdn.net/kouxi1/article/details/126707153