链接脚本(.ld文件)语法详解

链接脚本是告诉链接器如何将编译后的代码和数据放置到内存中的配置文件。

1. MEMORY 块 - 定义物理内存区域

MEMORY
{
  名称 (属性) : ORIGIN = 起始地址, LENGTH = 大小
}
属性含义:
  • r = readable (可读)
  • w = writable (可写)
  • x = executable (可执行)
具体解释:
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
}

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

SECTIONS
{
  段名称 :
  {
    内容规则
  } >目标内存区域 AT>加载地址
}
详细语法解释:
示例1: ITCM段
.itcm_text :                    # 定义一个名为.itcm_text的段
{
  . = ALIGN(4);                 # 当前地址对齐到4字节边界
  *(.itcm_text)                 # 所有输入文件中.itcm_text段的内容
  *(.itcm_text*)                # 所有以.itcm_text开头的段(如.itcm_text.func1)
} >ITCMRAM AT> FLASH            # 运行时在ITCMRAM,但存储在FLASH中

关键概念:

  • >ITCMRAM = VMA(Virtual Memory Address) - 运行时地址
  • AT> FLASH = LMA(Load Memory Address) - 存储地址
  • 启动时需要将代码从FLASH复制到ITCMRAM

链接脚本(.ld文件)完整语法教程

一、基础概念

链接脚本(Linker Script)控制链接器如何将多个目标文件(.o)组合成最终的可执行文件。它定义了:

  • 程序的内存布局
  • 段(section)的放置位置
  • 符号(symbol)的地址分配

程序\段\符号

二、基本语法结构

/* 注释使用C风格 */
/* 1. 入口点定义 */
ENTRY(Reset_Handler)

/* 2. 内存区域定义 */
MEMORY
{
    /* 定义格式: 名称(属性) : ORIGIN = 起始地址, LENGTH = 大小 */
}

/* 3. 段定义 */
SECTIONS
{
    /* 定义各个段的内容和位置 */
}

/* 4. 符号定义 */
PROVIDE(symbol = value);

三、MEMORY命令详解

MEMORY
{
    /* 格式: NAME (ATTR) : ORIGIN = ADDR, LENGTH = SIZE */
    
    /* 属性说明:
       r = read (可读)
       w = write (可写)
       x = execute (可执行)
       a = allocatable (可分配)
       i = initialized (已初始化)
       l = load (同i)
       ! = 反转属性
    */
    
    ROM (rx)   : ORIGIN = 0x08000000, LENGTH = 256K
    RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 64K
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2M
    SRAM (!x)  : ORIGIN = 0x20000000, LENGTH = 128K  /* 不可执行 */
}

四、SECTIONS命令详解

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

查看实际段名:

// C代码
const char str1[] = "Hello";        // → .rodata.str1.1
const int array[] = {1,2,3};        // → .rodata.array
void func1(void) { }                // → .text.func1
static void func2(void) { }         // → .text.func2
int __attribute__((section(".mydata"))) var = 5;  // → .mydata
arm-none-eabi-objdump -h program.o

# 输出示例:
Sections:
  0 .text         00000040  # 主代码段
  1 .text.func1   00000020  # func1函数的段
  2 .text.func2   00000018  # func2函数的段
  3 .rodata       00000010  # 主只读数据段
  4 .rodata.str1.1 00000006 # 字符串常量
  5 .rodata.array 0000000c  # 数组常量

为什么使用通配符

/* 不使用通配符(繁琐) */
.text :
{
    main.o(.text)
    uart.o(.text)
    timer.o(.text)
    /* 需要列出每个文件... */
} >ROM

/* 使用通配符(简洁) */
.text :
{
    *(.text)        /* 一行搞定所有文件 */
    *(.text*)       /* 包括编译器生成的子段 */
} >ROM

常见模式对比

模式 含义 匹配示例
*(.text) 所有文件的.text段 只匹配.text
*(.text*) 所有文件中.text开头的段 .text, .text.startup, .text.exit
*(.text.*) 所有文件中.text.开头的子段 .text.startup, .text.exit(不匹配.text)
*(.text .text.*) 上述两者的组合 .text, .text.startup, .text.exit
main.o(.text) main.o的.text段 只有main.o中的.text
*.o(.text) 所有.o文件的.text段 所有目标文件的.text
lib*.a:(.text) lib开头的库的.text段 libm.a, libc.a中的.text
3. 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
}

五、特殊符号和内置函数

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
}

位置计数器点号(.)的含义

.(点号)是链接脚本中的"位置计数器"(Location Counter),表示当前的内存地址位置。把它想象成一个指针,指向当前正在安排内容的内存地址。

工作原理图解

