【问题标题】:Human readable string of 64bits time_t value人类可读的 64 位 time_t 值字符串
【发布时间】:2015-07-13 02:40:29
【问题描述】:

我正在尝试以人类可读的格式打印出日期,以获得可能的最大 time_t 值。 以下代码在 32 位机器上似乎工作得很好(使用 0x7fffffff 初始化 m_time),但它在 64 位机器上为理论上的最高值输出 null。这是 ctime 限制还是我错过了什么?

编译:gcc -Wall -g3 main.c -o time_test
主机:x86_64。

#include <stdio.h>
#include <time.h>
#include <stddef.h>

int main(int argc, char** argv) {
  time_t m_time = 0x7fffffffffffffff;
  time_t current_time;
  time(&current_time);

  printf("time_t info: sizeof [%ld] bytes or [%ld] bits.\n", sizeof(time_t), sizeof(time_t) *8 );
  printf("m_time val: [%ld]-> %s\n", m_time, ctime(&m_time)); 
  printf("current_time val: [%ld]-> %s\n", current_time, ctime(&current_time));
  return 0;
}

输出:

time_t info: sizeof [8] bytes or [64] bits.
m_time val: [9223372036854775807]-> (null)
current_time val: [1430678274]-> Sun May  3 15:37:54 2015

tks.

【问题讨论】:

  • "当遇到错误时,这些函数返回NULL 并将errno 设置为适当的值。" perror 说什么?
  • 确实有错误。错误:参数无效。
  • 务实地说,如果time_t 有超过 40 位(例如 64 位),则您不关心最大可表示时间。您和阅读该论坛的每个人(以及我们所有的孙子)都将死去,运行您的程序的计算机将全部被破坏,那时 C 将不复存在。 Y2038 problem 实际上没有任何等效的 64 位。所以只是特例 time_t 是 32 位。
  • 嗯,它可以报告未来 2920 亿年的日期。或者它可以假设周围不再有任何人来运行您的程序。你会怎么做?
  • 我肯定会报告日期。当我们开始为时间旅行设备开发固件时,这将是一个问题。我什至可以看到,用户被抛到时空连续体之外。这将是一个非常令人头疼的问题。

标签: c 32bit-64bit ctime


【解决方案1】:

顺便说一句,ctime (& ctime(3)) 被记录为给出一个字符串,其中年份由 位数字表示(总共 26 个字节)。所以最大时间是在 9999 年(肯定小于 64 位机器上的最大时间 time_ttime_t)。

另外(正如我评论的那样),务实地说,如果time_t 有超过 40 位(例如 64 位),您并不关心最大可表示时间。您和阅读该论坛的每个人(以及我们所有的孙子)都将死去,运行您的程序的计算机将全部被破坏,那时 C 将不复存在。 Y2038 problem 实际上没有任何等效的 64 位。所以只是time_t 是 32 位的特殊情况。

在 3000 年之后,任何 C 程序都不重要;软件、硬件、标准和人类技术专长不会持续那么久……

POSIX ctime documentation明确表示

尝试在纪元之前或 9999 年之后的时间使用 ctime()ctime_r() 会产生未定义的结果。参考asctime

顺便说一句,musl-libc 似乎符合标准:its time/__asctime.c(由ctime 间接调用)有一个很好的评论:

if (snprintf(buf, 26, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",
    __nl_langinfo(ABDAY_1+tm->tm_wday),
    __nl_langinfo(ABMON_1+tm->tm_mon),
    tm->tm_mday, tm->tm_hour,
    tm->tm_min, tm->tm_sec,
    1900 + tm->tm_year) >= 26)
{
    /* ISO C requires us to use the above format string,
     * even if it will not fit in the buffer. Thus asctime_r
     * is _supposed_ to crash if the fields in tm are too large.
     * We follow this behavior and crash "gracefully" to warn
     * application developers that they may not be so lucky
     * on other implementations (e.g. stack smashing..).
     */
    a_crash();
}

而 GNU glibc 在其 time/asctime.c 文件中:

