C ++指针奇怪的未定义行为
C++ pointer weird undefined behaviour
使用 -O2(或 -O3)和 运行 编译这个程序在我的机器上产生了有趣的结果。
#include <iostream>
using namespace std;
int main()
{
// Pointer to an int in the heap with a value of 5
int *p = new int(5);
// Deallocate the memory, but keep a dangling pointer
delete p;
// Write 123 to deallocated space
*p = 123;
// Allocate a long int in the heap
long *x = new long(456);
// Print values and pointers
cout << "*p: " << *p << endl;
cout << "*x: " << *x << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
cout << endl << "Changing nothing" << endl << endl;
// Print again without changing anything
cout << "*p: " << *p << endl;
cout << "*x: " << *x << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
return 0;
}
g++ -O2 code.cc; ./a.out
*p: 123
*x: 456
p: 0x112f010
x: 0x112f010
Changing nothing
*p: 456
*x: 456
p: 0x112f010
x: 0x112f010
我正在做的是在 p
指向的堆中写入已释放的 int
,然后分配地址为 x
的 long。我的编译器始终将 long 放在与 p
-> x == p
相同的地址上。
现在,当我取消引用 p
并打印它时,它保留了 123 的值,即使它已被长 456 重写。然后 *x
被打印为 456。更奇怪的是,后来, 在不改变任何东西的情况下,打印相同的值会产生预期的结果。我认为这是一种优化技术,它仅在打印值 *p
后使用时才初始化 *x,这将解释它。然而,一个 objdump 说的是别的东西。这是一个截断和评论 objdump -d a.out
:
00000000004008a0 <main>:
4008a0: 41 54 push %r12
4008a2: 55 push %rbp
Most likely the int allocation, where 0x4 is the size (4 bytes)
4008a3: bf 04 00 00 00 mov [=12=]x4,%edi
4008a8: 53 push %rbx
4008a9: e8 e2 ff ff ff callq 400890 <_Znwm@plt>
I have no idea what is going on here, but the pointer p is in 2 registers. Let's call the other one q.
q = p;
4008ae: 48 89 c3 mov %rax,%rbx
4008b1: 48 89 c7 mov %rax,%rdi
*p = 5;
4008b4: c7 00 05 00 00 00 movl [=12=]x5,(%rax)
delete p;
4008ba: e8 51 ff ff ff callq 400810 <_ZdlPv@plt>
*q = 123;
4008bf: c7 03 7b 00 00 00 movl [=12=]x7b,(%rbx)
The long allocation and some other stuff (?). (8 bytes)
4008c5: bf 08 00 00 00 mov [=12=]x8,%edi
4008ca: e8 c1 ff ff ff callq 400890 <_Znwm@plt>
4008cf: 44 8b 23 mov (%rbx),%r12d
4008d2: be e4 0b 40 00 mov [=12=]x400be4,%esi
4008d7: bf c0 12 60 00 mov [=12=]x6012c0,%edi
Initialization of the long before the printing
*p = 456;
4008dc: 48 c7 00 c8 01 00 00 movq [=12=]x1c8,(%rax)
4008e3: 48 89 c5 mov %rax,%rbp
The printing
4008e6: e8 85 ff ff ff callq 400870 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
........
现在,虽然*p
已经被long
初始化覆盖(4008dc
),它仍然打印为123。
我希望我在这里有任何意义,感谢您的帮助。
让我自己清楚:
我想弄清楚幕后发生了什么,编译器做了什么,以及为什么生成的编译代码与输出不一致。我知道这是未定义的行为,任何事情都可能发生。但这意味着编译器可以生成任何代码,而不是 CPU 将组成指令。欢迎任何想法。
PS:别担心,我不打算在任何地方使用它;)
编辑:在我朋友的机器上 (OS X) 即使经过优化,它也能产生预期的结果。
如您所述,这可能是由于编译器强制执行的优化。如果您使用 -O0 进行编译,那么它将打印 456 作为值。由于 p 被删除并且 x 被立即分配,x 将指向与 p 指向的相同地址(可能不总是相同的情况,但在您的测试中最有可能是这种情况)。因此,*p 和 *x 应该取消引用相同的值。
如果您更改打印语句的顺序,那么将始终为这些值打印 456。我已经更改了代码中前两个 cout 语句的顺序,如下所示:
#include <iostream>
using namespace std;
int main()
{
// Pointer to an int in the heap with a value of 5
int *p = new int(5);
// Deallocate the memory, but keep a dangling pointer
delete p;
// Write 123 to deallocated space
*p = 123;
// Allocate a long int in the heap
long *x = new long(456);
// Print values and pointers
cout << "*x: " << *x << endl;
cout << "*p: " << *p << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
cout << endl << "Changing nothing" << endl << endl;
// Print again without changing anything
cout << "*p: " << *p << endl;
cout << "*x: " << *x << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
return 0;
}
您不会在自己的源代码中或编译器对其执行的操作中找到答案,即使您从编译器生成了汇编输出。
C 运行时内存分配器未定义,它是已编译的二进制代码,链接到您的测试应用程序。当您调用 new 时,运行时库决定指针的去向。不能保证 new/delete/new 将意味着第二个新的给你相同的地址,它完全取决于实现。
如果您真的想知道,那么您需要使用完整的源代码(包括新的源代码)进行构建,然后阅读它的实现方式and/or在调试器中逐步执行以查看发生了什么.
您太早停止查看反汇编输出(或者至少您没有 post 接下来的几行,这与您的问题相关)。它们可能看起来像:
movl %r12d, %esi
movq %rax, %rdi
call _ZNSolsEi
movq %rax, %rdi
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
rbx
和 r12
是 GCC 在 Linux 上使用的 x64 ABI 中必须跨函数调用保留的寄存器。 long
分配后,你看到这条指令:
mov (%rbx),%r12d
rbx
在指令流前面的使用包括:
mov %rax,%rbx ; store the `p` pointer in `rbx`
...
movl [=12=]x7b,(%rbx) ; store 123 where `p` pointed (even though it has been freed before)
...
mov (%rbx),%r12d ; read that value - 123 - back and into `r12`
然后你会在上面我 post 编辑的片段中看到,这是没有进入你的问题的反汇编,对应于 cout << "*p: " << *p << endl
语句的一部分:
movl %r12d, %esi ; put 123 into `esi`, which is used to pass an argument to a function call
并打印 123
。
使用 -O2(或 -O3)和 运行 编译这个程序在我的机器上产生了有趣的结果。
#include <iostream>
using namespace std;
int main()
{
// Pointer to an int in the heap with a value of 5
int *p = new int(5);
// Deallocate the memory, but keep a dangling pointer
delete p;
// Write 123 to deallocated space
*p = 123;
// Allocate a long int in the heap
long *x = new long(456);
// Print values and pointers
cout << "*p: " << *p << endl;
cout << "*x: " << *x << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
cout << endl << "Changing nothing" << endl << endl;
// Print again without changing anything
cout << "*p: " << *p << endl;
cout << "*x: " << *x << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
return 0;
}
g++ -O2 code.cc; ./a.out
*p: 123
*x: 456
p: 0x112f010
x: 0x112f010
Changing nothing
*p: 456
*x: 456
p: 0x112f010
x: 0x112f010
我正在做的是在 p
指向的堆中写入已释放的 int
,然后分配地址为 x
的 long。我的编译器始终将 long 放在与 p
-> x == p
相同的地址上。
现在,当我取消引用 p
并打印它时,它保留了 123 的值,即使它已被长 456 重写。然后 *x
被打印为 456。更奇怪的是,后来, 在不改变任何东西的情况下,打印相同的值会产生预期的结果。我认为这是一种优化技术,它仅在打印值 *p
后使用时才初始化 *x,这将解释它。然而,一个 objdump 说的是别的东西。这是一个截断和评论 objdump -d a.out
:
00000000004008a0 <main>:
4008a0: 41 54 push %r12
4008a2: 55 push %rbp
Most likely the int allocation, where 0x4 is the size (4 bytes)
4008a3: bf 04 00 00 00 mov [=12=]x4,%edi
4008a8: 53 push %rbx
4008a9: e8 e2 ff ff ff callq 400890 <_Znwm@plt>
I have no idea what is going on here, but the pointer p is in 2 registers. Let's call the other one q.
q = p;
4008ae: 48 89 c3 mov %rax,%rbx
4008b1: 48 89 c7 mov %rax,%rdi
*p = 5;
4008b4: c7 00 05 00 00 00 movl [=12=]x5,(%rax)
delete p;
4008ba: e8 51 ff ff ff callq 400810 <_ZdlPv@plt>
*q = 123;
4008bf: c7 03 7b 00 00 00 movl [=12=]x7b,(%rbx)
The long allocation and some other stuff (?). (8 bytes)
4008c5: bf 08 00 00 00 mov [=12=]x8,%edi
4008ca: e8 c1 ff ff ff callq 400890 <_Znwm@plt>
4008cf: 44 8b 23 mov (%rbx),%r12d
4008d2: be e4 0b 40 00 mov [=12=]x400be4,%esi
4008d7: bf c0 12 60 00 mov [=12=]x6012c0,%edi
Initialization of the long before the printing
*p = 456;
4008dc: 48 c7 00 c8 01 00 00 movq [=12=]x1c8,(%rax)
4008e3: 48 89 c5 mov %rax,%rbp
The printing
4008e6: e8 85 ff ff ff callq 400870 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
........
现在,虽然*p
已经被long
初始化覆盖(4008dc
),它仍然打印为123。
我希望我在这里有任何意义,感谢您的帮助。
让我自己清楚: 我想弄清楚幕后发生了什么,编译器做了什么,以及为什么生成的编译代码与输出不一致。我知道这是未定义的行为,任何事情都可能发生。但这意味着编译器可以生成任何代码,而不是 CPU 将组成指令。欢迎任何想法。
PS:别担心,我不打算在任何地方使用它;)
编辑:在我朋友的机器上 (OS X) 即使经过优化,它也能产生预期的结果。
如您所述,这可能是由于编译器强制执行的优化。如果您使用 -O0 进行编译,那么它将打印 456 作为值。由于 p 被删除并且 x 被立即分配,x 将指向与 p 指向的相同地址(可能不总是相同的情况,但在您的测试中最有可能是这种情况)。因此,*p 和 *x 应该取消引用相同的值。 如果您更改打印语句的顺序,那么将始终为这些值打印 456。我已经更改了代码中前两个 cout 语句的顺序,如下所示:
#include <iostream>
using namespace std;
int main()
{
// Pointer to an int in the heap with a value of 5
int *p = new int(5);
// Deallocate the memory, but keep a dangling pointer
delete p;
// Write 123 to deallocated space
*p = 123;
// Allocate a long int in the heap
long *x = new long(456);
// Print values and pointers
cout << "*x: " << *x << endl;
cout << "*p: " << *p << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
cout << endl << "Changing nothing" << endl << endl;
// Print again without changing anything
cout << "*p: " << *p << endl;
cout << "*x: " << *x << endl;
cout << "p: " << p << endl;
cout << "x: " << x << endl;
return 0;
}
您不会在自己的源代码中或编译器对其执行的操作中找到答案,即使您从编译器生成了汇编输出。
C 运行时内存分配器未定义,它是已编译的二进制代码,链接到您的测试应用程序。当您调用 new 时,运行时库决定指针的去向。不能保证 new/delete/new 将意味着第二个新的给你相同的地址,它完全取决于实现。
如果您真的想知道,那么您需要使用完整的源代码(包括新的源代码)进行构建,然后阅读它的实现方式and/or在调试器中逐步执行以查看发生了什么.
您太早停止查看反汇编输出(或者至少您没有 post 接下来的几行,这与您的问题相关)。它们可能看起来像:
movl %r12d, %esi
movq %rax, %rdi
call _ZNSolsEi
movq %rax, %rdi
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
rbx
和 r12
是 GCC 在 Linux 上使用的 x64 ABI 中必须跨函数调用保留的寄存器。 long
分配后,你看到这条指令:
mov (%rbx),%r12d
rbx
在指令流前面的使用包括:
mov %rax,%rbx ; store the `p` pointer in `rbx`
...
movl [=12=]x7b,(%rbx) ; store 123 where `p` pointed (even though it has been freed before)
...
mov (%rbx),%r12d ; read that value - 123 - back and into `r12`
然后你会在上面我 post 编辑的片段中看到,这是没有进入你的问题的反汇编,对应于 cout << "*p: " << *p << endl
语句的一部分:
movl %r12d, %esi ; put 123 into `esi`, which is used to pass an argument to a function call
并打印 123
。