Mohon maaf sebelumnya atas kenaifan pertanyaan ini. Saya seorang seniman berusia 50 tahun yang mencoba memahami komputer dengan benar untuk pertama kalinya. Jadi begini.
Saya telah mencoba memahami bagaimana tipe data dan variabel ditangani oleh kompiler (dalam arti yang sangat umum, saya tahu ada banyak hal). Saya kehilangan sesuatu dalam pemahaman saya tentang hubungan antara penyimpanan dalam "tumpukan" dan tipe nilai, dan penyimpanan pada "tumpukan" dan jenis referensi (tanda kutip dimaksudkan untuk menandakan bahwa saya memahami bahwa istilah ini adalah abstraksi dan bukan untuk dianggap terlalu harfiah dalam konteks yang disederhanakan seperti cara saya membingkai pertanyaan ini). Bagaimanapun, ide simplistis saya adalah bahwa tipe-tipe seperti Boolean dan integer menggunakan "stack" karena mereka bisa, karena mereka dikenal sebagai entitas dalam hal ruang penyimpanan, dan cakupannya mudah dikontrol.
Tapi apa yang saya tidak dapatkan adalah bagaimana variabel pada stack kemudian dibaca oleh aplikasi - jika saya menyatakan dan menetapkan x
sebagai integer, katakanlah x = 3
, dan penyimpanan dicadangkan pada stack dan kemudian nilainya 3
disimpan di sana, dan kemudian di fungsi yang sama saya nyatakan dan tetapkan y
sebagai, katakanlah 4
, dan kemudian mengikuti yang kemudian saya gunakan x
dalam ungkapan lain, (katakanlah z = 5 + x
) bagaimana program bisa membaca x
untuk mengevaluasi z
ketika itu di bawahy
di tumpukan? Saya jelas kehilangan sesuatu. Apakah itu lokasi pada stack hanya tentang masa hidup / lingkup variabel, dan bahwa seluruh stack sebenarnya dapat diakses oleh program sepanjang waktu? Jika demikian, apakah itu menyiratkan ada beberapa indeks lain yang hanya menyimpan alamat variabel di stack untuk memungkinkan nilai yang akan diambil? Tapi kemudian saya pikir seluruh poin stack adalah bahwa nilai disimpan di tempat yang sama dengan alamat variabel? Dalam pikiranku yang lemah, sepertinya jika ada indeks lain ini, maka kita berbicara tentang sesuatu yang lebih seperti tumpukan? Saya jelas sangat bingung, dan saya hanya berharap ada jawaban sederhana untuk pertanyaan sederhana saya.
Terima kasih sudah membaca sejauh ini.
sumber
Jawaban:
Menyimpan variabel lokal pada stack adalah detail implementasi - pada dasarnya optimasi. Anda bisa memikirkannya dengan cara ini. Saat memasukkan fungsi, ruang untuk semua variabel lokal dialokasikan di suatu tempat. Anda kemudian dapat mengakses semua variabel, karena Anda tahu lokasi mereka entah bagaimana (ini adalah bagian dari proses alokasi). Saat meninggalkan fungsi, ruang tersebut tidak dapat dialokasikan (dibebaskan).
Tumpukan adalah salah satu cara untuk mengimplementasikan proses ini - Anda dapat menganggapnya sebagai semacam "tumpukan cepat" yang memiliki ukuran terbatas sehingga hanya cocok untuk variabel kecil. Sebagai optimasi tambahan, semua variabel lokal disimpan dalam satu blok. Karena setiap variabel lokal memiliki ukuran yang diketahui, Anda tahu offset dari masing-masing variabel di blok, dan itulah cara Anda mengaksesnya. Ini berbeda dengan variabel yang dialokasikan pada heap, yang alamatnya sendiri disimpan dalam variabel lain.
Akhirnya, izinkan saya menyebutkan bahwa dalam praktiknya, beberapa variabel lokal disimpan dalam register. Ini karena akses ke register lebih cepat daripada akses ke stack. Ini adalah cara lain untuk mengimplementasikan ruang untuk variabel lokal. Sekali lagi, kita tahu persis di mana variabel disimpan (kali ini bukan melalui offset, tetapi melalui nama register), dan jenis penyimpanan ini hanya sesuai untuk data kecil.
sumber
Memiliki
y
di tumpukan tidak secara fisik mencegahx
diakses, yang, seperti yang Anda tunjukkan, membuat tumpukan komputer berbeda dari tumpukan lainnya.Ketika sebuah program dikompilasi, posisi variabel dalam stack juga telah ditentukan sebelumnya (dalam konteks fungsi). Dalam contoh Anda, jika tumpukan berisi
x
dengany
"di atas" itu, maka program tahu di muka bahwax
akan 1 item di bawah bagian atas tumpukan sementara di dalam fungsi. Karena perangkat keras komputer dapat secara eksplisit meminta 1 item di bawah bagian atas tumpukan, komputer bisa mendapatkannyax
walaupuny
juga ada.Iya. Ketika Anda keluar dari suatu fungsi, penunjuk tumpukan bergerak kembali ke posisi sebelumnya, secara efektif menghapus
x
dany
, tetapi secara teknis mereka akan tetap ada sampai memori digunakan untuk sesuatu yang lain. Selain itu, jika fungsi Anda memanggil fungsi lain,x
dany
akan tetap ada dan dapat diakses dengan sengaja masuk terlalu jauh di tumpukan.sumber
Untuk memberikan contoh konkret tentang bagaimana kompiler mengelola tumpukan dan bagaimana nilai-nilai pada tumpukan diakses, kita dapat melihat penggambaran visual, ditambah kode yang dihasilkan oleh
GCC
dalam lingkungan Linux dengan i386 sebagai arsitektur target.1. Tumpuk frame
Seperti yang Anda ketahui, stack adalah lokasi di ruang alamat dari proses yang berjalan yang digunakan oleh fungsi , atau prosedur , dalam arti bahwa ruang dialokasikan pada stack untuk variabel yang dideklarasikan secara lokal, serta argumen yang dikirimkan ke fungsi ( ruang untuk variabel yang dideklarasikan di luar fungsi apa pun (yaitu variabel global) dialokasikan di wilayah yang berbeda dalam memori virtual). Ruang yang dialokasikan untuk semua data fungsi dirujuk ke bingkai tumpukan . Berikut ini adalah gambaran visual dari beberapa frame stack (dari Computer Systems: A Programmer's Perspective ):
2. Manajemen bingkai tumpukan dan lokasi variabel
Agar nilai yang ditulis ke tumpukan dalam bingkai tumpukan tertentu untuk dikelola oleh kompiler dan dibaca oleh program, harus ada beberapa metode untuk menghitung posisi nilai-nilai ini dan mengambil alamat memori mereka. Register dalam CPU disebut sebagai stack pointer dan bantuan pointer dasar dengan ini.
Pointer basis,
ebp
berdasarkan konvensi, berisi alamat memori bagian bawah, atau bagian bawah, dari tumpukan. Posisi semua nilai dalam bingkai tumpukan dapat dihitung menggunakan alamat pada penunjuk dasar sebagai referensi. Ini digambarkan dalam gambar di atas:%ebp + 4
adalah alamat memori yang disimpan dalam pointer dasar plus 4, misalnya.3. Kode yang dihasilkan kompiler
Mari kita gunakan contoh program sederhana yang ditulis dalam C untuk melihat bagaimana ini bekerja:
Mari kita periksa teks rakitan yang diproduksi oleh GCC untuk teks sumber C ini (saya membersihkannya sedikit demi kejelasan):
Apa yang kami amati adalah bahwa variabel x, y dan z terletak di alamat
%ebp - 12
,%ebp -8
dan%ebp - 4
, masing-masing. Dengan kata lain, lokasi variabel dalam bingkai tumpukanmain()
dihitung menggunakan alamat memori yang disimpan dalam register CPU%ebp
.4. Data dalam memori di luar penunjuk tumpukan berada di luar ruang lingkup
Tumpukan adalah wilayah dalam memori virtual, yang penggunaannya dikelola oleh kompiler. Compiler menghasilkan kode sedemikian rupa sehingga nilai-nilai di luar penunjuk tumpukan (nilai-nilai di atas tumpukan) tidak pernah direferensikan. Ketika suatu fungsi dipanggil, posisi penunjuk tumpukan berubah untuk menciptakan ruang pada tumpukan yang dianggap tidak "di luar batas", jadi bisa dikatakan.
Saat fungsi dipanggil dan kembali, penunjuk tumpukan dikurangi dan bertambah. Data yang ditulis ke tumpukan tidak hilang setelah di luar ruang lingkup, tetapi kompiler tidak menghasilkan instruksi yang merujuk data ini karena tidak ada cara bagi kompiler untuk menghitung alamat data ini menggunakan
%ebp
atau%esp
.5. Ringkasan
Kode yang dapat dieksekusi langsung oleh CPU dihasilkan oleh kompiler. Kompiler mengelola tumpukan, susunan bingkai untuk fungsi dan register CPU. Salah satu strategi yang digunakan oleh GCC untuk melacak lokasi variabel dalam bingkai tumpukan dalam kode yang dimaksudkan untuk dieksekusi pada arsitektur i386 adalah dengan menggunakan alamat memori dalam penunjuk basis bingkai tumpukan
%ebp
,, sebagai referensi dan menulis nilai variabel ke lokasi dalam bingkai tumpukan di offset ke alamat di%ebp
.sumber
Ada dua register khusus: ESP (penunjuk tumpukan) dan EBP (penunjuk dasar). Ketika prosedur dipanggil dua operasi pertama biasanya
Operasi pertama menyimpan nilai EBP pada stack, dan operasi kedua memuat nilai pointer stack ke dalam pointer basis (untuk mengakses variabel lokal). Jadi, EBP menunjuk ke lokasi yang sama dengan ESP.
Assembler menerjemahkan nama variabel menjadi offset EBP. Misalnya, jika Anda memiliki dua variabel lokal
x,y
, dan Anda memiliki sesuatu sepertimaka itu dapat diterjemahkan menjadi sesuatu seperti
Nilai offset 6 dan 14 dihitung pada waktu kompilasi.
Ini kira-kira cara kerjanya. Lihat buku kompiler untuk detailnya.
sumber
Anda bingung karena variabel lokal yang disimpan dalam stack tidak diakses dengan aturan akses stack: First In Last Out, atau hanya FILO .
Masalahnya adalah bahwa aturan FILO berlaku untuk urutan panggilan fungsi dan bingkai stack , daripada variabel lokal.
Apa itu bingkai tumpukan?
Ketika Anda memasukkan suatu fungsi, itu diberikan sejumlah memori pada stack, yang disebut stack frame. Variabel lokal dari fungsi disimpan dalam bingkai tumpukan. Anda dapat membayangkan bahwa ukuran bingkai tumpukan bervariasi dari satu fungsi ke fungsi lain karena setiap fungsi memiliki jumlah dan ukuran variabel lokal yang berbeda.
Bagaimana variabel lokal disimpan dalam bingkai tumpukan tidak ada hubungannya dengan FILO. (Bahkan urutan penampilan variabel lokal Anda dalam kode sumber Anda tidak memastikan bahwa variabel lokal akan disimpan dalam urutan itu.) Ketika Anda menyimpulkan dengan benar dalam pertanyaan Anda, "ada beberapa indeks lain yang hanya menyimpan alamat variabel-variabel tersebut. di tumpukan untuk memungkinkan nilai yang akan diambil ". Alamat variabel lokal biasanya dihitung dengan alamat basis , seperti alamat batas frame stack, dan nilai offset khusus untuk setiap variabel lokal.
Jadi kapan perilaku FILO ini muncul?
Sekarang, apa yang terjadi jika Anda memanggil fungsi lain? Fungsi callee harus memiliki bingkai tumpukan sendiri, dan bingkai tumpukan inilah yang didorong dalam tumpukan . Yaitu, bingkai tumpukan fungsi callee ditempatkan di atas bingkai tumpukan fungsi pemanggil. Dan jika fungsi callee ini memanggil fungsi lain, maka frame stack-nya akan didorong, sekali lagi, di atas stack.
Apa yang terjadi jika suatu fungsi kembali? Ketika fungsi callee kembali ke fungsi pemanggil, stack frame fungsi callee ini adalah muncul keluar dari tumpukan, membebaskan ruang untuk penggunaan masa depan.
Jadi dari pertanyaan Anda:
Anda cukup benar di sini karena nilai variabel lokal pada bingkai tumpukan tidak benar-benar dihapus ketika fungsi kembali. Nilai hanya tetap di sana, meskipun lokasi memori di mana ia disimpan bukan milik bingkai tumpukan fungsi apa pun. Nilai dihapus ketika beberapa fungsi lain memperoleh bingkai tumpukan yang mencakup lokasi dan menulis lebih dari beberapa nilai lainnya ke lokasi memori itu.
Lalu apa yang membedakan tumpukan dari tumpukan?
Stack dan heap sama dalam arti bahwa keduanya adalah nama yang merujuk pada ruang pada memori. Karena kami dapat mengakses lokasi mana saja di memori dengan alamatnya, Anda dapat mengakses lokasi mana pun di tumpukan atau tumpukan.
Perbedaannya datang dari janji yang dibuat oleh sistem komputer tentang bagaimana ia akan menggunakannya. Seperti yang Anda katakan, tumpukan adalah untuk tipe referensi. Karena nilai dalam tumpukan tidak ada hubungannya dengan bingkai tumpukan tertentu, ruang lingkup nilai tidak terikat ke fungsi apa pun. Variabel lokal, bagaimanapun, dicakup dalam suatu fungsi, dan meskipun Anda dapat mengakses nilai variabel lokal apa pun yang terletak di luar kerangka tumpukan fungsi saat ini, sistem akan mencoba memastikan bahwa perilaku semacam ini tidak terjadi, dengan menggunakan tumpukan frame. Ini memberi kita semacam ilusi bahwa variabel lokal dibatasi untuk fungsi tertentu.
sumber
Ada banyak cara untuk mengimplementasikan variabel lokal dengan sistem runtime bahasa. Menggunakan tumpukan adalah solusi efisien yang umum, digunakan dalam banyak kasus praktis.
Secara intuitif, stack pointer
sp
disimpan di sekitar saat runtime (dalam alamat tetap, atau dalam register - itu benar-benar penting). Asumsikan bahwa setiap "push" menambah penunjuk tumpukan.Pada waktu kompilasi, kompiler menentukan alamat masing-masing variabel sebagai
sp - K
manaK
konstanta yang hanya bergantung pada ruang lingkup variabel (karenanya dapat dihitung pada waktu kompilasi).Perhatikan bahwa kita menggunakan kata "tumpukan" di sini dalam arti longgar. Tumpukan ini tidak hanya diakses melalui operasi push / pop / top, tetapi juga diakses menggunakan
sp - K
.Sebagai contoh, pertimbangkan kodesemu ini:
Ketika prosedur dipanggil, argumen
x,y
dapat dikirimkan pada stack. Untuk kesederhanaan, anggap konvensi adalah penelepon mendorongx
pertama, laluy
.Kemudian, kompiler pada titik (1) dapat ditemukan
x
disp - 2
dany
disp - 1
.Pada titik (2), variabel baru dimasukkan dalam ruang lingkup. Kompiler menghasilkan kode yang menjumlahkan
x+y
, yaitu apa yang ditunjukkan olehsp - 2
dansp - 1
, dan mendorong hasil dari jumlah pada tumpukan.Pada titik (3),
z
dicetak. Compiler tahu itu adalah variabel terakhir dalam lingkup, jadi itu ditunjukkan olehsp - 1
. Ini tidak lagiy
, karenasp
diubah. Namun, untuk mencetaky
kompiler tahu bahwa ia dapat menemukannya, dalam lingkup ini, disp - 2
. Demikian pula,x
sekarang ditemukan disp - 3
.Pada titik (4), kita keluar dari cakupan.
z
muncul, dany
sekali lagi ditemukan di alamatsp - 1
, danx
disp - 2
.Ketika kami kembali, salah satu
f
atau pemanggil munculx,y
dari tumpukan.Jadi, menghitung
K
untuk kompiler adalah soal menghitung berapa banyak variabel dalam lingkup, secara kasar. Di dunia nyata, ini sebenarnya lebih kompleks karena tidak semua variabel memiliki ukuran yang sama, sehingga perhitungannyaK
sedikit lebih kompleks. Terkadang, stack juga berisi alamat pengirim untukf
, jadiK
harus "lewati" itu juga. Tapi ini teknis.Perhatikan bahwa, dalam beberapa bahasa pemrograman, berbagai hal dapat menjadi lebih kompleks jika fitur yang lebih kompleks harus ditangani. Misalnya prosedur bersarang memerlukan analisis yang sangat hati-hati, karena
K
sekarang harus "melewatkan" banyak alamat pengirim, terutama jika prosedur bertumpuk bersifat rekursif. Fungsi closures / lambdas / anonim juga membutuhkan perhatian untuk menangani variabel "yang ditangkap". Namun, contoh di atas harus menggambarkan ide dasar.sumber
Ide termudah adalah memikirkan variabel sebagai memperbaiki nama untuk alamat dalam memori. Memang, beberapa assembler menampilkan kode mesin seperti itu ("simpan nilai 5 di alamat
i
", di manai
ada nama variabel).Beberapa alamat ini "absolut", seperti variabel global, beberapa "relatif", seperti variabel lokal. Variabel (yaitu alamat) dalam fungsi relatif ke beberapa tempat pada "tumpukan" yang berbeda untuk setiap pemanggilan fungsi; dengan cara itu nama yang sama dapat merujuk ke objek aktual yang berbeda, dan panggilan melingkar ke fungsi yang sama adalah doa independen yang bekerja pada memori independen.
sumber
Item data yang dapat disimpan di tumpukan diletakkan di tumpukan - Ya! Ini adalah ruang premium. Juga, Setelah kami mendorong
x
ke tumpukan dan kemudian mendorongy
ke tumpukan, idealnya kita tidak dapat mengaksesx
sampaiy
ada. Kita perlu popy
untuk mengaksesx
. Anda mendapatkannya dengan benar.Tumpukan bukan dari variabel, tetapi dari
frames
Di mana Anda salah, adalah tentang tumpukan itu sendiri. Di tumpukan itu bukan item data yang didorong langsung. Sebaliknya, pada stack sesuatu yang disebut
stack-frame
didorong. Tumpukan-bingkai ini berisi item data. Meskipun Anda tidak dapat mengakses frame jauh di dalam stack, Anda dapat mengakses frame atas dan semua item data yang terkandung di dalamnya.Katakanlah kita memiliki item data kita dibundel dalam dua frame stack
frame-x
danframe-y
. Kami mendorong mereka satu demi satu. Sekarang selamaframe-y
berada di atasframe-x
, Anda tidak dapat secara ideal mengakses item data apa pun di dalamnyaframe-x
. Hanyaframe-y
terlihat. TETAPI yangframe-y
terlihat, Anda dapat mengakses semua data-item yang dibundel di dalamnya. Seluruh frame terlihat memperlihatkan semua item data yang terkandung di dalamnya.Akhir dari jawaban. Lebih banyak (kata-kata kasar) pada bingkai ini
Selama kompilasi, daftar semua fungsi dalam program dibuat. Kemudian untuk setiap fungsi dibuat daftar item data yang dapat ditumpuk . Kemudian untuk setiap fungsi
stack-frame-template
dibuat. Template ini adalah struktur data yang berisi semua variabel yang dipilih, ruang untuk input data fungsi, data output dll. Sekarang selama runtime, setiap kali suatu fungsi dipanggil, salinannyatemplate
diletakkan di stack - bersama dengan semua input dan variabel perantara . Ketika fungsi ini memanggil beberapa fungsi lain, maka salinan dari yang fungsi inistack-frame
disimpan di stack. Sekarang selama itu fungsi berjalan, ini fungsi ini item data yang diawetkan. Setelah itu fungsi berakhir, tumpukan-frame-nya muncul keluar. Sekarangini tumpukan-frame aktif dan fungsi ini dapat mengakses semua variabel nya.Perhatikan bahwa struktur dan komposisi bingkai tumpukan bervariasi dari bahasa pemrograman ke bahasa pemrograman. Bahkan dalam suatu bahasa mungkin ada perbedaan yang halus dalam implementasi yang berbeda.
Terima kasih telah mempertimbangkan CS. Saya seorang programmer sekarang hari mengambil pelajaran piano :)
sumber