文章

【笔记】Windows 上的 POSIX 环境与原生工具链

【笔记】Windows 上的 POSIX 环境与原生工具链

Cygwin、MinGW-w64 和 MSYS2 之所以难以记忆,不只是因为名字有历史包袱,更是因为它们本就不在同一个抽象层:Cygwin 是 POSIX 运行时兼容层与软件环境,MinGW-w64 是用来生成原生 Windows 程序的开发基础,MSYS2 则是把类 Unix 构建工具、包管理器和多套 Windows 工具链组织在一起的软件发行版。WSL 又是另一条路线:它提供的是 Linux 运行环境,而不是面向 Windows ABI 的 POSIX 兼容层。

1. 困惑的根源:把不同层次的名字并列比较

最容易形成的错误直觉是:

1
2
Cygwin / MinGW / MSYS2 / WSL
都是“在 Windows 上用 Linux 命令”的兼容层。

这个分类被用户界面加强了:它们都可以打开黑色终端,出现 Bash prompt,也都可以运行 lsgrepgcc。但是界面相似只能说明交互方式相似,不能说明底层的进程、ABI 和产物相同。

要拆开这些名字,需要先恢复一条完整的分层链路:

flowchart TB
    T[终端界面<br/>Windows Terminal / mintty]
    S[Shell 与构建工具<br/>Bash / PowerShell / Make / CMake]
    D[软件发行与包管理<br/>MSYS2 / Cygwin / Linux distribution]
    C[编译工具链<br/>GCC / Clang / MSVC]
    A[目标 ABI 与运行时<br/>Linux / Cygwin / MSYS / Windows UCRT]
    O[最终产物<br/>Linux ELF / Windows PE]

    D -->|provides| S
    D -->|provides| C
    T <-->|renders and receives input| S
    S -->|launches build commands| C
    C -->|targets| A
    A -->|determines format and dependencies| O

一个名字可能只回答其中一层,也可能同时包含多层:

名称主要所在层次它首先回答的问题
Windows Terminal终端界面如何展示输入输出
BashShell如何解析命令并启动进程
Cygwin运行时 + 软件环境如何在 Windows 上提供 POSIX 语义
MinGW-w64工具链支撑 + Windows SDK 类基础如何让 GCC/Clang 生成 Windows 程序
MSYS2软件发行与构建平台如何组织 Shell、构建工具、包和多套工具链
WSLLinux 运行与 Windows 集成环境如何在 Windows 上运行 Linux binary

因此,不应该先问“我用的是哪个黑色终端”,而应该问:

  1. 当前进程遵循 Linux、POSIX 兼容层,还是原生 Windows 语义?
  2. 编译器的 target 是什么?
  3. 产物是 ELF 还是 PE,运行时依赖哪些 DLL?

2. 三条不同的路线

2.1 Cygwin:把 POSIX 语义带到 Windows

如果一个 Unix 程序大量使用 fork、POSIX signal、Unix permission、PTY 和 /usr 路径,直接改写成 Win32 程序的成本会很高。Cygwin 的选择是在用户态提供 cygwin1.dll,将大量 POSIX 接口和语义映射到 Windows。

1
2
3
4
5
POSIX source
  → Cygwin headers and toolchain
  → Windows PE executable
  → cygwin1.dll
  → Win32 / Windows NT

它生成的是 Windows PE,不是 Linux ELF;但程序运行时需要 cygwin1.dll。所以 Cygwin 既不是 Linux kernel,也不是虚拟机。它是 Windows 上的 POSIX 平台。

这条路线的交换关系是:

  • 优点:Unix/POSIX 源码更容易移植,可以保留更完整的 POSIX 行为。
  • 代价:产物依赖 Cygwin runtime,路径、权限和进程模型与原生 Windows 存在边界。

Cygwin 解决的是“怎样让 Unix 程序继续按 POSIX 方式生活在 Windows 上”,而不是“怎样生成一个完全按 Windows 语义工作的程序”。

2.2 MinGW-w64:让 GCC/Clang 面向 Windows ABI

MinGW 是 Minimalist GNU for Windows 的缩写。MinGW-w64 是后续项目的名称;w64 来自其历史上对 64 位和新 Windows API 的扩展,不应理解为“只能编译 64 位程序”。

MinGW-w64 本身更接近一套开源 Windows SDK 与工具链支撑,主要包含:

  • Windows API 头文件;
  • Windows DLL 的 import library;
  • C Runtime 适配与补充运行库;
  • 让 GCC、Clang、assembler 和 linker 生成 Windows PE 的相关支持。

