64位系统上的NULL定义问题

Posted

技术标签:

【中文标题】64位系统上的NULL定义问题【英文标题】:NULL definition problem on 64 bit system 【发布时间】:2009-11-04 14:38:30 【问题描述】:

我正在使用 gcc 4.1.2 在 RHEL 5.1 64 位平台上运行。

我有一个实用函数:

void str_concat(char *buff, int buffSize, ...);

连接 char * 传入可变参数列表(...),而最后一个参数应该为 NULL,以指定参数的结尾。在 64 位系统上,NULL 是 8 个字节。

现在解决问题。我的应用程序直接/间接包含 2 个 stddef.h 文件。

第一个是/usr/include/linux/stddef.h,它定义NULL如下:

#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif

第二个是/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include/stddef.h

#if defined (_STDDEF_H) || defined (__need_NULL)
#undef NULL     /* in case <stdio.h> has defined it. */
#ifdef __GNUG__
#define NULL __null
#else   /* G++ */
#ifndef __cplusplus
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */
#endif  /* G++ */
#endif  /* NULL not defined and <stddef.h> or need NULL.  */
#undef  __need_NULL

当然我需要第二个,因为它将 NULL 定义为 __null(8 个字节),而第一个将其定义为整数 0(4 个字节)。

如何防止 /usr/include/linux/stddef.h 被错误地包含?

更新:

    编译行非常简单:

    g++ -Wall -fmessage-length=0 -g -pthread

    许多人建议通过 (void *)0。这当然会奏效。该功能在很多地方使用的问题,我的意思是很多地方。我想找到能够满足 C++ 标准承诺的解决方案 - NULL 为 8 字节大小。

【问题讨论】:

如果我们有您的编译行,查看包含路径中的目录会有所帮助。 是否可以看到生成的程序集输出? 我预处理文件 (gcc -E) 以证明 NULL 被 0 替换。 可以用-S编译看看生成的源码吗? “C++ 标准承诺的内容 - 8 字节大小的 NULL”。不,它非常不承诺任何这样的事情。附近没有。 【参考方案1】:

在这种情况下不存在“NULL 定义问题”。您尝试在代码中使用 NULL 的方式存在问题。

NULL 本身不能可移植地传递给 C/C++ 中的可变参数函数。您必须在传递之前显式转换它,即在您的情况下,您必须传递 (const char*) NULL 作为参数列表的终止符。

您的问题被标记为 C++。在任何情况下,无论大小如何,在 C++ 中 NULL 将始终定义为 integer 常量。在 C++ 中将 NULL 定义为指针是非法的。由于您的函数需要一个指针 (const char *),因此在 C++ 代码中没有任何 NULL 的定义适用。

对于更简洁的代码,您可以定义自己的常量,例如

const char* const STR_TERM = NULL;

并在对您的函数的调用中使用它。但是您将永远无法有意义地仅使用 NULL 来达到此目的。每当一个普通的NULL 作为可变参数传递时,它就是一个必须修复的明显的可移植性错误。

已添加:您的更新声称“C++ 标准承诺 NULL 为 8 字节大小”(我认为是在 64 位平台上)。这没有任何意义。 C++ 标准对NULL 没有任何类似的承诺。

NULL 旨在用作右值。它没有特定的大小,也没有有效使用 NULL,它的实际大小甚至可能无关紧要。


引用 ISO/IEC 14882:1998,第 18.1 节“类型”,第 4 段:

宏 NULL 是定义的实现 本国际标准中的 C++ 空指针常量 (4.10).180)

180) 可能的定义包括 0 和 0L,但不包括 (void*)0。

【讨论】:

完全可以接受传递非常量空指针 - 并且更短。 "NULL 不能单独传递给 C/C++ 中的可变参数函数。" - 不,它不能在 C++ 中完成。我很确定它在 C 中没问题。 @Chris Lutz:不,这在 C 中不好。你怎么知道你是通过 00L(void *) 0 还是别的什么?【参考方案2】:

一种解决方案 - 甚至可能是最好的,但肯定非常可靠 - 是将显式 null char 指针传递给您的函数调用:

str_concat(buffer, sizeof(buffer), "str1", "str2", ..., (char *)0);

或:

str_concat(buffer, sizeof(buffer), "str1", "str2", ..., (char *)NULL);

这是 POSIX 系统中 execl() 函数的标准推荐做法,例如,出于完全相同的原因 - 可变长度参数列表的尾随参数受通常的提升(char 或 short to int; float to double),但不能是类型安全的。

这也是为什么C++从业者普遍避免variable-length argument lists的原因;它们不是类型安全的。

【讨论】:

【参考方案3】:

删除__GNUG__case,并在第二个文件中反转 ifdef/endif,两个文件都可以:

#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif

也就是说,对于 C 编译,他们将 NULL 定义为 ((void *)0),对于 C++,他们定义为 0。

所以简单的答案是“不要编译为 C++”。

您真正的问题是您希望在可变参数列表中使用 NULL,以及编译器不可预测的参数大小。你可能会尝试写“(void *)0”而不是 NULL 来终止你的列表,并强制编译器传递一个 8 字节指针而不是 4 字节 int。

【讨论】:

因为它是传递的 'const char *' 值列表,所以使用 '(char *)0' 而不是 '(void *)0' 似乎是合乎逻辑的,尽管我同意最终结果无法区分。【参考方案4】:

您可能无法修复包含,因为系统包含是一个曲折的迷宫。

