gcc之ld链接脚本

gcc之ld链接脚本

这一篇准备谈谈链接的一些基础知识以及gcc ld链接脚本等知识。文中提到的内容都是基于linux系统。

1 为什么要链接?

假如我们将所有代码写到一个文件中(且不需要其它库支持)时,就不需要链接器了。很显然,如果代码开发规模很大,放到一个文件里缺点太多,如下:

  1. 代码阅读维护太困难;

  2. 每次有一点改动需要全部编译一遍,太耗时。如果分多个文件就可以采用make的增量编译(只编译有改动的部分),大大减少再次编译的耗时;

  3. 等等

基于以上缺点,所以我们需要将一个工程分为多个源文件,分别进行编译,然后通过链接器组合到一起,形成最终的单一的可执行程序。

链接器就是将各个目标文件(.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:

参考:

1 链接脚本(Linker Script)解析

2 《程序员的自我修养—链接、装载与库》

3 链接脚本解析

posted @ 2023-03-15 20:57  sureZ_ok  阅读(1365)  评论(0编辑  收藏  举报