重新定义NULL

时间:2011-02-28 12:53:33

标签: c null

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

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

我的问题是:这会与C标准发生冲突吗?据我所知,从标准中的7.17开始,宏是实现定义的。标准中是否还有其他内容表明NULL 必须为0?

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

7 个答案:

答案 0 :(得分:82)

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

然而,对编译器和标准库进行(重)修改后,用NULL表示替换位模式并不是不可能,同时仍然严格符合标准库。 足以简单地更改NULL本身的定义,因为NULL会评估为真。

具体来说,您需要:

  • 在指针(或转换为指针)的赋值中安排文字零,以转换为其他神奇值,例如-1
  • 安排指针和常量整数0之间的相等测试以检查魔术值(§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)以反映新的空指针表示。
  • 创建一种访问真实地址零的替代方法。

你做的一些事情必须处理。例如:

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

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

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

此外:

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

x不能保证为0

简而言之,C语言委员会显然考虑了这种情况,并考虑了那些为NULL选择替代表示的人。你现在所要​​做的就是对你的编译器进行重大修改,嘿,你已经完成了:)

作为旁注,可以在编译器正确之前使用源代码转换阶段实现这些更改。也就是说,而不是预处理器的正常流程 - >编译器 - >汇编程序 - >链接器,你要添加一个预处理器 - > NULL转换 - >编译器 - >汇编程序 - >连接。然后你可以进行如下转换:

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

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

答案 1 :(得分:19)

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

C实现由编译器及其头文件组成。如果您修改标头以重新定义NULL,但不修改编译器以修复静态初始化,那么您已创建了一个不合规的实现。这是整个实现一起采取的行为不正确,如果你打破它,你真的没有别人责备;)

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

答案 2 :(得分:7)

如果使用C std库,则会遇到可返回NULL的函数问题。例如malloc documentation个州:

  

如果功能未能分配   请求内存块,null   返回指针。

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

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

我会为你自己的用途定义你自己的NULL,“MYPRODUCT_NULL”,并避免或转换为/到C std库。

答案 3 :(得分:6)

单独保留NULL并将IO作为特殊情况处理为端口0x0000,可能使用汇编程序编写的例程,因此不受标准C语义的限制。 IOW,不要重新定义NULL,重新定义端口0x00000。

请注意,如果您正在编写或修改C编译器,则无论如何定义NULL,避免解除引用NULL所需的工作(假设在您的情况下CPU没有帮助)都是相同的,因此更容易将NULL定义为零,并确保不能从C中取消引用零。

答案 4 :(得分:3)

考虑到其他人提到的重新定义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 lib和表达式:

  if (pointer)
  {
     ...
  }

仍在工作

疯了我知道,但我以为我会把想法抛弃在那里:)。

答案 5 :(得分:2)

空指针的位模式可能与整数0的位模式不同。但是,NULL宏的扩展必须是空指针常量,即0的常量整数,可以进行转换to(void *)。

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

答案 6 :(得分:1)

你在找麻烦。将NULL重新定义为非空值将破坏此代码:

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