STM32两种不同的链接配置方式

对比两种不同的链接配置方式,主要体现在是否使用标准库、启动文件和系统调用支持方式的不同。下面详细逐项对比:


🧩 一、两个配置

配置 1:使用标准启动方式 + nano libc

make 复制代码
LIBS = -lc -lm -lnosys
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=... -Wl,--gc-sections

这是比较 标准、温和的嵌入式设置,特点如下:

项目 说明
-lc -lm 链接标准 libc (newlib) 和 libm (math)
-lnosys 提供空的系统调用桩(用于防止链接 _kill, _getpid 等失败)
-specs=nano.specs 使用 newlib-nano,是更轻量的 libc 版本
没有 -nostdlib 链接器仍然会使用 crt0.o 等默认启动代码
适合需要 printf, malloc 的工程,并希望较小代码体积

配置 2:完全裸机方式

make 复制代码
LDFLAGS_GCC += \
  -nostdlib -nostartfiles \
  -specs=nosys.specs -specs=nano.specs

这是更为彻底控制的裸机配置

项目 说明
-nostdlib 不链接任何标准运行时(包括 libc, libgcc, crt0)
-nostartfiles 不链接 crt0.o, crti.o, crtn.o 等启动代码
-specs=nosys.specs 不启用系统调用(不自动拉入 _kill, _getpid
-specs=nano.specs newlib-nano(但实际上 -nostdlib 会取消它除非手动链接)
❗ 必须自己提供:startup.s_start_exit_write 等运行时支持
适合极简/极控工程,如 bootloader、freestanding firmware

🆚 二、核心区别总结对比表:

特性 配置 1:标准启动 + newlib-nano 配置 2:裸机全控方式
启动文件(如 crt0.o ✅ 自动链接 ❌ 不自动链接
标准 C 库 (newlib) ✅ 默认链接 (-lc) ❌ 不链接(你要手动提供函数或库)
printf, malloc ✅ 可用 ❌ 除非你自己实现或手动链接
链接器脚本(-T linker.ld ✅ 必须提供 ✅ 必须提供
系统调用符号(如 _sbrk ✅ 自动拉入 nosys.specs 或你自定义 ❌ 你必须手动实现
控制程度 ⚪ 中等 🔴 极高,完全裸机

🔧 三、用法建议:

使用标准库,但减小体积(推荐大多数项目)

make 复制代码
LDFLAGS = -T linker.ld -specs=nano.specs -lc -lm -lnosys

适用于:USB、FreeRTOS、HAL、需要 printf、malloc 的项目。

你可以手动重定向 _write 来定制 printf 输出(如重定向到 UART、ITM)。


🔴 完全裸机 / 自己控制一切

make 复制代码
LDFLAGS = -nostdlib -nostartfiles -T linker.ld -Wl,--gc-sections

适用于:bootloader、自定义运行时、不想用 libc 的极简项目。

你需要提供:

  • startup.s(或 _start
  • _sbrk, _write, _exit, _kill 这些裸函数
  • 不要使用 malloc、printf,或者自己实现

✅ 总结一句话:

  • 你希望轻量而不想完全脱离标准库时 👉 用配置 1(nano.specs, -lnosys, -lc -lm
  • 你希望完全掌控链接行为并自己写所有底层实现时 👉 用配置 2(-nostdlib -nostartfiles

当你自己写了 start.s(或 crt0.s)作为启动入口,并正确配置链接脚本使用你的 Reset_Handler_start就不会再链接系统自带的 crt0.o 或其他启动代码了


🔍 为什么不会链接系统的 crt0.o

默认情况下,GNU 工具链(如 arm-none-eabi-gcc)会自动链接标准 C 运行时启动代码 crt0.o(或 crtbegin.ocrtend.o):

  • 前提是你没有加 -nostartfiles-nostdlib
  • 或者你没有提供自定义的入口符号(比如 Reset_Handler

一旦你这样做了,就不会链接系统的启动文件:

  1. 自己写了 start.s/crt0.s,并提供了 _startReset_Handler
  2. 链接器参数中明确指定了 ENTRY(Reset_Handler) 或使用 -nostartfiles

这样,链接器认为你已经准备好了启动逻辑,不需要默认的启动文件,就不会链接它们了。


🚫 如果没注意,会隐式链接系统的 crt0

例如,如果你:

  • 没有 -nostartfiles
  • 没有自己的 start.s
  • 用的是 main(),但没指定入口函数

那编译器会自动加上类似 /lib/gcc/.../crt0.o 来完成初始化。

这就是为啥你有时看到"程序居然能跑 main 但你没写启动文件"------是因为系统偷偷帮你加了。


✅ 总结一句话

只要你提供了自己的 start.s 并控制链接过程(比如用 -nostartfiles 或指定入口),系统就不会再加 crt0.o,你就是自己控制运行时的"导演"。


完全正确 ✅!

链接器(ld)识别你是否需要默认的启动文件(如 crt0.o,关键就是:


🔍 识别入口的两个方式:

✅ 方式 1:在链接脚本中写 ENTRY(...)

ld 复制代码
ENTRY(Reset_Handler)
  • 明确告诉链接器程序从哪开始执行
  • 通常你写了自己的 start.s,就会加这个
  • 会跳过默认的 crt0.o

✅ 方式 2:链接器默认查找 _start

如果你没写 ENTRY(...),链接器默认使用 _start 作为入口:

asm 复制代码
.global _start
_start:
    ...
  • 如果你提供了 _start,链接器会用它,不再链接系统的 _start(如 crt0.o)
  • 否则它会去找系统默认的启动文件来提供 _start

🔧 补充控制选项

GCC 选项 意义
-nostartfiles 不自动加 crt0.o 等启动文件(但还保留标准库)
-nostdlib 更彻底,连 libc(如 newlib/libm) 都不加
-Wl,-e,XXX 显式告诉链接器使用 XXX 作为程序入口,和 ENTRY() 是一样的意思
-T script.ld 指定你自己的链接脚本,通常其中会有 ENTRY()

✅ 总结一句话:

链接器只要看到你指定了 ENTRY() 或你自己提供了 _start(或 Reset_Handler),它就不会再偷偷地加系统的 crt0.o


如果你想彻底掌控裸机程序启动过程,那就要做到:

  • ✅ 写你自己的 start.s
  • ✅ 提供 ENTRY(...)
  • ✅ 明确使用 -nostartfiles(可选)
  • ✅ 提供自己的链接脚本 .ld

说到 crt0(C runtime zero),它是程序启动时的"第一块敲门砖",非常关键。


crt0 到底干了啥?

简单来说,crt0 是程序启动时执行的第一段代码,负责为你的程序搭建运行环境,让 main() 能正常运行。


crt0 的主要工作:

  1. 设置堆栈指针(Stack Pointer)

    在裸机环境下,程序一开始,堆栈指针(SP)没有被设置,crt0 会帮你把 SP 指向正确位置。

  2. 初始化数据段(.data)

    把存储在 Flash 中的初始化变量复制到 RAM。

  3. 清零 BSS 段(.bss)

    清理未初始化的全局变量区(把它们清成 0)。

  4. 调用全局构造函数

    对 C++ 来说,调用 __libc_init_array(),执行所有全局和静态对象的构造函数。

  5. 调用 main() 函数

    运行完初始化后,跳转到用户写的 main(),开始程序主体。

  6. 处理程序结束

    在裸机一般不会返回,但在某些环境,crt0 会处理 main 返回后的清理和退出。


形象比喻

  • crt0 就是舞台灯光和幕布的拉开动作,帮演员(main 函数)做好上场准备。

总结

crt0 负责
初始化硬件环境(堆栈指针)
复制初始化数据(.data)
清空未初始化数据区(.bss)
调用全局/静态构造函数(C++)
进入主函数 main()

当然可以!以下是一个适用于 ARM Cortex-M(如 STM32) 最小 crt0.s 启动文件 示例,内容包括堆栈初始化、.data/.bss 初始化和进入 main() 的全过程。


🔧 示例:最小裸机 crt0.s

asm 复制代码
    .syntax unified
    .cpu cortex-m3
    .thumb

    .global _start
    .global Reset_Handler
    .global main
    .extern __libc_init_array

/* 堆栈顶地址,由链接脚本定义 */
    .section .isr_vector, "a", %progbits
    .word _estack          /* 初始堆栈指针 */
    .word Reset_Handler    /* 复位中断向量 */

    .text
    .thumb_func
Reset_Handler:
_start:
    /* 初始化 .data 段 */
    ldr r0, =_sidata       /* Flash 中 .data 的起始地址 */
    ldr r1, =_sdata        /* RAM 中 .data 的目标起始地址 */
    ldr r2, =_edata        /* RAM 中 .data 的结束地址 */
data_copy:
    cmp r1, r2
    ittt lt
    ldrlt r3, [r0], #4
    strlt r3, [r1], #4
    blt data_copy

    /* 清零 .bss 段 */
    ldr r1, =_sbss
    ldr r2, =_ebss
    movs r3, #0
bss_zero:
    cmp r1, r2
    strlt r3, [r1], #4
    blt bss_zero

    /* 如果是 C++,调用构造函数数组 */
    bl __libc_init_array

    /* 跳转到 main */
    bl main

    /* 如果 main 返回,就停在这里 */
hang:
    b hang

🧩 链接脚本对应段(摘录)

你的链接脚本(.ld 文件)要定义这些符号:

ld 复制代码
_estack = ORIGIN(RAM) + LENGTH(RAM); /* 栈顶 */

_sidata = LOADADDR(.data); /* .data 在 Flash 中的起始 */
_sdata = ADDR(.data);      /* .data 在 RAM 中的起始 */
_edata = .;                /* .data 在 RAM 中的结束 */

_sbss = .;                 /* .bss 起始 */
_ebss = .;                 /* .bss 结束 */

✅ 支持 C/C++ 都可以

  • 如果你用 C++,__libc_init_array 会自动处理全局/静态对象构造。
  • 如果你只用 C,也可以不调用 __libc_init_array

适用范围

✅ 适用于:

  • STM32F1/F4/F7 等 Cortex-M 系列
  • 裸机工程(无 RTOS,无 HAL)

相关推荐
十月的皮皮4 小时前
stm20260720-从新手 C 到量产 STM32 工程:程序设计推导指南
c语言·开发语言·stm32·stm32cubemx·hal库
开发笔记-阿牛5 小时前
解析一款有意思的产品||CK6159A语音芯片设计的儿童专注力训练机-舒尔特玩具
stm32·单片机·嵌入式硬件·音频
搞全栈小苏7 小时前
SIMetrix_simplis8.4安装教程——附安装包
stm32·单片机·嵌入式硬件·电力电子
qq_573967828 小时前
STM32控制器
stm32·单片机·嵌入式硬件
某林2129 小时前
履带小车底盘stm32F103RCT6开发
人工智能·stm32·单片机·嵌入式硬件·架构·人机交互·ros2
做萤石二次开发的哈哈10 小时前
萤石 ERTC Windows SDK 包含哪些类型定义?
stm32·单片机·嵌入式硬件
亿道电子Emdoor11 小时前
【Aitium】PCB导出DXF文件出现eDuplicateRecordName报错
stm32·单片机·嵌入式硬件·altium
巴巴媛6661 天前
STM32学习笔记【38.DHT11实验】
笔记·stm32·学习
深圳市泰凌微电子1 天前
HK航顺 HK32F407RGT7替STM32F405RGT6 微控制器32位MCU
stm32·单片机·嵌入式硬件
小心亦新1 天前
STM32学习16--定时器输出比较2直流电机
stm32·嵌入式硬件·学习