关系应该写成:

1
2
3
4
GCC / Clang
  + MinGW-w64 headers and import libraries
  + Windows CRT and ABI
  → native Windows PE executable

MinGW-w64 不需要提供完整的 POSIX 进程模型。它的核心目标是让开源编译器生成原生 Windows 程序。这里的“原生”是指不依赖 cygwin1.dllmsys-2.0.dll,不是说程序不依赖 UCRT、libstdc++ 或其他 Windows DLL。

MinGW-w64 也不必须运行在 Windows 上。在 Linux 或 WSL 中执行:

1
x86_64-w64-mingw32-gcc hello.c -o hello.exe

仍可以生成 Windows PE。此时编译器的 host 是 Linux,target 是 Windows,属于交叉编译。因此,使用 MinGW-w64 不等于使用 MSYS2。

2.3 WSL:在 Windows 上运行 Linux 环境

WSL 的方向与 Cygwin/MinGW-w64 不同。特别是 WSL 2,Linux process 面向的是真实 Linux kernel 提供的 system call ABI:

1
2
3
4
Linux source
  → Linux GCC / Clang
  → Linux ELF
  → Linux kernel in WSL 2 managed VM

默认情况下,WSL 里的 /usr/bin/gcc 产出 Linux ELF,不是 Windows .exe。只有显式使用 MinGW-w64 交叉工具链,才会从 WSL 生成 Windows 产物。

因此“Windows 兼容层”这个说法本身缺少方向:

1
2
3
Cygwin:在 Windows 上实现 POSIX 语义
MinGW-w64:把开源编译工具对准 Windows ABI
WSL:在 Windows 系统中提供 Linux 运行环境

3. MSYS2 为什么最容易让人迷失

MSYS2 不是上述三条路线之外的第四种运行时。它是一个软件发行与构建平台,内部故意把两个世界组合到一起:

flowchart LR
    subgraph M[MSYS2 distribution]
        P[pacman and package repositories]
        subgraph U[MSYS/POSIX helper world]
            B[Bash / sed / awk / makepkg]
            R[msys-2.0.dll]
            B --> R
        end
        subgraph W[native Windows world]
            G[GCC / Clang / CMake / Python / libraries]
            ABI[MinGW-w64 + UCRT or MSVCRT]
            G --> ABI
        end
        P --> U
        P --> W
        U -->|orchestrates builds and invokes tools| W
    end

其中:

  • MSYS2 是整个发行版的名字;
  • MSYS 是 MSYS2 内部的一种环境;
  • msys-2.0.dll 是由 Cygwin runtime 派生而来的 POSIX 兼容运行时;
  • MinGW-w64 是 MSYS2 用来生成原生 Windows 软件的基础。

可以类比:“Ubuntu 包含 glibc,但 Ubuntu 不等于 glibc”。同样,“MSYS2 包含 MSYS runtime,但 MSYS2 不等于 MSYS runtime”。

3.1 同一个 Bash 窗口可以同时存在两种程序

在 MSYS2 UCRT64 终端中,PATH 通常以下列顺序开头:

1
/ucrt64/bin:/usr/bin

这意味着:

  • /usr/bin/bash.exe、传统 MSYS Git 和一些构建辅助工具属于 MSYS 世界,依赖 msys-2.0.dll
  • /ucrt64/bin/gcc.exe/ucrt64/bin/python.exe 及对应的库属于原生 Windows 世界;
  • Bash 使用 POSIX 风格构建流程,去调用原生 Windows compiler,最终生成不依赖 MSYS runtime 的 .exe

这是理解 MSYS2 的关键场景:

一个依赖 POSIX 兼容层的 Bash,正在组织一套生产原生 Windows 软件的工具链。

所以,不能因为“我打开的是 UCRT64 Shell”,就推出里面的所有进程都是 UCRT 程序。Shell 本身与 Shell 启动的程序可以属于不同运行时。

3.2 不同 MSYS2 入口不是不同的终端产品

安装 MSYS2 后可能看到:

  • MSYS2 MSYS;
  • MSYS2 UCRT64;
  • MSYS2 CLANG64;
  • MSYS2 CLANGARM64;
  • 旧的 MSYS2 MINGW64。

它们主要是不同的环境选择,通过 MSYSTEMPATH、包前缀和相关变量选择默认工具链。它们不是彼此独立的虚拟机,也不是五个无关的 Bash 实现。

