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

Mar*_*oom 32 c null-pointer undefined-behavior language-lawyer

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

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

示例案例(IA32e):

#include <stdint.h>

int main()
{
   uintptr_t zero = 0;

   char* p = (char*)zero;

   return *p;
}
Run Code Online (Sandbox Code Playgroud)

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

movzx eax, BYTE PTR [0]
ud2
Run Code Online (Sandbox Code Playgroud)

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

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


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

Mar*_*oom 21

我读了(部分)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;
Run Code Online (Sandbox Code Playgroud)

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

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

当我们写代码一样char* p = (char*)K,我们没有告诉编译器,使p指向建筑的地址 ķ,我们告诉它做一个指针出整数ķ,或在其他条款作出p指向(C摘要)地址ķ.

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

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

我自己的问题的答案很简单:没有标准方法,因为C标准文档中没有(架构)地址.这是每一个(建筑)地址,而不是只有真正int2ptr(0)一个1.


请注意 return *(volatile char*)0;

标准说

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

然后

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

抽象机器说*没有为空指针值定义,因此代码不应该与此不同

return *(char*)0;

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

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


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

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

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


Cli*_*nna 12

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

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

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

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

_memory = 0;
Run Code Online (Sandbox Code Playgroud)

然后在你的C代码中:

extern char _memory[];
Run Code Online (Sandbox Code Playgroud)

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

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