指针经过什么样的处理仍然有效?

时间:2016-12-29 14:25:49

标签: c pointers undefined-behavior

以下哪种处理和尝试恢复C指针的方法保证有效?

1)转换为无效指针并返回

int f(int *a) {
    void *b = a;
    a = b;
    return *a;
}

2)转换为适当大小的整数并返回

int f(int *a) {
    uintptr_t b = a;
    a = (int *)b;
    return *a;
}

3)一些简单的整数运算

int f(int *a) {
    uintptr_t b = a;
    b += 99;
    b -= 99;
    a = (int *)b;
    return *a;
}

4)整数运算非常重要,不足以模糊出处,但仍会保持价值不变

int f(int *a) {
    uintptr_t b = a;
    char s[32];
    // assume %lu is suitable
    sprintf(s, "%lu", b);
    b = strtoul(s);
    a = (int *)b;
    return *a;
}

5)更多的间接整数运算将使值保持不变

int f(int *a) {
    uintptr_t b = a;
    for (uintptr_t i = 0;; i++)
        if (i == b) {
            a = (int *)i;
            return *a;
        }
}

显然案例1是有效的,案例2肯定也必须如此。另一方面,我遇到了Chris Lattner的一篇文章 - 遗憾的是我现在找不到了 - 说类似于案例5的东西有效,标准许可编译器只是将其编译为无限循环。然而,每个案例看起来都是前一个案例的无可非议的延伸。

有效案例与无效案件之间的界线在哪里?

根据评论中的讨论添加:虽然我仍然无法找到启发案例5的帖子,但我不记得涉及什么类型的指针;特别是,它可能是一个函数指针,这可能就是为什么这个案例证明了无效的代码,而我的案例5是有效的代码。

第二个补充:好的,这是另一个说有问题的来源,这个我有一个链接。 https://www.cl.cam.ac.uk/~pes20/cerberus/notes30.pdf - 关于指针来源的讨论 - 说,并且有证据支持,如果编译器失去了指针来自哪里,它是未定义的行为。

3 个答案:

答案 0 :(得分:9)

根据C11 draft standard

示例1

有效,§6.5.16.1,即使没有明确的演员。

示例2

intptr_tuintptr_t类型是可选的。指定一个整数的指针需要一个显式的强制转换(§6.5.16.1),虽然gcc和clang只会在你没有的时候发出警告。有了这些注意事项,往返转换由§7.20.1.4有效。 ETA: John Bellinger提出,只有当您通过两种方式进行void*中间演员时才会指定行为。但是,gcc和clang都允许直接转换为文档扩展名。

示例3

安全,但只是因为你使用的是无符号算术,它不能溢出,因此可以保证获得相同的对象表示。 intptr_t可能会溢出!如果要安全地进行指针运算,可以将任何类型的指针转​​换为char*,然后在同一结构或数组中添加或减去偏移量。请注意,sizeof(char)始终为1 ETA:标准保证两个指针的比较相等,但您与Chisnall 等的链接给出了编译器假设两个指针不互为别名的示例。 / p>

示例4

始终,始终,始终检查缓冲区溢出,无论何时读取,特别是每当您写入缓冲区时!如果你能在数学上证明静态分析不会发生溢出?然后写出明确证明这一点的假设,assert()static_assert()它们没有改变。使用snprintf(),而不是已弃用,不安全的sprintf()!如果你没有记住这个答案,请记住!

绝对迂腐,可行的方法是使用<inttypes.h>中的格式说明符,并根据任何指针表示的最大值定义缓冲区长度。在现实世界中,您将使用%p格式打印指针。

你打算问的问题的答案是肯定的:重要的是你得到同样的对象表示。这是一个不那么人为的例子:

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int i = 1;
    const uintptr_t u = (uintptr_t)(void*)&i;
    uintptr_t v;

    memcpy( &v, &u, sizeof(v) );
    int* const p = (int*)(void*)v;

    assert(p == &i);
    *p = 2;
    printf( "%d = %d.\n", i, *p ); 

    return EXIT_SUCCESS;
}

所有这些都是对象表示中的位。此代码也遵循§6.5中的严格别名规则。它编译并运行良好的编译器给Chisnall 麻烦。

实施例5

这与上述相同。

一个极其迂腐的脚注,永远不会与你的编码相关:一些过时的深奥硬件具有符号整数的一个补码或符号和幅度表示,并且在这些上,可能有一个明显的负零值,可能会也可能不会陷阱。在某些CPU上,这可能是与正零不同的有效指针或空指针表示。在某些CPU上,正负零可能相等。

