Apa yang dilakukan Visual Studio dengan pointer yang dihapus dan mengapa?

130

Buku C ++ yang saya baca menyatakan bahwa ketika sebuah pointer dihapus menggunakan deleteoperator, memori di lokasi yang ditunjuknya adalah "dibebaskan" dan dapat ditimpa. Ini juga menyatakan bahwa pointer akan terus menunjuk ke lokasi yang sama sampai dipindahkan atau diatur ke NULL.

Namun dalam Visual Studio 2012; ini tampaknya tidak menjadi masalah!

Contoh:

#include <iostream>

using namespace std;

int main()
{
    int* ptr = new int;
    cout << "ptr = " << ptr << endl;
    delete ptr;
    cout << "ptr = " << ptr << endl;

    system("pause");

    return 0;
}

Ketika saya mengkompilasi dan menjalankan program ini saya mendapatkan output berikut:

ptr = 0050BC10
ptr = 00008123
Press any key to continue....

Jelas alamat yang ditunjuk oleh penunjuk berubah saat delete dipanggil!

Mengapa ini terjadi? Apakah ini ada hubungannya dengan Visual Studio secara khusus?

Dan jika delete dapat mengubah alamat yang ditunjuknya, mengapa tidak menghapus secara otomatis mengatur pointer ke NULLalih-alih beberapa alamat acak?

tjwrona1992
sumber
4
Hapus pointer, bukan berarti akan diatur ke NULL, Anda harus mengurusnya.
Mat
11
Saya tahu itu, tetapi buku yang saya baca secara spesifik mengatakan bahwa itu masih akan berisi alamat yang sama dengan yang ditunjukkan sebelum dihapus, tetapi isi dari alamat itu mungkin ditimpa.
tjwrona1992
6
@ tjwrona1992, ya, karena inilah yang biasanya terjadi. Buku ini hanya mencantumkan hasil yang paling mungkin, bukan aturan keras.
SergeyA
5
@ tjwrona1992 Buku C ++ yang saya baca - dan nama bukunya adalah ...?
PaulMcKenzie
4
@ tjwrona1992: Ini mungkin mengejutkan, tapi itu semua penggunaan nilai penunjuk yang tidak valid yang merupakan perilaku tidak terdefinisi, tidak hanya dereferencing. "Memeriksa di mana itu menunjuk ke" IS menggunakan nilai dengan cara yang tidak diizinkan.
Ben Voigt

Jawaban:

175

Saya perhatikan bahwa alamat yang disimpan ptrselalu ditimpa dengan 00008123...

Ini tampak aneh, jadi saya melakukan sedikit penggalian dan menemukan posting blog Microsoft ini berisi bagian yang membahas "Sanitisasi pointer otomatis ketika menghapus objek C ++".

... memeriksa NULL adalah konstruksi kode umum yang berarti bahwa pemeriksaan NULL yang sudah ada digabungkan dengan penggunaan NULL sebagai nilai sanitasi dapat dengan sengaja menyembunyikan masalah keamanan memori asli yang penyebab utamanya benar-benar perlu ditangani.

Untuk alasan ini kami telah memilih 0x8123 sebagai nilai sanitasi - dari perspektif sistem operasi, ini berada di halaman memori yang sama dengan alamat nol (NULL), tetapi pelanggaran akses di 0x8123 akan lebih baik bagi pengembang karena memerlukan perhatian lebih rinci. .

Tidak hanya menjelaskan apa yang Visual Studio lakukan dengan pointer setelah dihapus, itu juga menjawab mengapa mereka memilih TIDAK untuk mengaturnya NULLsecara otomatis!


"Fitur" ini diaktifkan sebagai bagian dari pengaturan "pemeriksaan SDL". Untuk mengaktifkan / menonaktifkannya buka: PROYEK -> Properti -> Properti Konfigurasi -> C / C ++ -> Umum -> pemeriksaan SDL

Untuk mengkonfirmasi ini:

Mengubah pengaturan ini dan menjalankan kembali kode yang sama menghasilkan output berikut:

ptr = 007CBC10
ptr = 007CBC10

"fitur" dalam tanda kutip karena dalam kasus di mana Anda memiliki dua petunjuk ke lokasi yang sama, memanggil delete hanya akan membersihkan SATU dari mereka. Yang lain akan dibiarkan menunjuk ke lokasi yang tidak valid ...


MEMPERBARUI:

Setelah 5 tahun pengalaman pemrograman C ++, saya menyadari bahwa seluruh masalah ini pada dasarnya adalah poin yang bisa diperdebatkan. Jika Anda seorang programmer C ++ dan masih menggunakan newdan deletemengelola pointer mentah alih-alih menggunakan pointer pintar (yang menghindari seluruh masalah ini), Anda mungkin ingin mempertimbangkan perubahan jalur karier untuk menjadi seorang programmer C. ;)