SECTIONS
{
    .text 0x08000000 :    /* .text段从0x08000000开始 */
    {
        . = ALIGN(4);     /* 步骤1: 确保当前地址是4的倍数 */
        *(.text)          /* 步骤2: 放置所有.text内容 */
        . = ALIGN(4);     /* 步骤3: 再次对齐 */
    } >FLASH
}

. = 赋值操作

操作 含义 示例 结果
. = 0x1000 设置当前地址为0x1000 . = 0x1000 当前地址变为0x1000
. = . + 100 当前地址前进100字节 . = . + 100 预留100字节空间
. = ALIGN(4) 对齐到4字节边界 见下方详解 地址变为4的倍数
. = ALIGN(0x100) 对齐到256字节边界 见下方详解 地址变为256的倍数

为什么需要对齐?

对齐要求 原因 示例
4字节对齐 ARM指令要求 函数地址必须4字节对齐
8字节对齐 栈指针要求 栈必须8字节对齐
32字节对齐 DMA/Cache要求 DMA缓冲区需要32字节对齐
256字节对齐 页边界 某些硬件特性需要页对齐
4K对齐 MMU页面 内存保护单元要求

ALIGN(4)的计算过程:

当前地址 → 对齐后地址
0x08000000 → 0x08000000 (已对齐,不变)
0x08000001 → 0x08000004 (向上对齐到4)
0x08000002 → 0x08000004 (向上对齐到4)
0x08000003 → 0x08000004 (向上对齐到4)
0x08000004 → 0x08000004 (已对齐,不变)
0x08000005 → 0x08000008 (向上对齐到4)

3. 符号定义和赋值
/* 全局符号定义 */
_stack_size = 0x400;            /* 简单赋值 */
_heap_size = DEFINED(_heap_size) ? _heap_size : 0x200;  /* 条件赋值 */

PROVIDE(_stack = ORIGIN(RAM) + LENGTH(RAM));  /* 弱符号定义 */
PROVIDE_HIDDEN(__preinit_array_start = .);    /* 隐藏符号 */

SECTIONS
{
    /* 段内符号定义 */
    .text :
    {
        __text_start = .;       /* 强符号 */
        *(.text)
        __text_end = .;
    } >ROM
}

六、高级特性

1. OVERLAY(覆盖段)
SECTIONS
{
    /* 多个段共享同一内存区域 */
    OVERLAY 0x20000000 : AT(0x08010000)
    {
        .overlay1 
        {
            *(.overlay1)
        }
        .overlay2 
        {
            *(.overlay2)
        }
    } >RAM
}
2. 段属性
SECTIONS
{
    /* NOLOAD: 不从文件加载 */
    .bss (NOLOAD) :
    {
        *(.bss)
        *(COMMON)
    } >RAM
    
    /* DSECT: 不分配内存 */
    .debug (DSECT) :
    {
        *(.debug)
    }
    
    /* COPY: 复制段 */
    /* INFO: 信息段 */
    /* OVERLAY: 覆盖段 */
}
3. KEEP防止垃圾回收
SECTIONS
{
    .init_array :
    {
        KEEP(*(SORT(.init_array.*)))
        KEEP(*(.init_array))
    } >ROM
    
    /* 中断向量表必须保留 */
    .isr_vector :
    {
        KEEP(*(.isr_vector))
    } >ROM
}

七、完整的STM32H743示例

/* 入口点 */
ENTRY(Reset_Handler)

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

/* 内存定义 */
MEMORY
{
    ITCMRAM (xrw)  : ORIGIN = 0x00000000, LENGTH = 64K
    DTCMRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 128K
    FLASH   (rx)   : ORIGIN = 0x08000000, LENGTH = 2048K
    RAM_D1  (xrw)  : ORIGIN = 0x24000000, LENGTH = 512K
    RAM_D2  (xrw)  : ORIGIN = 0x30000000, LENGTH = 288K
    RAM_D3  (xrw)  : ORIGIN = 0x38000000, LENGTH = 64K
}