/* We limit the size of the year which can be printed.  Using the %d
   format specifier used the addition of 1900 would overflow the
   number and a negative vaue is printed.  For some architectures we
   could in theory use %ld or an evern larger integer format but
   this would mean the output needs more space.  This would not be a
   problem if the 'asctime_r' interface would be defined sanely and
   a buffer size would be passed.  */
if (__glibc_unlikely (tp->tm_year > INT_MAX - 1900))
  {
  eoverflow:
    __set_errno (EOVERFLOW);
    return NULL;
  }

int n = __snprintf (buf, buflen, format,
          (tp->tm_wday < 0 || tp->tm_wday >= 7 ?
           "???" : ab_day_name (tp->tm_wday)),
          (tp->tm_mon < 0 || tp->tm_mon >= 12 ?
           "???" : ab_month_name (tp->tm_mon)),
          tp->tm_mday, tp->tm_hour, tp->tm_min,
            tp->tm_sec, 1900 + tp->tm_year);
if (n < 0)
 return NULL;
if (n >= buflen)
  goto eoverflow;

所以我相信 GNU glibc 和 musl-libc 在这方面都优于 MacOSX 实现(如 zneak's answer 中所引用的)。标准要求ctime 提供 26 个字节。此外,POSIX 2008 将ctime 标记为已过时,新代码应使用strftime(另请参阅strftime(3))。

【讨论】:

  • 很奇怪,asctime(相同的引用)没有提到 9999 年,而是说如果 tm_year 大于 {INT_MAX}-1990,则结果未定义。可能导致 UB 的原因是结果缓冲区被定义为 26 字节。这不是asctime_r 的问题,也不应该是ctime_r
【解决方案2】:

要深入了解这一点,最好的办法是找到一个实现并查看它的作用。我下载了苹果的Libc tarball for OS X 10.1.1(链接在this page),发现ctime是在stdtime/FreeBSD/localtime.c中定义的。

函数如下:

char *
ctime(timep)
const time_t * const    timep;
{
/*
** Section 4.12.3.2 of X3.159-1989 requires that
**  The ctime function converts the calendar time pointed to by timer
**  to local time in the form of a string.  It is equivalent to
**      asctime(localtime(timer))
*/
#ifdef __LP64__
    /*
     * In 64-bit, the timep value may produce a time value with a year
     * that exceeds 32-bits in size (won't fit in struct tm), so localtime
     * will return NULL.
     */
    struct tm *tm = localtime(timep);

    if (tm == NULL)
        return NULL;
    return asctime(tm);
#else /* !__LP64__ */
    return asctime(localtime(timep));
#endif /* __LP64__ */
}

second-hand reference 来看,struct tm 似乎是用整数定义的,tm_year 字段是从 1900 年开始的偏移量。假设符合这一点,即使是不符合要求的 ctime 也不可能接受第 2 年之后的时间戳31+1900-1。

这是一个程序,用于查找(并测试)ctime 将接受 Apple 实施的最大时间戳:

#include <limits.h>
#include <stdio.h>
#include <time.h>

int main(int argc, char** argv) {
    struct tm t = {
        .tm_sec = 59,
        .tm_min = 59,
        .tm_hour = 23,
        .tm_mday = 31,
        .tm_mon = 11,
        .tm_year = INT_MAX,
    };
    time_t max = mktime(&t);

    printf("Maximum time: %li\n", max);
    printf("ctime max: %s\n", ctime(&max));
    max++;
    printf("ctime max+1: %s\n", ctime(&max));
}

输出:

最长时间:67768036191694799
ctime max:12 月 31 日星期三 23:59:59 2147485547
ctime max+1: (null)

这是一个 56 位数字,因此 64 位 time_t 可以容纳的最大年份(尽管 struct tm 不能)可能介于 547,608,814,485 和 549,756,300,032 之间,或者说是宇宙年龄的 36 倍。换句话说,这将是一段时间。

不管怎样,Apple 的实现并不符合标准。该标准规定ctime 的输出必须适合 26 个字节,包括换行符和空字符。对于符合标准的实现,这意味着年份必须在 -999 和 9999 之间。

【讨论】:

    猜你喜欢
    • 2012-06-28
    • 1970-01-01
    • 2018-07-16
    • 2014-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-19
    相关资源
    最近更新 更多