文章目录
- [面向 MCU 与 RTOS 的 Newlib、Newlib-nano、`--specs=nano.specs` 与 `_REENT_SMALL` 说明](#面向 MCU 与 RTOS 的 Newlib、Newlib-nano、
--specs=nano.specs与_REENT_SMALL说明) -
- [1. 结论先行](#1. 结论先行)
- [2. 为什么嵌入式工程会遇到 libc 问题](#2. 为什么嵌入式工程会遇到 libc 问题)
- [3. newlib 是什么](#3. newlib 是什么)
- [4. newlib 在 MCU/RTOS 中通常提供哪些能力](#4. newlib 在 MCU/RTOS 中通常提供哪些能力)
-
- [4.1 内存与字符串函数](#4.1 内存与字符串函数)
- [4.2 格式化输出输入](#4.2 格式化输出输入)
- [4.3 动态内存](#4.3 动态内存)
- [4.4 errno 与可重入状态](#4.4 errno 与可重入状态)
- [4.5 时间、文件、进程相关接口](#4.5 时间、文件、进程相关接口)
- [5. newlib 的 syscall/stub 机制](#5. newlib 的 syscall/stub 机制)
-
- [5.1 必须认真实现的接口](#5.1 必须认真实现的接口)
- [5.2 可以最小实现的接口](#5.2 可以最小实现的接口)
- [5.3 通常不支持的接口](#5.3 通常不支持的接口)
- [6. RTOS 下的 reentrant:为什么 newlib 需要 `struct _reent`](#6. RTOS 下的 reentrant:为什么 newlib 需要
struct _reent) -
- [6.1 问题来源](#6.1 问题来源)
- [6.2 RTOS 需要做什么](#6.2 RTOS 需要做什么)
- [7. newlib-nano 是什么](#7. newlib-nano 是什么)
- [8. `--specs=nano.specs` 是什么](#8.
--specs=nano.specs是什么) -
- [8.1 GCC driver 与 spec 文件](#8.1 GCC driver 与 spec 文件)
- [8.2 它通常改变什么](#8.2 它通常改变什么)
- [8.3 怎么确认是否真的用了 newlib-nano](#8.3 怎么确认是否真的用了 newlib-nano)
-
- [方法 1:看最终链接命令](#方法 1:看最终链接命令)
- [方法 2:让 GCC 打印实际展开](#方法 2:让 GCC 打印实际展开)
- [方法 3:查 spec 文件路径](#方法 3:查 spec 文件路径)
- [方法 4:查 map 文件](#方法 4:查 map 文件)
- [9. `_REENT_SMALL` 是什么](#9.
_REENT_SMALL是什么) -
- [9.1 它是编译期宏,不是链接选项](#9.1 它是编译期宏,不是链接选项)
- [9.2 它解决什么问题](#9.2 它解决什么问题)
- [9.3 它的原理](#9.3 它的原理)
- [9.4 它和 newlib-nano 的关系](#9.4 它和 newlib-nano 的关系)
- [9.5 优点](#9.5 优点)
- [9.6 缺点和风险](#9.6 缺点和风险)
- [10. newlib-nano 与 `_REENT_SMALL` 的典型取舍](#10. newlib-nano 与
_REENT_SMALL的典型取舍) -
- [10.1 推荐使用 newlib-nano 的情况](#10.1 推荐使用 newlib-nano 的情况)
- [10.2 不建议盲目使用的情况](#10.2 不建议盲目使用的情况)
- [10.3 浮点 printf 的特殊问题](#10.3 浮点 printf 的特殊问题)
- [11. 哪些函数会触发 newlib 依赖](#11. 哪些函数会触发 newlib 依赖)
- [12. 如何不选 newlib,而是自行实现部分函数](#12. 如何不选 newlib,而是自行实现部分函数)
-
- [12.1 路线 A:仍链接 newlib,但替换少量函数](#12.1 路线 A:仍链接 newlib,但替换少量函数)
- [12.2 路线 B:链接 newlib-nano,并关闭重功能](#12.2 路线 B:链接 newlib-nano,并关闭重功能)
- [12.3 路线 C:完全不链接标准库,自行实现所需子集](#12.3 路线 C:完全不链接标准库,自行实现所需子集)
- [13. 自行实现函数时的建议边界](#13. 自行实现函数时的建议边界)
-
- [13.1 适合自实现的函数](#13.1 适合自实现的函数)
- [13.2 不建议轻易自实现的函数](#13.2 不建议轻易自实现的函数)
- [13.3 替换 printf 的常用策略](#13.3 替换 printf 的常用策略)
-
- [策略 1:宏替换](#策略 1:宏替换)
- [策略 2:强符号函数替换](#策略 2:强符号函数替换)
- [策略 3:链接器 wrap](#策略 3:链接器 wrap)
- [14. 工程裁剪 newlib/newlib-nano 的实用方法](#14. 工程裁剪 newlib/newlib-nano 的实用方法)
-
- [14.1 从 map 文件看真实来源](#14.1 从 map 文件看真实来源)
- [14.2 用 nm/size/objdump 辅助定位](#14.2 用 nm/size/objdump 辅助定位)
- [14.3 用链接选项裁剪未引用段](#14.3 用链接选项裁剪未引用段)
- [14.4 避免高成本 API](#14.4 避免高成本 API)
- [15. 在 RT-Thread 类工程中的建议配置](#15. 在 RT-Thread 类工程中的建议配置)
-
- [15.1 推荐检查命令](#15.1 推荐检查命令)
- [15.2 `_REENT_SMALL` 本地验证方法](#15.2
_REENT_SMALL本地验证方法)
- [16. 常见误区](#16. 常见误区)
-
- [16.1 "工程源码里搜不到 `_REENT_SMALL`,所以没生效"](#16.1 “工程源码里搜不到
_REENT_SMALL,所以没生效”) - [16.2 "开启 `_REENT_SMALL` 一定更好"](#16.2 “开启
_REENT_SMALL一定更好”) - [16.3 "用了 `--specs=nano.specs` 就一定没有完整 printf"](#16.3 “用了
--specs=nano.specs就一定没有完整 printf”) - [16.4 "不用 malloc,newlib 就不会用 heap"](#16.4 “不用 malloc,newlib 就不会用 heap”)
- [16.5 "只要实现 `_write`,printf 就完全安全"](#16.5 “只要实现
_write,printf 就完全安全”)
- [16.1 "工程源码里搜不到 `_REENT_SMALL`,所以没生效"](#16.1 “工程源码里搜不到
- [17. 推荐决策表](#17. 推荐决策表)
- [18. 最小化落地建议](#18. 最小化落地建议)
- [19. 参考资料](#19. 参考资料)

面向 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 裸机工程,常见启用方式是链接时加入:
sh
--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++ 异常等功能可能和代码体积目标冲突,需要按工程实际功能测试。
工程上更稳妥的规则是:
text
使用 newlib-nano,并确认工具链/RTOS 配套支持小 reent:可以定义 _REENT_SMALL
使用完整 newlib:不要随便手动定义 _REENT_SMALL
使用 RTOS 自带 tiny libc 或工程自实现 libc 子集:不要定义 _REENT_SMALL
不确定当前链接到哪个 libc:先查最终链接命令和 map 文件,不要只看 IDE 选项
2. 为什么嵌入式工程会遇到 libc 问题
在 Linux 应用开发里,printf()、malloc()、memcpy() 看起来是"天然存在"的,因为系统已经提供了完整 libc、内核系统调用、文件描述符、进程、终端、动态内存管理等运行环境。
MCU 裸机或 RTOS 不一样。上电后只有:
text
复位向量 -> startup -> 初始化 .data/.bss -> SystemInit -> main/thread entry
是否有文件系统、是否有标准输入输出、heap 从哪里来、stdout 写到哪里、errno 是全局还是线程局部、exit() 应该做什么,这些都不是硬件自动提供的。
因此,当你写下:
c
#include <stdio.h>
int main(void)
{
printf("hello\n");
while (1) {}
}
编译器和链接器看到的不只是一个 printf()。背后可能牵涉:
text
printf -> vfprintf -> FILE/stdout -> _write
malloc/free -> _sbrk 或 malloc lock
errno -> 当前线程的 reent 状态
exit/abort/assert -> _exit / signal / atexit
scanf -> _read
clock/time -> times / gettimeofday / time 相关适配
Sourceware 的 newlib 手册明确说明:newlib 的某些函数依赖底层操作系统服务;在嵌入式或 bare-board 场景,如果系统没有这些服务,至少需要提供空实现或最小实现,才能让程序和 libc.a 链接通过。[1](#1)
Memfault 的 "Bootstrapping libc with Newlib" 文章也用裸机例子说明:没有 libc 时,printf、memcpy、strncpy 等常见函数都不可用;即使源码没有显式调用某些函数,编译器也可能为了初始化或优化生成对 memset、memcpy 等库函数的引用。[2](#2)
3. newlib 是什么
newlib 是面向嵌入式系统的 C library。Sourceware 官方主页对 newlib 的定位是:intended for use on embedded systems 。[3](#3)
更工程化地说,newlib 做了两件事:
-
提供硬件无关的 C 标准库实现
例如字符串、内存、格式化输入输出、数学库、stdlib、time、errno、locale、部分 POSIX-like 包装函数等。
-
把少量硬件/OS 相关能力留给目标系统实现
例如
_write()如何输出、_read()如何读取、_sbrk()如何扩展 heap、_fstat()如何描述文件、_isatty()是否是终端等。
这正适合 MCU 场景:C 标准库大量逻辑可以复用,少量与板级/RTOS 强相关的入口由 BSP 或 RTOS 提供。
可以把它看成下面的层次:
text
应用层
app.c / middleware / drivers
C/C++ 标准接口
printf / snprintf / malloc / free / memcpy / strlen / errno / time / new / delete
newlib / newlib-nano
vfprintf / malloc allocator / reent / stdio / string / stdlib / libm
RTOS 或 BSP 适配
_write / _read / _sbrk / _fstat / _isatty / __malloc_lock / __malloc_unlock
硬件与内核对象
UART / USB CDC / RTT console / filesystem / heap / scheduler / mutex
在 RT-Thread 中,这类适配通常位于类似:
text
rt-thread/components/libc/compilers/newlib/syscalls.c
不同 BSP 或 RT-Thread 版本细节可能不同,但职责相近:把 newlib 的底层需求转接到 RT-Thread 的设备、heap、线程或 console 实现。
4. newlib 在 MCU/RTOS 中通常提供哪些能力
下面按工程影响分类说明。
4.1 内存与字符串函数
常见函数:
c
memcpy()
memmove()
memset()
memcmp()
strlen()
strcmp()
strncmp()
strcpy()
strncpy()
strchr()
strstr()
这类函数通常是最容易被间接引入的。即使你没有 #include <string.h>,编译器也可能在某些场景生成 memcpy / memset 调用。例如大对象初始化、结构体赋值、数组清零等。
如果完全不链接 newlib,就必须注意这些符号是否由编译器内建展开,还是需要外部实现。常见策略是:
text
1. 自己提供 memcpy/memset/memmove/memcmp 的最小实现
2. 保留编译器内建优化,但确保未内联场景有外部符号
3. 使用 -ffreestanding 明确 freestanding 环境假设
4. 必要时使用 -fno-builtin 或 -fno-builtin-xxx 限制编译器假设
4.2 格式化输出输入
常见函数:
c
printf()
printf()
snprintf()
vsnprintf()
sprintf()
vprintf()
scanf()
sscanf()
fprintf()
fread()
fwrite()
fopen()
fclose()
其中 printf() / snprintf() 是 MCU 工程最常见的体积来源。完整格式化库为了支持宽度、精度、浮点、长整型、locale、文件流等功能,通常会带入较多代码。
对 MCU 来说,通常建议:
text
日志输出优先使用 RTOS 自带轻量接口,例如 rt_kprintf
如果必须使用 snprintf,确认是否需要浮点、long long、宽字符等
避免在强实时路径调用 printf/snprintf
避免使用 scanf 系列,尤其是浮点 scanf
newlib-nano 为了减小体积,重写或裁剪了部分格式化输入输出路径。newlib-nano README 说明,其格式化输入输出实现不支持部分 newlib 配置选项,例如 C99 formats、long long、long double 等相关选项;浮点格式化需要显式引用 _printf_float 或 _scanf_float 支持函数。[4](#4)
4.3 动态内存
常见函数:
c
malloc()
calloc()
realloc()
free()
在 newlib 中,malloc() 需要有底层 heap 来源。传统裸机适配里常见 _sbrk() / _sbrk_r(),用于增长程序数据区。newlib 官方手册在 sbrk 说明中明确指出,malloc 及相关函数依赖它,因此独立系统中提供可工作的实现很有用。[1](#1)
在 RTOS 中,动态内存还有两个额外问题:
-
heap 来源
是 newlib 自己通过
_sbrk管理一段堆,还是转接到 RTOS heap,例如rt_malloc/pvPortMalloc。 -
线程安全
多线程同时调用
malloc/free时,需要锁。newlib 支持类似__malloc_lock()/__malloc_unlock()的目标相关钩子,RTOS 移植层通常要提供对应互斥保护。
如果工程禁止动态内存,可以选择:
text
1. 不使用 malloc/free/calloc/realloc
2. 对 _sbrk 返回失败,使误用尽早暴露
3. 对 malloc/free 做链接期包装,定位误用点
4. 使用静态内存池或 RTOS mempool 替代
4.4 errno 与可重入状态
常见对象和函数:
c
errno
strtok()
strtok_r()
rand()
srand()
asctime()
localtime()
gmtime()
很多 C 库函数需要保存状态。单线程裸机可以把这些状态放在全局变量里,但 RTOS 多线程会出现互相覆盖的问题。例如:
text
线程 A 调用 read,失败后 errno = EIO
线程 B 调用 malloc,失败后 errno = ENOMEM
线程 A 再读取 errno,可能读到线程 B 的错误
newlib 通过 struct _reent 和 _impure_ptr 等机制保存当前上下文的 libc 状态。简单理解:
text
struct _reent = 当前线程的 libc 私有状态
_impure_ptr = newlib 当前正在使用的 struct _reent 指针
RTOS 需要在任务切换或线程初始化时确保 newlib 能拿到当前线程对应的 _reent,否则 errno、strtok、部分 stdio 状态等就可能在线程之间串扰。
Sourceware newlib 手册说明,newlib 的 errno 宏是其支持 reentrant routines 的一部分;OS interface 调用返回的全局 errno 会被记录到相应 reentrancy structure 中。[1](#1)
4.5 时间、文件、进程相关接口
常见函数:
c
clock()
time()
times()
fstat()
stat()
open()
read()
write()
lseek()
close()
_exit()
fork()
execve()
kill()
wait()
对 MCU 来说,这些接口经常只是为了满足链接而提供 stub。比如没有进程概念,fork()、execve()、wait() 可以直接返回错误;没有文件系统,open()、lseek()、stat() 可以返回失败;只有控制台输出时,write() 可以只处理 stdout/stderr。
这也是为什么很多裸机工程里能看到类似实现:
c
int _isatty(int fd)
{
(void)fd;
return 1;
}
int _fstat(int fd, struct stat *st)
{
(void)fd;
st->st_mode = S_IFCHR;
return 0;
}
void _exit(int status)
{
(void)status;
while (1) {}
}
这些实现不是为了提供完整 POSIX,而是为了让 newlib 中依赖 OS 服务的函数可以链接,并在不支持的场景中可控失败。
5. newlib 的 syscall/stub 机制
newlib 官方手册列出了一组 OS interface definitions。对 bare-board 系统,如果底层没有提供这些服务,至少需要提供空实现或最小实现。典型符号包括:[1](#1)
text
_exit
close
environ
execve
fork
fstat
getpid
isatty
kill
link
lseek
open
read
sbrk
stat
times
unlink
wait
write
Memfault 的文章也列出了类似集合,并说明这些是 newlib 期望底层"操作系统"提供的 system calls。[2](#2)
在 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 版本,例如:
c
int _write_r(struct _reent *ptr, int fd, const void *buf, size_t len);
void *_sbrk_r(struct _reent *ptr, ptrdiff_t incr);
char *_asctime_r(const struct tm *tm, char *buf);
这些 _xxx_r() 函数比普通函数多一个 struct _reent * 参数,用来明确当前上下文。普通接口内部则通过当前 _impure_ptr 或类似机制取得当前线程的 reent 状态。
6.2 RTOS 需要做什么
一个比较完整的 RTOS/newlib 集成通常要处理:
text
1. 每个线程是否有自己的 struct _reent
2. 线程创建时是否初始化 reent
3. 线程退出时是否清理 reent 相关资源
4. 上下文切换时 newlib 当前 reent 指针是否正确
5. malloc/free 是否有锁
6. stdio 是否允许跨线程访问
如果 RTOS 没有正确处理这些问题,表面现象可能是:
text
errno 在线程间串扰
printf 偶发崩溃或输出交错
malloc/free 多线程下 heap 损坏
strtok 在多个线程中互相干扰
线程退出后仍有 newlib 分配的资源泄漏
7. newlib-nano 是什么
newlib-nano 是 newlib 的小型化变体,目标是减少嵌入式工程中的代码体积和 RAM 占用。
newlib-nano README 说明,newlib-nano 与 newlib 的使用方式基本相同,但会使用一组面向小体积的配置选项,例如:[4](#4)
text
--enable-newlib-reent-small
--disable-newlib-fvwrite-in-streamio
--disable-newlib-fseek-optimization
--disable-newlib-wide-orient
--enable-newlib-nano-malloc
--disable-newlib-unbuf-stream-opt
--enable-lite-exit
--enable-newlib-global-atexit
这些选项体现了 newlib-nano 的设计方向:
text
减小 struct _reent
减小 stdio 路径
减少宽字符/stream orientation 相关支持
使用 nano malloc
简化 exit 行为
把 atexit 数据从每线程 reent 中移出,便于未使用时被裁剪
Zephyr 文档也把 full newlib 描述为能力更完整、偏性能、footprint 明显大于 nano 变体的版本。[5](#5)
MCU on Eclipse 的文章总结过 newlib-nano 的适用场景:更看重小代码体积、小 RAM、较小 heap 占用,而不是完整特性和最高性能。[6](#6)
8. --specs=nano.specs 是什么
8.1 GCC driver 与 spec 文件
gcc 不是单一编译器可执行文件,而是一个 driver。它会按阶段调用预处理器、编译器、汇编器和链接器。GCC 官方文档说明,编译过程可包含预处理、编译、汇编、链接四个阶段。[7](#7)
GCC Internals 文档进一步说明,gcc driver 会根据命令行参数决定调用哪些子程序以及传递哪些选项,这种行为由 spec strings 控制;内建 spec strings 可以通过 -specs= 指定 spec 文件来覆盖。[8](#8)
因此:
sh
--specs=nano.specs
不是 C 代码里的宏,也不是简单的 -D 选项。它是告诉 GCC driver:使用名为 nano.specs 的 spec 文件,改变默认链接规则。
8.2 它通常改变什么
在 Arm GNU Toolchain 这类裸机工具链中,nano.specs 通常会影响:
text
1. 链接普通 libc 还是 libc_nano
2. 链接普通 libstdc++ 还是 libstdc++_nano
3. 链接启动文件、结束文件、libnosys/semihosting 支持的组合
4. 是否引入某些 nano 专用库名或路径
Metin Balci 的文章通过 STM32CubeIDE 和 Arm GNU Toolchain 示例解释了 nano.specs、newlib-nano、nosys.specs、libnosys 的关系。[9](#9)
Arm 工具链相关文档也说明,newlib-nano 被作为独立包提供,并且意图等价于 GCC 使用 --specs=nano.specs 的方式。[10](#10)
8.3 怎么确认是否真的用了 newlib-nano
不要只看 IDE 图形选项。最可靠的是看最终链接命令和 map 文件。
方法 1:看最终链接命令
构建日志中应能看到类似:
sh
arm-none-eabi-gcc ... --specs=nano.specs ...
如果没有出现在最终链接命令中,IDE 里的 "Use newlib-nano" 勾选项可能没有实际生效。
方法 2:让 GCC 打印实际展开
sh
arm-none-eabi-gcc -v --specs=nano.specs main.o -o app.elf
或:
sh
arm-none-eabi-gcc -### --specs=nano.specs main.o -o app.elf
-### 会打印将要执行的子命令,适合检查 driver 如何传参。
方法 3:查 spec 文件路径
sh
arm-none-eabi-gcc --print-file-name=nano.specs
如果返回具体路径,说明工具链中存在该 spec 文件。
方法 4:查 map 文件
在 map 文件中搜索:
text
libc_nano.a
libstdc++_nano.a
libg_nano.a
libnosys.a
librdimon.a
是否出现取决于工具链版本和链接参数,但 libc_nano.a 是最直接的信号。
9. _REENT_SMALL 是什么
9.1 它是编译期宏,不是链接选项
_REENT_SMALL 通常通过编译参数或预包含头文件定义,例如:
sh
-D_REENT_SMALL
或:
c
#define _REENT_SMALL
它影响 newlib 头文件,尤其是:
c
#include <sys/reent.h>
在 sys/reent.h 中,_REENT_SMALL 决定 struct _reent 的布局。newlib 相关头文件注释说明:如果定义 _REENT_SMALL,会尽可能让 struct _reent 变小,方式是把尽可能多的内容改为首次使用时分配。[11](#11)
9.2 它解决什么问题
RTOS 每个线程可能需要一个 newlib reent 状态。如果 struct _reent 很大,线程数一多,RAM 占用会明显增加。
假设:
text
普通 struct _reent 约 1 KiB
_REENT_SMALL 后约 200~300 B
线程数 10 个
那么每线程节省约 700~800 B,总体可能节省 7~8 KiB。这对 64 KiB / 128 KiB SRAM 的 MCU 很明显。
具体数值会随 newlib 版本、目标架构、配置选项不同而变化,不能直接照搬别人的数值。应在本地工具链中测量。
9.3 它的原理
未开启 _REENT_SMALL 时,struct _reent 更倾向于直接包含较多状态。优点是访问直接,某些状态不需要首次调用时分配;缺点是每个线程都预留较大空间。
开启 _REENT_SMALL 后,struct _reent 变成更小的入口结构。部分成员改成指针或按需初始化。例如只有当你真的使用某些 stdio、locale、转换、时间或大整数相关逻辑时,才分配对应内部对象。
因此,它本质是:
text
用"首次使用时的额外路径/可能分配"换"每个线程常驻 RAM 减少"
9.4 它和 newlib-nano 的关系
newlib-nano 的构建配置通常包含:
text
--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 的情况
text
MCU SRAM/Flash 较紧张
RTOS 线程较多
只需要基本 C 函数、轻量 printf/snprintf
日志主要走 RTOS 自带 console
不依赖完整文件系统 stdio
不使用或极少使用 scanf
不需要 C++ exception / RTTI / locale / 宽字符
10.2 不建议盲目使用的情况
text
项目依赖完整 stdio 文件流
需要大量 fopen/fread/fwrite/fprintf
需要复杂 locale / 宽字符 / C99 格式化细节
需要 C++ 异常、完整 libstdc++ 行为
heap 禁用或极小,但 libc 功能可能触发懒分配
无法确认工具链 newlib-nano 与头文件宏配置是否一致
10.3 浮点 printf 的特殊问题
newlib-nano 为了减少体积,通常不会默认带入完整浮点格式化支持。如果你写:
c
printf("voltage=%f\n", v);
可能需要链接:
sh
-u _printf_float
scanf 浮点类似:
sh
-u _scanf_float
newlib-nano README 明确说明,需要格式化浮点输入输出的程序必须在链接时显式引用 _scanf_float 或 _printf_float。[4](#4)
工程建议:
text
能不用浮点 printf 就不用
日志中把浮点转成整数缩放输出,例如 mV、uA、Q 格式
如果必须启用,单独评估 .text/.rodata 增量
示例:
c
/* 避免 */
printf("voltage=%f V\n", voltage);
/* 推荐 */
printf("voltage=%ld.%03ld V\n", mv / 1000, mv % 1000);
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 的某个函数,可以在程序中定义该函数,链接器找到你的实现后就不会继续从静态库中找。[2](#2)
典型替换目标:
text
printf
snprintf
malloc/free
_write/_read/_sbrk
_exit
示例:用轻量 printf 替换标准 printf:
c
#include <stdarg.h>
int printf(const char *fmt, ...)
{
va_list ap;
int ret;
va_start(ap, fmt);
ret = rt_vprintf(fmt, ap); /* 示例:转接到 RTOS 自己的输出接口 */
va_end(ap);
return ret;
}
更常见的是宏映射:
c
#define printf rt_kprintf
#define snprintf rt_snprintf
#define sprintf rt_sprintf
宏映射简单,但只影响包含该宏的编译单元。库内部或第三方对象文件已经引用的 printf 不会被宏替换。因此对全局替换,函数强符号替换或链接器 wrap 更可控。
12.2 路线 B:链接 newlib-nano,并关闭重功能
这是 MCU 工程的常规优化路线:
sh
--specs=nano.specs
-Wl,--gc-sections
-ffunction-sections
-fdata-sections
再配合:
text
不启用 _printf_float / _scanf_float
不用 scanf
不用 iostream
不用 C++ exception / RTTI
不用 full FILE* 文件流
这条路线不是"不用 newlib",但往往是投入产出最高的优化方式。
12.3 路线 C:完全不链接标准库,自行实现所需子集
如果要完全不使用 newlib,可以使用:
sh
-nostdlib
或更细地控制:
sh
-nodefaultlibs
-nostartfiles
区别大致是:
| 选项 | 影响 |
|---|---|
-nostdlib |
不使用标准启动文件和标准库 |
-nodefaultlibs |
不使用默认系统库,但启动文件仍可能使用 |
-nostartfiles |
不使用标准启动文件,但默认库仍可能使用 |
完全自实现时,你需要至少考虑:
text
1. startup 代码
2. linker script
3. 向量表
4. .data/.bss 初始化
5. main 入口
6. memcpy/memset/memmove/memcmp
7. 除法/取模等 libgcc 辅助函数
8. __aeabi_* 符号,视架构和编译选项而定
9. C++ 构造函数数组,如果使用 C++
10. assert/abort/exit 等处理
注意:即使不用 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:宏替换
c
#define printf rt_kprintf
优点:最小改动。
缺点:只对包含宏的源码有效,不影响库和已编译对象。
策略 2:强符号函数替换
c
int printf(const char *fmt, ...)
{
/* 自己实现或转接 */
}
优点:链接层全局有效。
缺点:要确保 ABI 和返回值语义合理,避免与库内部符号冲突。
策略 3:链接器 wrap
sh
-Wl,--wrap=printf
然后实现:
c
int __wrap_printf(const char *fmt, ...)
{
/* 自定义实现 */
}
优点:可拦截并统计调用。
缺点:构建系统要支持,第三方库和 LTO 场景需要测试。
14. 工程裁剪 newlib/newlib-nano 的实用方法
14.1 从 map 文件看真实来源
打开 map 文件,搜索:
text
printf
vfprintf
malloc
_sbrk
_write
scanf
_printf_float
_scanf_float
libc.a
libc_nano.a
libstdc++
重点看是哪一个对象文件把大模块拉进来的。例如:
text
main.o -> printf -> vfprintf -> libc_nano.a(lib_a-nano-vfprintf.o)
如果某个第三方库调用了 printf,即使应用层宏替换了 printf,也可能仍然把标准 printf 拉进来。
14.2 用 nm/size/objdump 辅助定位
sh
arm-none-eabi-size app.elf
arm-none-eabi-nm -S --size-sort app.elf
arm-none-eabi-objdump -t app.elf
查找最大符号:
sh
arm-none-eabi-nm -S --size-sort app.elf | tail -50
查找 printf 相关符号:
sh
arm-none-eabi-nm app.elf | grep -E "printf|vfprintf|scanf|float"
14.3 用链接选项裁剪未引用段
编译:
sh
-ffunction-sections -fdata-sections
链接:
sh
-Wl,--gc-sections
这样每个函数/数据更容易被链接器独立回收。注意:如果有函数指针表、初始化数组、链接脚本 KEEP 段,需要确认不会误删必要内容。
14.4 避免高成本 API
高成本 API 包括:
text
printf float
scanf
iostream
locale
wchar
filesystem stdio
C++ exception
替代建议:
| 需求 | 替代方案 |
|---|---|
| 日志输出 | 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,比较稳妥的配置路径是:
text
1. 明确 libc 选择:tiny libc / newlib / newlib-nano 只能选一个主路径
2. 如果选 newlib-nano,最终链接命令必须有 --specs=nano.specs
3. 如果定义 _REENT_SMALL,确认工具链 newlib-nano 本身也按 small reent 构建
4. 确认 RT-Thread 的 newlib syscalls.c 被编译
5. printf/snprintf 如果映射到 RT-Thread,检查第三方库是否仍引用标准 printf
6. 多线程使用 malloc/free 时,确认 malloc lock 和 heap 路径
7. 不启用浮点 printf,除非 map 文件确认可接受
8. 每次调整 libc 配置后,重新检查 map 和 size
15.1 推荐检查命令
sh
# 1. 确认 nano.specs 路径
arm-none-eabi-gcc --print-file-name=nano.specs
# 2. 确认链接命令展开
arm-none-eabi-gcc -### --specs=nano.specs main.o -o app.elf
# 3. 查 map 文件
Select-String -Path app.map -Pattern "libc_nano|libc.a|printf|malloc|_sbrk|_write"
# 4. 查符号
arm-none-eabi-nm -S --size-sort app.elf | tail -50
# 5. 查是否带浮点 printf
arm-none-eabi-nm app.elf | grep _printf_float
15.2 _REENT_SMALL 本地验证方法
创建:
c
#include <sys/reent.h>
char reent_size_check[sizeof(struct _reent)];
分别编译:
sh
arm-none-eabi-gcc -c reent_size.c -o reent_normal.o
arm-none-eabi-gcc -c reent_size.c -D_REENT_SMALL -o reent_small.o
查看数组大小:
sh
arm-none-eabi-nm -S reent_normal.o | grep reent_size_check
arm-none-eabi-nm -S reent_small.o | grep reent_size_check
如果两个大小不同,说明 _REENT_SMALL 确实影响了当前工具链头文件里的 struct _reent。
16. 常见误区
16.1 "工程源码里搜不到 _REENT_SMALL,所以没生效"
不一定。_REENT_SMALL 通常是在工具链 newlib 头文件里使用,例如 sys/reent.h,不是 RTOS 工程源码直接使用。
应该在工具链目录搜索:
powershell
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 占用",建议按以下顺序做:
text
第一步:确认最终链接命令是否使用 --specs=nano.specs
第二步:确认 map 文件里是否使用 libc_nano.a
第三步:确认 _REENT_SMALL 是否由构建系统统一定义,而不是零散手写
第四步:确认 RTOS newlib syscalls.c 被编译并转接到正确设备/heap
第五步:禁用浮点 printf,除非必须
第六步:把日志输出统一到 RTOS 自带接口
第七步:用 nm/map 定位最大符号,再决定是否替换 printf/snprintf/malloc
第八步:若仍然太大,再考虑 -nostdlib 自实现 libc 子集
不建议一开始就完全去掉 newlib。多数 MCU/RTOS 工程中,newlib-nano + 正确 syscall + 小心使用 stdio,已经能达到较好的平衡。完全自实现 libc 子集适合产品线高度受控、对体积极端敏感、测试体系完善的项目。
19. 参考资料
-
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 等章节。 ↩︎ ↩︎ ↩︎ ↩︎
-
Memfault Interrupt, "From Zero to main(): Bootstrapping libc with Newlib", https://interrupt.memfault.com/blog/bootstrapping-libc-with-newlib 。该文从裸机启动角度介绍 newlib、syscall、构造函数、多线程和替换部分/全部 C 标准库的方法。 ↩︎ ↩︎ ↩︎
-
Sourceware, "The Newlib Homepage", https://sourceware.org/newlib/ 。官方主页将 newlib 定位为用于嵌入式系统的 C library。 ↩︎
-
newlib-nano README, https://github.com/32bitmicro/newlib-nano-2/blob/master/newlib/README.nano 。该 README 描述了 newlib-nano 的配置选项、格式化 I/O 限制以及
_printf_float/_scanf_float的链接需求。 ↩︎ ↩︎ ↩︎ -
Zephyr Project Documentation, "Newlib", https://docs.zephyrproject.org/latest/develop/languages/c/newlib.html 。该文说明 Zephyr 中 newlib/full newlib/newlib nano 的定位与 footprint 差异。 ↩︎
-
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 体积问题。 ↩︎
-
GNU GCC Manual, "Options Controlling the Kind of Output", https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html 。该文档说明 GCC 编译过程可包括预处理、编译、汇编和链接阶段。 ↩︎
-
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。 ↩︎ -
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的作用。 ↩︎ -
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的对应关系。 ↩︎ -
newlib
sys/reent.h示例,https://chromium.googlesource.com/native_client/nacl-newlib/+/a9ae3c60b36dea3d8a10e18b1b6db952d21268c2/newlib/libc/include/sys/reent.h 。其中注释说明定义_REENT_SMALL时会尽可能减小struct _reent,并将很多内容改为首次使用时分配。不同工具链版本的具体头文件可能不同,应以本地工具链为准。 ↩︎