软件构建工具生态
系统框架
┌───────────────────────────────┐
│ 配置层 (Configuration) │
│ • Kconfig (Linux, Zephyr) │
│ • Kconfig-frontends (menu) │
│ → 决定编译哪些功能模块 │
│ → 生成 .config 文件 │
└───────────────────────────────┘│▼
┌───────────────────────────────┐
│ 构建系统生成层 (Meta-Build) │
│ • CMake │
│ • Meson │
│ • Autotools (configure) │
│ • GN, Bazel, Buck │
│ → 解析工程结构 & 配置 │
│ → 生成规则文件 │
│ (Makefile / build.ninja) │
└───────────────────────────────┘│▼
┌───────────────────────────────┐
│ 构建执行层 (Build Executor) │
│ • Make │
│ • Ninja │
│ • MSBuild (Windows) │
│ • Gradle (Java/Android) │
│ → 执行具体编译命令 │
│ → 调用编译器 │
└───────────────────────────────┘│▼
┌───────────────────────────────┐
│ 编译器 (Compiler Toolchain) │
│ • GCC / Clang / MSVC │
│ • Emscripten (WebAssembly) │
│ • arm-none-eabi-gcc (嵌入式) │
│ → 把源码编译成目标文件/库/可执行 │
└───────────────────────────────┘│▼
┌───────────────────────────────┐
│ 产物 (Artifacts) │
│ • 可执行文件 (ELF, EXE) │
│ • 静态库 (.a, .lib) │
│ • 动态库 (.so, .dll) │
│ • WebAssembly (.wasm) │
└───────────────────────────────┘
比喻:
- 生成器 (CMake / Meson / Bazel) = 建筑师,负责出图纸(规则文件)。
- 执行器 (Make / Ninja / MSBuild) = 工地工头,照图纸分配任务。
- 编译器 (GCC / Clang / MSVC) = 工人,真正把砖(源码)砌成房子(可执行文件)。
- 辅助工具 = 供应链和加速器,让工地运转更快更稳。
生成器 / 元构建工具(高层):这些工具和 CMake 属于同类,都不直接编译,而是生成规则文件。
- 这些工具负责 描述工程结构,然后生成底层构建文件。CMake 就在这一层。
- CMake → 跨平台通用的生成器。
- Meson → 类似 CMake,搭配 Ninja,语法更现代。
- Autotools (GNU Autoconf/Automake) → 传统 Unix 世界,写 configure 脚本。
- Bazel (Google 出品) → 强调可重现、分布式构建。
- Buck (Meta 出品) → 针对大规模 C++/Java 工程。
- GN (Generate Ninja, Chromium 用) → 专门为生成 Ninja 构建文件设计,取代 GYP。
构建执行器(中层):这些和 Make/Ninja 是同类,主要区别在生态和性能。
- 这些是真正的 构建工具,读规则文件去调用编译器。
- Make → 经典,执行 Makefile。
- Ninja → 新一代,快,执行 build.ninja。
- MSBuild → Windows/.NET 世界,执行 .csproj/.sln。
- Gradle → Java/Android 生态常用。
- Ant (老 Java 工具) → 逐渐被 Gradle/Maven 替代。
编译器(底层)执行器最终调用的就是编译器:
- GCC(GNU Compiler Collection)
- Clang/LLVM
- MSVC (Microsoft Visual C++)
- ICC (Intel C Compiler, 已转向 oneAPI DPC++)
- Emscripten (emcc) → 把 C/C++ 转为 WebAssembly
相关工具(辅助层)在构建生态里,还有一些配套工具:
- Pkg-config → 帮助查找依赖库的头文件路径、链接参数。
- Conan / vcpkg → C/C++ 包管理器,类似 npm/pip。
- ccache → 编译缓存,加速二次构建。
- distcc / icecc → 分布式编译,把任务分发到多台机器。
- Docker → 提供一致的编译环境,避免“在我机子上能跑”。
场景 | 生成器 (高层) | 执行器 (中层) | 编译器 (底层) | 说明 |
---|---|---|---|---|
Linux 桌面应用 | CMake / Meson / Autotools | Make / Ninja | GCC / Clang | 经典组合:CMake + Ninja + GCC ,性能好、兼容性强 |
Windows 桌面应用 | CMake / MSBuild / QMake | MSBuild (VS) / Ninja | MSVC / Clang | VS 开发常用 MSBuild;跨平台项目偏好 CMake + Ninja |
macOS/iOS 应用 | CMake / Xcode | Xcode (xcodebuild) / Ninja | Clang (Apple LLVM) | 苹果生态强绑定 Clang/Xcode;跨平台可用 CMake |
Android (NDK, JNI) | CMake / Gradle | Ninja (NDK) / Gradle | Clang (NDK) | 上层 APK 构建靠 Gradle,底层 native 用 CMake + Ninja |
WebAssembly (Emscripten) | CMake / Meson | Ninja | Emscripten (emcc, em++) | Qt WASM、游戏引擎常用组合 |
嵌入式 ARM (裸机/RTOS) | CMake | Ninja / Make | GCC (arm-none-eabi) / LLVM | 常见 STM32、nRF、ESP32 SDK 都支持 CMake |
大型项目 (Chromium, TensorFlow) | GN / Bazel / Buck | Ninja | Clang / GCC | Ninja 的速度优势在大项目里特别明显 |
Java/Android 应用 (非 NDK) | Gradle | Gradle | javac / kotlinc | 完全 Java/Kotlin 生态,不涉及 C/C++ |
老派 Unix 项目 | Autotools (configure) | Make | GCC | 很多历史项目依旧在用 ./configure && make |
CPU 架构与编译器
┌───────────────┐│ 源代码 (C/C++) │└───────────────┘│▼┌───────────────────────────────────────────┐│ 编译器前端 (Front-end) ││ 解析语法、检查语义、支持 C/C++ 标准 ││ ─────────────────────────────────── ││ GCC | Clang/LLVM | MSVC │└───────────────────────────────────────────┘│▼┌───────────────────────────────────────────┐│ 编译器后端 (Back-end) ││ 不同 CPU 架构的代码生成 (Codegen) ││ ││ x86 / x86_64 → PC, 服务器 ││ ARM-M (裸机) → MCU, IoT ││ ARM-A (Linux) → 树莓派, 手机 ││ RISC-V → 嵌入式, Linux SBC ││ MIPS → 路由器, 机顶盒 ││ PowerPC → 工控, 游戏机 ││ SPARC → 服务器 │└───────────────────────────────────────────┘│▼┌───────────────────────────────────────────┐│ 目标文件 / 可执行文件 ││ .o / .elf / .bin / .exe / .so / .dll │└───────────────────────────────────────────┘
一个典型的 GCC 工具链名字形如:
<arch>-<vendor>-<os>-<abi>
- arch → CPU 架构 (x86, arm, riscv…)
- vendor → 厂商/发行版 (gnu, w64, unknown…),vendor 字段经常被省略,特别是 pc、unknown 这种没太大意义的值。
- os → 目标操作系统 (linux, none, mingw32…)
- abi → 应用二进制接口 (eabi, gnu, musl, hardfp…)
- 光从 gcc 工具链的名字,并不能具体区分 ABI。
x86\_64-linux-gnu-gcc
、aarch64-linux-gnu-gcc
,这两个从名字上看他们的 ABI 都是 GNU,其实差异很大。
工具链名 | 架构 (arch) | 操作系统 (os) | ABI/用途 | 输出文件格式 | 典型应用 |
---|---|---|---|---|---|
x86_64-linux-gnu-gcc | x86_64 | Linux | GNU libc | ELF | 常见 Linux 程序 |
i686-linux-gnu-gcc | x86 (32-bit) | Linux | GNU libc | ELF | 老 32 位 Linux 程序 |
arm-none-eabi-gcc | ARM Cortex-M | None (裸机) | EABI (无 OS) | ELF / BIN | STM32, nRF52, RTOS |
arm-linux-gnueabihf-gcc | ARMv7 (32-bit) | Linux | GNU libc, 硬浮点 | ELF | 树莓派 32-bit Linux |
aarch64-linux-gnu-gcc | ARMv8 (64-bit) | Linux | GNU libc | ELF | 树莓派 64-bit Linux, Android |
riscv64-unknown-elf-gcc | RISC-V | None (裸机) | ELF | ELF / BIN | RISC-V MCU |
riscv64-unknown-linux-gnu-gcc | RISC-V | Linux | GNU libc | ELF | RISC-V Linux SBC |
x86_64-w64-mingw32-gcc | x86_64 | Windows (MinGW) | Win32 API | PE (.exe/.dll) | 交叉编 Windows 程序 |
i686-w64-mingw32-gcc | x86 (32-bit) | Windows (MinGW) | Win32 API | PE | 老 Windows 程序 |
powerpc-linux-gnu-gcc | PowerPC | Linux | GNU libc | ELF | 工控机, 老游戏机 |
mipsel-linux-gnu-gcc | MIPS (小端) | Linux | GNU libc | ELF | 路由器, 机顶盒 |
ABI 是什么?
- 如果说 API(应用编程接口)是“源代码层面的约定”(比如 printf() 怎么调用),
- 那 ABI(应用二进制接口)就是“编译后机器码层面的约定”:函数参数怎么传递(寄存器还是栈?顺序?)
- 返回值怎么传递
- 数据类型的大小和对齐方式(int 是 32 位还是 64 位?)
- 调用系统调用的方式(Linux syscall、Windows syscall 不一样)
- 动态库的符号如何导出/解析
- 只要 ABI 不兼容,就算源代码一样,编译出来的二进制也跑不通。
工具链名 | ABI 字段 | 含义 |
---|---|---|
x86_64-linux-gnu | gnu | 使用 GNU libc (glibc) 的 Linux ABI |
x86_64-linux-musl | musl | 使用 musl libc 的 Linux ABI(常见于 Alpine Linux) |
arm-none-eabi | eabi | Embedded ABI,ARM 裸机/RTOS,不依赖操作系统 |
arm-linux-gnueabihf | gnueabihf | Linux + glibc + 硬件浮点 (hard-float ABI) |
arm-linux-gnueabi | gnueabi | Linux + glibc + 软件浮点 (soft-float ABI) |
riscv64-unknown-elf | elf | 没有 OS,用 ELF 作为目标文件格式 |
ABI 与编程语言的关系
┌───────────────────────────────┐│ ABI ││ (应用二进制接口, EABI/gnu…) │└───────────────────────────────┘│┌───────────────────┼───────────────────┐▼ ▼ ▼直接编译型语言 依赖运行时语言 虚拟机语言(遵循 ABI) (解释器遵循 ABI) (VM 遵循 ABI)───────────── ───────────── ─────────────• C • Python • Java• C++ • Lua • Kotlin/JVM• Rust • Ruby • Scala• Go (部分实现) • MicroPython* • .NET/Mono• Fortran • PHP • C# ───────────── ───────────── ─────────────
直接编译型语言:
- 编译器直接输出目标机器码 (ELF/PE/Mach-O)。
- 必须严格遵循 ABI 的函数调用、数据布局、异常规则。
- 例子:C, C++, Rust, Fortran。
- 在 ARM EABI、Linux glibc、Windows PE 下都能直接跑。
依赖运行时语言:
- 自身不能直接编译成裸机机器码。
- 需要一个 解释器 (interpreter) 或 运行时 (runtime),而这个解释器是用 C/C++ 写的。
- 所以它们是 间接遵循 ABI。
- 例子:Python (CPython), Lua, Ruby, PHP。
- 在嵌入式 MCU 里 → 可以跑 MicroPython (因为解释器本身是 C 写的,遵循 EABI)。
虚拟机语言
- 语言本身运行在 虚拟机 (JVM, CLR) 上,不能直接映射到裸机 ABI。
- VM 是用 C/C++ 写的,所以 VM 遵循 ABI,而语言通过 VM 间接遵循。
- 例子:Java, Kotlin, Scala (JVM),C#/.NET。
- 在 ARM Linux 上跑 Java 程序,其实是 JVM 遵循 Linux ABI。
C 库与系统调用
常见 C 库及适用平台
C 库 | 特点 | 适用平台 |
---|---|---|
glibc (GNU C Library) | 功能最全,POSIX 标准实现,支持多线程、locale、动态加载。体积大。 | Linux 桌面、服务器 |
musl | 轻量化,静态链接友好,二进制小。 | Alpine Linux、容器、嵌入式 Linux |
uClibc | 比 glibc 小,适合路由器等小型 Linux。 | OpenWrt、嵌入式 Linux |
newlib | 面向裸机/RTOS,只有最基本的 C 标准库,没有系统调用,需要用户自己实现 write() 等。 | ARM Cortex-M、RISC-V MCU (arm-none-eabi-gcc) |
picolibc | newlib 的轻量化现代替代品,适合极小 MCU。 | STM32, nRF52, Zephyr RTOS |
bionic | Google 为 Android 写的 C 库,轻量,兼容 Linux 内核。 | Android (ARM/x86) |
dietlibc | 极简 C 库,超小,功能不全。 | 超小嵌入式 Linux |
MSVCRT / UCRT | Windows 的 C 运行时库(CRT),提供 C API + WinAPI 绑定。 | Windows |
Darwin libc | macOS/iOS 的 C 库,基于 BSD libc。 | Apple 系统 |
【用户态 User Space】
──────────────────────────────────────────────应用程序 (App)││ 1. 调用标准库函数│ printf("hi"); fopen("file.txt", "r");▼C 库 (glibc / musl / newlib …)││ 2. 解析参数,准备系统调用号│ 比如 write(fd=1, buf="hi", len=3)││ 3. 调用封装好的 syscall 接口▼系统调用接口 (syscall wrapper)││ 4. 触发特权指令│ x86: int 0x80 / syscall│ ARM: svc #0│
───────────────────────[ 用户态 → 内核态 切换 ]───────────────────────
【内核态 Kernel Space】│▼内核系统调用分发 (syscall table)││ 5. 根据系统调用号跳转到对应内核函数│ sys_write(), sys_open(), sys_fork()│▼内核子系统├─ 进程管理 (fork, exec, wait)├─ 内存管理 (mmap, brk, page fault)├─ 文件系统 (open, read, write)├─ 网络协议栈 (socket, send, recv)└─ 驱动程序 (控制硬件 IO)││ 6. 执行完毕,返回结果 (返回值 / errno)▼
───────────────────────[ 内核态 → 用户态 切换 ]───────────────────────
【用户态 User Space】│▼C 库 (处理返回值)││ 7. 封装返回值,转换为用户能理解的形式│ 如果失败 → 设置 errno▼应用程序││ 8. 得到最终结果│ (比如 printf 成功返回字符数)
──────────────────────────────────────────────
【硬件 Hardware】受内核驱动控制的设备 (CPU/内存/磁盘/网卡/显示器 …)
在 MCU(裸机/RTOS)平台上,到底有没有“系统调用”?newlib 里是怎么实现的?
系统调用的本质
- 在 Linux/Windows 这类操作系统里:
- 系统调用 (syscall) 是进入内核的“入口函数”,例如 read()、write()、open()。
- 在 MCU(裸机/RTOS) 平台上:
- 没有真正的“内核”,所以严格意义上 没有系统调用。
- 但为了兼容标准 C 库(如 printf、malloc),C 库还是需要某种“底层实现”。
newlib 是一款专为嵌入式系统设计的 C 标准库,它提供了 C 标准 API (printf, malloc, time …),但底层功能必须由用户自己实现。
它采用了一种 “半系统调用 (syscall stubs)” 的机制:
- newlib 里有一组 弱符号函数(weak functions),例如:
_write
_read
_sbrk
(堆空间扩展,malloc 用)_open
,_close
,_lseek
_exit
- 这些函数默认是空壳(weak implementation)。
- 用户必须自己实现这些函数,把它们映射到 MCU 的外设或 RTOS API 上。
- 这样
printf("hi")
在 newlib 里会调用_write(fd, buf, len)
,而 _write 由你来实现(比如写到 UART)。