Pertanyaan Dapatkah memori variabel lokal diakses di luar ruang lingkupnya?


Saya memiliki kode berikut.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

Dan kode ini hanya berjalan tanpa pengecualian runtime!

Hasilnya adalah 58

Bagaimana bisa? Bukankah memori variabel lokal tidak dapat diakses di luar fungsinya?


872
2018-06-22 20:01


asal


Jawaban:


Bagaimana bisa? Bukankah memori variabel lokal tidak dapat diakses di luar fungsinya?

Anda menyewa kamar hotel. Anda meletakkan buku di laci atas meja di samping tempat tidur dan pergi tidur. Anda memeriksa keesokan paginya, tetapi "lupa" untuk mengembalikan kunci Anda. Anda mencuri kunci!

Seminggu kemudian, Anda kembali ke hotel, tidak check in, menyelinap masuk ke kamar lama Anda dengan kunci curian Anda, dan lihat di laci. Buku Anda masih ada di sana. Mengherankan!

Bagaimana itu bisa terjadi? Bukankah isi laci kamar hotel tidak dapat diakses jika Anda belum menyewa kamar?

Yah, jelas skenario itu bisa terjadi di dunia nyata tidak masalah. Tidak ada kekuatan misterius yang menyebabkan buku Anda hilang ketika Anda tidak lagi berwenang untuk berada di ruangan. Juga tidak ada kekuatan misterius yang menghalangi Anda memasuki ruangan dengan kunci curian.

Manajemen hotel tidak wajib untuk menghapus buku Anda. Anda tidak membuat kontrak dengan mereka yang mengatakan bahwa jika Anda meninggalkan barang di belakang, mereka akan mencincangnya untuk Anda. Jika Anda secara ilegal masuk kembali ke kamar Anda dengan kunci curian untuk mendapatkannya kembali, staf keamanan hotel tidak wajib untuk menangkap Anda menyelinap masuk. Anda tidak membuat kontrak dengan mereka yang mengatakan "jika saya mencoba menyelinap kembali ke kamar saya nanti, Anda harus menghentikan saya." Sebaliknya, Anda menandatangani kontrak dengan mereka yang mengatakan "Saya berjanji untuk tidak menyelinap kembali ke kamar saya nanti", sebuah kontrak yang kamu putus.

Dalam situasi ini segalanya bisa terjadi. Buku itu bisa ada di sana - Anda beruntung. Buku orang lain bisa ada di sana dan Anda bisa berada di dapur hotel. Seseorang bisa berada di sana tepat ketika Anda masuk, merobek buku Anda menjadi potongan-potongan. Hotel dapat menghapus meja dan buku sepenuhnya dan menggantinya dengan lemari. Seluruh hotel bisa saja dirobohkan dan diganti dengan stadion sepak bola, dan Anda akan mati dalam ledakan saat Anda menyelinap.

Anda tidak tahu apa yang akan terjadi; ketika Anda keluar dari hotel dan mencuri kunci untuk digunakan secara ilegal nanti, Anda menyerahkan hak untuk hidup di dunia yang dapat diprediksi dan aman karena kamu memilih untuk melanggar aturan sistem.

C ++ bukan bahasa yang aman. Dengan senang hati Anda akan melanggar aturan sistem. Jika Anda mencoba melakukan sesuatu yang ilegal dan bodoh seperti kembali ke ruangan yang tidak diizinkan masuk dan mengobrak-abrik meja yang mungkin tidak ada lagi, C ++ tidak akan menghentikan Anda. Bahasa yang lebih aman daripada C + + memecahkan masalah ini dengan membatasi kekuatan Anda - dengan memiliki kontrol yang lebih ketat terhadap kunci, misalnya.

MEMPERBARUI

Astaga, jawaban ini mendapat banyak perhatian. (Saya tidak yakin mengapa - saya menganggapnya sebagai analogi "menyenangkan", tapi apa pun.)

Saya pikir mungkin erat untuk memperbarui ini sedikit dengan beberapa pemikiran teknis.

