@[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 标准库实现。它提供 memcpymemsetstrlenprintfsnprintfmallocfreeerrnotimestrtok 等标准 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.alibstdc++_nano.a 等 nano 版本库,而不是普通 libc.a / libstdc++.a

_REENT_SMALL 是另一个层面的配置。它是预处理宏,影响 newlib 头文件中 struct _reent 的布局。struct _reent 是 newlib 保存线程局部 libc 状态的核心结构,例如 errnostrtok 状态、部分 stdio 状态、随机数状态、时间函数临时状态等。开启 _REENT_SMALL 后,struct _reent 会尽量变小,很多内容改为“首次使用时再分配”。这对 RTOS 多线程非常有价值,因为如果每个线程都带一个 _reent,单线程节省几百字节,多线程就可能节省数 KB RAM。

但它不是无条件更好。核心风险有三个:

  1. 必须与实际链接的 newlib/newlib-nano ABI 配套,否则头文件看到的结构布局和库内部实现不一致,会导致难以定位的运行时错误。
  2. 可能引入首次调用时的动态分配,如果 heap 很小、禁用 heap 或 malloc 锁不正确,问题会更早暴露。
  3. 复杂 stdio、locale、宽字符、浮点格式化、C++ 异常等功能可能和代码体积目标冲突,需要按工程实际功能测试。

工程上更稳妥的规则是:

1
2
3
4
使用 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 不一样。上电后只有:

1
复位向量 -> startup -> 初始化 .data/.bss -> SystemInit -> main/thread entry

是否有文件系统、是否有标准输入输出、heap 从哪里来、stdout 写到哪里、errno 是全局还是线程局部、exit() 应该做什么,这些都不是硬件自动提供的。

因此,当你写下:

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("hello\n");
while (1) {}
}

编译器和链接器看到的不只是一个 printf()。背后可能牵涉:

1
2
3
4
5
6
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 链接通过。[^sourceware-libc-system-calls]

Memfault 的 “Bootstrapping libc with Newlib” 文章也用裸机例子说明:没有 libc 时,printfmemcpystrncpy 等常见函数都不可用;即使源码没有显式调用某些函数,编译器也可能为了初始化或优化生成对 memsetmemcpy 等库函数的引用。[^memfault-newlib]

3. newlib 是什么

newlib 是面向嵌入式系统的 C library。Sourceware 官方主页对 newlib 的定位是:intended for use on embedded systems。[^sourceware-newlib-home]

更工程化地说,newlib 做了两件事:

  1. 提供硬件无关的 C 标准库实现
    例如字符串、内存、格式化输入输出、数学库、stdlib、time、errno、locale、部分 POSIX-like 包装函数等。

  2. 把少量硬件/OS 相关能力留给目标系统实现
    例如 _write() 如何输出、_read() 如何读取、_sbrk() 如何扩展 heap、_fstat() 如何描述文件、_isatty() 是否是终端等。

这正适合 MCU 场景:C 标准库大量逻辑可以复用,少量与板级/RTOS 强相关的入口由 BSP 或 RTOS 提供。

可以把它看成下面的层次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
应用层
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 中,这类适配通常位于类似:

1
rt-thread/components/libc/compilers/newlib/syscalls.c

不同 BSP 或 RT-Thread 版本细节可能不同,但职责相近:把 newlib 的底层需求转接到 RT-Thread 的设备、heap、线程或 console 实现。

4. newlib 在 MCU/RTOS 中通常提供哪些能力

下面按工程影响分类说明。

4.1 内存与字符串函数

常见函数:

1
2
3
4
5
6
7
8
9
10
11
memcpy()
memmove()
memset()
memcmp()
strlen()
strcmp()
strncmp()
strcpy()
strncpy()
strchr()
strstr()

这类函数通常是最容易被间接引入的。即使你没有 #include <string.h>,编译器也可能在某些场景生成 memcpy / memset 调用。例如大对象初始化、结构体赋值、数组清零等。

