在 Windows 上测试 2038 年问题
2038 年 1 月 19 日 03:14:07 UTC,从 1970 年起计秒数的带符号 32 位计数器会用尽。Windows 的时钟会继续走下去——而保留了自己那份 32 位副本的程序、文件格式和库则不会。下面介绍如何在不把整台机器的日期设到 2038 年的前提下,在单个应用里找出它们。
这个缺陷还可能藏在哪里
Windows 的时钟是从 1601 年起以 100 纳秒为步长计数的 64 位计数器,所以它没有 2038 年这个坎。C 运行时库的 time_t 自 Visual Studio 2005 起默认就是 64 位。剩下的是选用了 32 位时间、或在途中丢掉了高半部分的代码:
- 用
_USE_32BIT_TIME_T编译的 32 位构建,或直接调用_time32和__time32_t的代码。Microsoft 自己的文档就警告,这样的应用可能在 2038 年 1 月 18 日之后失败。 - 把 64 位时间存进
int或long。 在 Windows 上这两者都是 32 位,即使在 64 位程序里也一样。 - 用 32 位保存 1970 年起秒数的库、文件格式、网络协议和数据库列。
- 会加年份的代码。 二十年的许可证、三十年的还款计划,或有效期到 2040 年的证书,在 2038 年到来之前很久就越过了这条界线。
在单个应用里越过这条界线
这个上限是以 UTC 定义的,而 --at 按会话时区读取,所以自己写时刻时请把时区固定为 UTC。现成的场景已经带有绝对的 UTC 时刻,不需要时区。
# 场景:会话恰好从上限处开始,03:14:07 UTC chrono run "C:\apps\test.exe" --preset year-2038 --report session.txt # 提前十七秒开始,让应用现场越过这条界线 chrono run "C:\apps\test.exe" --at 2038-01-19T03:13:50 --zone +00:00 --report session.txt # 更早开始并以 60 倍速运行——大约 14 秒后到达上限 chrono run "C:\apps\test.exe" --at 2038-01-19T03:00:00 --zone +00:00 --mode x60 # 或者用今天的日期运行,在应用打开时直接跳过去 chrono run "C:\apps\test.exe" --jump-after 5:2038-01-19T03:14:05 --zone +00:00
越过上限一秒后,带符号的 32 位计数器变成 -2,147,483,648,读作 1901 年 12 月 13 日 20:45:52 UTC。1901 年的日期、负数、突然过期或排到最前面的条目、从日期代码里抛出的异常——从外面看,溢出就是这个样子。
把真实时钟设到 2038 年是到达这里的另一条路,也是破坏最大的一条。修改系统日期会发生什么 →
确认它确实到达了程序
一个只是因为程序从未见过 2038 年才通过的测试,比没有测试更糟。每次会话结束时的报告会说明程序读取了哪个时钟、读了多少次:
Chrono Mock - session report
target: .\build\test.exe
verdict: WORKS - time substitution took effect (processes: 1)
every process that read time saw the session clock
session: fake clock reached 2038-01-19T03:14:08
real elapsed 1.3s, fake elapsed 1.3s
covered channels (substituted, with call counts, 10 calls in all):
- pid 21240: GetSystemTimeAsFileTime (5 calls)
- pid 21240: GetLocalTime (5 calls)
session 这一行确认应用的时钟越过了上限。带调用次数的 works 判定表示程序读取了时钟并拿到了 2038 年的日期;works partially 则会列出它通过其他途径读取的通道。判定的含义 →
通过的测试没有证明什么
- 只有程序自己的时钟动了。 它从文件或数据库里加载的日期就是存在那里的值,所以数据里出现的 2038 年日期是另一项测试。日期计算器可以替你算出这类日期:
chrono calc --base 2038-01-19T03:14:07 --zone +00:00 --shift +1s - 程序所通信的服务器仍然运行在真实日期上。 那一侧的 32 位上限无法触及,会话会在发现程序打开网络连接时发出警告。