STM32两种不同的链接配置方式
对比两种不同的链接配置方式,主要体现在是否使用标准库、启动文件和系统调用支持方式的不同。下面详细逐项对比:
🧩 一、两个配置
配置 1:使用标准启动方式 + nano libc
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:完全裸机方式
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 或你自定义 | ❌ 你必须手动实现 |
控制程度 | ⚪ 中等 | 🔴 极高,完全裸机 |
🔧 三、用法建议:
✅ 使用标准库,但减小体积(推荐大多数项目)
LDFLAGS = -T linker.ld -specs=nano.specs -lc -lm -lnosys
适用于:USB、FreeRTOS、HAL、需要 printf、malloc 的项目。
你可以手动重定向 _write
来定制 printf
输出(如重定向到 UART、ITM)。
🔴 完全裸机 / 自己控制一切
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.o
、crtend.o
):
- 但前提是你没有加
-nostartfiles
或-nostdlib
- 或者你没有提供自定义的入口符号(比如
Reset_Handler
)
一旦你这样做了,就不会链接系统的启动文件:
- 自己写了
start.s
/crt0.s
,并提供了_start
或Reset_Handler
- 链接器参数中明确指定了
ENTRY(Reset_Handler)
或使用-nostartfiles
这样,链接器认为你已经准备好了启动逻辑,不需要默认的启动文件,就不会链接它们了。
🚫 如果没注意,会隐式链接系统的 crt0
例如,如果你:
- 没有
-nostartfiles
- 没有自己的
start.s
- 用的是
main()
,但没指定入口函数
那编译器会自动加上类似 /lib/gcc/.../crt0.o
来完成初始化。
这就是为啥你有时看到“程序居然能跑 main 但你没写启动文件”——是因为系统偷偷帮你加了。
✅ 总结一句话
只要你提供了自己的
start.s
并控制链接过程(比如用-nostartfiles
或指定入口),系统就不会再加crt0.o
,你就是自己控制运行时的“导演”。
完全正确 ✅!
链接器(ld
)识别你是否需要默认的启动文件(如 crt0.o
),关键就是:
🔍 识别入口的两个方式:
✅ 方式 1:在链接脚本中写 ENTRY(...)
ENTRY(Reset_Handler)
- 明确告诉链接器程序从哪开始执行
- 通常你写了自己的
start.s
,就会加这个 - 会跳过默认的
crt0.o
✅ 方式 2:链接器默认查找 _start
如果你没写 ENTRY(...)
,链接器默认使用 _start
作为入口:
.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 的主要工作:
-
设置堆栈指针(Stack Pointer)
在裸机环境下,程序一开始,堆栈指针(SP)没有被设置,crt0 会帮你把 SP 指向正确位置。 -
初始化数据段(.data)
把存储在 Flash 中的初始化变量复制到 RAM。 -
清零 BSS 段(.bss)
清理未初始化的全局变量区(把它们清成 0)。 -
调用全局构造函数
对 C++ 来说,调用__libc_init_array()
,执行所有全局和静态对象的构造函数。 -
调用 main() 函数
运行完初始化后,跳转到用户写的main()
,开始程序主体。 -
处理程序结束
在裸机一般不会返回,但在某些环境,crt0 会处理 main 返回后的清理和退出。
形象比喻
- crt0 就是舞台灯光和幕布的拉开动作,帮演员(main 函数)做好上场准备。
总结
crt0 负责 |
---|
初始化硬件环境(堆栈指针) |
复制初始化数据(.data) |
清空未初始化数据区(.bss) |
调用全局/静态构造函数(C++) |
进入主函数 main() |
当然可以!以下是一个适用于 ARM Cortex-M(如 STM32)的最小 crt0.s
启动文件示例,内容包括堆栈初始化、.data/.bss
初始化和进入 main()
的全过程。
🔧 示例:最小裸机 crt0.s
.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, r2ittt ltldrlt r3, [r0], #4strlt r3, [r1], #4blt data_copy/* 清零 .bss 段 */ldr r1, =_sbssldr r2, =_ebssmovs r3, #0
bss_zero:cmp r1, r2strlt r3, [r1], #4blt bss_zero/* 如果是 C++,调用构造函数数组 */bl __libc_init_array/* 跳转到 main */bl main/* 如果 main 返回,就停在这里 */
hang:b hang
🧩 链接脚本对应段(摘录)
你的链接脚本(.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)