符合C标准的方式来访问空指针地址?

在C中,引用空指针是未定义行为,但是空指针值具有位表示,在某些体系结构中它使其指向有效地址(例如,地址0)。
为了清楚起见,我们将此地址称为空指针地址

假设我想在C中编写一个软件,在一个无限制访问内存的环境中。 假设我想在空指针地址处写一些数据: 我将如何以标准兼容的方式实现这一点?

示例案例(IA32e):

#include  int main() { uintptr_t zero = 0; char* p = (char*)zero; return *p; } 

当使用带有-O3的 gcc与IA32e编译时,此代码将转换为

 movzx eax, BYTE PTR [0] ud2 

由于UB(0是空指针的位表示)。

由于C接近低级编程,我相信必须有一种方法来访问空指针地址并避免UB。


只是要清楚
我问的是标准对此有何看法,而不是如何以实现定义的方式实现这一点。
我知道后者的答案。

我读了(部分)C99标准以清除我的想法。 我找到了我自己的问题感兴趣的部分,我写这个作为参考。

免责声明
我是一个绝对的初学者,90%或更多的我写的是错的,没有意义,或者可能会打破你的烤面包机。 我也试图从标准中提出一个基本原理,通常会带来灾难性和天真的结果(如评论中所述)。
不读。
请咨询@Olaf,获取正式和专业的答案。

对于以下内容,术语架构地址设计了处理器看到的存储器地址(逻辑,虚拟,线性,物理或总线地址)。 换句话说,您将在汇编中使用的地址。


在第6.3.2.3节中。 它读

值为0的整型常量表达式或类型为void *的表达式称为空指针常量如果将空指针常量转换为指针类型 ,则保证将结果指针(称为空指针)与指向任何对象或函数的指针进行比较。

并且关于整数到指针的转换

整数可以转换为任何指针类型。 除了先前指定的[即对于空指针常量的情况]结果是实现定义的 ,可能未正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示

这些意味着编译器要兼容,只需要实现一个从整数到指针的函数int2ptr

  1. 根据定义, int2ptr(0)空指针
    请注意int2ptr(0)不强制为0.它可以是任何位表示。
  2. * int2ptr(n!= 0)没有约束。
    注意 ,这意味着int2ptr不需要是identity函数,也不是返回有效指针的函数!

鉴于下面的代码

 char* p = (char*)241; 

该标准绝对不保证表达式*p = 56; 将写入架构地址241
因此它没有直接访问任何其他架构地址(包括int2ptr(0) ,由空指针设计的地址,如果有效)。

简单地说标准不涉及架构地址,而是指针,它们的比较,转换和它们的操作

当我们编写像char* p = (char*)K这样的代码时,我们并没有告诉编译器使p指向架构地址 K ,我们告诉它从整数K中指出一个指针,或者在其他术语中使用p指向(C抽象)地址K.

空指针和(架构)地址0x0不相同 (cit。),因此对于由整数K和(架构)地址K构成的任何其他指针也是如此。

由于某些原因,童年遗产,我认为C中的整数文字可用于表达建筑地址,而我错了 ,而且恰好在我使用的编译器中(有点)正确。

我自己的问题的答案很简单: 没有标准方法,因为C标准文档中没有(架构)地址 。 这适用于每个(架构)地址,而不仅仅是int2ptr(0) 1


关于return *(volatile char*)0;注意事项return *(volatile char*)0;

标准说

如果为指针分配了无效值[空指针值是无效值] ,则unary *运算符的行为未定义。

然后

因此,任何涉及这种[volatile]对象的表达式都应严格按照抽象机的规则进行评估。

抽象机器说*对于空指针值是未定义的,因此代码不应该与此不同

return *(char*)0;

这也是未定义的。
事实上,他们没有区别 ,至少在GCC 4.9中,两者都按照我的问题中的说明进行编译。

对于GCC,实现定义的访问0架构地址的方法是使用-fno-isolate-erroneous-paths-dereference标志,该标志产生“预期的”汇编代码。


用于将指针转换为整数或整数到指针的映射函数旨在与执行环境的寻址结构一致。

不幸的是它说&得到了它的操作数的地址,我相信这有点不合适,我会说它会产生一个指向它的操作数的指针。 考虑一个已知位于16位地址空间中的地址0xf1的变量a ,并考虑实现int2ptr(n)= 0x8000 |的编译器 n &a将产生一个指针,其位表示为0x80f1 ,而不是a的地址。

