@[toc]
面向 MCU 与 RTOS 的 Newlib、Newlib-nano、--specs=nano.specs 与 _REENT_SMALL 说明
适用对象:使用 GCC/Arm GNU Toolchain、RT-Thread、FreeRTOS、Zephyr 或类似 MCU RTOS 的嵌入式 C/C++ 工程。
重点问题:newlib 是什么、为什么 MCU 工程会链接它、newlib-nano 如何减少体积、--specs=nano.specs和_REENT_SMALL的作用与风险,以及如何不选用 newlib 而改为工程自实现或替换部分函数。
1. 结论先行
在 MCU/RTOS 工程里,newlib 可以理解为 GCC 裸机/嵌入式工具链常用的 C 标准库实现。它提供 memcpy、memset、strlen、printf、snprintf、malloc、free、errno、time、strtok 等标准 C 或类 POSIX 接口,但它并不知道你的硬件上有什么 UART、文件系统、heap、线程或设备驱动。因此,newlib 需要工程或 RTOS 提供底层适配层,例如 _write()、_read()、_sbrk()、_fstat()、_isatty() 等 syscall/stub。
对资源受限 MCU,通常不会直接使用完整 newlib,而是使用 newlib-nano。newlib-nano 是面向代码体积和 RAM 占用优化的 newlib 变体。对于 Arm GCC 裸机工程,常见启用方式是链接时加入:
1 | --specs=nano.specs |
这个选项不是 C 宏,而是 GCC driver 的 spec 文件选择,会影响链接阶段选择的库和启动/结束文件规则。通常它会让工程链接 libc_nano.a、libstdc++_nano.a 等 nano 版本库,而不是普通 libc.a / libstdc++.a。
_REENT_SMALL 是另一个层面的配置。它是预处理宏,影响 newlib 头文件中 struct _reent 的布局。struct _reent 是 newlib 保存线程局部 libc 状态的核心结构,例如 errno、strtok 状态、部分 stdio 状态、随机数状态、时间函数临时状态等。开启 _REENT_SMALL 后,struct _reent 会尽量变小,很多内容改为“首次使用时再分配”。这对 RTOS 多线程非常有价值,因为如果每个线程都带一个 _reent,单线程节省几百字节,多线程就可能节省数 KB RAM。
但它不是无条件更好。核心风险有三个:
- 必须与实际链接的 newlib/newlib-nano ABI 配套,否则头文件看到的结构布局和库内部实现不一致,会导致难以定位的运行时错误。
- 可能引入首次调用时的动态分配,如果 heap 很小、禁用 heap 或 malloc 锁不正确,问题会更早暴露。
- 复杂 stdio、locale、宽字符、浮点格式化、C++ 异常等功能可能和代码体积目标冲突,需要按工程实际功能测试。
工程上更稳妥的规则是:
1 | 使用 newlib-nano,并确认工具链/RTOS 配套支持小 reent:可以定义 _REENT_SMALL |
2. 为什么嵌入式工程会遇到 libc 问题
在 Linux 应用开发里,printf()、malloc()、memcpy() 看起来是“天然存在”的,因为系统已经提供了完整 libc、内核系统调用、文件描述符、进程、终端、动态内存管理等运行环境。
MCU 裸机或 RTOS 不一样。上电后只有:
1 | 复位向量 -> startup -> 初始化 .data/.bss -> SystemInit -> main/thread entry |
是否有文件系统、是否有标准输入输出、heap 从哪里来、stdout 写到哪里、errno 是全局还是线程局部、exit() 应该做什么,这些都不是硬件自动提供的。
因此,当你写下:
1 |
|
编译器和链接器看到的不只是一个 printf()。背后可能牵涉:
1 | printf -> vfprintf -> FILE/stdout -> _write |
Sourceware 的 newlib 手册明确说明:newlib 的某些函数依赖底层操作系统服务;在嵌入式或 bare-board 场景,如果系统没有这些服务,至少需要提供空实现或最小实现,才能让程序和 libc.a 链接通过。[^sourceware-libc-system-calls]
Memfault 的 “Bootstrapping libc with Newlib” 文章也用裸机例子说明:没有 libc 时,printf、memcpy、strncpy 等常见函数都不可用;即使源码没有显式调用某些函数,编译器也可能为了初始化或优化生成对 memset、memcpy 等库函数的引用。[^memfault-newlib]
3. newlib 是什么
newlib 是面向嵌入式系统的 C library。Sourceware 官方主页对 newlib 的定位是:intended for use on embedded systems。[^sourceware-newlib-home]
更工程化地说,newlib 做了两件事:
提供硬件无关的 C 标准库实现
例如字符串、内存、格式化输入输出、数学库、stdlib、time、errno、locale、部分 POSIX-like 包装函数等。把少量硬件/OS 相关能力留给目标系统实现
例如_write()如何输出、_read()如何读取、_sbrk()如何扩展 heap、_fstat()如何描述文件、_isatty()是否是终端等。
这正适合 MCU 场景:C 标准库大量逻辑可以复用,少量与板级/RTOS 强相关的入口由 BSP 或 RTOS 提供。
可以把它看成下面的层次:
1 | 应用层 |
在 RT-Thread 中,这类适配通常位于类似:
1 | rt-thread/components/libc/compilers/newlib/syscalls.c |
不同 BSP 或 RT-Thread 版本细节可能不同,但职责相近:把 newlib 的底层需求转接到 RT-Thread 的设备、heap、线程或 console 实现。
4. newlib 在 MCU/RTOS 中通常提供哪些能力
下面按工程影响分类说明。
4.1 内存与字符串函数
常见函数:
1 | memcpy() |
这类函数通常是最容易被间接引入的。即使你没有 #include <string.h>,编译器也可能在某些场景生成 memcpy / memset 调用。例如大对象初始化、结构体赋值、数组清零等。
如果完全不链接 newlib,就必须注意这些符号是否由编译器内建展开,还是需要外部实现。常见策略是:
1 | 1. 自己提供 memcpy/memset/memmove/memcmp 的最小实现 |
4.2 格式化输出输入
常见函数:
1 | printf() |
其中 printf() / snprintf() 是 MCU 工程最常见的体积来源。完整格式化库为了支持宽度、精度、浮点、长整型、locale、文件流等功能,通常会带入较多代码。
对 MCU 来说,通常建议:
1 | 日志输出优先使用 RTOS 自带轻量接口,例如 rt_kprintf |
newlib-nano 为了减小体积,重写或裁剪了部分格式化输入输出路径。newlib-nano README 说明,其格式化输入输出实现不支持部分 newlib 配置选项,例如 C99 formats、long long、long double 等相关选项;浮点格式化需要显式引用 _printf_float 或 _scanf_float 支持函数。[^newlib-nano-readme]
4.3 动态内存
常见函数:
1 | malloc() |
在 newlib 中,malloc() 需要有底层 heap 来源。传统裸机适配里常见 _sbrk() / _sbrk_r(),用于增长程序数据区。newlib 官方手册在 sbrk 说明中明确指出,malloc 及相关函数依赖它,因此独立系统中提供可工作的实现很有用。[^sourceware-libc-system-calls]
在 RTOS 中,动态内存还有两个额外问题:
heap 来源
是 newlib 自己通过_sbrk管理一段堆,还是转接到 RTOS heap,例如rt_malloc/pvPortMalloc。线程安全
多线程同时调用malloc/free时,需要锁。newlib 支持类似__malloc_lock()/__malloc_unlock()的目标相关钩子,RTOS 移植层通常要提供对应互斥保护。
如果工程禁止动态内存,可以选择:
1 | 1. 不使用 malloc/free/calloc/realloc |
4.4 errno 与可重入状态
常见对象和函数:
1 | errno |
很多 C 库函数需要保存状态。单线程裸机可以把这些状态放在全局变量里,但 RTOS 多线程会出现互相覆盖的问题。例如:
1 | 线程 A 调用 read,失败后 errno = EIO |
newlib 通过 struct _reent 和 _impure_ptr 等机制保存当前上下文的 libc 状态。简单理解:
1 | struct _reent = 当前线程的 libc 私有状态 |
RTOS 需要在任务切换或线程初始化时确保 newlib 能拿到当前线程对应的 _reent,否则 errno、strtok、部分 stdio 状态等就可能在线程之间串扰。
Sourceware newlib 手册说明,newlib 的 errno 宏是其支持 reentrant routines 的一部分;OS interface 调用返回的全局 errno 会被记录到相应 reentrancy structure 中。[^sourceware-libc-system-calls]
4.5 时间、文件、进程相关接口
常见函数:
1 | clock() |
对 MCU 来说,这些接口经常只是为了满足链接而提供 stub。比如没有进程概念,fork()、execve()、wait() 可以直接返回错误;没有文件系统,open()、lseek()、stat() 可以返回失败;只有控制台输出时,write() 可以只处理 stdout/stderr。
这也是为什么很多裸机工程里能看到类似实现:
1 | int _isatty(int fd) |
这些实现不是为了提供完整 POSIX,而是为了让 newlib 中依赖 OS 服务的函数可以链接,并在不支持的场景中可控失败。
5. newlib 的 syscall/stub 机制
newlib 官方手册列出了一组 OS interface definitions。对 bare-board 系统,如果底层没有提供这些服务,至少需要提供空实现或最小实现。典型符号包括:[^sourceware-libc-system-calls]
1 | _exit |
Memfault 的文章也列出了类似集合,并说明这些是 newlib 期望底层“操作系统”提供的 system calls。[^memfault-newlib]
在 MCU 工程中,这些 syscall 可以分为三类。
5.1 必须认真实现的接口
| 接口 | 典型触发函数 | 工程建议 |
|---|---|---|
_write / write |
printf、puts、fwrite |
转接到 UART、RTT、USB CDC、RTOS console 或设备框架 |
_read / read |
scanf、getchar、fread |
不用输入时可以返回 0 或错误;用 shell/console 时接设备读取 |
_sbrk / _sbrk_r |
malloc、部分 stdio、部分 reent 懒分配 |
如果允许 heap,要和 linker script/RTOS heap 一致;如果禁用 heap,应显式失败 |
__malloc_lock / __malloc_unlock |
多线程 malloc/free |
用 RTOS mutex/critical section 保护 |
5.2 可以最小实现的接口
| 接口 | 常见最小策略 |
|---|---|
_fstat |
把 stdout/stderr 视为字符设备,返回 S_IFCHR |
_isatty |
对 console fd 返回 1 |
_lseek |
无文件系统时返回 0 或 -1,视函数调用路径而定 |
_close |
无文件系统时返回 -1 |
_getpid |
返回固定值 1 |
_kill |
返回 -1 并设置 errno |
5.3 通常不支持的接口
| 接口 | 原因 |
|---|---|
fork |
MCU RTOS 通常没有进程复制语义 |
execve |
通常没有进程镜像替换语义 |
wait |
通常没有子进程 |
link / unlink |
无文件系统时不支持 |
6. RTOS 下的 reentrant:为什么 newlib 需要 struct _reent
6.1 问题来源
C 标准库里有一些接口天然带内部状态:
| 状态类型 | 例子 |
|---|---|
| 错误码 | errno |
| 字符串分割状态 | strtok |
| 随机数状态 | rand / srand |
| 时间临时缓冲 | asctime / localtime / gmtime |
| stdio 状态 | stdin / stdout / stderr / FILE / 缓冲区 |
| malloc 状态 | heap 管理结构、锁 |
在单线程系统里,这些状态可以放全局变量。RTOS 多线程下,全局状态会互相污染。因此 newlib 为许多接口提供 reentrant 版本,例如:
1 | int _write_r(struct _reent *ptr, int fd, const void *buf, size_t len); |
这些 _xxx_r() 函数比普通函数多一个 struct _reent * 参数,用来明确当前上下文。普通接口内部则通过当前 _impure_ptr 或类似机制取得当前线程的 reent 状态。
6.2 RTOS 需要做什么
一个比较完整的 RTOS/newlib 集成通常要处理:
1 | 1. 每个线程是否有自己的 struct _reent |
如果 RTOS 没有正确处理这些问题,表面现象可能是:
1 | errno 在线程间串扰 |
7. newlib-nano 是什么
newlib-nano 是 newlib 的小型化变体,目标是减少嵌入式工程中的代码体积和 RAM 占用。
newlib-nano README 说明,newlib-nano 与 newlib 的使用方式基本相同,但会使用一组面向小体积的配置选项,例如:[^newlib-nano-readme]
1 | --enable-newlib-reent-small |
这些选项体现了 newlib-nano 的设计方向:
1 | 减小 struct _reent |
Zephyr 文档也把 full newlib 描述为能力更完整、偏性能、footprint 明显大于 nano 变体的版本。[^zephyr-newlib]
MCU on Eclipse 的文章总结过 newlib-nano 的适用场景:更看重小代码体积、小 RAM、较小 heap 占用,而不是完整特性和最高性能。[^mcuoneclipse-stdlib]
8. --specs=nano.specs 是什么
8.1 GCC driver 与 spec 文件
gcc 不是单一编译器可执行文件,而是一个 driver。它会按阶段调用预处理器、编译器、汇编器和链接器。GCC 官方文档说明,编译过程可包含预处理、编译、汇编、链接四个阶段。[^gcc-overall]
GCC Internals 文档进一步说明,gcc driver 会根据命令行参数决定调用哪些子程序以及传递哪些选项,这种行为由 spec strings 控制;内建 spec strings 可以通过 -specs= 指定 spec 文件来覆盖。[^gcc-spec-files]
因此:
1 | --specs=nano.specs |
不是 C 代码里的宏,也不是简单的 -D 选项。它是告诉 GCC driver:使用名为 nano.specs 的 spec 文件,改变默认链接规则。
8.2 它通常改变什么
在 Arm GNU Toolchain 这类裸机工具链中,nano.specs 通常会影响:
1 | 1. 链接普通 libc 还是 libc_nano |
Metin Balci 的文章通过 STM32CubeIDE 和 Arm GNU Toolchain 示例解释了 nano.specs、newlib-nano、nosys.specs、libnosys 的关系。[^metinbalci-nano]
Arm 工具链相关文档也说明,newlib-nano 被作为独立包提供,并且意图等价于 GCC 使用 --specs=nano.specs 的方式。[^arm-toolchain-newlib]
8.3 怎么确认是否真的用了 newlib-nano
不要只看 IDE 图形选项。最可靠的是看最终链接命令和 map 文件。
方法 1:看最终链接命令
构建日志中应能看到类似:
1 | arm-none-eabi-gcc ... --specs=nano.specs ... |
如果没有出现在最终链接命令中,IDE 里的 “Use newlib-nano” 勾选项可能没有实际生效。
方法 2:让 GCC 打印实际展开
1 | arm-none-eabi-gcc -v --specs=nano.specs main.o -o app.elf |
或:
1 | arm-none-eabi-gcc -### --specs=nano.specs main.o -o app.elf |
-### 会打印将要执行的子命令,适合检查 driver 如何传参。
方法 3:查 spec 文件路径
1 | arm-none-eabi-gcc --print-file-name=nano.specs |
如果返回具体路径,说明工具链中存在该 spec 文件。
方法 4:查 map 文件
在 map 文件中搜索:
1 | libc_nano.a |
是否出现取决于工具链版本和链接参数,但 libc_nano.a 是最直接的信号。
9. _REENT_SMALL 是什么
9.1 它是编译期宏,不是链接选项
_REENT_SMALL 通常通过编译参数或预包含头文件定义,例如:
1 | -D_REENT_SMALL |
或:
1 |
它影响 newlib 头文件,尤其是:
1 |
在 sys/reent.h 中,_REENT_SMALL 决定 struct _reent 的布局。newlib 相关头文件注释说明:如果定义 _REENT_SMALL,会尽可能让 struct _reent 变小,方式是把尽可能多的内容改为首次使用时分配。[^reent-h]
9.2 它解决什么问题
RTOS 每个线程可能需要一个 newlib reent 状态。如果 struct _reent 很大,线程数一多,RAM 占用会明显增加。
假设:
1 | 普通 struct _reent 约 1 KiB |
那么每线程节省约 700800 B,总体可能节省 78 KiB。这对 64 KiB / 128 KiB SRAM 的 MCU 很明显。
具体数值会随 newlib 版本、目标架构、配置选项不同而变化,不能直接照搬别人的数值。应在本地工具链中测量。
9.3 它的原理
未开启 _REENT_SMALL 时,struct _reent 更倾向于直接包含较多状态。优点是访问直接,某些状态不需要首次调用时分配;缺点是每个线程都预留较大空间。
开启 _REENT_SMALL 后,struct _reent 变成更小的入口结构。部分成员改成指针或按需初始化。例如只有当你真的使用某些 stdio、locale、转换、时间或大整数相关逻辑时,才分配对应内部对象。
因此,它本质是:
1 | 用“首次使用时的额外路径/可能分配”换“每个线程常驻 RAM 减少” |
9.4 它和 newlib-nano 的关系
newlib-nano 的构建配置通常包含:
1 | --enable-newlib-reent-small |
这表示库本身按小 reent 支持构建。应用侧再定义 _REENT_SMALL,使源码编译时看到的 struct _reent 布局与库一致。
二者层级不同:
| 项 | 层级 | 作用 |
|---|---|---|
--specs=nano.specs |
GCC driver / 链接规则 | 选择 newlib-nano 相关库和链接规则 |
_REENT_SMALL |
预处理 / 头文件 ABI | 改变 struct _reent 结构定义 |
--enable-newlib-reent-small |
newlib 构建配置 | 构建支持 small reent 的库 |
9.5 优点
| 优点 | 说明 |
|---|---|
| 降低每线程 RAM | RTOS 多线程场景收益最大 |
| 更符合 newlib-nano 目标 | 小代码、小数据、小 heap 压力 |
| 对少量 libc 功能使用者更划算 | 主要用 memcpy、snprintf、轻量日志时,常驻状态不必很大 |
9.6 缺点和风险
| 风险 | 说明 |
|---|---|
| ABI/布局不一致 | 头文件定义 _REENT_SMALL,但链接库不是对应配置,可能运行时错误 |
| 首次使用路径更复杂 | 某些状态首次使用时才分配或初始化,执行路径不可完全视为常数 |
| 依赖 heap/锁正确性 | 懒分配可能调用 malloc;malloc 锁、heap、_sbrk 必须可靠 |
| 调试更复杂 | 崩溃可能出现在第一次调用某个 libc 功能,而不是启动阶段 |
| 对复杂 libc 功能不友好 | 文件、locale、宽字符、复杂 stdio、C++ 异常等需要充分测试 |
10. newlib-nano 与 _REENT_SMALL 的典型取舍
10.1 推荐使用 newlib-nano 的情况
1 | MCU SRAM/Flash 较紧张 |
10.2 不建议盲目使用的情况
1 | 项目依赖完整 stdio 文件流 |
10.3 浮点 printf 的特殊问题
newlib-nano 为了减少体积,通常不会默认带入完整浮点格式化支持。如果你写:
1 | printf("voltage=%f\n", v); |
可能需要链接:
1 | -u _printf_float |
scanf 浮点类似:
1 | -u _scanf_float |
newlib-nano README 明确说明,需要格式化浮点输入输出的程序必须在链接时显式引用 _scanf_float 或 _printf_float。[^newlib-nano-readme]
工程建议:
1 | 能不用浮点 printf 就不用 |
示例:
1 | /* 避免 */ |
11. 哪些函数会触发 newlib 依赖
下面这个表按“容易把库拉进来”的程度分类。
| 使用内容 | 可能拉入的 newlib 模块 | 底层需求 | 代码体积风险 |
|---|---|---|---|
memcpy/memset/memmove |
string/memory | 通常无 syscall | 低 |
strlen/strcmp/strchr |
string | 通常无 syscall | 低 |
snprintf/vsnprintf |
formatted output | 可能涉及 reent/locale/转换 | 中到高 |
printf/puts |
stdio/vfprintf | _write、stdout、stdio state |
中到高 |
scanf/sscanf |
formatted input | _read、转换、缓冲 |
高 |
malloc/free |
allocator | _sbrk 或 RTOS heap、锁 |
中 |
assert/abort/exit |
exit/signal/stdio flush | _exit、可能 stdio flush |
中 |
time/clock |
time | times / time source |
中 |
fopen/fread/fwrite |
full stdio/files | open/read/write/lseek/close/fstat | 高 |
C++ new/delete |
libstdc++ / malloc | malloc/free、异常相关 | 中到高 |
| C++ iostream | libstdc++ iostream | stdio、locale、heap | 很高 |
12. 如何不选 newlib,而是自行实现部分函数
有三种路线,风险和工作量不同。
12.1 路线 A:仍链接 newlib,但替换少量函数
这是最常用、风险最低的方式。
因为 newlib 通常是静态库,且每个函数在库中是独立目标文件。链接器查找符号时,如果你的工程对象文件已经提供了同名强符号,通常就不会再从静态库里拉取对应对象。Memfault 文章也说明了这种替换方式:想替换 newlib 的某个函数,可以在程序中定义该函数,链接器找到你的实现后就不会继续从静态库中找。[^memfault-newlib]
典型替换目标:
1 | printf |
示例:用轻量 printf 替换标准 printf:
1 |
|
更常见的是宏映射:
1 |
宏映射简单,但只影响包含该宏的编译单元。库内部或第三方对象文件已经引用的 printf 不会被宏替换。因此对全局替换,函数强符号替换或链接器 wrap 更可控。
12.2 路线 B:链接 newlib-nano,并关闭重功能
这是 MCU 工程的常规优化路线:
1 | --specs=nano.specs |
再配合:
1 | 不启用 _printf_float / _scanf_float |
这条路线不是“不用 newlib”,但往往是投入产出最高的优化方式。
12.3 路线 C:完全不链接标准库,自行实现所需子集
如果要完全不使用 newlib,可以使用:
1 | -nostdlib |
或更细地控制:
1 | -nodefaultlibs |
区别大致是:
| 选项 | 影响 |
|---|---|
-nostdlib |
不使用标准启动文件和标准库 |
-nodefaultlibs |
不使用默认系统库,但启动文件仍可能使用 |
-nostartfiles |
不使用标准启动文件,但默认库仍可能使用 |
完全自实现时,你需要至少考虑:
1 | 1. startup 代码 |
注意:即使不用 newlib,也通常仍会用到 libgcc。libgcc 提供编译器运行时辅助函数,例如某些软浮点、64 位除法、内建操作支持等。完全去掉 libgcc 的难度远高于去掉 newlib。
13. 自行实现函数时的建议边界
13.1 适合自实现的函数
| 函数 | 原因 |
|---|---|
memcpy/memset/memmove/memcmp |
实现简单,体积可控,常被编译器隐式引用 |
strlen/strcmp/strncmp |
简单、确定性强 |
putchar/puts/printf |
可直接绑定 RTOS console,避免完整 stdio |
_write/_read/_sbrk/_exit |
本来就需要 BSP/RTOS 适配 |
malloc/free |
如果要统一到 RTOS heap,适合替换 |
13.2 不建议轻易自实现的函数
| 函数/模块 | 原因 |
|---|---|
完整 snprintf |
格式化规则复杂,边界情况多 |
scanf |
格式化输入复杂,容易出错,体积也大 |
| 浮点格式化 | 舍入、NaN/Inf、精度、宽度处理复杂 |
strtod/printf float |
测试成本高 |
| locale/wchar | MCU 中通常不用,完整实现不划算 |
| C++ iostream | 依赖复杂,不适合作为轻量替换目标 |
13.3 替换 printf 的常用策略
策略 1:宏替换
1 |
优点:最小改动。
缺点:只对包含宏的源码有效,不影响库和已编译对象。
策略 2:强符号函数替换
1 | int printf(const char *fmt, ...) |
优点:链接层全局有效。
缺点:要确保 ABI 和返回值语义合理,避免与库内部符号冲突。
策略 3:链接器 wrap
1 | -Wl,--wrap=printf |
然后实现:
1 | int __wrap_printf(const char *fmt, ...) |
优点:可拦截并统计调用。
缺点:构建系统要支持,第三方库和 LTO 场景需要测试。
14. 工程裁剪 newlib/newlib-nano 的实用方法
14.1 从 map 文件看真实来源
打开 map 文件,搜索:
1 | printf |
重点看是哪一个对象文件把大模块拉进来的。例如:
1 | main.o -> printf -> vfprintf -> libc_nano.a(lib_a-nano-vfprintf.o) |
如果某个第三方库调用了 printf,即使应用层宏替换了 printf,也可能仍然把标准 printf 拉进来。
14.2 用 nm/size/objdump 辅助定位
1 | arm-none-eabi-size app.elf |
查找最大符号:
1 | arm-none-eabi-nm -S --size-sort app.elf | tail -50 |
查找 printf 相关符号:
1 | arm-none-eabi-nm app.elf | grep -E "printf|vfprintf|scanf|float" |
14.3 用链接选项裁剪未引用段
编译:
1 | -ffunction-sections -fdata-sections |
链接:
1 | -Wl,--gc-sections |
这样每个函数/数据更容易被链接器独立回收。注意:如果有函数指针表、初始化数组、链接脚本 KEEP 段,需要确认不会误删必要内容。
14.4 避免高成本 API
高成本 API 包括:
1 | printf float |
替代建议:
| 需求 | 替代方案 |
|---|---|
| 日志输出 | RTOS console / ringbuffer / ITM / RTT / UART driver |
| 浮点日志 | 固定点整数缩放输出 |
| 简单格式化 | tiny printf / RTOS snprintf |
| 动态内存 | RTOS heap / mempool / slab / 静态分配 |
| 文件 I/O | RTOS DFS 或明确封装的 block/file API |
| C++ 对象 | 禁用 exception/RTTI,避免 iostream |
15. 在 RT-Thread 类工程中的建议配置
如果你的工程是 RT-Thread + Arm GCC + MCU,比较稳妥的配置路径是:
1 | 1. 明确 libc 选择:tiny libc / newlib / newlib-nano 只能选一个主路径 |
15.1 推荐检查命令
1 | # 1. 确认 nano.specs 路径 |
15.2 _REENT_SMALL 本地验证方法
创建:
1 |
|
分别编译:
1 | arm-none-eabi-gcc -c reent_size.c -o reent_normal.o |
查看数组大小:
1 | arm-none-eabi-nm -S reent_normal.o | grep reent_size_check |
如果两个大小不同,说明 _REENT_SMALL 确实影响了当前工具链头文件里的 struct _reent。
16. 常见误区
16.1 “工程源码里搜不到 _REENT_SMALL,所以没生效”
不一定。_REENT_SMALL 通常是在工具链 newlib 头文件里使用,例如 sys/reent.h,不是 RTOS 工程源码直接使用。
应该在工具链目录搜索:
1 | Select-String -Path "D:\arm-gnu-toolchain\arm-none-eabi\include\**\*.h" -Pattern "_REENT_SMALL" |
16.2 “开启 _REENT_SMALL 一定更好”
不对。它减少常驻 _reent RAM,但可能增加首次调用路径和 heap 依赖。只有在 newlib-nano/RTOS 支持配套时才建议开启。
16.3 “用了 --specs=nano.specs 就一定没有完整 printf”
不对。newlib-nano 只是默认更小。只要你启用 _printf_float、使用复杂格式化或引入 C++ iostream,体积仍然可能明显增加。
16.4 “不用 malloc,newlib 就不会用 heap”
不一定。部分 stdio、reent small 懒分配、C++ runtime 或第三方库可能内部使用动态内存。要靠 map、符号和运行时断点确认。
16.5 “只要实现 _write,printf 就完全安全”
不对。printf 还可能涉及 stdio 锁、缓冲、reent、格式化转换、浮点、heap。强实时路径最好避免标准 printf。
17. 推荐决策表
| 工程情况 | 建议 |
|---|---|
只用基本 C、RTOS 日志、少量 snprintf |
newlib-nano + _REENT_SMALL,并验证 map |
| RTOS 线程多,RAM 紧张 | 优先验证 _REENT_SMALL 节省量 |
| 禁止 heap | 避免复杂 stdio;_sbrk 返回失败;替换 printf/snprintf |
| 需要文件系统和完整 FILE* | full newlib 或充分测试 newlib-nano |
| 需要 printf 浮点 | 显式启用 _printf_float,但评估体积 |
| 追求最小体积 | 自实现 libc 子集 + 替换 printf + 避免 newlib stdio |
| 使用 C++ | 禁用异常/RTTI/iostream;注意 libstdc++_nano 与 malloc |
| 不确定当前实际 libc | 先看最终链接命令和 map,不根据 IDE 勾选项判断 |
18. 最小化落地建议
如果目标是“既保持工程稳定,又减少代码/RAM 占用”,建议按以下顺序做:
1 | 第一步:确认最终链接命令是否使用 --specs=nano.specs |
不建议一开始就完全去掉 newlib。多数 MCU/RTOS 工程中,newlib-nano + 正确 syscall + 小心使用 stdio,已经能达到较好的平衡。完全自实现 libc 子集适合产品线高度受控、对体积极端敏感、测试体系完善的项目。
19. 参考资料
[^sourceware-newlib-home]: Sourceware, “The Newlib Homepage”, https://sourceware.org/newlib/ 。官方主页将 newlib 定位为用于嵌入式系统的 C library。
[^sourceware-libc-system-calls]: Sourceware, “The Red Hat newlib C Library”, https://sourceware.org/newlib/libc.html 。参考其中 Introduction、System Calls、Definitions for OS interface、Reentrant covers for OS subroutines、Reentrancy 等章节。
[^gcc-overall]: GNU GCC Manual, “Options Controlling the Kind of Output”, https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html 。该文档说明 GCC 编译过程可包括预处理、编译、汇编和链接阶段。
[^gcc-spec-files]: GNU GCC Internals, “Specifying Subprocesses and the Switches to Pass to Them”, https://gcc.gnu.org/onlinedocs/gccint/Spec-Files.html 。该文档说明 gcc 是 driver,并且 -specs= 可指定 spec 文件覆盖内建 spec strings。
[^newlib-nano-readme]: newlib-nano README, https://github.com/32bitmicro/newlib-nano-2/blob/master/newlib/README.nano 。该 README 描述了 newlib-nano 的配置选项、格式化 I/O 限制以及 _printf_float / _scanf_float 的链接需求。
[^reent-h]: newlib sys/reent.h 示例,https://chromium.googlesource.com/native_client/nacl-newlib/+/a9ae3c60b36dea3d8a10e18b1b6db952d21268c2/newlib/libc/include/sys/reent.h 。其中注释说明定义 _REENT_SMALL 时会尽可能减小 struct _reent,并将很多内容改为首次使用时分配。不同工具链版本的具体头文件可能不同,应以本地工具链为准。
[^memfault-newlib]: Memfault Interrupt, “From Zero to main(): Bootstrapping libc with Newlib”, https://interrupt.memfault.com/blog/bootstrapping-libc-with-newlib 。该文从裸机启动角度介绍 newlib、syscall、构造函数、多线程和替换部分/全部 C 标准库的方法。
[^metinbalci-nano]: Metin Balci, “Demystifying Arm GNU Toolchain Specs: nano and nosys”, https://metebalci.com/blog/demystifying-arm-gnu-toolchain-specs-nano-and-nosys/ 。该文结合 STM32CubeIDE 和 Arm GNU Toolchain 说明 nano.specs、newlib-nano、nosys.specs 和 libnosys 的作用。
[^mcuoneclipse-stdlib]: MCU on Eclipse, “Which Embedded GCC Standard Library? newlib, newlib-nano, …”, https://mcuoneclipse.com/2023/01/28/which-embedded-gcc-standard-library-newlib-newlib-nano/ 。该文讨论嵌入式 GCC 标准库选择、newlib-nano 的适用场景和浮点 printf/scanf 体积问题。
[^zephyr-newlib]: Zephyr Project Documentation, “Newlib”, https://docs.zephyrproject.org/latest/develop/languages/c/newlib.html 。该文说明 Zephyr 中 newlib/full newlib/newlib nano 的定位与 footprint 差异。
[^arm-toolchain-newlib]: Arm Toolchain repository, “Experimental newlib support”, https://github.com/arm/arm-toolchain/blob/arm-software/arm-software/embedded/docs/newlib.md 。该文说明 Arm Toolchain for Embedded 中 newlib 支持及 newlib-nano 与 GCC --specs=nano.specs 的对应关系。