环境默认编译器架构C RuntimeC++ 标准库
MSYSGCCx86-64MSYS/Cygwin runtimelibstdc++
UCRT64GCCx86-64UCRTlibstdc++
CLANG64Clang/LLVMx86-64UCRTlibc++
CLANGARM64Clang/LLVMARM64UCRTlibc++
MINGW64GCCx86-64旧 MSVCRTlibstdc++

UCRT 是从 Visual Studio 2015 开始引入的 Universal C Runtime,在 Windows 10 及以后是操作系统组件。相比旧 MSVCRT,它更接近现代 C 标准,也与现代 MSVC 生态共享更合适的 C runtime 基线。根据写作时的 MSYS2 官方文档,新项目不确定时应优先选 UCRT64;传统 MINGW64 环境已在逐步弃用。

3.3 包名也在表达 ABI 边界

MSYS2 的 pacman 能安装两类本质不同的包:

1
2
3
4
5
# MSYS package:通常安装到 /usr,依赖 msys-2.0.dll
pacman -S git

# UCRT64 package:安装到 /ucrt64,是原生 Windows build
pacman -S mingw-w64-ucrt-x86_64-gcc

包前缀不是冗余噪声,而是在编码 target 和 ABI:

包前缀目标环境
无前缀MSYS
mingw-w64-ucrt-x86_64-UCRT64
mingw-w64-clang-x86_64-CLANG64
mingw-w64-clang-aarch64-CLANGARM64
mingw-w64-x86_64-旧 MINGW64

这也解释了为什么只看软件名不够。同样叫 Git、Python 或 Make 的包,可能分别属于 MSYS runtime 和原生 Windows runtime。

4. 路径转换是两个世界相遇的痕迹

MSYS2 中同时存在 Unix/MSYS 和 Windows 两种路径命名:

1
2
/c/Users/alice     <-> C:\Users\alice
/ucrt64/bin        <-> C:\msys64\ucrt64\bin

当 MSYS 程序启动原生 Windows 程序时,MSYS2 会尝试将命令行参数和环境变量中像 Unix 路径的部分自动转换为 Windows 路径。这对 Autotools 调用 Windows compiler 很方便,但它只能基于字符串做启发式判断。

例如:

1
2
3
adb push file /sdcard/0/
program.exe /switch
docker run -v /host:/container image

/sdcard/0/ 可能是 Android 路径,/switch 可能是 Windows 选项,Docker volume 参数又同时包含宿主机和容器路径。自动转换无法凭空知道这些语义,因此会出现看似“参数被神秘改写”的问题。

可以显式转换路径:

1
2
cygpath -w /c/Users/alice
cygpath -u 'C:\Users\alice'

也可以对特定命令排除自动参数转换:

1
MSYS2_ARG_CONV_EXCL='*' command ...

这不是一个偶然的小功能,而是 MSYS2 架构的直接结果:一边是 POSIX 构建工具,另一边是原生 Windows 程序,参数穿越边界时必须处理路径命名差异。

与相邻环境对比:

1
2
3
4
MSYS2: /c/Users/alice
Cygwin:/cygdrive/c/Users/alice
WSL:   /mnt/c/Users/alice
Windows:C:\Users\alice

它们可能最终访问同一批 NTFS 文件,却不属于同一套路径解析和文件系统语义。

5. 终端标题问题暴露了哪些边界

一个很好的区分性案例是:在 Windows Terminal 中打开 WSL 后,为什么终端不能稳定地根据 Linux 前台进程自动更新标题?

因为这里至少有四个不同角色:

1
2
3
4
5
Windows Terminal
  → 负责终端显示与标签页
  → WSL 互操作边界
  → Linux shell
  → shell 的 Linux 前台进程组

Windows Terminal 是终端界面,不是 Linux 内核中管理 foreground process group 的 TTY 子系统。它能显示 Shell 或应用程序通过 OSC 控制序列上报的标题,却不等于它能直接查询 WSL 内部的 Linux 前台进程。要在执行 vimnpmpython 时更新标题,通常要由 Bash/Zsh 的 preexec/precmd 钩子或具体应用主动发送标题。

这个现象可以用来校准整个知识网络:

  • Windows Terminal 只决定终端 UI,不决定 Shell 和程序 ABI;
  • Bash 只是 Shell,并不自动意味着 Linux;
  • WSL Bash 是 Linux ELF process,MSYS2 Bash 则是依赖 msys-2.0.dll 的 Windows PE process;
  • 同样的 prompt 与命令体验,可以建立在完全不同的运行时之上;
  • 跨越 Windows/Linux 或 MSYS/native Windows 边界时,不能假设进程、路径和终端状态完全透明。