Compiler berada dalam bisnis menghasilkan kode yang mengelola penyimpanan data yang dimanipulasi oleh program itu. Ada banyak cara yang berbeda untuk menghasilkan kode untuk mengelola memori, tetapi seiring waktu, dua teknik dasar telah menjadi mengakar.

Yang pertama adalah memiliki semacam tempat penyimpanan "lama tinggal" di mana "seumur hidup" setiap byte dalam penyimpanan - yaitu, periode waktu ketika itu secara sah terkait dengan beberapa variabel program - tidak dapat dengan mudah diprediksi sebelumnya. waktu. Kompilator menghasilkan panggilan ke "heap manager" yang tahu bagaimana mengalokasikan penyimpanan secara dinamis ketika dibutuhkan dan mengambilnya kembali ketika tidak diperlukan lagi.

Yang kedua adalah memiliki semacam tempat penyimpanan "berumur pendek" di mana masa hidup setiap byte dalam penyimpanan sudah dikenal, dan, khususnya, masa hidup penyimpanan mengikuti pola "bersarang". Yaitu, alokasi dari variabel-variabel berumur pendek yang paling lama tumpang tindih dengan tumpang tindih alokasi variabel berumur pendek yang datang setelahnya.

Variabel lokal mengikuti pola terakhir; ketika sebuah metode dimasukkan, variabel lokalnya menjadi hidup. Ketika metode itu memanggil metode lain, variabel lokal metode baru menjadi hidup. Mereka akan mati sebelum variabel lokal metode pertama mati. Urutan relatif dari awal dan akhir masa penyimpanan penyimpanan yang terkait dengan variabel lokal dapat dikerjakan sebelumnya.

Untuk alasan ini, variabel lokal biasanya dihasilkan sebagai penyimpanan pada struktur data "tumpukan", karena tumpukan memiliki properti yang pertama kali mendorongnya akan menjadi hal terakhir yang muncul.

Ini seperti hotel memutuskan untuk hanya menyewakan kamar secara berurutan, dan Anda tidak dapat memeriksa sampai semua orang dengan nomor kamar lebih tinggi dari yang Anda check out.

Jadi mari kita pikirkan tentang tumpukan. Dalam banyak sistem operasi Anda mendapatkan satu tumpukan per utas dan tumpukan dialokasikan untuk ukuran tetap tertentu. Ketika Anda memanggil metode, hal-hal didorong ke stack. Jika Anda kemudian meneruskan pointer ke stack kembali dari metode Anda, seperti yang dilakukan poster asli di sini, itu hanya penunjuk ke tengah beberapa blok memori juta-byte yang benar-benar valid. Dalam analogi kami, Anda check out dari hotel; ketika Anda melakukannya, Anda baru saja keluar dari ruang yang ditempati dengan jumlah tertinggi. Jika tidak ada orang lain yang memeriksa setelah Anda, dan Anda kembali ke kamar Anda secara ilegal, semua barang Anda dijamin masih ada di sana di hotel khusus ini.

Kami menggunakan tumpukan untuk toko sementara karena mereka benar-benar murah dan mudah. Implementasi C ++ tidak diperlukan untuk menggunakan setumpuk untuk penyimpanan penduduk setempat; itu bisa menggunakan heap. Tidak, karena itu akan membuat program lebih lambat.

Implementasi C ++ tidak diperlukan untuk meninggalkan sampah yang tersisa di tumpukan tersentuh sehingga Anda dapat kembali untuk itu nanti secara ilegal; itu sangat legal bagi compiler untuk menghasilkan kode yang kembali ke nol semuanya di "ruang" yang baru saja Anda kosongkan. Itu bukan karena lagi, itu akan mahal.

Implementasi C ++ tidak diperlukan untuk memastikan bahwa ketika stack secara logis menyusut, alamat yang digunakan untuk menjadi valid masih dipetakan ke dalam memori. Implementasi diperbolehkan untuk memberitahu sistem operasi "kita sudah selesai menggunakan tumpukan halaman ini sekarang. Sampai saya mengatakan sebaliknya, mengeluarkan pengecualian yang menghancurkan proses jika ada yang menyentuh halaman tumpukan yang sebelumnya-valid". Sekali lagi, implementasi tidak benar-benar melakukan itu karena lambat dan tidak perlu.

