合法的 FILETIME 最大值是多少?使用它安全吗?
文章目录
合法的 FILETIME 最大值是多少?使用它安全吗?
What’s the highest legal FILETIME? Is it safe to use? — devblogs.microsoft.com/oldnewthing · 9 小时前 · ⚙️ 工程

「FILETIME 的最大合法值是多少」这个问题看起来只需要查一次文档,但真正的答案取决于这个 64 位整数接下来会被谁读取。FILETIME 用两个 DWORD——dwLowDateTime 与 dwHighDateTime——拼成一个 64 位计数,单位是 100 纳秒(一个 tick),起点是 1601 年 1 月 1 日 UTC。如果把它当作无符号 64 位整数,上限 0xFFFFFFFFFFFFFFFF 落在公元 60056 年;但内核、NTFS 以及大量内部代码按有符号 64 位来解释它,任何大于等于 0x8000000000000000 的值都会变成 1601 年之前的负数,因此真正可移植的上界收缩到 0x7FFFFFFFFFFFFFFF,约合公元 30828 年 9 月。即便停在这个上界,下游仍然可能更窄:FileTimeToSystemTime 产出的 SYSTEMTIME 用 WORD 保存年份(最大 65535),.NET 的 DateTime 只能到 9999 年 12 月 31 日 23:59:59.9999999,对应 FILETIME 2650467743999999999;32 位 time_t 在 2038 年 1 月 19 日 03:14:07 UTC 溢出;数据库的 DATETIME2、TIMESTAMP 以及 HTTP 使用的 RFC 3339 字符串各有更早的天花板。作者的核心观点是:不存在一个放之四海而皆准的最大值,安全的做法是根本不靠近边界——把时间戳限制在业务真正需要的范围内,在类型转换的每一道关口显式校验,并避免把 0x7FFFFFFFFFFFFFFF 或 0xFFFFFFFFFFFFFFFF 当作「永不过期」的哨兵值随意传递。这些值在有符号比较、加减运算、时区转换、格式化输出和跨语言序列化中会以完全不同的方式失败:有的 API 直接返回 FALSE 并设置 last error,有的静默回绕成 1601 年附近,有的在序列化阶段抛异常。文章还提醒,0 值(1601 年 1 月 1 日)在很多子系统里被当作「时间未设置」,因此 0 与最大合法值构成了 FILETIME 语义区间的两端,二者都不应作为普通时间戳参与算术。换句话说,FILETIME 的危险不在于它不够大,而在于它太大,超出了几乎所有消费方的表示能力,而调用方往往直到运行时才发现这一点。
核心论点
- FILETIME 本身只是一个 64 位计数,单位是自 1601 年 1 月 1 日 UTC 起的 100 纳秒间隔,由 dwLowDateTime 与 dwHighDateTime 两个 DWORD 组成,拼接时必须借助 64 位中间类型做移位或运算,否则高 32 位会被悄悄截断;按无符号解释,它的上限 0xFFFFFFFFFFFFFFFF 对应公元 60056 年,而按有符号解释(内核与 NTFS 等子系统的做法),0x8000000000000000 及以上的值全部落在 1601 年之前,因此真正跨子系统安全的最大值是 0x7FFFFFFFFFFFFFFF,约合公元 30828 年 9 月。
- 真正限制 FILETIME 可用范围的往往不是它自己,而是消费它的类型:SYSTEMTIME 的 wYear 是 WORD,最大 65535;.NET 的 DateTime.MaxValue 是 9999-12-31 23:59:59.9999999,对应 FILETIME 2650467743999999999;Unix 的 32 位 time_t 在 2038-01-19 03:14:07 UTC 就会溢出;SQL Server 的 datetime 与 datetime2 上限同为 9999-12-31。Win32 的 FileTimeToSystemTime 与 SystemTimeToFileTime 在超出可表示范围时会直接返回 FALSE 并设置 last error,而不是给出一个近似值,这意味着边界错误会以调用失败的形式暴露,而不是被静默修正。
- 最容易被忽视的是把极大值当作哨兵:不少代码用 0x7FFFFFFFFFFFFFFF 或 0xFFFFFFFFFFFFFFFF 表示「永不过期」或「无期限」,但这些值一旦进入有符号比较、加减运算或序列化路径就会变形——加上一个正的时间间隔会溢出成负数,转成 Unix 时间戳会被截断,转成 ISO 8601 字符串可能得到 60056 年这样的结果,部分解析器还会直接拒绝。相反的一端同样有坑:0 表示 1601-01-01,在很多子系统里被当作「时间未设置」,因此 0 与最大值构成了语义区间的两端,二者都不应被当作普通时间戳参与算术或格式化。
- 稳妥的工程做法是自建契约而不是依赖 64 位的宽度:把内部时间戳限制在 1601-01-01 到 9999-12-31 这一区间(它也是 .NET、SQL Server、ISO 8601 与多数 Web 协议的公共交集),在 API 边界、序列化前后和类型转换处显式校验,用命名常量替代散落的魔数,并明确规定「未设置」与「永不过期」使用独立的标志位而不是特殊时间值来表达;如果确实需要表达更远的未来,就应该换用范围更大且语义明确的表示方式,而不是把 FILETIME 推到极限。
💡 为什么值得读: 因为它用有符号与无符号解释的边界值、.NET 与 SYSTEMTIME 的具体上限,说清楚了为什么把「永不过期」写成 0x7FFFFFFFFFFFFFFF 是一颗会在比较、运算和序列化时引爆的定时炸弹。
🏷️ FILETIME, Windows, API, time
留下一点回声