终端标题看似是一个 UI 小问题,实际上是“终端、Shell、运行时和操作系统边界不是同一层”的直观证据。

6. 产物和 ABI 比 Shell 外观更值得关注

在任意一个类 Unix Shell 中执行:

1
gcc hello.c -o hello

它可能得到完全不同的产物:

gcc 来源产物主要运行边界
WSL /usr/bin/gccLinux ELFLinux kernel
Cygwin GCCWindows PEcygwin1.dll
MSYS GCCWindows PEmsys-2.0.dll
MSYS2 UCRT64 GCCWindows PEWindows UCRT / Win32
WSL 中的 x86_64-w64-mingw32-gccWindows PEWindows runtime,属于交叉编译

可以使用以下命令建立技术锚点:

1
2
3
4
5
6
7
8
9
10
11
12
# 现在使用的是哪个编译器
command -v gcc
gcc -dumpmachine

# MSYS2 环境选择
echo "$MSYSTEM"
echo "$MINGW_PREFIX"
echo "$MINGW_PACKAGE_PREFIX"

# 产物格式与 DLL import
file program.exe
objdump -p program.exe | grep 'DLL Name'

判断运行时时,可优先寻找:

1
2
3
4
cygwin1.dll   → Cygwin program
msys-2.0.dll → MSYS program
ucrtbase.dll / kernel32.dll 等,且没有上述两个 DLL
              → native Windows program

“原生 Windows”也不等于“可以无条件混用”。GCC libstdc++ 与 MSVC STL、不同 C++ ABI、不同 CRT 内部结构仍可能不兼容。跨 DLL 边界传递 C++ object、FILE* 或由一边分配再由另一边释放的内存时,必须明确 ABI 和 allocator 边界。C ABI、COM 或经过设计的 FFI 接口通常更适合做稳定边界。

7. 如何选择

需求默认选择原因
Linux 服务端开发WSL 2 + Linux distribution开发、产物和部署目标都是 Linux
在 Windows 上用 GCC 构建原生软件MSYS2 UCRT64有现代包管理、Unix 构建工具和 Windows 原生工具链
Linux CI 产出 Windows .exeLinux/WSL + MinGW-w64 cross toolchain无需为了 target Windows 引入完整 MSYS2
尽量少改 POSIX 源码地移植到 WindowsCygwincygwin1.dll 提供更完整的 POSIX 语义
Visual Studio、Windows SDK 和微软 C++ 生态MSVC对 Windows 平台和微软 ABI/工具集成最直接
只需 Git 和少量 Bash 命令Git for Windows / Git Bash不必维护完整的通用构建发行版

对主要从事 Linux 服务端开发的工程师,最稳定的默认是:

  1. 日常 Linux 开发留在 WSL;
  2. 需要 Windows 原生 GCC 生态时使用 MSYS2 UCRT64;
  3. 只需从 Linux 生成 Windows 产物时,直接使用 MinGW-w64 交叉工具链;
  4. 只有明确需要较完整 POSIX-on-Windows 语义时才选 Cygwin。

8. 复习索引

8.1 一句话心智模型

Cygwin 用 Windows 运行时模拟 POSIX 平台;MinGW-w64 让 GCC/Clang 面向 Windows ABI;MSYS2 用前者的派生环境组织构建,用后者生产原生 Windows 软件;WSL 则直接提供 Linux 运行环境。

8.2 四个比喻

  • Cygwin:在 Windows 土地上建立一套 POSIX 行为规则。
  • MinGW-w64:教 GCC/Clang 按 Windows ABI 生产软件。
  • MSYS2:一座工厂,工人使用 Unix 风格的工具,生产线制造原生 Windows 产品。
  • WSL 2:在 Windows 管理的边界内运行一个真实 Linux kernel 与 Linux userspace。

8.3 遇到陌生环境时只问三个问题

  1. 这是终端、Shell、发行版、工具链,还是运行时?
  2. 当前 compiler target 是 Linux 还是 Windows?
  3. 产物是 ELF 还是 PE,它依赖 Linux kernel、cygwin1.dllmsys-2.0.dll 还是 Windows CRT?

只要这三个问题有答案,黑色终端的外观、Shell 名称和历史缩写就不会再干扰判断。

9. 核验锚点

本文由作者按照 CC BY 4.0 进行授权