Sebaliknya, implementasi memungkinkan Anda membuat kesalahan dan lolos begitu saja. Sebagian besar waktu. Sampai suatu hari sesuatu yang benar-benar mengerikan menjadi salah dan prosesnya meledak.

Ini bermasalah. Ada banyak aturan dan sangat mudah untuk mematahkannya secara tidak sengaja. Saya tentu sudah berkali-kali. Dan yang lebih buruk, masalah sering kali hanya permukaan ketika ingatan terdeteksi menjadi miliaran nanodetik korup setelah korupsi terjadi, ketika sangat sulit untuk mengetahui siapa yang mengacaukannya.

Bahasa yang lebih aman dari memori memecahkan masalah ini dengan membatasi kekuatan Anda. Dalam "normal" C # tidak ada cara untuk mengambil alamat lokal dan mengembalikannya atau menyimpannya untuk nanti. Anda dapat mengambil alamat lokal, tetapi bahasanya dirancang dengan cerdas sehingga tidak mungkin menggunakannya setelah masa hidup lokal berakhir. Untuk mengambil alamat lokal dan mengembalikannya, Anda harus memasukkan kompiler dalam mode "tidak aman" khusus, dan masukkan kata "tidak aman" dalam program Anda, untuk menarik perhatian pada fakta bahwa Anda mungkin melakukan sesuatu yang berbahaya yang dapat melanggar aturan.

Untuk bacaan lebih lanjut:


4577
2018-06-23 05:43



Apa yang Anda lakukan di sini hanyalah membaca dan menulis ke memori itu biasanya menjadi alamat a. Sekarang kamu di luar foo, itu hanya penunjuk ke beberapa area memori acak. Kebetulan bahwa dalam contoh Anda, bahwa area memori tidak ada dan tidak ada yang lain menggunakannya saat ini. Anda tidak merusak apa pun dengan terus menggunakannya, dan belum ada yang ditimpa. Oleh karena itu, 5 masih ada di sana. Dalam program nyata, memori itu akan segera digunakan kembali dan Anda akan memecahkan sesuatu dengan melakukan ini (meskipun gejalanya mungkin tidak muncul sampai jauh di kemudian hari!)

Ketika Anda kembali dari foo, Anda memberi tahu OS bahwa Anda tidak lagi menggunakan memori itu dan itu dapat dipindahkan ke sesuatu yang lain. Jika Anda beruntung dan tidak pernah dipindahtugaskan, dan OS tidak menangkap Anda menggunakannya lagi, maka Anda akan lolos dengan kebohongan. Kemungkinannya adalah Anda akhirnya akan menulis lebih dari apa pun yang berakhir dengan alamat itu.

Sekarang jika Anda bertanya-tanya mengapa compiler tidak mengeluh, itu mungkin karena foo dihilangkan dengan optimasi. Biasanya akan memperingatkan Anda tentang hal semacam ini. C mengasumsikan Anda tahu apa yang Anda lakukan, dan secara teknis Anda belum melanggar ruang lingkup di sini (tidak ada referensi untuk itu a sendiri di luar foo), hanya aturan akses memori, yang hanya memicu peringatan dan bukan kesalahan.

Singkatnya: ini biasanya tidak akan berhasil, tetapi terkadang secara kebetulan.


260
2018-05-19 02:33



Karena ruang penyimpanan tidak diinjak dulu. Jangan mengandalkan perilaku itu.


134
2018-06-22 14:15



Sedikit tambahan untuk semua jawaban:

jika Anda melakukan sesuatu seperti itu:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

output mungkin akan: 7

Itu karena setelah kembali dari foo () stack dibebaskan dan kemudian digunakan kembali oleh boo (). Jika Anda merakit eksekutor, Anda akan melihatnya dengan jelas.


71
2018-06-22 14:15



Di C ++, Anda bisa mengakses alamat apa pun, tetapi itu tidak berarti Anda harus. Alamat yang Anda akses tidak lagi valid. Saya t bekerja karena tidak ada lagi yang mengacak memori setelah foo kembali, tetapi bisa crash dalam banyak keadaan. Coba analisis program Anda dengan Valgrind, atau bahkan hanya mengumpulkannya dioptimalkan, dan lihat ...


