Clang 5.0 和 UBsan 的指针加法和整数溢出?

Pointer addition and integer overflow with Clang 5.0 and UBsan?

我试图理解我们最近在使用 Clang 5.0 和未定义行为消毒器 (UBsan) 时解决的问题。我们有在向前或向后方向处理缓冲区的代码。简化后的大小写为 similar to the code shown below.

0-len 可能看起来有点不寻常,但它是早期 Microsoft .Net 编译器所需要的。 Clang 5.0 和 UBsan produced integer overflow findings:

adv-simd.h:1138:26: runtime error: addition of unsigned offset to 0x000003f78cf0 overflowed to 0x000003f78ce0
adv-simd.h:1140:26: runtime error: addition of unsigned offset to 0x000003f78ce0 overflowed to 0x000003f78cd0
adv-simd.h:1142:26: runtime error: addition of unsigned offset to 0x000003f78cd0 overflowed to 0x000003f78cc0
...

第1138、1140、1142行(和朋友)是增量,可能 由于 0-len.

向后退步
ptr += inc;

根据 Pointer comparisons in C. Are they signed or unsigned?(也讨论了 C++),指针既没有符号也没有符号。我们的偏移量是无符号的,我们依靠无符号整数换行来实现反向步幅。

代码在 GCC UBsan 和 Clang 4 以及更早的 UBsan 下运行良好。我们最终用 help with the LLVM devs 为 Clang 5.0 清除了它。我们需要使用 ptrdiff_t.

而不是 size_t

我的问题是,构造中的整数 overflow/undefined 行为在哪里? ptr + <unsigned> 是如何导致有符号整数溢出并导致未定义行为的?


这是一个镜像真实代码的 MSVC。

#include <cstddef>
#include <cstdint>
using namespace std;

uint8_t buffer[64];

int main(int argc, char* argv[])
{
    uint8_t * ptr = buffer;
    size_t len = sizeof(buffer);
    size_t inc = 16;

    // This sets up processing the buffer in reverse.
    //   A flag controls it in the real code.
    if (argc%2 == 1)
    {
        ptr += len - inc;
        inc = 0-inc;
    }

    while (len > 16)
    {
        // process blocks
        ptr += inc;
        len -= 16;
    }

    return 0;
}

指针加整数的定义是(N4659 expr.add/4):

我在这里使用了一张图片以保留格式(这将在下面讨论)。

请注意,这是一个新的措辞,它取代了以前标准中不太明确的描述。

在您的代码中(当 argc 为奇数时)我们最终得到的代码等同于:

uint8_t buffer[64];
uint8_t *ptr = buffer + 48;
ptr = ptr + (SIZE_MAX - 15);

对于应用于您的代码的标准引用中的变量,i48j(SIZE_MAX - 15)n64.

现在的问题是0≤i+j≤n是否成立。如果我们将 "i + j" 解释为 表达式 i + j 的结果,则等于 32 小于 n。但是如果是指数学结果那么它比n.

大很多

标准在这里使用数学方程的字体,不使用源代码的字体。 也不是有效的运算符。所以我认为他们打算用这个等式来描述数学值,即这是未定义的行为。

C 标准将类型 ptrdiff_t 定义为指针差分运算符产生的类型。一个系统可能有一个 32 位 size_t 和一个 64 位 ptrdiff_t;这样的定义自然适合使用 64 位线性或准线性指针但确实要求每个对象小于 4GiB 的系统。

如果已知每个对象小于 2GiB,存储类型 ptrdiff_t 而不是 size_t 的值可能会使程序不必要地低效。然而,在这种情况下,代码不应使用 size_t 来保存可能为负的指针差异,而应使用 int32_t [如果每个对象小于 2GiB,这将足够大]。即使 ptrdiff_t 是 64 位,类型 int32_t 的值在从任何指针中添加或减去之前也会被正确地符号扩展。