tjwrona1992
sumber
12
Itu temuan yang bagus. Saya berharap MS akan lebih baik mendokumentasikan perilaku debugging seperti ini. Sebagai contoh, alangkah baiknya untuk mengetahui versi kompiler mana yang mulai menerapkan ini dan opsi apa yang mengaktifkan / menonaktifkan perilaku.
Michael Burr
5
"dari perspektif sistem operasi, ini berada di halaman memori yang sama dengan alamat nol" - huh? Bukankah ukuran halaman standar (mengabaikan halaman besar) pada x86 masih 4kb untuk kedua windows dan linux? Meskipun saya samar-samar mengingat sesuatu tentang 64kb pertama ruang alamat di blog Raymond Chen, jadi dalam praktiknya saya mengambil hasil yang sama,
Voo
12
@Voo windows menyimpan nilai 64kB pertama (dan terakhir) sebagai ruang mati untuk menjebak. 0x8123 jatuh di sana dengan baik
ratchet freak
7
Sebenarnya, itu tidak mendorong kebiasaan buruk, dan itu tidak memungkinkan Anda untuk melewatkan pengaturan pointer ke NULL - itulah alasan mengapa mereka menggunakan 0x8123bukan 0. Pointernya masih tidak valid, tetapi menyebabkan pengecualian ketika mencoba melakukan dereferensi (baik), dan tidak lulus pemeriksaan NULL (juga bagus, karena kesalahan untuk tidak melakukan itu). Di mana tempat untuk kebiasaan buruk? Ini benar-benar hanya sesuatu yang membantu Anda melakukan debug.
Luaan
3
Yah, itu tidak dapat mengatur keduanya, jadi ini adalah pilihan terbaik kedua. Jika Anda tidak menyukainya, matikan saja cek SDL - Saya menemukan itu agak berguna, terutama ketika men-debug kode orang lain.
Luaan
30

Anda melihat efek samping dari /sdlopsi kompilasi. Dihidupkan secara default untuk proyek VS2015, memungkinkan pemeriksaan keamanan tambahan di luar yang disediakan oleh / gs. Gunakan Proyek> Properti> C / C ++> Umum> Pengaturan pemeriksaan SDL untuk mengubahnya.

Mengutip dari artikel MSDN :

  • Melakukan sanitasi pointer terbatas. Dalam ekspresi yang tidak melibatkan referensi dan dalam tipe yang tidak memiliki destruktor yang ditentukan pengguna, referensi pointer diatur ke alamat yang tidak valid setelah panggilan untuk menghapus. Ini membantu untuk mencegah penggunaan kembali referensi basi pointer.

Perlu diingat bahwa pengaturan pointer yang dihapus ke NULL adalah praktik yang buruk ketika Anda menggunakan MSVC. Ini mengalahkan bantuan yang Anda dapatkan dari Debug Heap dan opsi / sdl ini, Anda tidak lagi dapat mendeteksi panggilan bebas / hapus yang tidak valid dalam program Anda.

Hans Passant
sumber
1
Dikonfirmasi Setelah menonaktifkan fitur ini, pointer tidak lagi diarahkan. Terima kasih telah menyediakan pengaturan aktual yang mengubahnya!
tjwrona1992
Hans, apakah masih dianggap praktik buruk untuk menetapkan pointer yang dihapus ke NULL dalam kasus di mana Anda memiliki dua pointer yang menunjuk ke lokasi yang sama? Ketika Anda melakukannya delete, Visual Studio akan membiarkan pointer kedua menunjuk ke lokasi aslinya yang sekarang tidak valid.
tjwrona1992
1
Cukup tidak jelas bagi saya sihir seperti apa yang Anda harapkan terjadi dengan mengatur pointer ke NULL. Pointer lain itu tidak jadi tidak menyelesaikan apa pun, Anda masih memerlukan pengalokasi debug untuk menemukan bug.
Hans Passant
3
VS tidak membersihkan pointer. Itu merusak mereka. Jadi, program Anda akan macet saat Anda menggunakannya. Alokasi pengalokasian melakukan banyak hal yang sama dengan memori tumpukan. Masalah besar dengan NULL, itu tidak cukup korup. Kalau tidak, strategi umum, google "0xdeadbeef".
Hans Passant
1
Mengatur pointer ke NULL masih jauh lebih baik daripada membiarkannya menunjuk ke alamat sebelumnya yang sekarang tidak valid. Mencoba menulis ke pointer NULL tidak akan merusak data apa pun dan mungkin akan merusak program. Mencoba menggunakan kembali pointer pada saat itu mungkin bahkan tidak membuat crash program, itu hanya dapat menghasilkan hasil yang sangat tidak terduga!
tjwrona1992
19

