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

Phi*_*ler 18 c c++ null-pointer undefined-behavior language-lawyer

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

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

int main() {

  char *c = NULL;
  c--;

  printf("c: %p\n", c);

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

然而,似乎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
Run Code Online (Sandbox Code Playgroud)

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

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

Alo*_*ave 20

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

char *c = NULL;
c--;
Run Code Online (Sandbox Code Playgroud)

是未定义的已定义行为,因为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指向数组对象的最后一个元素.如果指针操作数和结果指向相同的数组对象,或一个过去的数组对象的最后一个元素的元素两者,所述评估也不得产生溢出; 否则,行为未定义.

  • 显然,解除引用`NULL`指针是UB,就像'C++ 11中所描述的'NULL`指针一样. (2认同)

Ric*_*ith 16

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

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

  • [expr.add] p7的意图肯定似乎是在空指针中添加0或减去两个空指针,在C++中是明确定义的,但p5和p6明确表示行为是未定义的,通常,如果标准的一部分似乎定义了程序的行为,而另一部分则表示行为未定义,表示行为未定义的部分获胜. (3认同)
  • 我试图让p5和p6的措辞得到改善(还有其他一些问题)但到目前为止还没有取得任何成功.另请注意,p6的脚注描述了另一种不同且微妙不兼容的指针算法模型. (3认同)

sup*_*cat 6

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

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

捕获表单(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指针将以这种方式处理.我已经在至少一个编译器中看到了第一个行为,但是我不认为我已经看过第二个.