是否对空指针未定义行为执行算术运算?

它在我看来像下面的程序计算一个无效的指针,因为除了赋值和比较的平等之外, NULL是没有好处的:

 #include  #include  int main() { char *c = NULL; c--; printf("c: %p\n", c); return 0; } 

然而,似乎GCC或Clang针对未定义行为的警告或工具都没有说这实际上是UB。 这个算术实际上是否有效,而且我太迂腐了,或者这是我们应该报告的检查机制的缺陷吗?

测试:

 $ clang-3.3 -Weverything -g -O0 -fsanitize=undefined -fsanitize=null -fsanitize=address offsetnull.c -o offsetnull $ ./offsetnull c: 0xffffffffffffffff $ gcc-4.8 -g -O0 -fsanitize=address offsetnull.c -o offsetnull $ ./offsetnull c: 0xffffffffffffffff 

似乎很好地记录了Clang和GCC使用的AddressSanitizer更侧重于解除坏指针的引用,所以这很公平。 但其他检查也没有抓住它: – /

编辑 :我问这个问题的部分原因是-fsanitize标志能够动态检查生成的代码中的良好定义。 这是他们应该抓住的东西吗?

指向不指向数组的指针的指针算法是未定义的行为。
此外,取消引用NULL指针是未定义的行为。

 char *c = NULL; c--; 

是未定义的已定义行为,因为c未指向数组。

C ++ 11标准5.7.5:

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果元素和原始数组元素的下标的差异等于整数表达式。 换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)指向分别为数组对象的第i + n和第i – 第n个元素,只要它们存在。 此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素,如果表达式Q指向一个超过数组对象的最后一个元素,表达式(Q)-1指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为未定义。

是的,这是未定义的行为,并且是-fsanitize=undefined应该捕获的东西; 它已经在我的TODO列表上添加了一个检查。

FWIW,这里的C和C ++规则略有不同:向空指针添加0并从另一个中减去一个空指针在C中有未定义的行为,但在C ++中没有。 对空指针的所有其他算术在两种语言中都有未定义的行为。

不仅禁止对空指针进行算术运算,而且陷阱尝试取消引用的实现失败也会对空指针进行陷阱运算,这极大地降低了空指针陷阱的好处。

标准中没有定义任何情况,其中向空指针添加任何内容都可以产生合法的指针值; 此外,实现可以为此类操作定义任何有用行为的情况很少见,通常可以通过编译器内在函数(*)更好地处理。 然而,在许多实现中,如果没有捕获空指针算术,则向空指针添加偏移量可以产生指针,该指针虽然无效,但不再可识别为空指针。 尝试取消引用这样的指针不会被捕获,但可能触发任意效果。

捕获表单(null + offset)和(null-offset)的指针计算将消除这种危险。 请注意,保护不一定需要陷阱(指针为空),(空指针)或(null-null),而前两个表达式返回的值不太可能有任何用处[如果要实现指定null-null将产生零,针对该特定实现的代码有时可能比必须特殊情况为null代码更有效]它们不会生成无效指针。 此外,让(null + 0)和(null-0)产生空指针而不是陷阱不会危及安全性并且可能避免需要使用用户代码特殊情况的空指针,但是由于编译器的优点不那么引人注目将不得不添加额外的代码来实现这一目标。

(*)例如,8086编译器上的这种内在函数可能接受无符号的16位整数“seg”和“ofs”,并且在地址seg:ofs读取单词而没有空陷阱,即使地址恰好为零。 8086上的地址(0x0000:0x0000)是某些程序可能需要访问的中断向量,而地址(0xFFFF:0x0010)在只有20个地址线的旧处理器上访问与(0x0000:0x0000)相同的物理位置,在具有24个或更多地址线的处理器上访问物理位置​​0x100000。 在某些情况下,另一种方法是对指针进行特殊指定,这些指针预期指向C标准无法识别的事物(中断向量将符合条件),并避免空陷阱,或者指定volatile指针将以这种方式处理。 我已经在至少一个编译器中看到了第一个行为,但是我不认为我已经看过第二个。