21
0
0

GCC内LD链接脚本语法

2026-08-24
文章摘要
|

一、 声明式配置 DSL(领域特定语言)

不需要真正掌握,只需要会用

  • 无逻辑:没有 if/for/函数(极少例外,如 SORT

  • 无状态:不保存变量,不改变程序行为

  • 纯映射:只描述“输入段 → 输出段 → 物理地址”的映射关系

  • 非图灵完备:不能写算法,不能做计算(简单算术除外)

下面的代码块展示的是LD文件的常见功能块

/* 1. 入口点定义(程序从哪开始跑) */ 
ENTRY(Reset_Handler) 
/* 2. 内存区域定义(芯片物理资源) */ 
MEMORY { ... } 
/* 3. 段定义(内容放哪) */ 
SECTIONS { ... } 
/* 4. 符号定义(导出关键地址给C代码) */ 
PROVIDE(symbol = value); 
/* 5. 高级特性(覆盖段/断言等) */ 
ASSERT(...);

二、 LD文件的作用

  1. 规定程序的运行入口:通过 ENTRY() 明确告诉 CPU 上电复位后,第一行代码从哪里开始执行(如 Reset_Handler)。

  2. 规划内存的物理版图:通过 MEMORY 命令告诉链接器芯片上有哪些存储块(Flash、SRAM、DTCM等),以及它们的起始地址和大小。

  3. 定义段的分配规则:通过 SECTIONS 命令,将输入文件中的段(如机器码 .text、常量 .rodata、变量 .data)精确映射到具体的内存区域中。

  4. 导出关键地址给启动代码:通过定义特殊符号(如 _sdata, _edata, _sbss),在内存中打下“界桩”,供汇编启动代码在 C 语言 main() 函数执行前,精准地完成内存初始化和数据拷贝。

三、 基本语法

(1)MEMORY 块 - 定义物理内存区域(逻辑分区)

MEMORY
{
  名称 (属性) : ORIGIN = 起始地址, LENGTH = 大小
}

例如:

MEMORY
{
  ITCMRAM (xrw)  : ORIGIN = 0x00000000, LENGTH = 64K   # 指令紧耦合RAM,64KB,可执行
  DTCMRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 128K  # 数据紧耦合RAM,128KB
  FLASH   (rx)   : ORIGIN = 0x08000000, LENGTH = 2048K # Flash存储器,2MB,只读可执行
  RAM_D1  (xrw)  : ORIGIN = 0x24000000, LENGTH = 512K  # D1域RAM,512KB
  RAM_D2  (xrw)  : ORIGIN = 0x30000000, LENGTH = 288K  # D2域RAM,288KB
  RAM_D3  (xrw)  : ORIGIN = 0x38000000, LENGTH = 64K   # D3域RAM,64KB
  BACKUP  (xrw)  : ORIGIN = 0x38800000, LENGTH = 4K    # 备份RAM,4KB
}
  • 其中属性说明:
    r = read (可读)
    w = write (可写)
    x = execute (可执行)
    a = allocatable (可分配)
    i = initialized (已初始化)
    l = load (同i)
    ! = 反转属性

(2)SECTIONS 块-定义段的放置规则

SECTIONS
{
  段名称 :
  {
    内容规则
  } >目标内存区域 AT>加载地址
}

例如:

SECTIONS
{
    /* 段名 : {内容} >输出区域 */
    .text :
    {
        /* 特殊符号 */
        . = ALIGN(8);           /* 当前位置对齐到8字节 */
        _stext = .;             /* 定义段开始符号 */
        
        /* 输入段 */
        *(.text)                /* 所有文件的.text段 */
        *(.text*)               /* 所有以.text开头的段 */
        *(.glue_7)              /* ARM特定的粘合代码 */
        *(.glue_7t)
        
        KEEP(*(.init))          /* 保持段不被垃圾回收 */
        KEEP(*(.fini))
        
        . = ALIGN(4);
        _etext = .;             /* 定义段结束符号 */
    } >ROM
}

1.通配符规则

SECTIONS
{
    .text :
    {
        /* 文件通配符 */
        main.o(.text)           /* 特定文件的.text段 */
        *(.text)                /* 所有文件的.text段 */
        lib*.a:(.text)          /* 所有lib开头的库文件 */
        
        /* 段名通配符 */
        *(.text.*)              /* 如 .text.startup, .text.exit */
        *(.text.foo)            /* 特定的.text.foo段 */
        
        /* 排除规则 */
        *(EXCLUDE_FILE(boot.o debug.o) .text)  /* 排除特定文件 */
        
        /* 排序规则 */
        *(SORT(.text.*))        /* 按名称排序 */
        *(SORT_BY_NAME(.text.*))
        *(SORT_BY_ALIGNMENT(.text.*))
    } >ROM
}

*(.text)
│
└── 表示"所有输入文件"

*(.text*)
        │
        └── 表示"所有以.text开头的段"

*(.rodata)
│  │
│  └── 段名:.rodata段
└──── 文件:所有输入文件

# 含义:将所有输入文件中的.rodata段放到这里


*(.rodata*)
│  │      │
│  │      └── 通配符:匹配.rodata开头的所有段
│  └──────── 段名前缀:.rodata
└──────────── 文件:所有输入文件

含义:将所有输入文件中以.rodata开头的段都放到这里
匹配:.rodata、.rodata.str1.4、.rodata.constants等

SECTIONS
{
    .text :
    {
        /* 1. 特定文件的特定段 */
        main.o(.text)           /* 只要main.o文件的.text段 */
        
        /* 2. 所有文件的特定段 */
        *(.text)                /* 所有文件的.text段 */
        
        /* 3. 所有文件的匹配段 */
        *(.text*)               /* 所有文件中以.text开头的段 */
                               /* 匹配:.text、.text.startup、.text.exit等 */
        
        /* 4. 多个段名 */
        *(.text .text.*)        /* .text段和所有.text.开头的段 */
        
        /* 5. 特定库文件 */
        libxxx.a:(.text)        /* libxxx.a库中的.text段 */
        
        /* 6. 库文件通配符 */
        lib*.a:(.text)          /* 所有lib开头的库文件的.text段 */
        
        /* 7. 排除特定文件 */
        *(EXCLUDE_FILE(boot.o debug.o) .text)  /* 除了boot.o和debug.o外的所有.text段 */
        
        /* 8. 特定文件的所有段 */
        startup.o(*)            /* startup.o的所有段 */
    } >ROM
}

2. VMA和LMA(重要概念)

SECTIONS
{
    /* VMA = Virtual Memory Address (运行地址) */
    /* LMA = Load Memory Address (加载/存储地址) */
    
    /* 方式1: AT关键字 */
    .data :
    {
        *(.data)
    } >RAM AT>ROM               /* 运行在RAM,存储在ROM */
    
    /* 方式2: AT()函数 */
    .data : AT(0x08010000)
    {
        *(.data)
    } >RAM
    
    /* 方式3: 使用变量 */
    .data : AT(_sidata)
    {
        _sdata = .;             /* RAM中的起始地址 */
        *(.data)
        _edata = .;             /* RAM中的结束地址 */
    } >RAM
}

(3)特殊符号和内置函数

1. 位置计数器

SECTIONS
{
    .text :
    {
        . = 0x1000;             /* 设置当前地址 */
        *(.text)


        . = . + 0x100;          /* 预留256字节空间 */
        *(.rodata)
        . = ALIGN(0x10);        /* 对齐到16字节 */
    } >ROM
}

2.内置函数

SECTIONS
{
    .text :
    {
        /* 对齐函数 */
        . = ALIGN(4);           /* 对齐到4字节 */
        . = ALIGN(0x100);       /* 对齐到256字节 */
        
        /* 地址函数 */
        _data_lma = LOADADDR(.data);    /* 获取.data段的LMA */
        _text_size = SIZEOF(.text);     /* 获取.text段大小 */
        _ram_start = ORIGIN(RAM);       /* 获取RAM起始地址 */
        _ram_size = LENGTH(RAM);        /* 获取RAM大小 */
        
        /* 数学函数 */
        _value = MAX(0x1000, 0x2000);   /* 最大值 */
        _value = MIN(0x1000, 0x2000);   /* 最小值 */
        _value = ABSOLUTE(-100);        /* 绝对值 */
    } >ROM
}

3.对齐

对齐要求

原因

示例

4字节对齐

ARM指令要求

函数地址必须4字节对齐

8字节对齐

栈指针要求

栈必须8字节对齐

32字节对齐

DMA/Cache要求

DMA缓冲区需要32字节对齐

256字节对齐

页边界

某些硬件特性需要页对齐

4K对齐

MMU页面

内存保护单元要求

四、一个示例

下文是作者写的一个综合的 LD 文件示例

该LD文件来自于作者的一个项目,需要强制隔离推理段,以留出空间实现.

该示例不仅涵盖了基础的段隔离,还深入实践了VMA(运行地址)LMA(加载地址)的分离配置。

这份脚本展示了一个包含 Bootloader + App (A/B分区 OTA) + AI 推理 (TensorFlow Lite Micro) + TLS (线程局部存储) 的复杂系统架构。通过阅读这个示例,你可以直观地看到:

  1. Flash 分区规划:如何划分 Bootloader、Metadata、App A/B 区以及 AI 权重区。

  2. 内存隔离:如何利用 CCMRAM 存放 AI 推理数组,以及如何在特定 RAM 中保留 Boot Magic 标志位。

  3. 复杂段映射:如何处理 .data.ccmram 的 LMA 与 VMA 映射,以及多线程环境下的 .tdata.tbss 段布局。

ENTRY(Reset_Handler)


MEMORY
{
  RAM           (xrw) : ORIGIN = 0x20000000, LENGTH = 127K
  RESERVED_RAM  (rw)  : ORIGIN = 0x2001FC00, LENGTH = 1K
  CCMRAM        (xrw) : ORIGIN = 0x10000000, LENGTH = 64K   /* 推理数组 */
  BOOTLOADER    (rx)  : ORIGIN = 0x08000000, LENGTH = 32K   /*BOOTLOADER引导区*/
  METADATA      (rx)  : ORIGIN = 0x08008000, LENGTH = 16K   /*OTA结构体*/
  ERROR_MAIN    (rx)  : ORIGIN = 0x0800C000, LENGTH = 16K   /*AB区均失效的备用运行*/
  WEIGHTS       (rx)  : ORIGIN = 0x08010000, LENGTH = 64K   /*权重文件*/
  APP_A         (rx)  : ORIGIN = 0x08020000, LENGTH = 128K  /*APP A区*/
  APP_B         (rx)  : ORIGIN = 0x08040000, LENGTH = 128K  /*APP B区*/
  TFLM_FRAME    (rx)  : ORIGIN = 0x08060000, LENGTH = 128K  /*AI推理*/
}


_estack = ORIGIN(RAM) + LENGTH(RAM);    /*栈顶地址*/
_sstack = _estack - _Min_Stack_Size;    /*栈底地址*/


_Min_Heap_Size = 0x200;      /*最小堆大小*/
_Min_Stack_Size = 0x1000 ;   /*最小栈大小*/




/*设计思路:函数跳转BL与数据访问LDR之间的障碍,经过查询BL 指令跳转范围是 ±16MB。*/
/*而 LDR指令的范围是 LDR R0, [PC, #imm],偏移量只有 ±4KB,因此会死机。避免“长跳转”惩罚*/
SECTIONS
{
    .tflm_text 0x08060000 : AT(0x08060000)  /*VMA == LMA。*/
    {
    _tflm_text_start = .;
    . = ALIGN(4);
    *libtflmicro.a:(.text .text.* .glue_7 .glue_7t)/*ARM 特有的“蹦床代码”,每个函数都是一个独立段*/
    . = ALIGN(4);
    _tflm_text_end = .;
    } >TFLM_FRAME

    /*链接器会自动把这段的起始地址,紧接在上一段(.tflm_text)的结束地址后面。*/

    .tflm_rodata :
    {
        _tflm_rodata_start = .;
        . = ALIGN(4);
        *libtflmicro.a:(.rodata .rodata.* .constdata)
        . = ALIGN(4);
        _tflm_rodata_end = .;
    } >TFLM_FRAME

    .isr_vector :
    {
        KEEP(*(.isr_vector))   /*禁止优化*/
    } >APP_A

    .text :/*代码段*/
    {
        . = ALIGN(4);
        *(EXCLUDE_FILE(*libtflmicro.a) .text)           
        *(EXCLUDE_FILE(*libtflmicro.a) .text*)          
        *(.glue_7)
        *(.glue_7t)
        *(.eh_frame)
        KEEP (*(.init))
        KEEP (*(.fini))
        . = ALIGN(4);
        _etext = .;
    } >APP_A

    /*这个是c++专用的全局对象的构造函数。*/
    .init_array :
    {
        . = ALIGN(4);
        PROVIDE(__init_array_start = .);
        KEEP(*(.init_array))
        KEEP(*(SORT(.init_array.*)))
        . = ALIGN(4);
        PROVIDE(__init_array_end = .);
    } >APP_A
    /*C++ 全局析构函数*/
    .fini_array :
    {
        . = ALIGN(4);
        KEEP(*(.fini_array))
        KEEP(*(SORT(.fini_array.*)))
        . = ALIGN(4);
    } >APP_A


    .rodata :/*只读数据段 const*/
    {
        . = ALIGN(4);
        *(EXCLUDE_FILE(*libtflmicro.a) .rodata)         
        *(EXCLUDE_FILE(*libtflmicro.a) .rodata*)        
        . = ALIGN(4);
    } >APP_A


    .data : /*数据段 已初始化的变量*/
    {
        . = ALIGN(4);
        _sdata = .;        
        *(.data)           
        *(.data*)          
        *(.RamFunc)       
        *(.RamFunc*)      

        . = ALIGN(4);
        _edata = .;       
    } >RAM AT> APP_A /*AT就是指定加载地址也就是FLASH,真正要读写变量,要去RAM里找*/

    _sidata = LOADADDR(.data);/*给启动文件指路,flash的起始点*/

    .ccmram (NOLOAD) :
    {
        . = ALIGN(4);
        _sccmram = .;
        *(.ccmram)
        *(.ccmram*)
        . = ALIGN(4);
        _eccmram = .;
    } >CCMRAM


    .bss : /*未初始化段 清零变量*/
    {
        . = ALIGN(4);
        _sbss = .;        
        __bss_start__ = _sbss;
        *(.bss)
        *(.bss*)
        *(COMMON)

        . = ALIGN(4);
        _ebss = .;         
        __bss_end__ = _ebss;
    } >RAM


    ._user_heap_stack (NOLOAD) :/*NOLOAD 关键字这表示这个段不需要被加载。它不占用 Flash 空间,也不包含任何初始数据。它只是告诉链接器:“在 RAM 里给我划这么一块地盘,大小是 _Min_Heap_Size + _Min_Stack_Size,但别往里面写东西。”*/
    {
        . = ALIGN(8);
        PROVIDE ( end = . );
        PROVIDE ( _end = . );
        . = . + _Min_Heap_Size;
        . = . + _Min_Stack_Size;
        . = ALIGN(8);
    } >RAM


    .Tensorflow_weights :
    {
        . = ALIGN(4);
        KEEP(*(.Tensorflow_weights))
        . = ALIGN(4);
    } >WEIGHTS

    .boot_magic (NOLOAD) :
    {
        . = ALIGN(4);
        KEEP(*(.boot_magic))
        . = ALIGN(4);
    } >RESERVED_RAM

    /DISCARD/ :
    {
        libc.a:* ( * )
        libm.a:* ( * )
        libgcc.a:* ( * )
    }
}

五、其他

  1. KEEP(*(.init)),在现代编译器优化等级较高(如 -Os, -O2)且开启了垃圾回收(GC sections, --gc-sections时,如果没有任何代码引用某个函数,链接器会把它删掉。

  2. 赋值上的问题:

    1. 普通赋值 _start = .:强制定义符号。如果 C 代码里也定义了全局变量叫 _start,链接器会报重定义错误 (Multiple Definition)

    2. PROVIDE 赋值 PROVIDE(_start = .)弱定义。意思是“如果 C 代码里没定义这个符号,就用我这个地址;如果 C 代码里定义了,听 C 代码的,我不报错”。

  3. 对齐:

    1. 段内对齐:本文的对齐统指为段内对齐,即. = ALIGN(32); ,当遇到该语句时它会在段内部往后推(填充 0x00),直到当前地址是 32 的倍数

    2. 段间对齐:.data ALIGN(16) :链接器会先算出 .text 结束后的地址,然后整体.data 段推到 0x1010。段与段之间干干净净,没有浪费空间。

  4. KEEP(*(SORT(.init_array.*))),它决定了 C++ 全局构造函数的执行顺序SORT(.init_array.*) 会按段名字符串排序,确保先定义的构造函数先执行。如果去掉 SORT,构造函数的调用顺序将是未定义的,可能导致依赖全局对象的代码在对象未初始化时就运行。

  5. 示例项目中,本质上是将多个部分程序的编译都统一为一个LD文件,如果在大型项目中,可以使用INCLUDE sections.ld 这是工业级项目的标准做法。

  6. 在C语言代码中,可以使用attribute((section(".Tensorflow_weights"))) const uint8_t model_weights[] = { ... };把相关内容放进段中。

六、参考用书

  • 《Linkers and Loaders》 (John R. Levine)

  • 《Linker Scripts》 (GNU Binutils Documentation)


受限于个人水平,文中难免有理解不到位或笔误之处。恳请各位大佬不吝赐教,欢迎邮件交流:sharkey_z@163.com

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

GCC内LD链接脚本语法
/archives/gccnei-ldlian-jie-jiao-ben-yu-fa
作者
Sharkey
发布于
2026-08-24
许可协议
CC BY-NC-SA 4.0

评论