如果完全不链接 newlib,就必须注意这些符号是否由编译器内建展开,还是需要外部实现。常见策略是:

1
2
3
4
1. 自己提供 memcpy/memset/memmove/memcmp 的最小实现
2. 保留编译器内建优化,但确保未内联场景有外部符号
3. 使用 -ffreestanding 明确 freestanding 环境假设
4. 必要时使用 -fno-builtin 或 -fno-builtin-xxx 限制编译器假设

4.2 格式化输出输入

常见函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
printf()
printf()
snprintf()
vsnprintf()
sprintf()
vprintf()
scanf()
sscanf()
fprintf()
fread()
fwrite()
fopen()
fclose()

其中 printf() / snprintf() 是 MCU 工程最常见的体积来源。完整格式化库为了支持宽度、精度、浮点、长整型、locale、文件流等功能,通常会带入较多代码。

对 MCU 来说,通常建议:

1
2
3
4
日志输出优先使用 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 支持函数。[^newlib-nano-readme]

4.3 动态内存

常见函数:

1
2
3
4
malloc()
calloc()
realloc()
free()

在 newlib 中,malloc() 需要有底层 heap 来源。传统裸机适配里常见 _sbrk() / _sbrk_r(),用于增长程序数据区。newlib 官方手册在 sbrk 说明中明确指出,malloc 及相关函数依赖它,因此独立系统中提供可工作的实现很有用。[^sourceware-libc-system-calls]

在 RTOS 中,动态内存还有两个额外问题:

  1. heap 来源
    是 newlib 自己通过 _sbrk 管理一段堆,还是转接到 RTOS heap,例如 rt_malloc / pvPortMalloc

  2. 线程安全
    多线程同时调用 malloc/free 时,需要锁。newlib 支持类似 __malloc_lock() / __malloc_unlock() 的目标相关钩子,RTOS 移植层通常要提供对应互斥保护。

如果工程禁止动态内存,可以选择:

1
2
3
4
1. 不使用 malloc/free/calloc/realloc
2. 对 _sbrk 返回失败,使误用尽早暴露
3. 对 malloc/free 做链接期包装,定位误用点
4. 使用静态内存池或 RTOS mempool 替代

4.4 errno 与可重入状态

常见对象和函数:

1
2
3
4
5
6
7
8
errno
strtok()
strtok_r()
rand()
srand()
asctime()
localtime()
gmtime()

很多 C 库函数需要保存状态。单线程裸机可以把这些状态放在全局变量里,但 RTOS 多线程会出现互相覆盖的问题。例如:

1
2
3
线程 A 调用 read,失败后 errno = EIO
线程 B 调用 malloc,失败后 errno = ENOMEM
线程 A 再读取 errno,可能读到线程 B 的错误

newlib 通过 struct _reent_impure_ptr 等机制保存当前上下文的 libc 状态。简单理解:

1
2
struct _reent = 当前线程的 libc 私有状态
_impure_ptr = newlib 当前正在使用的 struct _reent 指针

RTOS 需要在任务切换或线程初始化时确保 newlib 能拿到当前线程对应的 _reent,否则 errnostrtok、部分 stdio 状态等就可能在线程之间串扰。

Sourceware newlib 手册说明,newlib 的 errno 宏是其支持 reentrant routines 的一部分;OS interface 调用返回的全局 errno 会被记录到相应 reentrancy structure 中。[^sourceware-libc-system-calls]

4.5 时间、文件、进程相关接口

常见函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

这也是为什么很多裸机工程里能看到类似实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 系统,如果底层没有提供这些服务,至少需要提供空实现或最小实现。典型符号包括:[^sourceware-libc-system-calls]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_exit
close
environ
execve
fork
fstat
getpid
isatty
kill
link
lseek
open
read
sbrk
stat
times
unlink
wait
write

Memfault 的文章也列出了类似集合,并说明这些是 newlib 期望底层“操作系统”提供的 system calls。[^memfault-newlib]

在 MCU 工程中,这些 syscall 可以分为三类。

5.1 必须认真实现的接口

