重新定义 NULL

我正在为一个地址0x0000有效且包含端口 I/O 的系统编写 C 代码。因此,任何访问 NULL 指针的可能错误都不会被检测到,同时会导致危险的行为。

因此,我希望将 NULL 重新定义为另一个地址,例如一个无效的地址。如果我不小心访问这样一个地址,我会得到一个硬件中断,我可以处理错误。我碰巧可以访问这个编译器的 stddef.h,所以我实际上可以修改标准头文件并重新定义 NULL。

我的问题是: 这会与 C 标准冲突吗?从标准的7.17中我可以看出,宏是实现定义的。标准中是否有其他内容说明 NULL 必须的为0?

另一个问题是,许多编译器执行静态初始化时,无论数据类型如何,都将所有内容设置为零。尽管标准规定编译器应该将整数设置为零并指向 NULL。如果我要为我的编译器重新定义 NULL,那么我知道这样的静态初始化将会失败。我可以认为这是不正确的编译器行为,即使我大胆地改变了手动编译器标题?因为我确信这个特定的编译器在进行静态初始化时不会访问 NULL 宏。

5806 次浏览

标准规定值为0的整数常量表达式或转换为 void *类型的此类表达式为空指针常量。这意味着 (void *)0始终是一个空指针,但是给定 int i = 0;(void *)i不需要是。

C 实现由编译器及其头部组成。如果您修改头部以重新定义 NULL,但不修改编译器以修复静态初始化,那么您就创建了一个不一致的实现。错误的行为出现在整个实现中,如果你违反了它,那么你就真的没有其他人可以指责了;)

您必须修复的不仅仅是静态初始化,当然-给定一个指针 p,由于上述规则,if (p)等价于 if (p != NULL)

空指针的位模式可能与整数0的位模式不同。但是 NULL 宏的展开必须是一个空指针常量,即值为0的常量整数,可以强制转换为(void *)。

为了在保持一致的情况下实现您想要的结果,您必须修改(或者可能配置)您的工具链,但这是可以实现的。

If you use the C std library, you'll run into problems with functions that can return NULL. For example the Malloc 文档 states:

如果函数未能分配 请求的内存块为空 返回指针。

因为 malloc 和相关函数已经被编译成具有特定 NULL 值的二进制文件,所以如果重新定义 NULL,就不能直接使用 C std 库,除非您可以重新构建整个工具链,包括 C std 库。

另外,由于 std 库使用 NULL,如果在包含 std 头之前重新定义 NULL,可能会覆盖头中列出的 NULL 定义。任何内联的内容都与已编译的对象不一致。

I would instead define your own NULL, "MYPRODUCT_NULL", for your own uses and either avoid or translate from/to the C std library.

C 标准不要求空指针位于机器地址零处。但是,将 0常量转换为指针值必须得到 NULL指针(6.3.2.3/3) ,并且将空指针作为布尔值计算必须为 false。如果您真的想要一个零地址,而且 NULL不是零地址,那么这可能有点尴尬。

尽管如此,通过对编译器和标准库的(大量)修改,使用替代位模式表示 NULL并不是不可能的,同时仍然严格遵守标准库。然而,只要简单地改变 NULL本身的定义,没有就足够了,因为这样 NULL就会被评估为真。

Specifically, you would need to:

  • 将指针的赋值(或指针的强制类型转换)中的文本零转换为其他一些神奇的值,如 -1
  • Arrange for equality tests between pointers and a constant integer 0 to check for the magic value instead (§6.5.9/6)
  • 将指针类型作为布尔值进行计算的所有上下文排列,以检查是否与魔术值相等,而不是检查为零。这源于相等性测试语义,但编译器可能在内部以不同的方式实现它。见6.5.13.3,6.5.14/3,6.5.15/4,6.5.3.3/5,6.8.4.1/2,6.8.5/4
  • 正如 caf 指出的,更新静态对象初始化的语义(6.7.8/10)和部分复合初始化器(6.7.8/21) ,以反映新的空指针表示形式。
  • Create an alternate way to access true address zero.

有些事情是你做 没有必须处理的,例如:

int x = 0;
void *p = (void*)x;

在此之后,p不能保证是一个空指针。只需要处理常量分配(这是访问真实地址零的好方法)。同样地:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Also:

void *p = NULL;
int x = (int)p;

不能保证 x0

简而言之,C 语言委员会显然正在考虑这一条件,并考虑了那些选择 NULL 的替代代表的人。现在你所要做的就是对你的编译器做一些重大的改变,然后你就可以完成了:)

顺便说一句,在编译器正确之前,可以通过源代码转换阶段来实现这些更改。也就是说,您将添加一个预处理器-> 编译器-> 汇编器-> 链接器,而不是预处理器-> 编译器-> 汇编器-> 链接器的正常流程。然后你可以做这样的转换:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

这将需要一个完整的 C 解析器,以及类型解析器和 typedef 和变量声明的分析,以确定哪些标识符对应于指针。但是,通过这样做,您可以避免对编译器的代码生成部分进行更改。可能有助于实现这一点-我知道它的设计思想就是这样的转换。当然,您仍然可能需要对标准库进行更改。

考虑到其他人提到的重新定义 NULL 的极端困难,对于众所周知的硬件地址,可能更容易使用 重新定义解引用。在创建地址时,将1添加到每个已知地址,以便您的已知 IO 端口为:

  #define CREATE_HW_ADDR(x)(x+1)
#define DEREFERENCE_HW_ADDR(x)(*(x-1))


int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);


printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

如果您所关心的地址被分组在一起,您可以感到安全,添加1的地址不会与任何东西冲突(在大多数情况下不应该) ,您可能能够安全地这样做。然后你就不用担心重新构建你的工具链/std 库和表达式了:

  if (pointer)
{
...
}

还能用

我知道这很疯狂,但我只是想把这个想法说出来:)。

你这是自找麻烦。将 NULL重新定义为非空值会破坏这段代码:

if (myPointer)
{
// myPointer is not null
...
}

不要使用 NULL,将 IO 作为一种特殊情况对待,可以使用汇编程序编写的例程,因此不受标准 C 语义的约束。IOW,不要重新定义 NULL,重新定义端口0x00000。

Note that if you're writing or modifying a C compiler, the work required to avoid dereferencing NULL (assuming that in your case the CPU isn't helping) is the same no matter how NULL is defined, so it's easier to leave NULL defined as zero, and make sure that zero can't ever be dereferenced from C.