PS

标准说:

  

两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都指向超过最后一个元素的指针。相同的数组对象,或者一个是指向一个数组对象末尾的指针,另一个是指向不同数组对象的开头的指针,该数组对象恰好跟随地址空间中的第一个数组对象。

此外,如果两个数组对象是同一多维数组的连续行,则超过第一行结尾的一个是指向下一行开头的有效指针。因此,即使是故意设置导致与标准允许的错误允许的病态实现也只能在操作指针比较等于数组对象的地址时执行此操作,在这种情况下,实现可能在理论上决定将其解释为改为使用其他一些数组对象。

预期的行为很明显,指针比较等于&array1+1&array2等同于两者:它意味着让您将其与array1中的地址进行比较或取消引用它获得array2[0]。但是,标准实际上并没有这么说。

PPS

标准委员会has addressed some of these issues并建议C标准明确添加有关指针出处的语言。这将确定是否允许符合标准的实现假设由位操作创建的指针不会使另一个指针别名。

具体而言,拟议的更正将引入指针来源,并允许具有不同来源的指针不比较相等。它还会引入一个-fno-provenance选项,它可以保证任何两个指针比较相等,当且仅当它们具有相同的数字地址时。 (如上所述,两个对象指针相互比较相等的别名。)

答案 1 :(得分:3)

  

1)转换为无效指针并返回

这会产生一个等于原始指针的有效指针。该标准的第6.3.2.3/1段明确:

  

指向void的指针可以转换为指向任何对象类型的指针。指向任何对象类型的指针可以转换为指向void的指针,然后再返回;结果应该等于原始指针。

  

2)转换为适当大小的整数并返回

     

3)一些简单的整数运算

     

4)整数运算非常重要,不足以模糊出处,但仍会保持价值不变

     

5)更多的间接整数运算将使值保持不变

     

[...]显然案例1是有效的,案例2肯定也必须如此。另一方面,我遇到了Chris Lattner的一篇文章 - 遗憾的是我现在无法找到 - 说案例5无效,标准许可编译器将其编译为无限循环。

在指针和整数之间进行转换时,C确实需要强制转换,并且在示例代码中省略了一些。从这个意义上说,你的例子(2) - (5)都是不符合的,但对于这个答案的其余部分我会假装所需的演员阵容在那里。

尽管如此,所有这些示例都非常迂腐,它们具有实现定义的行为,因此它们并非严格符合。另一方面,&#34;实现定义&#34;行为仍然是定义的行为;这是否意味着你的代码是&#34;有效&#34;是否取决于您对该术语的含义。无论如何,编译器可能为任何示例发出的代码都是一个单独的问题。

这些是第6.3.2.3节(增加的重点)中标准的相关规定:

  

整数可以转换为任何指针类型。除非之前指定,否则 结果是实现定义的 ,可能未正确对齐,可能未指向引用类型的实体,并且可能是陷阱表示。

     

任何指针类型都可以转换为整数类型。除了之前指定的情况, 结果是实现定义的 。如果结果无法以整数类型表示,则行为未定义。结果不必在任何整数类型的值范围内。

uintptr_t的定义也与您的特定示例代码相关。标准以这种方式描述(C2011,7.20.1.4/1;强调增加):

  

一个无符号整数类型,其属性是任何有效指针 到void 都可以转换为此类型,然后转换回指针 到void ,结果将与原始指针进行比较。

您要在int *uintptr_t之间来回转换。 int *不是void *,因此7.20.1.4/1不适用于这些转换,并且行为是根据第6.3.2.3节的实现定义。

但是,假设您通过中间void *来回转换:

uintptr_t b = (uintptr_t)(void *)a;
a = (int *)(void *)b;

在提供uintptr_t(可选)的实现上,这将使您的示例(2 - 5)全部严格符合。在这种情况下,整数到指针转换的结果仅取决于uintptr_t对象的值,而不取决于如何获得该值。

至于您归功于Chris Lattner的说法,它们实际上是错误的。如果您准确地表示了它们,那么它们可能反映了实现定义的行为与 un 定义的行为之间的混淆。如果代码表现出不明确的行为,那么索赔可能会持有一些水,但事实上并非如此。