接口 典型触发函数 工程建议
_write / write printfputsfwrite 转接到 UART、RTT、USB CDC、RTOS console 或设备框架
_read / read scanfgetcharfread 不用输入时可以返回 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
2
3
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 集成通常要处理:

1
2
3
4
5
6
1. 每个线程是否有自己的 struct _reent
2. 线程创建时是否初始化 reent
3. 线程退出时是否清理 reent 相关资源
4. 上下文切换时 newlib 当前 reent 指针是否正确
5. malloc/free 是否有锁
6. stdio 是否允许跨线程访问

如果 RTOS 没有正确处理这些问题,表面现象可能是:

1
2
3
4
5
errno 在线程间串扰
printf 偶发崩溃或输出交错
malloc/free 多线程下 heap 损坏
strtok 在多个线程中互相干扰
线程退出后仍有 newlib 分配的资源泄漏

7. newlib-nano 是什么

newlib-nano 是 newlib 的小型化变体,目标是减少嵌入式工程中的代码体积和 RAM 占用。

newlib-nano README 说明,newlib-nano 与 newlib 的使用方式基本相同,但会使用一组面向小体积的配置选项,例如:[^newlib-nano-readme]

1
2
3
4
5
6
7
8
--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 的设计方向:

1
2
3
4
5
6
减小 struct _reent
减小 stdio 路径
减少宽字符/stream orientation 相关支持
使用 nano malloc
简化 exit 行为
把 atexit 数据从每线程 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
2
3
4
1. 链接普通 libc 还是 libc_nano
2. 链接普通 libstdc++ 还是 libstdc++_nano
3. 链接启动文件、结束文件、libnosys/semihosting 支持的组合
4. 是否引入某些 nano 专用库名或路径

