libc 结构¶
libc 是 newlib 的核心, 编译出 libc.a. 它的源码散在 newlib/libc/ 下十几个子目录里, 但最终会汇聚成一个归档文件. 这里说明这些目录怎么分工, 它们之间是什么关系, 以及为什么会有 libc/syscalls/syskill.c 这种文件.
目录布局¶
前五个目录是纯算法, 不碰硬件. reent/, syscalls/, sys/<os>/ 三层是 newlib 与操作系统打交道的核心. machine/<arch>/ 是性能优化和架构必备代码.
怎么构建成 libc.a¶
所有目录的源文件通过各自的 Makefile.inc 声明, 最终汇总到 automake 的 libc_a_SOURCES 变量里. 构建时:
- 每个
.c编译成libc_a-<名字>.o这样的 per-target 对象 (前缀libc_a-用于和 libm 的对象区分) ar把这些对象归档成libc.a- 特殊的
MATHOBJS_IN_LIBC机制:ldexp,frexp,modf等函数传统上既在libc.a也在libm.a里. 它们只在 libm 里编译一次, 归档时再从libm.a提取出来塞进libc.a
哪些源文件参与构建由 configure.host 决定: 它设置 sys_dir, machine_dir, syscall_dir 等变量, 对应 libc/sys/<os>/, libc/machine/<arch>/, libc/syscalls/ 是否编进来.
文件之间的关系: 三层调用链¶
newlib 把每一个依赖系统的函数拆成三层, 每层是一个独立的 .c 文件. 以 kill 为例:
每一层的职责不同:
- connector 层 (
syscalls/syskill.c): 提供干净的公共 API. 它做两件事: 一是把函数名从公共的kill接到下划线的_kill_r, 二是插入_REENT指针 - 可重入层 (
reent/signalr.c): 负责 errno 的按线程传递. 它先清 errno, 调用底层, 再把结果写回_REENT_ERRNO(ptr) - 系统层 (
sys/<os>/): 唯一真正发系统调用的地方, 移植时只写这一层
其它函数遵循同样的模式, 对应关系一目了然: sysread.c -> reent/readr.c -> _read, sysopen.c -> reent/openr.c -> _open, 以此类推.
为什么需要 syscalls/ 这一层¶
syscalls/syskill.c 看起来多余: 为什么不能直接让用户调 _kill_r, 省掉一个文件?
原因有三个:
-
公共 API 必须没有下划线和
_r后缀. 标准规定用户写kill(pid, sig), 而 newlib 内部的可重入约定要求_kill_r(reent, ...). connector 就是这两个命名约定之间的桥梁 -
抹平差异. 有些函数原型有可变参数. 例如
open的 mode 参数只在带O_CREAT时才出现,sysopen.c用va_arg把可变的公共原型收束成固定的_open_r(reent, path, flags, mode) -
目标可以覆盖. 如果某个目标自己实现了
_kill_r(定义了REENTRANT_SYSCALLS_PROVIDED), 那reent/signalr.c里的默认_kill_r就不会编译, 而 connector 不需要改, 照样把kill接到_kill_r. 同样, 如果目标只提供无下划线的kill(定义了MISSING_SYSCALL_NAMES), 头文件里的宏会把_kill映射成kill, connector 层依然原样工作
所以 syscalls/ 这一层的作用是隔离命名约定和原型的差异, 让可重入层和系统层都能各自独立地替换, 而不影响用户看到的接口.
本文档由 deepseek-v4-pro 撰写.