由UB递增NULL指针引起的错误示例

时间:2015-04-24 10:30:05

标签: c++ undefined-behavior

此代码:

int *p = nullptr;
p++;

导致未定义的行为,如Is incrementing a null pointer well-defined?

中所述

但在解释研究员为什么要避免使用UB时,除了说这是坏事,因为UB意味着任何事情都可能发生,我想举一些例子证明它。我有很多用于访问超出限制的数组,但我找不到一个。

我甚至尝试过

int testptr(int *p) {
    intptr_t ip;
    int *p2 = p + 1;
    ip = (intptr_t) p2;
    if (p == nullptr) {
        ip *= 2;
    }
    else {
        ip *= -2;
    } return (int) ip;
}

在单独的编译单元中希望优化编译器跳过测试,因为当p为空时,行int *p2 = p + 1;为UB,并且允许编译器假定代码不包含UB。 / p>

但是gcc 4.8.2(我没有可用的gcc 4.9)和clang 3.4.1都回答了正值!

有人建议使用更聪明的代码或其他优化编译器来增加空指针时出现问题吗?

4 个答案:

答案 0 :(得分:7)

这个例子怎么样:

int main(int argc, char* argv[])
{
    int a[] = { 111, 222 };

    int *p = (argc > 1) ? &a[0] : nullptr;
    p++;
    p--;

    return (p == nullptr);
}

根据面值,此代码显示:'如果有任何命令行参数,请初始化p以指向a[]的第一个成员,否则将其初始化为null。然后递增它,然后递减它,然后告诉我它是否为空。'

从表面上看,如果我们提供命令行参数,则返回'0'(表示p为非空),如果不提供,则返回'1'(表示空)。 请注意,我们决不取消引用p,如果我们提供参数,则p始终指向a[]的范围内。

使用命令行clang -S --std=c++11 -O2 nulltest.cpp(Cygwin clang 3.5.1)进行编译,生成以下生成的代码:

    .text
    .def     main;
    .scl    2;
    .type   32;
    .endef
    .globl  main
    .align  16, 0x90
main:                                   # @main
.Ltmp0:
.seh_proc main
# BB#0:
    pushq   %rbp
.Ltmp1:
    .seh_pushreg 5
    movq    %rsp, %rbp
.Ltmp2:
    .seh_setframe 5, 0
.Ltmp3:
    .seh_endprologue
    callq   __main
    xorl    %eax, %eax
    popq    %rbp
    retq
.Leh_func_end0:
.Ltmp4:
    .seh_endproc

此代码显示“返回0”。它甚至懒得检查命令行参数的数量。

(有趣的是,评论减少对生成的代码没有影响。)

答案 1 :(得分:6)

摘自http://c-faq.com/null/machexamp.html

  

问:严重的是,有任何实际的机器真的使用非零null   指针,或指向不同指针的不同表示   类型?

     

