取消引用常量未定义行为

Is Dereferencing a Constant Undefined Behavior

当然,下面的片段不是一个好主意:

char *vram = (char*)0xB8000;
memset(vram, 32, 0x18000);

这也不是:

volatile char *LCDC = (volatile char*)0xFF40;
char LCDCshadow = *LCDC;

下面显然是未定义的行为:

int *dontdoit = 0;
*dontdoit;

因为在指针上下文中使用 0 时,它成为空指针的值,取消引用空指针是未定义的行为。

但是前两个例子是未定义的行为,还是简单的实现defined/Unspecified
如果是后者,如何生成值为0的有效指针?

一个值为 0 的整数常量表达式,当转换为一个指针时,无论 NULL 指针的实际表示如何,都会产生一个 NULL 指针。

C standard 的第 6.3.2.3p3 节指出:

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.

将任何其他整数值转换为指针值是实现定义的。来自第 6.3.2.3p5 节:

An integer may be converted to any pointer type. Except as previously specified, 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.

以上内容通常适用于访问特定内存地址有意义的嵌入式实现。

如果您有一个支持非零 NULL 指针的实现,您可以通过变量将值 0 分配给它,例如:

int zero = 0;
int *zeroptr = (int *)zero;

在这种情况下,指针的值将为 0 但不会为 NULL。

The following snippet is, of course, not a good idea

是的,但这只是因为它不能在符合标准的 C 编译器上编译。 char *vram = 0xB8000; 甚至不是有效的 C,并且像所有无效的 C 一样具有未定义的行为。参见

但是这段代码没问题:

char *vram = (char*)0xB8000;
memset(vram, 32, 0x18000);

那里发生的事情超出了C语言的范围。


volatile char *LCDC = 0xFF40;
char LCDCshadow = *LCDC;

相同的错误,没有转换导致此处无效的 C。否则完美的代码。


And if it's the latter, how does one generate a valid pointer with a value of 0?

整型常量 0,或转换为空指针的常量 (void*)0 是一个名为 空指针常量 的特殊项。每当一个指针被分配一个空指针常量时,该指针将被转换为一个空指针。空指针的实际内容是实现定义的。在遇到像 int* ptr = 0; 这样的代码时,编译器可能会给 ptr 任何对给定实现有意义的值——不一定是 0.

例如:

uint8_t data [sizeof(int*)];
int* ptr = 0;
memcpy(data, &ptr, sizeof(int*));

对于 32 位指针,这不一定导致 data 成为 00 00 00 00 - 它是实现定义的。

这就是语言的设计方式,有一些混淆的意图来处理奇异地址等。但在实践中,这意味着我们永远无法创建指向物理地址零的指针。因此,具有这样一个物理地址的系统——最值得注意的是几乎每个创建的微控制器系统——都不会像 C 语言那样以任何不同的方式对待空指针。因为他们需要使用物理地址 0。这意味着在这样的系统上访问空指针将导致访问地址 0。

我在现实世界的系统中遇到过错误,其中意外的空指针访问导致 I/O 端口激活并因此导致硬件行为异常。

归根结底,空指针是一个已知的语言设计错误。他们应该使用不能与地址 0 混淆的关键字 null。 C++ 已尝试在以后的标准中修复此问题,但 C 仍然存在问题。

整数到指针的转换是一种构造,大多数(但不是所有)实现都可以有意义地支持它。此外,该标准一直专注于所有编译器需要支持的特性,而不是为那些提供建议大多数 编译器 应该 在可行时支持,并试图避免让编译器根据它们有意义地支持的功能接受或拒绝不同的句法结构。结果是所有编译器都必须在语法上接受从整数到指针的转换,无论它们是否会有意义地处理它,但标准没有描述它们有意义的任何情况。

即使在行为有意义的平台上,实现文档也并不总是清楚地说明支持和不支持哪些构造。例如考虑:

extern int x;
int test2(void)
{
    x=1;
    int res=*(int*)0x12345678;
    x=2;
    return res;
}

如果 x 是在汇编、链接器控制脚本或其他允许绝对放置的语言中定义的,程序员可能知道它将位于地址 0x12345678。虽然根据上述代码,clang 允许从地址 0x12345678 进行的符合 volatile 条件的读取可能与对 x 的第一次写入进行交互,但 gcc 不会。 gcc 的作者会采取标准不要求他们支持这种情况的态度,所以任何需要这种支持的代码都是 "broken",但标准不要求编译器支持 any 涉及整数到指针转换的有意义的结构,而不是产生空指针的结构。