/* 段定义 */
SECTIONS
{
    /* 中断向量表 */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } >FLASH
    
    /* 代码段 */
    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.glue_7)
        *(.glue_7t)
        *(.eh_frame)
        
        KEEP(*(.init))
        KEEP(*(.fini))
        
        . = ALIGN(4);
        _etext = .;
    } >FLASH
    
    /* 只读数据 */
    .rodata :
    {
        . = ALIGN(4);
        *(.rodata)
        *(.rodata*)
        . = ALIGN(4);
    } >FLASH
    
    /* ARM异常处理 */
    .ARM.extab :
    {
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } >FLASH
    
    .ARM :
    {
        __exidx_start = .;
        *(.ARM.exidx*)
        __exidx_end = .;
    } >FLASH
    
    /* 初始化数组 */
    .preinit_array :
    {
        PROVIDE_HIDDEN(__preinit_array_start = .);
        KEEP(*(.preinit_array*))
        PROVIDE_HIDDEN(__preinit_array_end = .);
    } >FLASH
    
    .init_array :
    {
        PROVIDE_HIDDEN(__init_array_start = .);
        KEEP(*(SORT(.init_array.*)))
        KEEP(*(.init_array*))
        PROVIDE_HIDDEN(__init_array_end = .);
    } >FLASH
    
    .fini_array :
    {
        PROVIDE_HIDDEN(__fini_array_start = .);
        KEEP(*(SORT(.fini_array.*)))
        KEEP(*(.fini_array*))
        PROVIDE_HIDDEN(__fini_array_end = .);
    } >FLASH
    
    /* ITCM代码段 */
    _siitcmram = LOADADDR(.itcmram);
    .itcmram :
    {
        . = ALIGN(4);
        _sitcmram = .;
        *(.itcmram)
        *(.itcmram*)
        . = ALIGN(4);
        _eitcmram = .;
    } >ITCMRAM AT> FLASH
    
    /* 初始化数据段 */
    _sidata = LOADADDR(.data);
    .data :
    {
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        *(.data*)
        . = ALIGN(4);
        _edata = .;
    } >DTCMRAM AT> FLASH
    
    /* 未初始化数据 */
    .bss :
    {
        . = ALIGN(4);
        _sbss = .;
        __bss_start__ = _sbss;
        *(.bss)
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
        __bss_end__ = _ebss;
    } >DTCMRAM
    
    /* 用户堆栈 */
    ._user_heap_stack :
    {
        . = ALIGN(8);
        PROVIDE(end = .);
        PROVIDE(_end = .);
        . = . + _Min_Heap_Size;
        . = . + _Min_Stack_Size;
        . = ALIGN(8);
    } >DTCMRAM
    
    /* D1域段 */
    .ram_d1 (NOLOAD) :
    {
        . = ALIGN(4);
        *(.ram_d1)
        *(.ram_d1*)
    } >RAM_D1
    
    /* D2域段 - DMA缓冲 */
    .ram_d2 (NOLOAD) :
    {
        . = ALIGN(32);
        *(.ram_d2)
        *(.ram_d2*)
    } >RAM_D2
    
    /* D3域段 - 低功耗 */
    .ram_d3 (NOLOAD) :
    {
        . = ALIGN(4);
        *(.ram_d3)
        *(.ram_d3*)
    } >RAM_D3
    
    /* 丢弃段 */
    /DISCARD/ :
    {
        libc.a(*)
        libm.a(*)
        libgcc.a(*)
    }
    
    /* 调试符号 */
    .ARM.attributes 0 : { *(.ARM.attributes) }
}

/* 导出符号供C代码使用 */
PROVIDE(_stack = ORIGIN(DTCMRAM) + LENGTH(DTCMRAM));
PROVIDE(_estack = _stack);
PROVIDE(_sidata = LOADADDR(.data));

八、调试技巧

1. MAP文件分析
# 生成MAP文件
arm-none-eabi-gcc -Wl,-Map=output.map

# MAP文件显示每个符号的地址
2. 查看段信息
# 查看段大小和地址
arm-none-eabi-size -A -x output.elf

# 查看所有段
arm-none-eabi-objdump -h output.elf

# 查看符号表
arm-none-eabi-nm output.elf
3. 常见链接脚本命令
/* ASSERT: 断言检查 */
ASSERT(_etext <= (ORIGIN(FLASH) + LENGTH(FLASH)), "FLASH overflow")
ASSERT(_edata <= (ORIGIN(RAM) + LENGTH(RAM)), "RAM overflow")

/* INCLUDE: 包含其他脚本 */
INCLUDE "common.ld"

/* INSERT: 插入内容 */
INSERT AFTER .text;

/* NOCROSSREFS: 禁止段间引用 */
NOCROSSREFS(.text .data)

九、实用模板和技巧

1. 计算段大小
SECTIONS
{
    .text :
    {
        __text_start = .;
        *(.text*)
        __text_end = .;
    } >FLASH
    
    /* 在C代码中使用 */
    /* extern uint32_t __text_start, __text_end; */
    /* size = &__text_end - &__text_start; */
}
2. 预留引导程序空间
MEMORY
{
    BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 32K
    FIRMWARE (rx)   : ORIGIN = 0x08008000, LENGTH = 480K
}
3. 创建固定地址表
SECTIONS
{
    .jump_table 0x08000100 :
    {
        KEEP(*(.jump_table))
    } >FLASH
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