无论如何获得其值,b都具有类型uintptr_t的确定值,并且循环最终必须将i增加到该值,此时{{1}块将运行。原则上,从if直接转换为uintptr_t的实现定义行为可能是疯狂的,例如跳过下一个语句(从而导致无限循环),但这种行为完全不可信。您遇到的每个实现都会在此时失败,或者在变量int *中存储一些值,然后,如果它没有崩溃,它将执行a语句。

答案 2 :(得分:2)

由于不同的应用程序领域需要以不同的方式操作指针的能力,并且由于出于某些目的的最佳实现可能完全不适用于某些其他方式,因此C标准将对各种操作的支持(或缺乏支持)视为实施质量问题。一般而言,为特定应用程序领域编写实现的人们应该比标准作者更熟悉该领域的程序员将使用哪些功能,并且人们要做出真诚的努力来产生适合于在该领域编写应用程序的高质量实现。无论标准是否要求,该字段都将支持这些功能。

在丹尼斯·里奇(Dennis Ritchie)发明的标准前语言中,被标识为相同地址的所有特定类型的指针都是等效的。如果指针上的任何操作序列最终会产生另一个标识相同地址的相同类型的指针,则该指针(本质上根据定义)将等于第一个指针。但是,C标准规定了某些情况,在这种情况下,指针可以标识存储中的相同位置,并且彼此之间无法区分而又不等同。例如,给定:

int foo[2][4] = {0};
int *p = foo[0]+4, *q=foo[1];

pq彼此相等,与foo[0]+4foo[1]比较。另一方面,尽管对p[-1]q[0]的评估将定义行为,但对p[0]q[-1]的评估将调用UB。不幸的是,尽管标准明确指出pq并不等效,但它并没有阐明是否对以下内容执行各种操作序列: p将产生一个在p可用的所有情况下都可用的指针,在 p或{{1}的所有情况下都可用的指针}将是可用的,仅在q可用的情况下才可用的指针,或在两者 qp都只有的情况下可用的指针将可用。

用于低级编程的质量实现通常应处理除涉及q指针的那些指针之外的其他指针操作,其方式应是产生一个在任何与之相等的指针都可以使用的情况下可用的指针可用的。不幸的是,该标准没有提供程序可以用来确定程序是否正在由适合于低级编程的高质量实现所处理的方法,并且如果不适合,则拒绝运行,因此大多数形式的系统编程都必须依靠高质量的实现即使本标准没有规定,也要以书面形式处理环境特征的某些动作。

顺便说一句,即使操纵指针的常规构造在不应该应用等效原理的情况下也没有任何创建指针的方式,但某些平台可能会定义创建“有趣”指针的方式。例如,如果通常会在空指针上捕获操作的实现是在有时可能需要访问地址为零的对象的环境中运行的,则它可能会定义一种特殊的语法来创建可用于访问的指针在创建地址的上下文中的任何地址,包括零。 “指向地址零的合法指针”可能会比较等于空指针(即使它们不相等),但执行往返转换为另一种类型并返回则很可能会将原来是合法指针的地址转换为地址零变成一个空指针。如果该标准要求 any 指针的往返转换必须产生与原始指针相同的可用指针,那么这将要求编译器在任何可能被删除的指针上省略null陷阱。即使以这种方式产生的结果,即使它们很有可能是通过空指针的双向来回产生的。

顺便说一下,从实际的角度来看,即使在restrict中,“现代”编译器有时也会尝试通过指针-整数-指针转换来跟踪指针的出处,使得通过转换相等整数产生的指针可能会有时被认为不能混叠。

例如,给定:

-fno-strict-aliasing

在没有标记行的情况下,gcc,icc和clang都将假定-即使使用#include <stdint.h> extern int x[],y[]; int test(void) { if (!x[0]) return 999; uintptr_t upx = (uintptr_t)x; uintptr_t upy = (uintptr_t)(y+1); //Consider code with and without the following line if (upx == upy) upy = upx; if ((upx ^ ~upy)+1) // Will return if upx != upy return 123; int *py = (int*)upy; *py += 1; return x[0]; } 时,对-fno-strict-aliasing的操作也不会影响*py,即使可以到达代码的唯一方法是*pxupx保持相同的值(这意味着upypx都是通过强制转换相同的{ {1}}值)。添加标记的行会导致icc和clang识别px和py可以标识相同的对象,但是gcc假定可以优化分配,即使这应该意味着py将从{{1}派生}-在这种情况下,高质量的编译器应该可以毫不费力地识别出可能存在的别名。

我不确定编译器作者跟踪uintptr_t值出处的努力会带来什么实际好处,因为在将转换结果用于“有趣”的方式。但是,考虑到编译器的行为,我不确定是否有任何好的方法可以保证整数和指针之间的转换以与所涉及的值一致的方式运行。