先说一个我真实遇到过的事。早些年做内部工具时一个模块在 Windows 7 测试机上跑得好好的搬到 Windows 10 用户机器上就报驱动加载错误。排查的第一步永远是问用户系统版本但很多用户自己都说不清楚。后来我干脆在日志里打印系统版本才意识到一个再基础不过的问题——用 C 获取本机系统版本居然有这么多隐藏坑。网上很多代码直接调 GetVersionEx在 Windows 10 上返回的还是 6.2完全不是真实版本。这篇文章就把我最终沉淀下来的一套跨平台获取系统版本的 C 源码、原理分析和踩坑记录完整放出来如果你也需要做客户端工具、诊断日志、授权判断或者想在程序里做兼容性分支这份内容可以直接抄作业。1. 为什么要获取系统版本不是炫技是客户端的刚需1.1 版本信息在真实项目里的四个核心用途第一个用途是兼容性分支。很多 API 是后续系统才引入的比如 Windows 8 之后才有的某些电源管理接口Linux 某个内核版本才开始支持的新系统调用。程序需要在老系统上降级走旧路径在新系统上启用新特性这时候就必须在运行期判断系统版本。第二个用途是诊断信息上报。线上用户报障时如果日志里只有堆栈没有系统版本你连环境都猜不准。我习惯在程序启动时把系统版本、内核版本、架构一起写进日志头部这样一拿到日志就能先排除环境因素排查效率翻倍。第三个用途是授权和激活逻辑。部分商业软件会规定“仅支持 Windows 10 及以上版本”或者对特定版本发放功能权限。虽然这类判断也可以靠 API 特性检测完成但有时候项目就是明确要求读取版本号做策略你绕不开。第四个用途是安装包和更新器的决策。安装程序需要根据系统版本决定是否自动安装 VC Redistributable、是否需要打某个补丁或者给出“当前系统过旧建议升级”的友好提示。这些场景都指向同一件事程序需要在运行期拿到准确、可比较、跨平台一致的系统版本信息。1.2 你真的理解“系统版本”四个字吗这里必须先厘清一个概念系统版本号和系统市场名称不是一回事。拿 Windows 来说Windows 10 和 Windows 11 的内核 NT 版本都是 10.0真正的区别在 build 号和 UBR 修订号。比如 Windows 10 22H2 是 10.0.19045.4529Windows 11 23H2 是 10.0.22631.XXXX。如果只拿主版本号判断你根本分不清用户跑的是 Windows 10 还是 Windows 11甚至分不清是 Server 还是桌面版。这一点在后面的避坑环节非常重要。再看 Linux。系统版本至少包含两层发行版版本比如 Ubuntu 22.04和内核版本比如 5.15.0-91-generic。两者经常被混用但含义完全不同。发行版版本决定了用户态软件包的兼容策略内核版本决定了驱动和系统调用层的能力。你给用户做兼容性判断时大多数情况下真正关心的是发行版版本但 uname() 取出来的却是内核版本这正是 Linux 平台最容易踩的坑。macOS 也一样。产品版本是 macOS 14.0Darwin 内核版本是 23.0.0两个数值体系完全不同必须做映射。所以“获取本机系统版本”这句话看着简单实际要回答的问题是我需要的是内核版本、市场版本还是两者都要输出格式怎么设计才能让后续代码既能显示又能比较2. 方案选型Windows、Linux、macOS 上分别怎么取2.1 Windows 平台GetVersionEx 为什么被“废弃”RtlGetVersion 怎么用很多老教程给的方案都是GetVersionExW。但这个 API 从 Windows 8.1 开始被微软标记为“受限”API。原因是为了兼容性系统检测程序是否在应用清单中声明支持对应的 Windows 版本如果没有声明API 会返回一个“模拟”的旧版本号——最常见的就是在 Windows 10 上返回 6.2也就是 Windows 8 的版本号。这个问题不是个例是微软设计好的行为目的是保护老程序不被新系统吓到。我刚开始做的时候也中招了日志里 Windows 10 用户全部显示 6.2排查了很久才发现是 manifest 的问题。解决方案有两种一种是在 exe 的 manifest 里声明支持的 Windows GUID骗过兼容层让 GetVersionEx 吐真话另一种是绕开这个 API直接调用 ntdll.dll 导出的RtlGetVersion。用RtlGetVersion有两个好处第一它不受 manifest 兼容层影响返回的就是系统真实版本号第二这个函数从 Windows 2000 到 Windows 11 全系列都存在不需要担心老系统找不到。虽然它没有写入官方 SDK 文档但实际上极其稳定业界很多专业诊断工具包括微软自己的工具都在用。2.2 Linux 平台uname 与 /etc/os-release 组合拳Linux 上获取系统信息最基础的调用是uname()它返回sysname、nodename、release、version、machine这些字段。其中release是内核版本machine是硬件架构比如 x86_64 或 aarch64。但前面说了内核版本不等于发行版版本。要拿到“Ubuntu 22.04”这样的信息需要读取/etc/os-release文件。这个文件被 systemd 统一标准化几乎所有主流发行版都有Ubuntu、Debian、CentOS、Fedora、openEuler、Alpine 都支持。格式是简单的 KEYVALUE核心字段有ID例如 ubuntu、VERSION_ID例如 22.04、PRETTY_NAME例如 Ubuntu 22.04.3 LTS。实际代码里建议两者都读用uname()拿内核版本和硬件架构用/etc/os-release拿发行版名称与版本号。如果/etc/os-release不存在再降级尝试/etc/redhat-release、/etc/lsb-release这些老文件。2.3 macOS 平台sysctl 的简洁之道macOS 上最省事的办法是sysctlbyname(kern.osproductversion, ...)这个键从 macOS 10.10 开始可以直接返回产品版本比如 “14.2.1”。在老系统上没有这个键那就要退回去读kern.osrelease拿到 Darwin 内核版本再映射回 macOS 版本。Darwin 版本号到 macOS 产品版本有对应关系比如 Darwin 22.x 对应 macOS 13 VenturaDarwin 23.x 对应 macOS 14 Sonoma。下面我整理了一张映射表这种信息在普通文档里很难找全建议收藏。Darwin 内核版本macOS 产品版本17.x10.13 High Sierra18.x10.14 Mojave19.x10.15 Catalina20.x11 Big Sur21.x12 Monterey22.x13 Ventura23.x14 Sonoma3. 完整源码实现跨平台 SystemVersion 类3.1 公共接口设计调用方只需要关心一个结构体我设计这套代码的目标很明确调用方不需要关心底层是哪个平台拿到一个结构体就能显示和比较。所以先定义统一的版本结构体和接口// system_version.h #pragma once #include string namespace sysinfo { enum class OsType { Unknown 0, Windows, Linux, MacOS }; struct SystemVersion { OsType os_type OsType::Unknown; int major 0; int minor 0; int build 0; int revision 0; std::string os_name; // 如 Windows 11 Pro / Ubuntu 22.04.3 LTS / macOS 14.2.1 std::string version_string; // 如 10.0.22631.3155 / 22.04 / 14.2.1 std::string kernel_version; // 如 5.15.0-91-generic / 23.0.0 }; bool GetSystemVersion(SystemVersion ver); bool IsWindows11OrGreater(const SystemVersion ver); int CompareVersion(const std::string v1, const std::string v2); } // namespace sysinfomajor/minor/build/revision拆成整数字段是为了比较和排序方便。os_name负责给人看version_string负责给日志用kernel_version单独存 Linux 和 macOS 的内核信息。这套设计我在多个项目里实跑过后续扩展硬件信息、架构信息都很顺手。3.2 Windows 实现RtlGetVersion 注册表补全Windows 实现的核心是动态加载 ntdll 的RtlGetVersion。为什么不直接链接ntdll.lib因为RtlGetVersion没有公开 SDK 导出动态加载是最稳妥的方式代码这样写#if defined(_WIN32) #include windows.h #include sstream namespace sysinfo { typedef LONG (NTAPI* pfnRtlGetVersion)(PRTL_OSVERSIONINFOW); bool GetSystemVersion(SystemVersion ver) { ver.os_type OsType::Windows; HMODULE hNtdll GetModuleHandleW(Lntdll.dll); if (!hNtdll) { return false; } auto fnRtlGetVersion (pfnRtlGetVersion)GetProcAddress(hNtdll, RtlGetVersion); if (!fnRtlGetVersion) { return false; } RTL_OSVERSIONINFOW ovi { 0 }; ovi.dwOSVersionInfoSize sizeof(ovi); if (fnRtlGetVersion(ovi) ! 0) { return false; } ver.major (int)ovi.dwMajorVersion; ver.minor (int)ovi.dwMinorVersion; ver.build (int)ovi.dwBuildNumber; // 注册表补充 UBR 修订号和产品名 HKEY hKey nullptr; if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, LSOFTWARE\\Microsoft\\Windows NT\\CurrentVersion, 0, KEY_READ | KEY_WOW64_64KEY, hKey) ERROR_SUCCESS) { DWORD dwType 0; DWORD dwSize sizeof(DWORD); DWORD ubr 0; if (RegQueryValueExW(hKey, LUBR, nullptr, dwType, (LPBYTE)ubr, dwSize) ERROR_SUCCESS dwType REG_DWORD) { ver.revision (int)ubr; } wchar_t buffer[256] { 0 }; dwSize sizeof(buffer); if (RegQueryValueExW(hKey, LProductName, nullptr, dwType, (LPBYTE)buffer, dwSize) ERROR_SUCCESS) { char name[256] { 0 }; WideCharToMultiByte(CP_ACP, 0, buffer, -1, name, sizeof(name), nullptr, nullptr); ver.os_name name; } RegCloseKey(hKey); } std::ostringstream oss; oss ver.major . ver.minor . ver.build; if (ver.revision 0) { oss . ver.revision; } ver.version_string oss.str(); return true; } bool IsWindows11OrGreater(const SystemVersion ver) { if (ver.os_type ! OsType::Windows) { return false; } if (ver.major 10) { return true; } // Windows 11 的 build 从 22000 开始 return ver.major 10 ver.build 22000; } } // namespace sysinfo #endif注意注册表路径里的KEY_WOW64_64KEY不能省略。如果省略32 位进程会因为注册表重定向读到 32 位视图可能读不到某些字段或读到的值不同。这是 Windows 上非常隐蔽的问题。PRTL_OSVERSIONINFOW这个类型在部分老 SDK 里可能没有声明可以直接换成自己定义的结构体字段和官方一样即可。我实际项目里就是手动定义结构体来避开 SDK 版本差异的。3.3 Linux 与 macOS 实现uname os-release sysctlLinux 侧核心代码是这样的读取/etc/os-release时直接把PRETTY_NAME和VERSION_ID都解析出来#if defined(__linux__) #include sys/utsname.h #include fstream #include sstream namespace sysinfo { bool GetSystemVersion(SystemVersion ver) { ver.os_type OsType::Linux; struct utsname uts; if (uname(uts) ! 0) { return false; } ver.kernel_version uts.release; std::ifstream file(/etc/os-release); std::string line; while (std::getline(file, line)) { if (line.rfind(PRETTY_NAME, 0) 0) { std::string value line.substr(line.find() 1); if (value.size() 2 value.front() value.back() ) { value value.substr(1, value.size() - 2); } ver.os_name value; } else if (line.rfind(VERSION_ID, 0) 0) { std::string value line.substr(line.find() 1); if (value.size() 2 value.front() value.back() ) { value value.substr(1, value.size() - 2); } ver.version_string value; std::istringstream iss(value); std::string part; int nums[3] { 0, 0, 0 }; int idx 0; while (std::getline(iss, part, .) idx 3) { nums[idx] std::stoi(part); } ver.major nums[0]; ver.minor nums[1]; ver.build nums[2]; } } return true; } } // namespace sysinfo #endifstd::stoi在部分嵌入式环境可能异常稳妥做法是手写字符串转整数。这段代码在 Ubuntu、Debian、openEuler、Alpine 上都实测过。如果遇到没有/etc/os-release的极老系统建议再降级读取/etc/redhat-release并用正则提取版本号但这类系统现在很少了属于兼容性补充。macOS 的实现更短核心就是sysctlbyname#if defined(__APPLE__) #include sys/sysctl.h #include cstring namespace sysinfo { bool GetSystemVersion(SystemVersion ver) { ver.os_type OsType::MacOS; char buffer[64] { 0 }; size_t len sizeof(buffer); if (sysctlbyname(kern.osproductversion, buffer, len, nullptr, 0) ! 0) { return false; } ver.version_string buffer; ver.os_name macOS ver.version_string; int nums[3] { 0, 0, 0 }; int idx 0; const char* p buffer; while (*p idx 3) { nums[idx] atoi(p); while (*p *p ! .) { p; } if (*p .) { p; } } ver.major nums[0]; ver.minor nums[1]; ver.build nums[2]; return true; } bool IsWindows11OrGreater(const SystemVersion ver) { return false; } } // namespace sysinfo #endifmacOS 上暂时没有等价于IsWindows11OrGreater的需求所以这里返回false占位。atoi在这里足够用因为它只需要解析以数字开头的版本号。3.4 语义化版本比较千万不要用字符串比版本号既然获取版本号是为了做判断那比较函数必须可靠。有人图省事直接if (str 10.0)这在 C 里是字典序比较结果完全错误——“9.0”会被判定为大于“10.0”因为字符9比1大。这个坑我见过不止一次。正确做法是把点分版本号拆成整数数组逐位比较缺失位按 0 处理#include vector #include algorithm namespace sysinfo { int CompareVersion(const std::string v1, const std::string v2) { auto split [](const std::string str) { std::vectorint parts; std::string cur; for (char ch : str) { if (ch . || ch -) { if (!cur.empty()) { parts.push_back(std::atoi(cur.c_str())); cur.clear(); } } else if (ch 0 ch 9) { cur.push_back(ch); } } if (!cur.empty()) { parts.push_back(std::atoi(cur.c_str())); } return parts; }; auto v1_parts split(v1); auto v2_parts split(v2); size_t n std::max(v1_parts.size(), v2_parts.size()); for (size_t i 0; i n; i) { int a i v1_parts.size() ? v1_parts[i] : 0; int b i v2_parts.size() ? v2_parts[i] : 0; if (a b) { return -1; } if (a b) { return 1; } } return 0; } } // namespace sysinfo这里我把分隔符同时处理.和-目的是适配 Linux 内核版本号比如5.15.0-91-generic。非数字字符会被跳过这样比较5.15.0-91-generic和5.15.0-42-generic时两个版本都得到[5, 15, 0, 91]和[5, 15, 0, 42]大小判断自然正确。调用方式很简单if (sysinfo::CompareVersion(ver.version_string, 10.0) 0) { // 当前系统主版本大于等于 10.0 }4. 避坑指南获取系统版本时最典型的 6 个坑4.1 坑 1Windows 10 返回 6.2问题出在 manifest这是 Windows 平台最常见的坑。之前说过GetVersionExW受兼容层影响如果你的 exe 没有嵌入 manifest或者 manifest 里的supportedOS没有声明 Windows 10 的 GUID系统就按“老程序”处理返回 6.2。这个坑的特征是你单独写个测试程序测试时一切正常因为 IDE 生成的工程通常会带上正确的 manifest但当你把代码编译成最终产品时或者在某些静态库工程里忘了嵌入 manifest版本号就突然变成假的了。排查时可以先看看GetSystemVersion拿到的值如果是6.2但系统明明是 Win10/Win11大概率就是 manifest 的问题。解决方案最省心的就是用RtlGetVersion完全没有这个困扰。4.2 坑 2软件工程里用 MSVC 编译 RtlGetVersion 调用报错有些朋友直接#include winternl.h后调用RtlGetVersion编译时会报一些结构体声明冲突或链接错误。因为winternl.h里很多类型和windows.h有重叠而且这个头文件本身并不保证所有版本 SDK 都有。我的建议是不要直接 includewinternl.h手动声明你要的结构体和函数指针类型。代码只用了四个字段手动定义一个结构体完全够用还省得跟系统头文件打架。这也是很多开源项目采用的做法。4.3 坑 3Linux 上把内核版本当发行版版本uname()返回的release是内核版本比如6.2.0-1014-azure。如果你只用它判断“当前系统是否支持某功能”有些场景是够的但如果你要在日志里显示“Ubuntu 22.04”拿内核版本当系统版本就完全不对味。正确做法一定是读/etc/os-release。我之前遇到过有人读/proc/version来获取系统信息那个文件里既包含内核版本又包含编译器信息格式混乱也不适合产品日志。4.4 坑 4Windows 11 识别不能只看主版本号Windows 11 的主版本号和 Windows 10 一样是 10.0所以major 10 minor 0无法区分两者。目前业界用的判断方法是看 build 号是否大于等于 22000。Windows 10 的最终 build 是 19045Windows 11 从 22000 起步这个分界线非常清晰。但从更长远的眼光看这套方法并不完美微软未来如果继续沿用 10.0 大版本号build 分界线就需要持续维护。更保险的组合判断是用RtlGetVersion读取 build 号再配合注册表里的ProductName和DisplayVersionWin11 22H2 之后注册表中有“23H2”“24H2”这类版本名做最终展示。4.5 坑 532 位进程读不到 64 位系统的准确注册表信息在 64 位 Windows 上32 位进程访问注册表某些路径时会触发重定向读到的可能是 32 位视图的副本。微软把一些系统信息键做了虚拟化CurrentVersion路径下部分值在 32 位视图里也存在但内容可能被改动或缺失。解决办法就是前面代码里反复强调的RegOpenKeyEx时加上KEY_WOW64_64KEY标志强制读取 64 位注册表视图。这个标志只在 32 位进程里起作用64 位进程传了也没副作用所以我建议统一加上。4.6 坑 6版本号里混入多余字符导致比较失败Windows 的 UBR 修订号可能包含多段Linux 内核 release 带-generic后缀macOS 早期版本可能出现10.15.7这种三段结构。如果比较函数没处理分隔符和非法字符很容易解析出错。我提供的CompareVersion函数已经处理了.和-两种分隔符并且自动跳过非数字字符。使用前唯一要注意的是不要直接拿用户的输入当版本号尽量使用GetSystemVersion输出的version_string或kernel_version确保输入格式可控。5. 常见问题速查与实操经验5.1 常见问题排查表我把实际项目里遇到最多的问题整理成了表格方便对照排查症状常见原因解决方案Win10/Win11 上 GetVersionEx 返回 6.2exe manifest 未声明支持当前系统改用 RtlGetVersion或补全 supportedOS GUIDLinux 返回内核版本而非发行版版本直接用了 uname().release读 /etc/os-release 获取 PRETTY_NAME/VERSION_IDmacOS 上版本字符串为空sysctlbyname 读取的键不支持降级读 kern.osrelease 并做 Darwin 映射32 位程序读不到系统注册表信息注册表重定向加 KEY_WOW64_64KEY 标志版本比较结果异常字符串字典序比较用拆分数字后的 CompareVersionRtlGetVersion 编译报错引入 winternl.h 冲突手动定义结构体避免 include5.2 我的实操习惯与多环境验证建议这套代码我在 Windows 7 SP1、Windows 10、Windows 11、Ubuntu 20.04、Ubuntu 22.04、CentOS 7、macOS 12/13/14 上都跑过。有一个小经验是做跨平台版本获取功能时不能只在开发者自己的机器上验证。因为同一行代码在 Win10 上正常不代表 Win11 也正常在 Ubuntu 上正常不代表 Alpine 的/etc/os-release格式也兼容。我一般会用一个简单的测试程序把GetSystemVersion输出逐项打出来再在目标系统上跑一遍核对三个东西os_name是否可读、version_string是否符合预期、CompareVersion比较结果是否和人工判断一致。日志输出建议固定格式比如OS : Windows 11 Pro Version : 10.0.22631.3155 Kernel : - OS : Ubuntu 22.04.3 LTS Version : 22.04 Kernel : 5.15.0-91-generic这样的日志拿到手就能定位问题不用再回头问用户“你是什么系统”。如果你也需要在崩溃转储或日志文件里写环境信息强烈建议把这段系统信息采集做成独立模块每次启动只调用一次后续所有日志共用这份缓存数据。最后分享一个我用过最顺手的扩展思路把SystemVersion结构体再塞进一个EnvironmentInfo结构体加上硬件架构、CPU 核数、内存大小、磁盘剩余空间做成一个printEnvironmentInfo()函数。任何项目在任何平台遇到疑难问题上来先打印一张环境信息表能省掉大量来回沟通的成本。版本获取只是第一步但它一定是整套环境诊断最扎实的底座。