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

时间:2013-03-25 05:41:05

标签: c++ c language-lawyer undefined-behavior null-pointer

在我看来,以下程序计算一个无效指针,因为NULL对于除了赋值和比较之外的任何东西都没有好处:

#include <stdlib.h>
#include <stdio.h>

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标志启用动态检查生成的代码中的良好定义。这是他们应该抓住的东西吗?

3 个答案:

答案 0 :(得分:20)

指向未指向数组的指针的指针算法是未定义的行为 此外,取消引用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指向数组对象的最后一个元素。如果指针操作数和结果都指向同一个数组对象的元素,或者一个过去   数组对象的最后一个元素,评估不应产生溢出;否则,行为未定义。

答案 1 :(得分:16)

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

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

答案 2 :(得分:6)

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

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

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

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