A: Prime 50系列使用了段07777,偏移量为0   指针,至少对于PL / I。后来的模型使用了段0,偏移0为   C中的空指针,需要新的指令,如TCNP(测试   C Null Pointer),显然是所有现存的[footnote]   写得不好的C代码做出了错误的假设。年纪大了,   有文字说服的Prime机器也因要求更大而臭名昭着   字节指针(char *)比字指针(int *')。

     

Data General的Eclipse MV系列有三个架构   支持指针格式(字,字节和位指针),其中两个   由C编译器使用:char *void *的字节指针,以及字   其他一切的指针。由于历史原因   来自16位Nova线的32位MV线的演变   指针和字节指针有偏移,间接和环   保护位在单词的不同位置。传递不匹配   函数的指针格式导致保护错误。   最终,MV C编译器添加了许多兼容性选项来尝试   处理具有指针类型不匹配错误的代码。

     

一些Honeywell-Bull主机使用位模式06000   (内部)空指针。

     

CDC Cyber​​ 180系列有48位指针,由一个环组成,   段和偏移量。大多数用户(在第11环中)都有空指针   0xB00000000000。在旧的CDC补充机器上很常见   使用全一位字作为各种数据的特殊标志,   包括无效地址。

     

旧的HP 3000系列使用不同的字节寻址方案   地址而不是字地址;像上面的几台机器一样   因此,它为char *void *使用了不同的表示形式   指针比其他指针。

     

Symbolics Lisp Machine,一个标记的架构,甚至没有   传统的数字指针;它使用了<NIL, 0>对(基本上是一个   不存在&lt;对象,偏移&gt; handle)作为C空指针。

     

取决于所使用的“内存模型”,8086系列处理器(PC   兼容性)可以使用16位数据指针和32位函数   指针,反之亦然。

     

一些64位Cray机器在a的低48位代表int *   字; char *另外使用高16位中的一些来表示a   一个字内的字节地址。

鉴于那些空指针在引用的机器中有一个奇怪的位模式表示,你输入的代码:

int *p = nullptr;
p++;

不会给出大多数人期望的价值(0 + sizeof(*p))。

相反,您将拥有一个基于您的机器特定nullptr位模式的值(除非编译器具有空指针算法的特殊情况,但由于标准没有强制要求,您很可能会面临未定义具有“可见”具体效果的行为。)

答案 2 :(得分:2)

理想的C实现,当不用于需要使用程序员知道具有意义但编译器没有意义的指针的各种系统编程时,确保每个指针都有效或可识别为无效,并且将陷阱任何时候代码试图取消引用无效指针(包括null)或使用非法手段创建一些不是有效指针但可能被误认为是的东西。在大多数平台上,生成的代码在所有情况下强制实施这样的约束都会非常昂贵,但防止许多常见的错误情况要便宜得多。

在许多平台上,让编译器生成相当于*foo=23的{​​{1}}代码相对便宜。即使是20世纪80年代的原始编译器也经常有这样的选择。然而,如果编译器允许空指针以不再可识别为空指针的方式递增,则这种陷印的有用性可能会大大丧失。因此,一个好的编译器应该在启用错误捕获时将if (!foo) NULL_POINTER_TRAP(); else *foo=23;替换为foo++;。可以说,真正的#十亿美元的错误&#34;没有发明空指针,但更确切地说,有些编译器会捕获直接的空指针存储,但不会捕获空指针算法。

鉴于理想的编译器会在尝试增加空指针时陷阱(许多编译器因性能而不是语义而无法这样做),我认为没有理由为什么代码应该期望这样的增量具有意义。在几乎任何情况下,程序员可能期望编译器为这样的构造赋予意义[例如, foo = (foo ? foo+1 : (NULL_POINTER_TRAP(),0));产生一个指向地址5的指针,程序员最好使用其他构造来形成所需的指针(例如((char*)0)+5)。

答案 3 :(得分:1)

这只是为了完成,但@HansPassant在评论中提出的链接确实应该被引用作为答案。

所有引用都是here,以下是一些摘录

本文是关于 C抽象的新的内存安全解释 提供更强保护的机器 安全性和调试 ... [作者] 表明可以实现内存安全 C不仅支持C抽象机器 如规定,但更广泛的解释仍然兼容 使用现有代码。通过在硬件中强制执行模型, 我们的实现提供了可以使用的内存安全性 为C ...提供高级安全属性

[实现] 内存功能表示为 三元组(基础,绑定,权限),松散包装 转换为256位值。这里base为虚拟提供了一个偏移量 地址区域,并限制区域的大小 访问...特殊能力 加载和存储指令允许溢出功能 堆栈或存储在数据结构中,就像指针一样... 不允许指针扣除的警告。

添加权限允许功能 是令牌授予引用内存的某些权限。 例如,内存功能可能具有权限 阅读数据和功能,但不要写它们(或只是 写数据但不写功能)。 尝试任何一个 不允许的操作会导致陷阱

[] 结果证实可以保留强者 能力系统内存模型的语义(提供 不可绕过的内存保护)而不会牺牲 低级语言的优点。

(强调我的)

这意味着即使它不是一个可操作的编译器,也存在一些研究来构建一个可以捕获不正确的指针用法并且已经发布的研究。