Metin Balci 的文章通过 STM32CubeIDE 和 Arm GNU Toolchain 示例解释了 nano.specs、newlib-nano、nosys.specslibnosys 的关系。[^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
2
3
4
5
libc_nano.a
libstdc++_nano.a
libg_nano.a
libnosys.a
librdimon.a

是否出现取决于工具链版本和链接参数,但 libc_nano.a 是最直接的信号。

9. _REENT_SMALL 是什么

9.1 它是编译期宏,不是链接选项

_REENT_SMALL 通常通过编译参数或预包含头文件定义,例如:

1
-D_REENT_SMALL

或:

1
#define _REENT_SMALL

它影响 newlib 头文件,尤其是:

1
#include <sys/reent.h>

sys/reent.h 中,_REENT_SMALL 决定 struct _reent 的布局。newlib 相关头文件注释说明:如果定义 _REENT_SMALL,会尽可能让 struct _reent 变小,方式是把尽可能多的内容改为首次使用时分配。[^reent-h]

9.2 它解决什么问题

RTOS 每个线程可能需要一个 newlib reent 状态。如果 struct _reent 很大,线程数一多,RAM 占用会明显增加。

假设:

1
2
3
普通 struct _reent 约 1 KiB
_REENT_SMALL 后约 200~300 B
线程数 10 个

那么每线程节省约 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 功能使用者更划算 主要用 memcpysnprintf、轻量日志时,常驻状态不必很大

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
2
3
4
5
6
7
MCU SRAM/Flash 较紧张
RTOS 线程较多
只需要基本 C 函数、轻量 printf/snprintf
日志主要走 RTOS 自带 console
不依赖完整文件系统 stdio
不使用或极少使用 scanf
不需要 C++ exception / RTTI / locale / 宽字符

10.2 不建议盲目使用的情况

1
2
3
4
5
6
项目依赖完整 stdio 文件流
需要大量 fopen/fread/fwrite/fprintf
需要复杂 locale / 宽字符 / C99 格式化细节
需要 C++ 异常、完整 libstdc++ 行为
heap 禁用或极小,但 libc 功能可能触发懒分配
无法确认工具链 newlib-nano 与头文件宏配置是否一致

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
2
3
能不用浮点 printf 就不用
日志中把浮点转成整数缩放输出,例如 mV、uA、Q 格式
如果必须启用,单独评估 .text/.rodata 增量

示例:

1
2
3
4
5
/* 避免 */
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 的某个函数,可以在程序中定义该函数,链接器找到你的实现后就不会继续从静态库中找。[^memfault-newlib]

典型替换目标:

1
2
3
4
5
printf
snprintf
malloc/free
_write/_read/_sbrk
_exit

示例:用轻量 printf 替换标准 printf

1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

更常见的是宏映射:

1
2
3
#define printf   rt_kprintf
#define snprintf rt_snprintf
#define sprintf rt_sprintf

宏映射简单,但只影响包含该宏的编译单元。库内部或第三方对象文件已经引用的 printf 不会被宏替换。因此对全局替换,函数强符号替换或链接器 wrap 更可控。

12.2 路线 B:链接 newlib-nano,并关闭重功能

这是 MCU 工程的常规优化路线:

1
2
3
4
--specs=nano.specs
-Wl,--gc-sections
-ffunction-sections
-fdata-sections

再配合:

1
2
3
4
5
不启用 _printf_float / _scanf_float
不用 scanf
不用 iostream
不用 C++ exception / RTTI
不用 full FILE* 文件流

这条路线不是“不用 newlib”,但往往是投入产出最高的优化方式。

12.3 路线 C:完全不链接标准库,自行实现所需子集

如果要完全不使用 newlib,可以使用:

1
-nostdlib

或更细地控制:

1
2
-nodefaultlibs
-nostartfiles

区别大致是:

选项 影响
-nostdlib 不使用标准启动文件和标准库
-nodefaultlibs 不使用默认系统库,但启动文件仍可能使用
-nostartfiles 不使用标准启动文件,但默认库仍可能使用

完全自实现时,你需要至少考虑:

1
2
3
4
5
6
7
8
9
10
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,也通常仍会用到 libgcclibgcc 提供编译器运行时辅助函数,例如某些软浮点、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
#define printf rt_kprintf

优点:最小改动。
缺点:只对包含宏的源码有效,不影响库和已编译对象。

策略 2:强符号函数替换

1
2
3
4
int printf(const char *fmt, ...)
{
/* 自己实现或转接 */
}

优点:链接层全局有效。
缺点:要确保 ABI 和返回值语义合理,避免与库内部符号冲突。

策略 3:链接器 wrap

1
-Wl,--wrap=printf

然后实现:

1
2
3
4
int __wrap_printf(const char *fmt, ...)
{
/* 自定义实现 */
}

优点:可拦截并统计调用。
缺点:构建系统要支持,第三方库和 LTO 场景需要测试。

14. 工程裁剪 newlib/newlib-nano 的实用方法

14.1 从 map 文件看真实来源

打开 map 文件,搜索:

1
2
3
4
5
6
7
8
9
10
11
printf
vfprintf
malloc
_sbrk
_write
scanf
_printf_float
_scanf_float
libc.a
libc_nano.a
libstdc++

重点看是哪一个对象文件把大模块拉进来的。例如:

1
main.o -> printf -> vfprintf -> libc_nano.a(lib_a-nano-vfprintf.o)

如果某个第三方库调用了 printf,即使应用层宏替换了 printf,也可能仍然把标准 printf 拉进来。

14.2 用 nm/size/objdump 辅助定位

1
2
3
arm-none-eabi-size app.elf
arm-none-eabi-nm -S --size-sort app.elf
arm-none-eabi-objdump -t 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
2
3
4
5
6
7
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,比较稳妥的配置路径是:

1
2
3
4
5
6
7
8
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 推荐检查命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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 本地验证方法

创建:

1
2
3
#include <sys/reent.h>

char reent_size_check[sizeof(struct _reent)];

分别编译:

1
2
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

查看数组大小:

1
2
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 工程源码直接使用。

应该在工具链目录搜索:

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
2
3
4
5
6
7
8
第一步:确认最终链接命令是否使用 --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-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.specslibnosys 的作用。

[^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 的对应关系。