为什么要分步进行位移操作?

8

Linux内核中,我发现了以下代码:

static inline loff_t pos_from_hilo(unsigned long high, unsigned long low)
{
#define HALF_LONG_BITS (BITS_PER_LONG / 2)
    return (((loff_t)high << HALF_LONG_BITS) << HALF_LONG_BITS) | low;
}

该代码用于将系统调用参数组合成一个更宽的变量,例如在ia32上,pwritev的偏移量在两个32位寄存器中指定。
在x64上,loff_t和unsigned long都是64位宽。在这种情况下,高变量被忽略,只使用低变量。在ia32上,loff_t为64位宽,unsigned long为32位宽。在这种情况下,将组合两个参数high和low。
我想知道为什么代码要进行两次位移而不是一次。关于这段代码有更多的信息,请参见提交消息和LWN文章:系统调用和64位架构,但没有解释双重位移的原因。
1个回答

4
以下在测试应用中的警告帮助我弄清了这个问题:
test.c:8:27: warning: left shift count >= width of type [-Wshift-count-overflow]
    8 |     return (((loff_t)high << (2*HALF_LONG_BITS))) | low;

双位移操作保护程序免于未定义行为。根据C规范(来自此处):
6.5.7 3) ... 如果右操作数的值为负,或大于等于提升后的左操作数的宽度,则其行为未定义。
在64位机器上,loff_t和long的宽度均为64位。如果我们一次性进行位移操作,会将high左移64位,这根据上述声明是未定义的行为。分两步执行可以将high变为0。
PS:我编写了一个测试程序来调查这个问题,令人惊讶的是,当我将这两个位移操作替换为一个单独的位移操作时,得到了不同的结果。

1
我可能在这里漏掉了什么,但如果(预期的?)净结果是左移类型宽度恰好等于位数的数量,为什么不只是return low;呢? - Bob__
1
@Bob__: 我认为 BITS_PER_LONGunsigned long 的宽度,但 loff_t 可能更宽。我相信这个想法是为了编写代码,以便在 loff_tunsigned long 更宽时将 highlow 打包到返回值中,并且如果它们具有相同的宽度,则仅返回 low - Nate Eldredge
@NateEldredge 如果我没记错的话,鉴于 /include/linux/types.h/include/uapi/asm-generic/posix_types.h 中的 typedefloff_t 最终会成为一个 long long,因此用两个 unsigned long 填充它是否会导致 UB - Bob__
2
@Bob__: 内核是使用-fno-strict-overflow构建的,因此有符号整数溢出不是UB,而是保证会环绕。所以我认为一切都很好。在一个unsigned long为32位且loff_t为64位的系统上,您将highlow打包到返回值中,如果high太大,那么发生的所有情况就是返回值为负数(可能在其他地方进行检查)。如果unsigned longloff_t都是64位,则(((loff_t)high << HALF_LONG_BITS) << HALF_LONG_BITS)保证计算结果为0,然后我们只返回low - Nate Eldredge
1
@NateEldredge 我明白了,谢谢。我本来会写类似于这个的东西,但那已经不重要了。 - Bob__

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