Ini juga menyatakan bahwa pointer akan terus menunjuk ke lokasi yang sama sampai dipindahkan atau diatur ke NULL.

Itu jelas informasi yang menyesatkan.

Jelas alamat yang ditunjuk oleh penunjuk berubah saat delete dipanggil!

Mengapa ini terjadi? Apakah ini ada hubungannya dengan Visual Studio secara khusus?

Ini jelas dalam spesifikasi bahasa. ptrtidak valid setelah panggilan ke delete. Menggunakan ptrsetelah deleted adalah penyebab perilaku tidak terdefinisi. Jangan lakukan itu. Lingkungan run time bebas untuk melakukan apa pun yang diinginkan ptrsetelah panggilan ke delete.

Dan jika delete dapat mengubah alamat yang ditunjuknya, mengapa tidak menghapus secara otomatis mengatur pointer ke NULL alih-alih beberapa alamat acak ???

Mengubah nilai pointer ke nilai lama apa pun ada dalam spesifikasi bahasa. Sejauh mengubahnya menjadi NULL, saya akan mengatakan, itu akan buruk. Program akan berperilaku lebih waras jika nilai pointer diatur ke NULL. Namun, itu akan menyembunyikan masalahnya. Ketika program dikompilasi dengan pengaturan optimisasi yang berbeda atau porting ke lingkungan yang berbeda, masalah kemungkinan akan muncul pada saat yang paling tidak tepat.

R Sahu
sumber
1
Saya tidak percaya itu menjawab pertanyaan OP.
SergeyA
Tidak setuju bahkan setelah diedit. Mengaturnya ke NULL tidak akan menyembunyikan masalah - pada kenyataannya, itu akan MENGUNGKAPKAN dalam banyak kasus daripada tanpa itu. Ada alasan mengapa implementasi normal tidak melakukan ini, dan alasannya berbeda.
SergeyA
4
@SergeyA, sebagian besar implementasi tidak melakukannya demi efisiensi. Namun, jika suatu implementasi memutuskan untuk mengaturnya, lebih baik mengaturnya ke sesuatu yang bukan NULL. Itu akan mengungkapkan masalah lebih cepat daripada jika itu diatur ke NULL. Diatur ke NULL, memanggil deletedua kali pada pointer tidak akan menyebabkan masalah. Itu jelas tidak baik.
R Sahu
Tidak, bukan efisiensi - setidaknya, itu bukan perhatian utama.
SergeyA
7
@SergeyA Menetapkan pointer ke nilai yang tidak NULLtetapi juga pasti di luar ruang alamat proses akan memaparkan lebih banyak kasus daripada dua alternatif. Membiarkannya menggantung tidak akan menyebabkan segfault jika digunakan setelah dibebaskan; mengaturnya untuk NULLtidak akan menyebabkan segfault jika deleted lagi.
Blacklight Shining
10
delete ptr;
cout << "ptr = " << ptr << endl;

Secara umum bahkan membaca (seperti yang Anda lakukan di atas, perhatikan: ini berbeda dari dereferencing) nilai-nilai pointer tidak valid (pointer menjadi tidak valid misalnya ketika Anda delete) adalah implementasi perilaku yang ditentukan. Ini diperkenalkan dalam CWG # 1438 . Lihat juga di sini .

Harap perhatikan bahwa sebelum itu nilai pembacaan dari pointer yang tidak valid adalah perilaku yang tidak terdefinisi, jadi apa yang Anda miliki di atas akan menjadi perilaku yang tidak terdefinisi, yang berarti apa pun bisa terjadi.

