访问空指针地址的符合 C 标准的方法?
Posted
技术标签:
【中文标题】访问空指针地址的符合 C 标准的方法?【英文标题】:C standard compliant way to access null pointer address? 【发布时间】:2016-06-02 22:21:01 【问题描述】:在 C 中,引用 空指针 是未定义行为,但是空指针值具有位表示,在某些体系结构中使其指向有效地址(例如地址 0)。 为了清楚起见,我们称这个地址为空指针地址。
假设我想在一个不受限制地访问内存的环境中用 C 语言编写一个软件。进一步假设我想在空指针地址写入一些数据:我将如何以符合标准的方式实现?
示例案例(IA32e):
#include <stdint.h>
int main()
uintptr_t zero = 0;
char* p = (char*)zero;
return *p;
当使用 gcc 和 -O3 为 IA32e 编译时,此代码将转换为
movzx eax, BYTE PTR [0]
ud2
由于UB(0是空指针的位表示)。
既然C接近低级编程,相信一定有办法访问空指针地址,避免UB。
只是为了清楚 我问的是标准对此有何规定,不是如何以实现定义的方式实现这一点。 我知道后者的答案。
【问题讨论】:
空指针和地址0x0不一样。 我认为您应该使用针对预期环境的编译器进行尝试。 没有符合标准的方法来做到这一点,因为标准没有提供访问任意内存的方法。您将不得不做一些与实现相关的事情。检查你的编译器文档,看看你的实现允许什么。 “我问的是标准对此有何规定,而不是如何以实现定义的方式实现这一点。” - 这导致根本没有答案。因为分配给整数类型zero
的0
到指针的转换已经是未定义的行为。该标准仅允许将指针转换为该类型并返回。即使使用不同的指针类型也已经是 UB。
@2501:嗯,这取决于。在简单的标准兼容上下文中,指针的值是毫无意义的。基本上,一个指针可以是一个空指针,或者指向一个“数组”(其中包括单个对象,这些对象是长度为1的数组)。无论哪种方式,实际的位表示都是特定于实现的。只有 null 指针 或者它们指向同一个“数组” - 或者正好超过最后一个元素时才允许比较两个指针。但是对于例如嵌入式系统您必须“改变规则”并依赖特定的,即实现定义的行为。
【参考方案1】:
C 标准不要求实现具有任何形状或形式类似于整数的地址;它所需要的只是如果类型 uintptr_t 和 intptr_t 存在,则将指针转换为 uintptr_t 或 intptr_t 的行为将产生一个数字,并将该数字直接转换回与原始指针相同的类型将产生一个等于原始指针的指针。
虽然建议使用类似于整数的地址的平台应以熟悉此类映射的人不会感到惊讶的方式定义整数和地址之间的转换,但这不是必需的,并且依赖于此类建议的代码将不严格遵守。
尽管如此,我建议如果质量实现指定它通过简单的按位映射执行整数到指针的转换,并且如果代码想要访问地址零的合理原因可能是,它应该考虑语句喜欢:
*((uint32_t volatile*)0) = 0x12345678;
*((uint32_t volatile*)x) = 0x12345678;
作为写入地址 0 和地址 x 的请求,即使 x 恰好为零,即使实现通常会陷入困境 空指针访问。这种行为不是“标准的”,只要 标准没有说明指针和整数之间的映射,但是 尽管如此,一个高质量的实现应该是明智的。
【讨论】:
【参考方案2】:正如 OP 正确的 concluded in her answer to her own question:
没有标准方式,因为 C 标准文档中没有(架构)地址。对于每个(架构)地址都是如此,不仅是 int2ptr(0) 一个。
但是,想要直接访问内存的情况很可能是使用自定义链接描述文件的情况。 (即某种嵌入式系统的东西。)所以我会说,执行 OP 要求的标准兼容方式是在链接器脚本中导出(架构)地址的符号,而不是在C 代码本身。
该方案的一种变体是在地址 0 处定义一个符号,并简单地使用它来导出任何其他所需的地址。为此,在链接器脚本的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 编译器,每个链接器的链接器脚本可能有不同的语法。此外,生成的代码可能效率较低,因为编译器不知道被访问的地址。但是我认为这仍然为问题增加了一个有趣的视角,所以请原谅稍微偏离主题的答案..)
【讨论】:
请注意,在使用此类构造时可能需要禁用某些优化,并且某些无法禁用此类优化的编译器可能根本无法可靠地支持此类构造。例如,给定char *p = _memory; ... if (p) ...
甚至if ((uintptr_t)p)
,编译器可能会认为p
的地址不可能匹配空指针的地址(因为它被分配了_memory
的地址)并省略比较,造成不可知的混乱。
这个。这不仅是该问题的正确答案,而且是处理应放置到特定固定平台相关内存地址的数据的唯一正确方法。使用硬编码指针很常见,但却是错误的。【参考方案3】:
任何解决方案都将依赖于实现。必要的。 ISO C 没有描述 C 程序运行的环境;相反,符合 C 程序在各种环境(«数据处理系统»)中的样子。该标准确实不能保证通过访问不是对象数组的地址(即您 可见分配的地址,而不是环境,您会得到什么。
因此,我会使用标准留下的东西作为实现定义(甚至作为条件支持)而不是未定义的行为*:内联汇编。对于 GCC/clang:
asm volatile("movzx 0, %%eax;") // *(int*)0;
还值得一提的是独立环境,您似乎所处的环境。标准说明了这种执行模型(强调我的):
§ 5.1.2
定义了两种执行环境:独立和托管。 [...]
§ 5.1.2.1,逗号 1
在独立环境中(C 程序的执行可能在没有任何操作系统优势的情况下发生),程序启动时调用的函数的名称和类型是实现定义的。除了第 4 节要求的最小集合之外,独立程序可用的任何库设施都是实现定义的。 [...]
请注意,它并不是说您可以随意访问任何地址。
不管这意味着什么。当 you 是标准委托控制的实现时,情况会有所不同。
所有引述均来自草案 N. 1570。
【讨论】:
该标准不要求任何实现适用于任何特定目的。事实上,作者认识到(在基本原理中)一个实现可能同时符合要求和无用。虽然独立实现不需要定义 any 手段,通过这些手段,程序可以以不同于int main(void) volatile int dummy; while(!dummy)
的方式运行,质量独立实现将定义有用的行为,即使在标准不需要它的情况下也是如此。 【参考方案4】:
我假设您要问的问题是:
如何访问内存以使指向该内存的指针与空指针具有相同的表示形式?
根据标准的字面意思,这是不可能的。 6.3.2.3/3 说任何指向对象的指针必须与空指针比较不相等。
因此我们所说的这个指针一定不能指向一个对象。但是引用运算符*
,应用于对象指针,仅指定它指向对象时的行为。
话虽如此,C语言中的对象模型从来没有被严格规定过,所以我不会在上面的解释中过分重视。尽管如此,在我看来,无论您想出什么解决方案,都必须依赖于正在使用的任何编译器的非标准行为。
我们在其他答案中看到了一个例子,其中 gcc 的优化器在处理的后期检测到一个全位为零的指针并将其标记为 UB。
【讨论】:
即使我要求访问地址 100,也无法以 C 标准方式完成。虽然我发现这个问题是由于无法访问空指针地址,但这不是值为 0 的指针的问题,这是任何值的指针的问题。整数常量根本没有指定机器地址(映射是实现定义的),这就是我所缺少的。至于具体实现方式,GCC 整数常量实际上确实指定了地址,并且 -fno-isolate-erroneous-paths-dereference 阻止了ud2
陷阱的生成。
实现可以定义一个转换(char *)100
。我认为这是一个单独的问题
A NULL
指针“保证不等于指向任何对象或函数的指针”,这意味着编译器永远无法生成地址为空指针位置的对象,它确实并不意味着在该位置实际上不可能有一个对象(只是您不能将(有效)指针指向该对象)。访问NULL
地址处的有效对象是实现定义的,而不是未定义 行为。 NULL
可能指向一个有效的object,它根本就不是一个有效的pointer(被视为未对齐,即实现定义)。跨度>
@yyny 空指针不指向位置(在抽象机器中,这是 C 的定义方式)【参考方案5】:
我阅读了(部分)C99 标准以理清思路。我找到了对我自己的问题感兴趣的部分,我写这个作为参考。
免责声明 我是一个绝对的初学者,我写的 90% 或更多都是错误的,没有意义,或者可能会破坏你的烤面包机。我还尝试从标准中找出理由,通常会产生灾难性和幼稚的结果(如评论中所述)。 不要阅读。 请咨询@Olaf,以获得正式和专业的答案。
对于下文,术语架构地址设计了处理器所看到的内存地址(逻辑、虚拟、线性、物理或总线地址)。换句话说,您将在汇编中使用的地址。
在第 6.3.2.3 节中。上面写着
值为 0 的整数常量表达式,或转换为
void *
类型的此类表达式称为 空指针常量。 如果将空指针常量转换为指针类型,则生成的指针称为空指针,保证比较不相等 指向任何对象或函数的指针。
关于整数到指针的转换
整数可以转换为任何指针类型。除先前指定的 [即对于空指针常量], 结果是实现定义的,可能没有正确对齐,可能不指向 引用类型的实体,可能是陷阱表示†。
这些意味着编译器,为了兼容,只需要实现一个函数int2ptr从整数到指针
-
根据定义,int2ptr(0) 是 空指针。注意 int2ptr(0)不强制为 0。它可以是任何位表示。
*int2ptr(n != 0) 没有约束。注意这意味着 int2ptr 不需要是恒等函数,也不需要是返回有效指针!
给定下面的代码
char* p = (char*)241;
标准绝对不保证表达式*p = 56;
将写入架构地址241。因此它没有提供直接访问任何其他架构地址的方法(包括int2ptr(0),空指针设计的地址,如果有效)。
简单地说,标准不处理架构地址,而是处理指针、它们的比较、转换和它们的操作‡。
当我们编写像char* p = (char*)K
这样的代码时,我们并不是告诉编译器让p
指向架构地址 K,我们是在告诉它做一个整数K中的指针,或者换句话说,使p
指向(C抽象)地址K。
Null 指针和(架构)地址 0x0 不相同(同上),因此对于由整数 K 和 (建筑)地址K。
出于某些原因,童年的遗产,我认为 C 中的整数文字可以用来表示架构地址,而不是 我错了,而这恰好在编译器中(某种程度上)是正确的我正在使用。
我自己的问题的答案很简单:没有标准方法,因为 C 标准文档中没有(架构)地址。对于每个(架构)地址都是如此,而不仅仅是 int2ptr(0) 一个1。
注意return *(volatile char*)0;
标准是这样说的
如果一个 无效值[空指针值是无效值] 已分配给指针,一元 * 运算符的行为未定义。
还有那个
因此任何引用的表达式 对这样的[volatile]对象,要严格按照抽象机的规则进行求值。
抽象机器说 *
未定义空指针值,因此代码不应与此不同
return *(char*)0;
这也是未定义的。确实它们没有区别,至少在 GCC 4.9 中,两者都按照我的问题中所述的说明进行编译。
实现定义的访问 0 架构地址的方式是,对于 GCC,使用产生“预期”汇编代码的 -fno-isolate-erroneous-paths-dereference 标志。
†用于将指针转换为整数或将整数转换为指针的映射函数旨在 与执行环境的寻址结构保持一致。
‡不幸的是,它说&
产生其操作数的地址,我认为这有点不恰当,我会说它产生一个指向其操作数的指针。考虑一个变量a
,它已知位于 16 位地址空间中的地址 0xf1,并考虑一个实现 int2ptr(n) = 0x8000 | 的编译器。 n。 &a
会产生一个指针,其位表示为 0x80f1,它不是a
的地址。
1这对我来说很特别,因为它是我的实现中唯一无法访问的。
【讨论】:
我想你已经了解了这里的基本概念。本质上,您不应该将指针视为“内存中的地址”,这样可以避免大多数误解。 这似乎有效:volatile uintptr_t addr = 0; return *(volatile char *)(addr);
。但它会导致发出额外的内存操作。最好直接在机器码中写入对地址 0 的访问。
地址不仅仅是数字。请参阅我关于 C 和 C++ 中的指针的许多问题(大部分都受到不良欢迎),例如 Are pointer variables just integers with some operators or are they “symbolic”?以上是关于访问空指针地址的符合 C 标准的方法?的主要内容,如果未能解决你的问题,请参考以下文章