使用 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 个字节作为要打印的字符。它甚至不知道堆栈上还有更多数据,即它应该打印的实际字符。