使用 long long integer 存储 32 位指针导致 printf 出错
Using long long integer to store 32 bit pointer causes printf to bug
我对 C 指针进行了一番研究,发现了一个相当奇怪的行为。
考虑以下代码:
int
main ()
{
char charac = 'r';
long long ptr = (long long) &charac; // Stores the address of charac into a long long variable
printf ("[ptr] points to %p containing the char %c\n", ptr, *(char*)ptr);
}
在 64 位架构上
现在为 64 位目标架构编译时(编译命令:gcc -Wall -Wextra -std=c11 -pedantic test.c -o test
),一切正常,执行给出
> ./test
[ptr] points to 0x7fff3090ee47 containing the char r
在 32 位架构上
但是,如果编译目标是 32 位架构(使用编译命令:gcc -Wall -Wextra -std=c11 -pedantic -ggdb -m32 test.c -o test
),执行会给出这个奇怪的结果:
> ./test
[ptr] points to 0xff82d4f7 containing the char �
现在最奇怪的部分是,如果我将前面代码中的 printf
调用更改为 printf ("[ptr] contains the char %c\n", *(char*)ptr);
,执行会给出正确的结果:
> ./test
[ptr] contains the char r
问题似乎只出现在 32 位 arch 上,我不明白为什么 printf
调用更改导致执行行为不同。
PS:也许值得一提的是底层机器是 x86 64 位架构,但是使用gcc
.
中的 -m32
选项触发的 32 位兼容模式
你基本上是在欺骗你的编译器。
你告诉 printf
你传递了一个指针作为格式字符串后的第一个参数。但是你传递了一个整型变量。
虽然这始终是未定义的行为,但只要预期类型和传递的类型的大小相同,它可能会以某种方式起作用。那就是 "undefined behaviour" 中的 "undefined"。它也没有定义为崩溃或立即显示不良结果。它可能只是假装工作,等着从后面打你。
如果您的 long long
有 64 位而指针只有 32 位,则您的堆栈布局被破坏导致 printf
从错误的位置读取。
根据您的体系结构和工具,当您使用可变参数列表调用函数时,您的堆栈很可能看起来像这样:
+---------------+---------------+---------------+
| last fixed par| Par 1 type1 | Par 2 type2 |
| x bytes | x bytes | x bytes |
+---------------+---------------+---------------+
未知参数被压入堆栈,最后压入签名中的最后一个已知参数。 (其他已知参数此处忽略)
然后函数可以使用 va_arg
和朋友遍历参数列表。为此,函数必须知道传递了哪些类型的参数。 printf
函数使用格式说明符来决定从堆栈中使用哪个参数。
现在到了什么都看你说实话的地步了
你告诉你的编译器:
+---------------+---------------+---------------+
| format char* | Par 1 void* | Par 2 int |
| 4 bytes | 4 bytes | 4 bytes |
+---------------+---------------+---------------+
对于第一个参数 (%p
),编译器占用 4 个字节,这是 void*
的大小。然后,参数 2 (%c
) 需要另外 4 个字节(int
的大小)。
(注意:最后一个参数打印为字符,即最后只使用 1 个字节。由于函数调用的整数类型提升规则没有正确的参数类型规范,参数存储为 int
在堆栈上。因此在这种情况下 printf
也必须消耗 int
的字节。)
现在让我们看看您的函数调用(您真正放入 printf
的内容):
+---------------+-------------------------------+---------------+
| format char* | Par 1 long long | Par 2 int |
| 4 bytes | 8 bytes | 4 bytes |
+---------------+-------------------------------+---------------+
你还声称提供了一个指针和一个4字节的整型参数。
但是现在第一个参数带有额外的 4 个字节的长度,printf
函数仍然不知道。
如您所说,该函数读取指针的 4 个字节。这可能符合 long long
的前 4 个字节,但不会消耗剩余的 4 个字节。
现在用于 %c
格式的下 4 个字节已被读取,但我们仍在读取您的 long long
的后半部分,无论这可能是什么,这都不是您想要的。
最后,当函数 returns.
时,压入的整数仍未被触及。
这就是为什么你不应该搞乱奇怪的类型转换和错误类型的原因。
这也是您应该在编译期间查看警告的原因。
一个大问题:您为 integer/pointer 恶作剧使用了错误的类型。类型 intptr_t
是一个可以存储指针的整数类型。
那么,32 位架构出了什么问题?
类型 long long int
是(使用 gcc)64 位类型。但是,具有 %p
格式的 printf
命令需要接收一个 32 位指针,而不是 64 位指针。
对 printf 的调用将在调用堆栈上产生:(仅用于说明目的,细节可能有所不同)
pointer to format string
ptr (8 bytes)
*(char *)ptr (at least 1 byte, likely 4)
printf
读取格式字符串,发现它应该接收一个 32 位指针和一个 char
。然后它读取 ptr
的前 4 个字节作为要读取的指针,接下来的 1-4 个字节作为要打印的字符。它甚至不知道堆栈上还有更多数据,即它应该打印的实际字符。
我对 C 指针进行了一番研究,发现了一个相当奇怪的行为。
考虑以下代码:
int
main ()
{
char charac = 'r';
long long ptr = (long long) &charac; // Stores the address of charac into a long long variable
printf ("[ptr] points to %p containing the char %c\n", ptr, *(char*)ptr);
}
在 64 位架构上
现在为 64 位目标架构编译时(编译命令:gcc -Wall -Wextra -std=c11 -pedantic test.c -o test
),一切正常,执行给出
> ./test
[ptr] points to 0x7fff3090ee47 containing the char r
在 32 位架构上
但是,如果编译目标是 32 位架构(使用编译命令:gcc -Wall -Wextra -std=c11 -pedantic -ggdb -m32 test.c -o test
),执行会给出这个奇怪的结果:
> ./test
[ptr] points to 0xff82d4f7 containing the char �
现在最奇怪的部分是,如果我将前面代码中的 printf
调用更改为 printf ("[ptr] contains the char %c\n", *(char*)ptr);
,执行会给出正确的结果:
> ./test
[ptr] contains the char r
问题似乎只出现在 32 位 arch 上,我不明白为什么 printf
调用更改导致执行行为不同。
PS:也许值得一提的是底层机器是 x86 64 位架构,但是使用gcc
.
-m32
选项触发的 32 位兼容模式
你基本上是在欺骗你的编译器。
你告诉 printf
你传递了一个指针作为格式字符串后的第一个参数。但是你传递了一个整型变量。
虽然这始终是未定义的行为,但只要预期类型和传递的类型的大小相同,它可能会以某种方式起作用。那就是 "undefined behaviour" 中的 "undefined"。它也没有定义为崩溃或立即显示不良结果。它可能只是假装工作,等着从后面打你。
如果您的 long long
有 64 位而指针只有 32 位,则您的堆栈布局被破坏导致 printf
从错误的位置读取。
根据您的体系结构和工具,当您使用可变参数列表调用函数时,您的堆栈很可能看起来像这样:
+---------------+---------------+---------------+
| last fixed par| Par 1 type1 | Par 2 type2 |
| x bytes | x bytes | x bytes |
+---------------+---------------+---------------+
未知参数被压入堆栈,最后压入签名中的最后一个已知参数。 (其他已知参数此处忽略)
然后函数可以使用 va_arg
和朋友遍历参数列表。为此,函数必须知道传递了哪些类型的参数。 printf
函数使用格式说明符来决定从堆栈中使用哪个参数。
现在到了什么都看你说实话的地步了
你告诉你的编译器:
+---------------+---------------+---------------+
| format char* | Par 1 void* | Par 2 int |
| 4 bytes | 4 bytes | 4 bytes |
+---------------+---------------+---------------+
对于第一个参数 (%p
),编译器占用 4 个字节,这是 void*
的大小。然后,参数 2 (%c
) 需要另外 4 个字节(int
的大小)。
(注意:最后一个参数打印为字符,即最后只使用 1 个字节。由于函数调用的整数类型提升规则没有正确的参数类型规范,参数存储为 int
在堆栈上。因此在这种情况下 printf
也必须消耗 int
的字节。)
现在让我们看看您的函数调用(您真正放入 printf
的内容):
+---------------+-------------------------------+---------------+
| format char* | Par 1 long long | Par 2 int |
| 4 bytes | 8 bytes | 4 bytes |
+---------------+-------------------------------+---------------+
你还声称提供了一个指针和一个4字节的整型参数。
但是现在第一个参数带有额外的 4 个字节的长度,printf
函数仍然不知道。
如您所说,该函数读取指针的 4 个字节。这可能符合 long long
的前 4 个字节,但不会消耗剩余的 4 个字节。
现在用于 %c
格式的下 4 个字节已被读取,但我们仍在读取您的 long long
的后半部分,无论这可能是什么,这都不是您想要的。
最后,当函数 returns.
这就是为什么你不应该搞乱奇怪的类型转换和错误类型的原因。
这也是您应该在编译期间查看警告的原因。
一个大问题:您为 integer/pointer 恶作剧使用了错误的类型。类型 intptr_t
是一个可以存储指针的整数类型。
那么,32 位架构出了什么问题?
类型 long long int
是(使用 gcc)64 位类型。但是,具有 %p
格式的 printf
命令需要接收一个 32 位指针,而不是 64 位指针。
对 printf 的调用将在调用堆栈上产生:(仅用于说明目的,细节可能有所不同)
pointer to format string
ptr (8 bytes)
*(char *)ptr (at least 1 byte, likely 4)
printf
读取格式字符串,发现它应该接收一个 32 位指针和一个 char
。然后它读取 ptr
的前 4 个字节作为要读取的指针,接下来的 1-4 个字节作为要打印的字符。它甚至不知道堆栈上还有更多数据,即它应该打印的实际字符。