• [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) }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    使用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
    
    • 1
    • 2
    • 3
    • 4

    2.3 MEMORY

    MEMORY 
    {
        NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2
        NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中,数字标签1:用于本地引用。后缀为f表示向前跳转;后缀为b表示向后跳转。

    注意:经过测试,实际上不加PROVIDE关键字,在链接文件中定义的变量(符号)也可以在目标文件中直接使用

    2.5 HIDDEN

    语法:HIDDEN (symbol = expression),对于ELF目标端口,符号将被隐藏且不被导出(输出文件中不可见),示例:

    HIDDEN (private_symbol = .);
    
    • 1

    2.6 PROVIDE_HIDDEN

    语法:PROVIDE_HIDDEN (symbol = expression),是PROVIDE 和HIDDEN的结合体,类似于局部变量(外部程序不能使用)。示例:

    PROVIDE_HIDDEN (__preinit_array_start = .);
    
    • 1

    2.7 SECTIONS结构

    SECTIONS
    {
           ...
          secname [start_ADDR] [(TYPE)] : [AT (LMA_ADDR)]
          { 
            contents 
          } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
          ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    [ ]内的内容是可选项

    • 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 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    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 
    
    • 1

    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 = .);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    此外,链接脚本中定义了_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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.8 KEEP

    当链接器使用--gc-sections进行垃圾回收时,链接器可能将某些它认为没用的 section 过滤掉,此时就有必要强制让链接器保留一些特定的 section,KEEP()可以使得被标记section的内容不被清除(即防止被优化)。示例:

    .fini :
    {
        KEEP(*(SORT_NONE(.fini)))
            . = ALIGN(4);
    } >FLASH AT>FLASH
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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) 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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" */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.12 EXCLUDE_FILE

    语法:EXCLUDE_FILE(FILENAME1 FILENAME2)剔除指定的输入文件,示例:

    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
    
    • 1

    即去除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 
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158

    这里主要说明下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:
    	[...]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    .option norelax表示不支持链接器松弛,但仅在push与pop中间这一行,并不是全局的。因为.option push的作用是将当前设置入栈,随后.option pop 又将入栈的设置弹了出来。松弛链接需要gp寄存器,在代码刚启动时gp寄存器还没有设置,因此在配置la gp, __global_pointer$,需要暂时禁用。

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

    img


    References

    END

  • 相关阅读:
    Python 基于 urllib 使用 Handler 处理器(代理)
    C++ const与符号表
    Python--测试代码
    Python基础学习
    程序员都看不懂的代码
    Linux—系统基础一
    Excel生成 chart 混合图表
    服务注册发现_服务自保和服务剔除机制
    (附源码)ssm汽车租赁 毕业设计 271621
    【C语言】结构体内存对齐
  • 原文地址:https://blog.csdn.net/kouxi1/article/details/126707153