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 中不好。你怎么知道你是通过0
、0L
、(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位机器上的用法