62
2018-06-22 14:12



Anda tidak pernah membuang pengecualian C + + dengan mengakses memori yang tidak valid. Anda hanya memberikan contoh ide umum referensi lokasi memori yang sewenang-wenang. Saya bisa melakukan hal yang sama seperti ini:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Di sini saya hanya memperlakukan 123456 sebagai alamat ganda dan menuliskannya. Sejumlah hal bisa terjadi:

  1. q sebenarnya mungkin benar-benar menjadi alamat yang valid dari dua kali lipat, mis. double p; q = &p;.
  2. q mungkin menunjuk ke suatu tempat di dalam memori yang dialokasikan dan saya hanya menimpa 8 byte di sana.
  3. q menunjukkan di luar memori yang dialokasikan dan manajer memori sistem operasi mengirimkan sinyal kesalahan segmentasi ke program saya, menyebabkan runtime untuk menghentikannya.
  4. Anda memenangkan lotere.

Cara Anda mengaturnya adalah sedikit lebih masuk akal bahwa alamat yang dikembalikan menunjuk ke area memori yang valid, karena mungkin hanya akan sedikit lebih jauh ke bawah tumpukan, tetapi masih merupakan lokasi yang tidak valid yang tidak dapat Anda akses dalam mode deterministik.

Tidak ada yang akan secara otomatis memeriksa validitas semantik alamat memori seperti itu untuk Anda selama eksekusi program normal. Namun, debugger memori seperti valgrind Dengan senang hati akan melakukan ini, jadi Anda harus menjalankan program Anda melalui itu dan menyaksikan kesalahan.


58
2018-06-23 04:45



Apakah Anda mengkompilasi program Anda dengan pengoptimal diaktifkan?

Fungsi foo () cukup sederhana dan mungkin telah digarisbawahi / diganti dalam kode yang dihasilkan.

Tetapi saya setuju dengan Mark B bahwa perilaku yang dihasilkan tidak terdefinisi.


27
2018-05-19 02:33



Masalahmu tidak ada hubungannya dengan itu cakupan. Dalam kode yang Anda tunjukkan, fungsinya main tidak melihat nama-nama dalam fungsi foo, jadi Anda tidak dapat mengakses a di foo langsung dengan ini nama di luar foo.

Masalah yang Anda alami adalah mengapa program tidak menandakan kesalahan saat merujuk memori ilegal. Ini karena standar C ++ tidak menentukan batas yang sangat jelas antara memori ilegal dan memori hukum. Mereferensikan sesuatu dalam tumpukan tumpukan terkadang menyebabkan kesalahan dan terkadang tidak. Tergantung. Jangan mengandalkan perilaku ini. Asumsikan itu akan selalu menghasilkan kesalahan ketika Anda memprogram, tetapi menganggap itu tidak akan pernah menandakan kesalahan ketika Anda debug.


20
2018-06-24 21:57



Anda baru saja mengembalikan alamat memori, itu diizinkan tetapi mungkin kesalahan.

Ya jika Anda mencoba untuk memperhatikan bahwa alamat memori Anda akan memiliki perilaku tidak terdefinisi.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}

16
2018-06-24 22:04



Itu klasik perilaku tidak terdefinisi yang telah dibahas di sini bukan dua hari yang lalu - cari di sekitar situs untuk sedikit. Singkatnya, Anda beruntung, tetapi apa pun bisa terjadi dan kode Anda membuat akses tidak valid ke memori.


14
2018-06-23 15:31



Perilaku ini tidak terdefinisi, seperti yang ditunjukkan Alex - faktanya, kebanyakan kompiler akan memperingatkan untuk tidak melakukan ini, karena ini adalah cara mudah untuk mendapatkan crash.

Untuk contoh dari jenis perilaku menyeramkan Anda mungkin untuk mendapatkan, coba contoh ini:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Ini mencetak "y = 123", tetapi hasil Anda mungkin bervariasi (sangat!). Penunjuk Anda mengalahkan variabel lokal yang tidak terkait lainnya.


14
2018-06-24 21:57