【问题标题】:Redefining NULL重新定义 NULL
【发布时间】:2011-07-05 18:18:08
【问题描述】:

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

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

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

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

【问题讨论】:

  • 这是一个非常好的问题。我没有给你的答案,但我不得不问:你确定不能像在“正常”系统中那样将你的有效东西移到 0x00 并让 NULL 成为一个无效地址吗?如果您不能,那么唯一可以安全使用的无效地址将是您可以确定您可以分配然后mprotect 确保安全的地址。或者,如果平台没有 ASLR 等,地址超出平台物理内存。祝你好运。
  • 如果您的代码使用if(ptr) { /* do something on ptr*/ },它将如何工作?如果 NULL 定义与 0x0 不同,它会起作用吗?
  • C 指针与内存地址没有强制关系。只要遵守指针运算规则,指针值可以是任何值。大多数实现选择使用内存地址作为指针值,但它们可以使用任何东西,只要它是同构的。
  • @bdonlan 这也会违反 MISRA-C 中的(咨询)规则。
  • @Andreas 是的,这也是我的想法。硬件人员不应该被允许设计软件应该运行的硬件! :)

标签: c null


【解决方案1】:

C 标准不要求空指针位于机器的地址零处。但是,将 0 常量转换为指针值必须产生 NULL 指针(第 6.3.2.3/3 节),并且将空指针评估为布尔值必须为 false。如果您真的确实想要一个零地址,而NULL 不是零地址,这可能会有点尴尬。

尽管如此,通过对编译器和标准库进行(大量)修改,让NULL 用另一种位模式表示,同时仍然严格符合标准库并非不可能。然而,仅仅更改NULL 本身的定义是不够的,因为NULL 的计算结果为true。

具体来说,您需要:

  • 在对指针的赋值(或对指针的强制转换)中安排文字零,以将其转换为其他一些魔法值,例如 -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 可能对实现这一点很有用 - 我知道它的设计考虑了这样的转换。当然,您仍然可能需要对标准库进行更改。

【讨论】:

  • 好吧,我没有找到 §6.3.2.3 中的文字,但我怀疑会有这样的声明 somewhere :)。我想这回答了我的问题,按照标准,除非我想编写一个新的 C 编译器来支持我,否则我不允许重新定义 NULL :)
  • 一个好的技巧是破解编译器,以便指针整数转换 XOR 一个特定的值,该值是一个无效的指针,并且仍然微不足道,目标架构可以廉价地做到这一点(通常,这将是具有单个位集的值,例如 0x20000000)。
  • 您需要在编译器中更改的另一件事是初始化具有复合类型的对象 - 如果对象已部分初始化,则必须将不存在显式初始化程序的任何指针初始化为NULL.
【解决方案2】:

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

C 实现由编译器及其头文件组成。如果您修改标头以重新定义NULL,但不修改编译器来修复静态初始化,那么您创建了一个不符合要求的实现。行为不正确的是整个实现加在一起,如果你破坏了它,你真的没有人可以责怪;)

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

【讨论】:

    【解决方案3】:

    如果您使用 C 标准库,您会遇到可以返回 NULL 的函数的问题。例如malloc documentation 状态:

    如果函数分配失败 请求的内存块,null 返回指针。

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

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

    我会改为定义您自己的 NULL,“MYPRODUCT_NULL”,供您自己使用,并避免或转换自 C std 库。

    【讨论】:

      【解决方案4】:

      不考虑 NULL 并将 IO 到端口 0x0000 视为特殊情况,可能使用用汇编程序编写的例程,因此不受标准 C 语义的约束。 IOW,不要重新定义NULL,重新定义端口0x00000。

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

      【讨论】:

      • 这个问题只会在意外访问NULL时出现,而不是在有意访问端口时。那我为什么要重新定义端口 I/O 呢?它已经在正常工作了。
      • @Lundin 是否意外,NULL 只能在使用*pp[]p() 的C 程序中取消引用,因此编译器只需要关心关于保护 IO 端口 0x0000 的那些。
      • @Lundin 您问题的第二部分:一旦您限制从 C 内部访问地址零,您需要另一种方法来访问端口 0x0000。用汇编程序编写的函数可以做到这一点。在 C 中,端口可以映射到 0xFFFF 或其他什么,但最好使用函数而忘记端口号。
      【解决方案5】:

      考虑到其他人提到的重新定义 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 不会与任何内容发生冲突(在大多数情况下不应该),您也许可以安全地执行此操作.然后您无需担心重新构建您的工具链/标准库和以下形式的表达式:

        if (pointer)
        {
           ...
        }
      

      还在工作

      我知道这很疯狂,但我只是想我会把这个想法扔出去:)。

      【讨论】:

      • 这个问题只会在意外访问NULL时出现,而不是在有意访问端口时。那我为什么要重新定义端口 I/O 呢?它已经在正常工作了。
      • @LundIn 我想你必须选择哪个更痛苦,调整重建整个工具链或更改代码的这一部分。
      【解决方案6】:

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

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

      【讨论】:

        【解决方案7】:

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

        如果(我的指针) { // myPointer 不为空 ... }

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2023-03-23
          • 2012-08-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多