Giorgim
sumber
3
Kutipan juga relevan dari [basic.stc.dynamic.deallocation]: "Jika argumen yang diberikan ke fungsi deallokasi di perpustakaan standar adalah pointer yang bukan nilai null pointer, fungsi deallocation harus membatalkan alokasi penyimpanan yang dirujuk oleh pointer, menjadikan semua pointer yang tidak valid merujuk ke sembarang bagian dari penyimpanan yang tidak dapat dialokasikan "dan aturan dalam [conv.lval](bagian 4.1) yang mengatakan membaca (lvalue-> konversi nilai) setiap nilai pointer yang tidak valid adalah perilaku yang ditentukan implementasi.
Ben Voigt
Bahkan UB dapat diimplementasikan dengan cara tertentu oleh vendor tertentu sehingga dapat diandalkan, setidaknya untuk kompiler itu. Jika Microsoft memutuskan untuk menerapkan fitur pointer-sanitization mereka sebelum CWG # 1438, itu tidak akan membuat fitur itu lebih atau kurang dapat diandalkan, dan khususnya itu tidak benar bahwa "apa pun bisa terjadi" jika fitur itu dihidupkan , terlepas dari apa yang dikatakan standar.
Kyle Strand
@KyleStrand: Saya pada dasarnya memberi definisi tentang UB ( blog.regehr.org/archives/213 ).
giorgim
1
Bagi sebagian besar komunitas C ++ di SO, "apa pun bisa terjadi" dianggap sepenuhnya terlalu harfiah . Saya pikir ini konyol . Saya memahami definisi UB, tetapi saya juga mengerti bahwa kompiler hanyalah bagian dari perangkat lunak yang diimplementasikan oleh orang sungguhan, dan jika orang-orang tersebut mengimplementasikan kompiler sehingga berperilaku dengan cara tertentu, begitulah cara kompiler akan berperilaku , terlepas dari apa yang dikatakan standar. .
Kyle Strand
1

Saya percaya, Anda menjalankan semacam mode debug dan VS sedang berusaha untuk menunjuk kembali pointer Anda ke beberapa lokasi yang diketahui, sehingga upaya lebih lanjut untuk dereferensi itu dapat dilacak dan dilaporkan. Coba kompilasi / jalankan program yang sama dalam mode rilis.

Pointer biasanya tidak diubah di deletedalam demi efisiensi dan untuk menghindari memberikan gagasan keselamatan yang keliru. Mengatur hapus pointer ke nilai yang telah ditentukan sebelumnya tidak akan ada gunanya di sebagian besar skenario kompleks, karena pointer yang dihapus kemungkinan hanya satu dari beberapa yang menunjuk ke lokasi ini.

Faktanya, semakin saya memikirkannya, semakin saya menemukan bahwa VS salah ketika melakukannya, seperti biasa. Bagaimana jika pointernya adalah const? Apakah masih akan mengubahnya?

SergeyA
sumber
Yup, bahkan pointer konstan diarahkan ke 8123 yang misterius ini!
tjwrona1992
Ada lagi batu ke VS :) Pagi ini seseorang bertanya mengapa mereka harus menggunakan g ++ bukan VS. Ini dia.
SergeyA
7
@SergeyA tetapi dari sisi lain dereffing bahwa pointer yang dihapus akan menunjukkan kepada Anda dengan segfault bahwa Anda mencoba melakukan deref pointer yang dihapus dan itu tidak akan sama dengan NULL. Dalam kasus lain, itu hanya akan macet jika halaman juga dibebaskan (yang sangat tidak mungkin). Gagal lebih cepat; selesaikan lebih cepat.
ratchet freak
1
@ratchetfreak "Gagal cepat, selesaikan lebih cepat" adalah mantra yang sangat berharga, tetapi "Gagal cepat dengan menghancurkan bukti forensik kunci" tidak memulai mantra yang begitu berharga. Dalam kasus sederhana, mungkin nyaman, tetapi dalam kasus yang lebih rumit (yang kita cenderung paling membutuhkan bantuan), menghapus informasi berharga mengurangi alat saya yang tersedia untuk menyelesaikan masalah.
Cort Ammon
2
@ tjwrona1992: Microsoft melakukan hal yang benar di sini, menurut saya. Membersihkan satu pointer lebih baik daripada tidak melakukan sama sekali. Dan jika ini menyebabkan Anda masalah dalam debugging, letakkan break point sebelum panggilan delete yang buruk. Kemungkinannya adalah bahwa tanpa sesuatu seperti ini Anda tidak akan pernah menemukan masalah. Dan jika Anda memiliki solusi yang lebih baik untuk menemukan bug ini, gunakan itu dan mengapa Anda peduli dengan apa yang Microsoft lakukan?
Zan Lynx
0

Setelah menghapus pointer, memori yang ditunjukkannya mungkin masih valid. Untuk memanifestasikan kesalahan ini, nilai pointer diatur ke nilai yang jelas. Ini sangat membantu proses debugging. Jika nilainya diatur NULL, itu mungkin tidak pernah muncul sebagai bug potensial dalam aliran program. Jadi itu mungkin menyembunyikan bug saat Anda menguji nanti NULL.

Poin lain adalah, bahwa beberapa pengoptimal waktu berjalan dapat memeriksa nilai itu dan mengubah hasilnya.

Pada waktu sebelumnya MS mengatur nilainya 0xcfffffff.

Karsten
sumber