何时可以访问指向“已死”对象的指针?

53
首先,澄清一下,我不是在谈论取消引用无效指针的问题!
考虑以下两个例子。 示例1
typedef struct { int *p; } T;

T a = { malloc(sizeof(int) };
free(a.p);  // a.p is now indeterminate?
T b = a;    // Access through a non-character type?

示例2

void foo(int *p) {}

int *p = malloc(sizeof(int));
free(p);   // p is now indeterminate?
foo(p);    // Access through a non-character type?

问题

上述示例中是否有任何一个会引起未定义的行为?

背景

这个问题是针对这个讨论提出的。建议是,例如,指针参数可以通过x86段寄存器传递给函数,这可能会导致硬件异常。

从C99标准中,我们了解到以下内容(重点是我的):

[3.17] 不确定的值 - 未指定的值或陷阱表示

然后:

[6.2.4 p2] 当对象指向达到其生命周期的末尾时,指针的值变得不确定

然后:

[6.2.6.1 p5] 某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示,并且被lvalue表达式读取而没有字符类型,则行为未定义。如果这样的表示是由修改所有或任何部分的对象的副作用所产生的,而这些对象不具有字符类型的lvalue表达式,则行为未定义。这种表示称为陷阱表示

综合考虑,我们对访问指向“死”对象的指针有什么限制?

补充说明

尽管我引用了C99标准,但我想知道在任何C++标准中行为是否有所不同。


3
你以出色的方式引用了标准——从这些话中,我清楚地看到,即使不对无效指针进行解引用操作,任何使用无效指针的方式都会引起未定义行为。 - user529758
1
@Devolus:是的,这也是我的直觉。但标准似乎相对明确。而AProgrammer在链接讨论中提出了一个很好的观点,如果段寄存器参与其中,这确实可能会导致硬件异常。 - Oliver Charlesworth
3
@willj: 那是正确的。但是,标准告诉我们指针现在是不确定的。 - Oliver Charlesworth
1
自己编写mallocfree早已引起未定义的行为。 根据7.1.3:“如果程序在保留的上下文中声明或定义标识符(除7.1.4允许的情况外),或将保留的标识符定义为宏名称,则其行为未定义。” - R.. GitHub STOP HELPING ICE
3
@willj,这不是关于修改那个值的问题。很可能指针仍然具有相同的值。但是,如果该值被复制到某个地方,它可能会经过特殊的指针寄存器(例如x86中的段寄存器),在那里硬件可能由于指针无效而导致陷阱发生。 - Shahbaz
显示剩余16条评论
4个回答

31

示例2无效。您问题中的分析是正确的。

示例1有效。结构类型永远不会包含陷阱表示,即使其成员之一包含陷阱表示。这意味着,在可能导致问题的系统上,结构赋值必须实现为按字节复制,而不是逐个成员复制。

6.2.6 类型的表示

6.2.6.1 通用

6 [...] 结构体或联合体对象的值永远不是陷阱表示,即使结构体或联合体对象的某个成员的值是陷阱表示。


@hvd:这似乎更像是标准应该编写的方式,尽管我希望编写者进一步指定陷阱表示的存在或类似它们的东西必须是实现定义的,但后果不需要如此。 - supercat
@supercat 任何能够在至少2的大小分配内存成功的实现,必须具有陷阱表示:在malloc的结果中添加一个字节,您将获得一个指针,不允许与在调用malloc之前有效的任何指针值相等。因此,在调用malloc之前,该表示形式是一个陷阱表示。 - user743382
@hvd: 我所见到的“trap representation”术语的用途表明,当作为右值读取时,其值将以某种方式破坏正常的程序流程,希望能够识别为陷阱,但其详细信息超出了C标准的范围。基本上,我想看到的是,标准应该说一个实现必须指定在什么情况下语句 p=q; (给定相同类型(任何类型)的非别名变量 pq )可能会做一些除了使 p 持有一个至少与 q 中的内容定义一样良好的值之外的其他事情。 - supercat
@supercat 标准对陷阱表示有非常明确的定义:它是一种不代表值的表示。C99 6.2.6.1p5:“某些对象表示不需要表示对象类型的值。[...]这种表示称为陷阱表示。”你指的是其他东西。无论如何,自C11以来,即使类型没有陷阱表示,读取不确定值仍然大多是未定义的,因此它不会给你带来太多好处。 - user743382
1
@supercat 可分析性 或许对此有所帮助(但不适用于您之前的评论)。自 C11 起,一个实现可以定义 __STDC_ANALYZABLE__ 来指示未定义行为的影响是有限的,除了关键的未定义行为。而读取陷阱表示并非关键的未定义行为:如果定义了 __STDC_ANALYZABLE__,则可能导致程序中止,但不会完全破坏程序的执行。 - user743382
显示剩余11条评论

15

我的理解是,只有非字符类型才能具有trap representations,但任何类型都可以具有不确定的值,访问具有不确定值的对象会引发未定义行为。最臭名昭著的例子可能是OpenSSL对未初始化对象的无效使用作为随机种子。

因此,对你的问题的答案将是:永远不。

顺便说一下,在freerealloc后,指向对象的指针本身也是不确定的一个有趣的结果是,这个习惯用法会引发未定义的行为:

void *tmp = realloc(ptr, newsize);
if (tmp != ptr) {
    /* ... */
}

1
关于“访问对象...”的问题,标准中有一个我没有引用的脚注:“因此,自动变量可以被初始化为陷阱表示而不会导致未定义的行为,但是在存储正确值之前不能使用变量的值。”听起来好像对这样的对象进行写入是可以接受的。 - Oliver Charlesworth
3
当然可以。否则你怎么能做类似这样的事:free(x); x = NULL; - Shahbaz
4
@OliCharlesworth,我认为这句话说:“如果一个对象的存储值具有这样的表示,并且通过lvalue表达式读取...”,说明它可以被写入,但不能从中读取。 - Shahbaz
1
void *tmp = realloc(ptr, newsize); << 如果realloc失败,那么tmp是有效的(NULL),而ptr仍然有效。当tmp==NULL时,这不是未定义行为。 - jim mcnamara
4
当然可以。但这是针对成功情况下的未定义行为,这就是重点所在。 - R.. GitHub STOP HELPING ICE
显示剩余2条评论

0
即使没有任何干扰其表示的位的情况下,说指针值变得不确定,很可能是为了适应“as-if”规则。如果存在一些操作序列,其行为可能受到有用的优化转换的影响,那么“as-if”规则要求该序列中至少有一个操作被描述为调用未定义行为,以证明由优化引起的任何可观察怪异都是合理的。
考虑以下函数:
void test(int *p1, uint64_t ofs)
{
  int ret;
  int *p2 = malloc(sizeof (int));
  if ((uintptr_t)p1 == (uintptr_t)p2+ofs)
  {
    *p2 = 1;
    *p1 = 2;
    doSomething(*p2);
  }
  free(p2);
  return p2;
}

在大多数可能调用该函数的情况下,将调用doSomething(*p2)替换为doSomething(2)可以提高性能,而不会影响行为,除非p1是指向已释放存储区域的指针,其地址恰好与malloc()返回的新区域的地址相同。当由malloc()重新使用的存储区域变得可重用时,将p1视为不确定的,可以使编译器忽略地址可能与某个未来分配的地址匹配的可能性。

-1

C++讨论

简短回答:在C++中,没有访问“读取”类实例的概念;您只能“读取”非类对象,并且这是通过左值到右值转换完成的。

详细回答:

typedef struct { int *p; } T;

T 表示一个未命名的类。为了讨论方便,我们将这个类命名为 T

struct T {
    int *p; 
};

由于您没有声明拷贝构造函数,编译器会默认为您声明一个,因此类定义如下:
struct T {
    int *p; 
    T (const T&);
};

所以我们有:

T a;
T b = a;    // Access through a non-character type?

是的,确实如此;这是通过复制构造函数进行初始化,因此编译器将生成复制构造函数定义;该定义等同于

inline T::T (const T& rhs) 
    : p(rhs.p) {
}

所以你正在作为指针访问该值,而不是一堆字节。

如果指针值无效(未初始化、已释放),则行为未定义。


实际上,对于类的左值,也可以进行左值到右值的转换。这种情况通常出现在通过省略号传递类的左值时进行函数调用的情况下。 - Johannes Schaub - litb
就非联合类对象而言,这是正确的。联合体是按“位”复制的。 - Johannes Schaub - litb
这一切与问题无关,除了最后一句话...你没有给出任何理由。 - M.M
1
问题中的示例是关于在释放指针所指向的空间后继续使用指针的情况。而在你的代码中,你复制了一个未初始化的指针,这是不同的情况。此外,关于类的所有内容都是无关紧要的,你同样可以写成 int *a; int *b = a; - M.M
所有与类有关的东西都是无关紧要的,与问题中的“示例1”相关。 - curiousguy
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接