在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。
要明确
我问的是标准对此有何看法,不如何以实现定义的方式实现这一点。
我知道后者的答案。
答案 0 :(得分:20)
我读了(部分)C99标准以清除我的想法。我找到了我自己感兴趣的部分,我将此作为参考。
<强>声明强>
我是一个绝对的初学者,我所写的90%或更多是错误的,没有意义,或者可能会让你失去烤面包机。我也试图从标准中提出一个理由,通常会带来灾难性和天真的结果(如评论中所述)。
不要读。
请咨询@Olaf,获取正式和专业的答案。
对于以下内容,术语架构地址设计了处理器看到的内存地址(逻辑,虚拟,线性,物理或总线地址)。换句话说,您将在汇编中使用的地址。
在第6.3.2.3节中。它读取
值为0的整型常量表达式,或者类型为
void *
的表达式,称为空指针常量。 如果空指针常量转换为指针类型,则生成的指针(称为空指针)保证比较不等 指向任何对象或函数的指针。
关于整数到指针的转换
整数可以转换为任何指针类型。除了之前指定的 [即。对于空指针常量的情况] , 结果是实现定义的,可能没有正确对齐,可能不指向 引用类型的实体,可能是陷阱表示†。
这意味着编译器要兼容,只需要实现从整数到指针的函数 int2ptr
鉴于以下代码
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;
标准说
如果是 无效值 [空指针值是无效值] 已分配给指针,unary *运算符的行为未定义。
那个
因此任何表达都是指 对于这样的 [volatile] 对象,应严格按照抽象机的规则进行评估。
抽象机器说*
未定义空指针值,因此代码不应与此不同
return *(char*)0;
也未定义。
确实他们没有区别,至少在GCC 4.9中,两者都按照我的问题中的说明进行编译。
实现定义的访问0架构地址的方法是,对于GCC,使用 -fno-isolate-erroneous-paths-dereference 标志,该标志产生&#34;期望&#34 ;汇编代码。
†用于将指向整数或整数的指针转换为指针的映射函数旨在 与执行环境的寻址结构保持一致。
‡不幸的是它说&
产生了它的操作数的地址,我相信这有点不合适,我会说它产生一个指向其操作数的指针。考虑一个已知位于16位地址空间中的地址 0xf1 的变量a
,并考虑实现 int2ptr(n)= 0x8000 |的编译器。 Ñ。 &a
将产生一个指针,其位表示为 0x80f1 ,不 a
的地址。
1 这对我来说很特别,因为在我的实施中,它是唯一一个无法访问的人。
答案 1 :(得分:10)
OP正确concluded in her answer to her own question:
没有标准方法,因为C标准文档中没有(架构)地址。这适用于每个(架构)地址,而不仅仅是int2ptr(0)地址。
但是,人们希望直接访问内存的情况可能是使用自定义链接描述文件的情况。 (即某种嵌入式系统的东西。)所以我会说,执行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编译器,并且每个链接器可能对其链接器脚本具有不同的语法。此外,生成的代码可能效率较低,因为编译器不知道正在访问的地址。但我认为这仍然为这个问题增加了一个有趣的视角,所以请原谅稍微偏离主题的答案..)
答案 2 :(得分:3)
无论什么解决方案都依赖于实现。 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草案。
答案 3 :(得分:3)
C标准不要求实现具有以任何形式或形式类似整数的地址;所有它需要的是,如果类型uintptr_t和intptr_t存在,将指针转换为uintptr_t或intptr_t的行为将产生一个数字,并且将该数字直接转换回与原始指针相同的类型将产生等于原始指针的指针。
虽然建议使用类似整数的地址的平台应该以一种对熟悉这种映射的人不足为奇的方式定义整数和地址之间的转换,但这不是必需的,并且依赖于此类建议的代码将是不严格遵守。
尽管如此,我建议如果质量实现指定它通过简单的按位映射执行整数到指针的转换,并且如果可能有合理的理由为什么代码想要访问地址零,那么它应该考虑语句像:
*((uint32_t volatile*)0) = 0x12345678;
*((uint32_t volatile*)x) = 0x12345678;
作为写入地址零和地址x的请求,即使顺序也是如此 x恰好为零,即使实现通常会陷阱 空指针访问。这样的行为不是标准的#34;就此而言 Standard没有说明指针和整数之间的映射,但是 尽管如此,良好的质量实施仍应表现得合理。
答案 4 :(得分:1)
我假设您提出的问题是:
如何访问内存,使得指向该内存的指针与空指针具有相同的表示形式?
根据标准的字面读数,这是不可能的。 6.3.2.3/3表示任何指向对象的指针都必须与null指针进行比较。
因此,我们所讨论的这个指针一定不能指向一个对象。但是,应用于对象指针的deference运算符*
仅指定在指向对象的情况下的行为。
话虽如此,C中的对象模型从未被严格规定,所以我不会过多地重视上述解释。然而,在我看来,无论你提出什么样的解决方案,都必须依赖于任何编译器正在使用的非标准行为。
我们在其他答案中看到了这样的一个例子,其中gcc的优化器在处理的后期检测到一个全位零指针并将其标记为UB。