指针经过什么样的治疗仍然有效?

rwa*_*ace 16 c pointers undefined-behavior

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

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

int f(int *a) {
    void *b = a;
    a = b;
    return *a;
}
Run Code Online (Sandbox Code Playgroud)

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

int f(int *a) {
    uintptr_t b = a;
    a = (int *)b;
    return *a;
}
Run Code Online (Sandbox Code Playgroud)

3)一些简单的整数运算

int f(int *a) {
    uintptr_t b = a;
    b += 99;
    b -= 99;
    a = (int *)b;
    return *a;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

5)更多间接整数运算,保持值不变

int f(int *a) {
    uintptr_t b = a;
    for (uintptr_t i = 0;; i++)
        if (i == b) {
            a = (int *)i;
            return *a;
        }
}
Run Code Online (Sandbox Code Playgroud)

显然案例1是有效的,案例2肯定也必须如此.我遗憾的是现在找不到- -在另一方面,我碰到一个帖子由克里斯·拉特纳来到说类似于壳体5东西不是有效,即标准许可证编译器只是把它编译成一个无限循环.然而,每个案例看起来都是前一个案例的无可非议的延伸.

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

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

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

Dav*_*lor 9

根据C11标准草案:

例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 等人的联系.给出了编译器假设两个指针不互为别名的示例.

例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;
}
Run Code Online (Sandbox Code Playgroud)

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

例5

这与上面相同.

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

PS

标准说:

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

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

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

PPS

标准委员会已经解决了其中一些问题,并建议C标准明确添加有关指针出处的语言.这将确定是否允许一致的实现假设由位操作创建的指针不会使另一个指针别名.

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