在FreeBSD 7上快速修复32位(2GB限制)的fseek / ftell。

6
我有一个在FreeBSD上运行的旧的32位C/C++程序,数百个用户远程使用它,并且作者不会修复它。它是以不安全的方式编写的,所有文件偏移量都作为无符号32位偏移量存储在内部,并且使用了ftell/fseek函数。在FreeBSD 7(软件的主机平台)中,这意味着ftellfseek使用32位有符号长整型。
 int fseek(FILE *stream, long offset, int whence);

 long ftell(FILE *stream);

我需要快速修复程序,因为在收集数据13年后,一些内部数据文件突然达到了2^31文件大小(2 147 483 7yy字节),现在对于任何请求,内部fseek/ftell断言都会失败。
在FreeBSD7世界中,有用于2GB+文件的fseeko/ftello hack。
 int
 fseeko(FILE *stream, off_t offset, int whence);

 off_t
 ftello(FILE *stream);

这里的off_t类型没有被很好地定义;我现在所知道的是,它有8字节大小,并且看起来像long long或者unsigned long long(我不知道是哪一个)。

如果我要处理最多4GB的文件,那么将所有的ftell替换为ftello,并将所有的fseek替换为fseeko(使用sed -i 's/ftell/ftello',对于seek也是一样),是否足够安全可靠呢?

 unsigned long offset1,offset2; //32bit
 offset1 = (compute + it) * in + some - arithmetic;
 fseek(file, 0, SEEK_END);
 fseek(file, 4, SEEK_END); // or other small int constant

 offset2 = ftell(file);
 fseek(file, offset1, SEEK_SET);  // No usage of SEEK_CUR

以及这些调用的组合。

off_t 的符号是什么?将 64 位 off_t 分配给无符号 32 位偏移量是否安全?它适用于范围在 2GB 到 4GB 之间的字节吗?

除了 ftell/fseek,还可以使用哪些函数来处理偏移量?


那么...你有所有的源代码吗?如果是这样,为什么不进行全面更新,而不是表面上的修复呢?补丁(正如你在问题中建议的)可能会出现问题,并可能导致数据损坏。 - Mahonri Moriancumer
我需要快速修复(用户几个小时后就会醒来...),但我不理解所有的代码(20 KLoC)。哪些代码可能会破坏数据?我这里有很多grep,而且我没有看到其他fseek / ftell的用法。程序进行了很多偏移量计算,所以我不能切换到fgetpos / fsetpos。一些文件偏移量也存储在文件内,并且有20多个相互关联的数据文件。迁移到64位偏移量将破坏文件格式并需要文件转换。 - osgx
明白了。(曾经历过,做过那个...)只是提醒一下...确保现有的2GB数据得到保留,以便在需要时可以恢复。这段代码的许可证是什么?这段代码是“开源”的吗? - Mahonri Moriancumer
你能提供GitHub上代码的URL吗? - Mahonri Moriancumer
Joe,我能看到使用了无符号32位int,并且偶尔会计算offset。我怎样才能检查计算是否正确呢?我可以用“my_fseek”宏替换所有的fseek来检查计算的正确性吗? - osgx
显示剩余3条评论
1个回答

12

FreeBSD的fseeko()ftello()被记录为符合POSIX.1-2001标准,这意味着off_t是一个有符号的整数类型

在FreeBSD 7上,你可以安全地执行:

off_t          actual_offset;
unsigned long  stored_offset;

if (actual_offset >= (off_t)0 && actual_offset < (off_t)4294967296.0)
    stored_offset = (unsigned long)actual_offset;
else
    some_fatal_error("Unsupportable file offset!");

(在 LP64 架构中,上述代码将是愚蠢的,因为 off_tlong 都将是 64 位有符号整数。即使这样也是安全的;只是愚蠢的,因为支持所有可能的文件偏移量。)

人们经常会被这个东西咬伤,原因是偏移量的计算必须使用 off_t。也就是说,仅仅将结果转换为 off_t 是不够的,你必须将参与运算的 values 转换为 off_t。(从技术上讲,你只需要确保每个算术操作都在 off_t 精度下进行,但我发现如果我只是按惯例和习惯性地将所有操作数都强制转换为 off_t 更容易记住规则。)例如:

off_t          offset;
unsigned long  some, values, used;

offset = (off_t)some * (off_t)value + (off_t)used;
fseeko(file, offset, SEEK_SET);

通常偏移量计算用于查找特定记录中的字段;算法往往保持不变。如果可能的话,我真的建议您将查找操作移到辅助函数中:

int fseek_to(FILE *const file,
             const unsigned long some,
             const unsigned long values,
             const unsigned long used)
{
    const off_t  offset = (off_t)some * (off_t)value + (off_t)used;
    if (offset < (off_t)0 || offset >= (off_t)4294967296.0)
        fatal_error("Offset exceeds 4GB; I must abort!");
    return fseeko(file, offset, SEEK_SET);
}

现在,如果你恰好处于一个幸运的位置,你知道所有的偏移都对齐了(比如,对齐到整数 4),那么你可以使用上述方法的扩展来为自己争取几年时间来重写应用程序:

#define BIG_N 4

int fseek_to(FILE *const file,
             const unsigned long some,
             const unsigned long values,
             const unsigned long used)
{
    const off_t  offset = (off_t)some * (off_t)value + (off_t)used;
    if (offset < (off_t)0)
        fatal_error("Offset is negative; I must abort!");
    if (offset >= (off_t)(BIG_N * 2147483648.0))
        fatal_error("Offset is too large; I must abort!");
    if ((offset % BIG_N) && (offset >= (off_t)2147483648.0))
        fatal_error("Offset is not a multiple of BIG_N; I must abort!");
    return fseeko(file, offset, SEEK_SET);
}

