C标准兼容的方式来访问空指针地址?

C standard compliant way to access null pointer address?

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

假设我想在一个内存访问不受限制的环境中用 C 编写一个软件。进一步假设我想在空指针地址写入一些数据:我如何以符合标准的方式实现它?

示例 (IA32e):

#include <stdint.h>

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。


说清楚
我在问标准对此有何评论,NOT 如何以实现定义的方式实现这一点。
我知道后者的答案。

我阅读了(部分)C99 标准以理清思绪。我找到了我自己的问题感兴趣的部分,我写这篇文章作为参考。

免责声明
我是一个绝对的初学者,我写的 90% 或更多的东西都是错误的,毫无意义,或者可能会破坏你的烤面包机。我也试图从标准中找出一个基本原理,结果往往是灾难性的和幼稚的(如评论中所述)。
不读。
请咨询@Olaf,以获得正式和专业的答案。

在下文中,术语架构地址 设计了处理器所见的内存地址(逻辑、虚拟、线性、物理或总线地址)。换句话说,您将在汇编中使用的地址。


在第 6.3.2.3 节中。它显示

An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

关于整数到指针的转换

An integer may be converted to any pointer type. Except as previously specified [i.e. for the case of null pointer constant], the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.

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

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

给出下面的代码

char* p = (char*)241;

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

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

当我们编写像 char* p = (char*)K 这样的代码时,我们并没有告诉编译器使 p 指向 架构地址 K,我们告诉它从整数 K 中创建一个指针,或者换句话说,使 p 指向(C 抽象)地址 K

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

出于某些原因,童年的遗产,我认为 C 中的整型文字可以用来表示建筑地址,而不是 我错了 那只是恰好是(有点) 在我使用的编译器中正确。

我自己的问题的答案很简单:没有标准方法,因为 C 标准文档中没有(架构)地址。对于每个(架构)地址都是如此,不仅是 int2ptr(0) one1.


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

标准说

If an invalid value [a null pointer value is an invalid value] has been assigned to the pointer, the behavior of the unary * operator is undefined.

还有那个

Therefore any expression referring to such an [volatile] object shall be evaluated strictly according to the rules of the abstract machine.

抽象机表示 * 未定义空指针值,因此代码不应与此代码不同

return *(char*)0;

这也是未定义的。
事实上它们没有区别,至少对于 GCC 4.9,两者都编译为我的问题中所述的说明。

访问 0 体系结构地址的实现定义方法是,对于 GCC,使用 -fno-isolate-erroneous-paths-dereference 标志产生 "expected"汇编代码。


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

不幸的是它说 & 产生其操作数的地址,我认为这有点不合适,我会说它产生一个指向其操作数的指针。考虑一个已知位于地址 0xf1 的变量 a 在 16 位地址 space 中,并考虑一个实现 int2ptr(n ) = 0x8000 | n&a 会产生一个指针,其位表示为 0x80f1,它是 而不是 a.[=154= 的地址]

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

我假设你问的问题是:

How do I access memory such that a pointer to that memory has the same representation as the null pointer?

根据标准的字面解读,这是不可能的。 6.3.2.3/3 说任何指向对象的指针都必须与空指针比较不相等。

所以我们说的这个指针一定不能指向一个对象。但是,应用于对象指针的遵从运算符 * 仅指定指向对象时的行为。


话虽如此,C 中的对象模型从未被严格指定,因此我不会对上述解释给予过多的重视。尽管如此,在我看来,无论您提出什么解决方案,都将不得不依赖于正在使用的编译器的非标准行为。

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

任何解决方案都将依赖于实现。必要的。 ISO C 不描述 C 程序运行的环境;相反,符合 C 程序在各种环境(«数据处理系统»)中的样子。标准确实不能保证您通过访问不是对象数组的地址会得到什么,即 you visibly 分配的东西,而不是环境.

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

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

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

§ 5.1.2

Two execution environments are defined: freestanding and hosted. [...]

§ 5.1.2.1,逗号 1

In a freestanding environment (in which C program execution may take place without any benefit of an operating system), the name and type of the function called at program startup are implementation-defined. Any library facilities available to a freestanding program, other than the minimal set required by clause 4, are implementation-defined. [...]

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


不管那是什么意思。当 you 是标准委托控制的实现时,情况有点不同。

所有引述均来自 N.1570 草案。

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

虽然建议使用类似于整数的地址的平台应该以熟悉此类映射的人不会感到意外的方式定义整数和地址之间的转换,但这不是必需的,并且代码依赖于此类建议不会严格遵守。

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

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

作为写入地址 0 和地址 x 的请求,即使 x 恰好为零,即使实现通常会陷入 空指针访问。这种行为不是 "standard",因为 标准没有提到指针和整数之间的映射,但是 尽管如此,一个高质量的实施应该表现得明智。

正如 OP 正确 :

There is no standard way because there are no (architectural) address in the C standard document. This is true for every (architectural) address, not only the int2ptr(0) one.

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

该方案的一个变体是在地址零处定义一个符号,然后简单地使用它来导出任何其他所需的地址。为此,将如下内容添加到链接描述文件的 SECTIONS 部分(假设使用 GNU ld 语法):

_memory = 0;

然后在您的 C 代码中:

extern char _memory[];

现在可以例如使用例如 char *p = &_memory[0];(或简单地 char *p = _memory;)创建指向零地址的指针,而无需将 int 转换为指针。类似地,int addr = ...; char *p_addr = &_memory[addr]; 将创建一个指向地址 addr 的指针,而无需在技术上将 int 强制转换为指针。

(这当然避免了最初的问题,因为链接器独立于 C 标准和 C 编译器,并且每个链接器的链接描述文件的语法可能不同。另外,生成的代码可能效率较低,因为编译器不知道访问的地址。但我认为这仍然为问题增加了一个有趣的角度,所以请原谅略微偏离主题的答案..)