1这对我来说很特别,因为在我的实现中,它是唯一一个无法访问的。

由于OP 在回答她自己的问题时已经正确地得出结论 :

没有标准方法,因为C标准文档中没有(架构)地址。 这适用于每个(架构)地址,而不仅仅是int2ptr(0)地址。

但是,人们希望直接访问内存的情况可能是使用自定义链接描述文件的情况。 (即某种嵌入式系统的东西。)所以我想说,执行OP要求的标准兼容方式是在链接器脚本中导出(架构)地址的符号,而不是打扰在C代码本身。

该方案的一种变体是在地址零处定义符号,并简单地使用该符号来导出任何其他所需地址。 为此,在链接器脚本的SECTIONS部分添加如下内容(假设GNU ld语法):

 _memory = 0; 

然后在你的C代码中:

 extern char _memory[]; 

现在可以使用例如char *p = &_memory[0];来创建指向零地址的指针char *p = &_memory[0]; (或者只是char *p = _memory; ),而不是将int转换为指针。 同样, int addr = ...; char *p_addr = &_memory[addr]; int addr = ...; char *p_addr = &_memory[addr]; 将创建一个指向地址addr的指针,而无需在技术上将int转换为指针。

(这当然避免了原始问题,因为链接器独立于C标准和C编译器,并且每个链接器的链接器脚本可能具有不同的语法。此外,生成的代码可能效率较低,因为编译器不是知道正在访问的地址。但我认为这仍然为这个问题增加了一个有趣的视角,所以请原谅稍微偏离主题的答案..)

无论什么解决方案都依赖于实现。 Needfully。 ISO C没有描述C程序运行的环境; 相反,在各种环境(“数据处理系统”)中, 符合 C的程序是什么样的。 标准无法保证通过访问不是对象数组的地址,即 可见地分配的内容,而不是环境,可以获得什么。

因此,我会使用标准离开的东西作为实现定义(甚至有条件支持)而不是未定义的行为*:内联汇编。 对于GCC / clang:

 asm volatile("movzx 0, %%eax;") // *(int*)0; 

还值得一提的是独立环境,你似乎所处的环境。标准说明了这个执行模型(强调我的):

§5.1.2

定义了两个执行环境: 独立和托管。 […]

§5.1.2.1,逗号1

在独立环境中( 可以在没有操作系统任何好处的情况下执行C程序 ),程序启动时调用的函数的名称和类型是实现定义的。 除了第4节要求的最小集合之外,任何独立程序可用的库设施都是实现定义的。 […]

请注意,它并没有说您可以随意访问任何地址。


无论这意味着什么。 是标准委托控制的实现时,情况会有所不同。

所有报价均来自N. 1570草案。

我假设你问的问题是:

如何访问内存,使得指向该内存的指针与空指针具有相同的表示forms?

根据标准的字面解读,这是不可能的。 6.3.2.3/3表示任何指向对象的指针都必须与null指针进行比较。

因此,我们所讨论的这个指针一定不能指向一个对象。 但是,应用于对象指针的deference运算符*仅指定在指向对象的情况下的行为。


话虽如此,C中的对象模型从未被严格规定,所以我不会过多地重视上述解释。 然而,在我看来,无论你提出什么样的解决方案,都必须依赖于使用任何编译器的非标准行为。

我们在其他答案中看到了这样的一个例子,其中gcc的优化器在处理的后期检测到一个全位零指针并将其标记为UB。

C标准不要求实现具有以任何forms或forms的整数相似的地址; 所有它需要的是,如果类型uintptr_t和intptr_t存在,将指针转换为uintptr_t或intptr_t的行为将产生一个数字,并将该数字直接转换回与原始指针相同的类型将产生一个等于原始指针的指针。

虽然建议使用类似整数的地址的平台应该以一种对熟悉这种映射的人不足为奇的方式定义整数和地址之间的转换,但这不是必需的,并且依赖于这种建议的代码不会严格符合。

尽管如此,我建议如果质量实现指定它通过简单的按位映射执行整数到指针的转换,并且如果可能有合理的理由为什么代码想要访问地址零,那么它应该考虑如下语句:

 *((uint32_t volatile*)0) = 0x12345678; *((uint32_t volatile*)x) = 0x12345678; 

作为写入地址零和地址x的请求,按顺序即使x恰好为零,即使实现通常会捕获空指针访问。 这种行为不是“标准的”,只要标准没有说明指针和整数之间的映射,但是高质量的实现应该仍然表现得很明智。