int fseek_big(FILE *const file, const unsigned long position)
{
    off_t  offset;
    if (position >= 2147483648UL)
        offset = (off_t)2147483648UL
               + (off_t)BIG_N * (off_t)(position - 2147483648UL);
    else
        offset = (off_t)position;
    return fseeko(file, offset, SEEK_SET);
}

unsigned long ftell_big(FILE *const file)
{
    off_t  offset;
    offset = ftello(file);
    if (offset < (off_t)0)
        fatal_error("Offset is negative; I must abort!");
    if (offset < (off_t)2147483648UL)
        return (unsigned long)offset;
    if (offset % BIG_N)
        fatal_error("Offset is not a multiple of BIG_N; I must abort!");
    if (offset >= (off_t)(BIG_N * 2147483648.0))
        fatal_error("Offset is too large; I must abort!");
    return (unsigned long)2147483648UL
         + (unsigned long)((offset - (off_t)2147483648UL) / (off_t)BIG_N);
}
逻辑很简单:如果offset小于2的31次方,它将按原样使用。否则,它将由值2的31次方+BIG_N×(offset-2的31次方)来表示。BIG_N必须满足offset 2的31次方及以上始终是BIG_N的倍数。

显然,你只能使用上面三个函数以及所需的fseek_to()变体,只要它们进行相同的检查,并使用不同的参数和公式进行offset计算,你就可以支持高达2147483648 + BIG_N × 2147483647的文件大小。对于BIG_N==4,这是10 GiB(减去4字节;确切地说是10,737,418,236字节)。

有问题吗?

编辑以澄清:

首先,用调用fseek_pos(file, position)替换fseek(file, position, SEEK_SET)

static inline void fseek_pos(FILE *const file, const unsigned long position)
{
    if (fseeko(file, (off_t)position, SEEK_SET))
        fatal_error("Cannot set file position!");
}

替换 fseek(file, position, SEEK_END) 为调用 fseek_end(file, position)(为了对称性——我假设此处的位置通常为字面整数),

static inline void fseek_end(FILE *const file, const off_t relative)
{
    if (fseeko(file, relative, SEEK_END))
        fatal_error("Cannot set file position!");
}

最后,使用 ftell_pos(file) 调用 ftell(file):


static inline unsigned long ftell_pos(FILE *const file)
{
    off_t position;
    position = ftello(file);
    if (position == (off_t)-1)
        fatal_error("Lost file position!");
    if (position < (off_t)0 || position >= (off_t)4294967296.0)
        fatal_error("File position outside the 4GB range!");
    return (unsigned long)position;
}

由于您的架构和操作系统中,unsigned long是32位无符号整型,而off_t是64位有符号整型,因此可以获得完整的4GB范围。

对于偏移量计算,请定义一个或多个类似于以下函数的函数:

static inline void fseek_to(FILE *const file, const off_t term1,
                                              const off_t term2,
                                              const off_t term3)
{
    const off_t position = term1 * term2 + term3;

    if (position < (off_t)0 || position >= (off_t)4294967296.0)
        fatal_error("File position outside the 4GB range!");
    if (fseeko(file, position, SEEK_SET))
        fatal_error("Cannot set file position!");
}
对于每个偏移量计算算法,定义一个fseek_to变体。命名参数使算术合理。如上所述,将参数设为const off_t,这样在算术运算中就不需要额外的强制转换。只有参数和定义计算算法的const off_t position =行会因变异函数而有所不同。 有问题吗?

4
+100,几乎完美的胜利。我本来也想发类似的内容,但是你比我先发了。就我而言,我查看了FXR(源代码才是唯一的真相),所以发现 off_t/sys/types.h, line 219 中是从 __off_t 转换而来的。而 __off_t 又在 /sys/_types.h, line 53 中是从 __int64_t 转换而来的。 - Iwillnotexist Idonotexist
2
@IwillnotexistIdonotexist:你说得对,我应该检查一下来源。我有点懒,而且由于我的Google搜索没有找到FXR -- 我甚至忘记它的存在了!-- 但是我找到了man页面,所以我就用那个了。虽然根据C标准规则,off_t必须是有符号的才能对SEEK_CURSEEK_SET进行负偏移量的寻址才有意义。我主要担心使用off_t时算术运算出错;这是我回答中的主要观点。 - Nominal Animal
ftell/ftello怎么样?内部所有偏移变量都是无符号32位。其中一些是计算的,一些存储/加载在数据库文件中。感谢您提供的代码示例! - osgx
@osgx:使用第一个代码片段。给定 unsigned long store_offset; off_t actual_offset;,执行 actual_offset = ftello(file); 并且 if (actual_offset >= (off_t)0 && actual_offset < (off_t)4294967296.0) store_offset = (unsigned long)actual_offset;,否则您将超出 4GB 范围。我假设这些偏移量没有以任何方式对齐,是吗? - Nominal Animal
1
@osgx:实际上我没有。(off_t)2147483648.0(off_t)4294967296.0off_t类型的字面常量。编译器将在编译时进行转换。当我不想担心大整数常量需要什么样的后缀(无、LLL)以及我知道该常量可以精确地表示为doubleoff_t(或任何目标类型)时,我会使用这种方式。你为什么不接受我的答案呢? - Nominal Animal
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接