什么时候访问“死”对象的指针有效?

问题描述 投票:0回答:4

首先,澄清一下,我不是谈论取消引用无效指针!

考虑以下两个示例。

示例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] 某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示形式,并且由不具有字符类型的左值表达式读取,则行为未定义。如果这样的表示是由副作用产生的,即通过不具有字符类型的左值表达式修改对象的全部或任何部分,则行为是未定义的。这样的表示称为 trap 表示

将所有这些放在一起,我们在访问“死”对象的指针时有哪些限制?

附录

虽然我在上面引用了 C99 标准,但我很想知道任何 C++ 标准中的行为是否有所不同。

c pointers language-lawyer undefined-behavior
4个回答
31
投票

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

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

6.2.6 类型的表示

6.2.6.1 概述

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


15
投票

我的解释是,虽然只有非字符类型可以具有陷阱表示,但任何类型都可以具有不确定的值,并且以任何方式访问具有不确定值的对象都会调用未定义的行为。最臭名昭著的例子可能是 OpenSSL 无效地使用未初始化的对象作为随机种子。

所以,你的问题的答案是:永远不会。

顺便说一下,一个有趣的结果是,在 free

realloc
之后,不仅指向的对象而且
指针本身
都不确定,这个习惯用法会调用未定义的行为:

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

0
投票

说指针值变得不确定,即使没有任何东西干扰表示它的位,也可能是为了适应“假设”规则。如果存在一些动作序列,其行为可能明显受到有用的优化转换的影响,则假设规则要求该序列中的至少一个动作被描述为调用未定义行为,以证明优化所产生的任何可观察到的怪癖是合理的。

考虑以下函数:

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()
返回的新区域的地址。当由此识别的存储有资格被
p1
重用时,将
malloc()
视为不确定,将允许编译器忽略可能发现该地址与某些未来分配的地址相匹配的可能性。


-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) {
}

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

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

© www.soinside.com 2019 - 2024. All rights reserved.