21

在我看来,下面的程序计算了一个无效的指针,因为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标志可以动态检查生成的代码中的明确性。这是他们应该抓住的东西吗?

4

3 回答 3

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 指向数组对象的最后一个元素。如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。

于 2013-03-25T05:45:12.670 回答
17

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

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

于 2013-07-31T19:59:02.813 回答
6

不仅禁止空指针上的算术运算,而且捕获尝试取消引用以也捕获空指针上的算术的实现的失败大大降低了空指针陷阱的好处。

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

捕获 (null+offset) 和 (null-offset) 形式的指针计算将消除这种危险。请注意,保护不一定需要捕获 (pointer-null)、(null-pointer) 或 (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指针将以这种方式处理。我在至少一个编译器中看到了第一个行为,但我认为我没有看到第二个。

于 2015-08-04T21:03:35.387 回答