您可以使用 (void*)0 或 (char*)0 代替 NULL 来解决此问题。

在考虑之后,我拒绝了我之前重新定义 NULL 的想法。那将是一件坏事,并且可能会弄乱许多其他代码。

【讨论】:

重新定义 NULL 会导致比引发问题的问题更大的疯狂。【参考方案5】:

我之前回答过以下答案。但是后来我发现我误解了一些信息并给出了错误的答案。只是出于好奇,我用VS2008做了同样的测试,得到了不同的结果。这只是大脑锻炼......

为什么需要第二个?两个标题都说同样的话。 你写 0 或 NULL 或 ((void *)0) 都没有关系 所有这些都将占用 8 个字节。

我在 64 位平台上使用 GCC 4.1.3 进行了快速测试

#include <string.h>

void str_concat(char *po_buf, int pi_max, ...)

    strcpy(po_buf, "Malkocoglu"); /* bogus */


int main()

    char buf[100];
    str_concat(buf, 100, "abc", 1234LL, "def", 5678LL, "ghi", 2345LL, "jkl", 6789LL, "mno", 3456LL, 0, "pqx", 0);
    return 1;

这是编译器生成的程序集……

main:
.LFB3:
    pushq   %rbp
.LCFI3:
    movq    %rsp, %rbp
.LCFI4:
    subq    $192, %rsp
.LCFI5:
    leaq    -112(%rbp), %rdi
    movl    $0, 64(%rsp)                          0
    movq    $.LC2, 56(%rsp)                       "pqx"
    movl    $0, 48(%rsp)                          0
    movq    $3456, 40(%rsp)                       3456LL
    movq    $.LC3, 32(%rsp)                       "mno"
    movq    $6789, 24(%rsp)                       6789LL
    movq    $.LC4, 16(%rsp)                       "jkl"
    movq    $2345, 8(%rsp)                        2345LL
    movq    $.LC5, (%rsp)                         "ghi"
    movl    $5678, %r9d                           5678LL
    movl    $.LC0, %r8d                           "def"
    movl    $1234, %ecx                           1234LL
    movl    $.LC1, %edx                           "abc"
    movl    $100, %esi                            100
    movl    $0, %eax
    call    str_concat
    movl    $1, %eax
    leave
    ret

注意所有的堆栈位移都是 8 字节...

编译器将 0 视为 32 位数据类型。 虽然它在 堆栈指针,推送的值不应该是 32 位的!

我用VS2008做了同样的测试,汇编输出如下:

mov QWORD PTR [rsp+112], 0
lea rax, OFFSET FLAT:$SG3597
mov QWORD PTR [rsp+104], rax
mov QWORD PTR [rsp+96], 0
mov QWORD PTR [rsp+88], 3456        ; 00000d80H
lea rax, OFFSET FLAT:$SG3598
mov QWORD PTR [rsp+80], rax
mov QWORD PTR [rsp+72], 6789        ; 00001a85H
lea rax, OFFSET FLAT:$SG3599
mov QWORD PTR [rsp+64], rax
mov QWORD PTR [rsp+56], 2345        ; 00000929H
lea rax, OFFSET FLAT:$SG3600
mov QWORD PTR [rsp+48], rax
mov QWORD PTR [rsp+40], 5678        ; 0000162eH
lea rax, OFFSET FLAT:$SG3601
mov QWORD PTR [rsp+32], rax
mov r9d, 1234               ; 000004d2H
lea r8, OFFSET FLAT:$SG3602
mov edx, 100                ; 00000064H
lea rcx, QWORD PTR buf$[rsp]
call    ?str_concat@@YAXPEADHZZ         ; str_concat

这一次编译器生成不同的代码,它将 0 视为 64 位数据类型(注意 QWORD 关键字)。值和堆栈位移都是正确的。 VS 和 GCC 的行为不同。

【讨论】:

其实我刚刚振作起来,int 64 位平台上的 4 个字节对吗? 它取决于系统/编译器/ABI,但是通常 int 是 4 个字节,但 0 不是。 0 是一个数字,它不是一个类型(如 int/long/char),它被提升为任何编译器认为它应该是的...... 投反对票,因为 0 不是 8 个字节。在 Gentoo AMD64 的早期,我使用一些 Gnome 软件遇到了这个问题。在可变参数列表中,C 不知道将 0 转换为指针,因此您必须强制转换为 void*。 64 系统上的常数 0 是 4 个字节。只需尝试打印 sizeof(0) 和 sizeof(NULL) ,您就会看到。在我的情况下,大小确实很重要,因为我在堆栈上传递了 NULL C 和 C++ 都不是绝对要求,但两种标准的“精神”都是 64 位系统上的 sizeof(int)*CHAR_BIT==64。问题是,两者都没有提供 short short,因此编译器供应商没有合理的 32 位和 16 位类型名称,但他们有很多大于 int 的类型名称。结果int 最终为 32 位。请注意,NULL 也可以定义为0ULL,在这种情况下,它的类型在这些系统上为 64 位。

以上是关于64位系统上的NULL定义问题的主要内容,如果未能解决你的问题,请参考以下文章

64位 regsrv win10_Regsvr32 在64位机器上的用法

64位 regsrv win10_Regsvr32 在64位机器上的用法

指定 64 位对齐

32位和64位系统的区别

x86 OS 的 64 位处理器上的 64 位整数运算

Regsvr32 在64位机器上的用法(转载)