Saya mengerti apa itu stack pointer - tapi untuk apa ini digunakan?


11

Pointer tumpukan menunjuk ke bagian atas tumpukan, yang menyimpan data berdasarkan apa yang kita sebut dasar "LIFO". Untuk mencuri analogi orang lain, itu seperti setumpuk piring tempat Anda meletakkan dan mengambil piring di atasnya. Penunjuk tumpukan, OTOH, menunjuk ke "piringan" atas tumpukan. Setidaknya, itu berlaku untuk x86.

Tetapi mengapa komputer / program "peduli" apa yang menunjuk ke penunjuk tumpukan? Dengan kata lain, tujuan apa yang memiliki penunjuk tumpukan dan mengetahui di mana ia menunjuk untuk melayani?

Penjelasan yang bisa dimengerti oleh programmer C akan dihargai.


Karena Anda tidak dapat melihat bagian atas tumpukan ram seperti Anda dapat melihat bagian atas tumpukan piring.
tkausl


8
Anda tidak mengambil piring dari bagian bawah tumpukan. Anda menambahkan satu di atas dan orang lain mengambilnya dari atas . Anda sedang memikirkan antrian di sini.
Kilian Foth

@Snowman Hasil edit Anda tampaknya mengubah arti pertanyaan. moonman239, dapatkah Anda memverifikasi apakah perubahan Snowman akurat, khususnya penambahan "Apa tujuan tumpukan ini sebenarnya berfungsi, bukan untuk menjelaskan strukturnya?"
8bittree

1
@ 8bittree Silakan lihat deskripsi edit: Saya menyalin pertanyaan sebagaimana dinyatakan dalam baris subjek ke dalam tubuh pertanyaan. Tentu saja, saya selalu terbuka terhadap kemungkinan bahwa saya mengubah sesuatu dan penulis asli selalu bebas untuk memutar kembali atau mengedit posting.

Jawaban:


23

Apa tujuan tumpukan ini sebenarnya berfungsi, bukan untuk menjelaskan strukturnya?

Anda memiliki banyak jawaban yang secara akurat menggambarkan struktur data yang disimpan di tumpukan, yang saya perhatikan adalah kebalikan dari pertanyaan yang Anda ajukan.

Tujuan stack berfungsi adalah: stack adalah bagian dari reifikasi kelanjutan dalam bahasa tanpa coroutine .

Mari kita buka itu.

Secara sederhana, jawaban atas pertanyaan "apa yang akan terjadi selanjutnya dalam program saya?" Pada setiap titik dalam setiap program, sesuatu akan terjadi selanjutnya. Dua operan akan dihitung, kemudian program melanjutkan dengan menghitung jumlah mereka, dan kemudian program melanjutkan dengan menetapkan jumlah ke suatu variabel, dan kemudian ... dan seterusnya.

Reifikasi hanyalah kata yang sulit untuk membuat implementasi konkret dari konsep abstrak. "Apa yang terjadi selanjutnya?" adalah konsep abstrak; cara tumpukan diletakkan adalah bagian dari bagaimana konsep abstrak diubah menjadi mesin nyata yang benar-benar menghitung sesuatu.

Coroutine adalah fungsi yang dapat mengingat di mana mereka berada, memberikan kontrol ke coroutine lain untuk sementara waktu, dan kemudian melanjutkan di mana mereka tinggalkan nanti, tetapi tidak harus segera setelah hasil coroutine yang baru saja disebut. Pikirkan "hasil pengembalian" atau "tunggu" di C #, yang harus mengingat di mana mereka berada ketika item berikutnya diminta atau operasi asinkron selesai. Bahasa dengan coroutine atau fitur bahasa yang serupa membutuhkan struktur data yang lebih maju daripada tumpukan untuk mengimplementasikan kelanjutan.

Bagaimana cara stack mengimplementasikan kelanjutan? Jawaban lain mengatakan bagaimana. Stack menyimpan (1) nilai variabel dan temporari yang masa pakainya diketahui tidak lebih besar dari aktivasi metode saat ini, dan (2) alamat kode lanjutan yang terkait dengan aktivasi metode terbaru. Dalam bahasa-bahasa dengan pengecualian penanganan stack juga dapat menyimpan informasi tentang "kelanjutan kesalahan" - yaitu, apa yang akan dilakukan program selanjutnya ketika situasi luar biasa terjadi.

Biarkan saya mengambil kesempatan ini untuk mencatat bahwa tumpukan tidak memberi tahu Anda "dari mana saya berasal?" - meskipun sering digunakan dalam debugging. Tumpukan memberitahu Anda di mana Anda akan pergi berikutnya , dan apa nilai-nilai variabel aktivasi akan ketika Anda sampai di sana . Fakta bahwa dalam bahasa tanpa coroutine, di mana Anda akan pergi berikutnya hampir selalu dari mana Anda berasal membuat jenis debugging ini lebih mudah. Tetapi tidak ada persyaratan bahwa penyusun menyimpan informasi tentang dari mana kontrol berasal jika ia dapat pergi tanpa melakukannya. Optimasi tail-call misalnya menghancurkan informasi tentang dari mana kontrol program berasal.

Mengapa kita menggunakan tumpukan untuk mengimplementasikan kelanjutan dalam bahasa tanpa coroutine? Karena karakteristik aktivasi metode sinkron adalah bahwa pola "menunda metode saat ini, aktifkan metode lain, lanjutkan metode saat ini dengan mengetahui hasil dari metode yang diaktifkan" ketika dikomposisikan dengan sendirinya secara logis membentuk setumpuk aktivasi. Membuat struktur data yang mengimplementasikan perilaku seperti tumpukan ini sangat murah dan mudah. Mengapa begitu murah dan mudah? Karena set chip telah selama beberapa dekade dirancang khusus untuk membuat pemrograman semacam ini mudah bagi penulis kompiler.


Perhatikan bahwa kutipan yang Anda referensikan secara keliru ditambahkan dalam edit oleh pengguna lain dan sejak itu telah diperbaiki, menjadikan jawaban ini tidak cukup menjawab pertanyaan.
8bittree

2
Saya cukup yakin penjelasan yang seharusnya meningkatkan kejelasan. Saya tidak sepenuhnya yakin bahwa "tumpukan adalah bagian dari reifikasi kelanjutan dalam bahasa tanpa coroutine" bahkan mendekati hal itu :-)

4

Sebagian besar penggunaan dasar stack adalah untuk menyimpan alamat pengirim untuk berbagai fungsi:

void a(){
    sub();
}
void b(){
    sub();
}
void sub() {
    //should i got back to a() or to b()?
}

dan dari sudut pandang C ini saja. Dari sudut pandang kompiler:

  • semua argumen fungsi dilewatkan oleh register CPU - jika tidak ada cukup register argumen akan diletakkan di tumpukan
  • setelah fungsi berakhir (sebagian besar) register harus memiliki nilai yang sama seperti sebelum memasukkannya - sehingga register yang digunakan didukung pada stack

Dan dari sudut pandang OS: program dapat diinterupsi kapan saja jadi setelah kita selesai dengan tugas sistem kita harus mengembalikan keadaan CPU, jadi mari kita simpan semuanya di stack

Semua ini berhasil karena kita tidak peduli berapa banyak item yang sudah ada di stack atau berapa banyak item yang akan ditambahkan orang lain di masa depan, kita hanya perlu tahu berapa banyak kita memindahkan penunjuk stack dan mengembalikannya setelah kita selesai.


1
Saya pikir lebih akurat untuk mengatakan bahwa argumen didorong pada stack, meskipun sering sebagai register optimasi digunakan pada prosesor yang memiliki register gratis yang cukup untuk tugas tersebut. Itu adalah nit, tapi saya pikir itu lebih cocok dengan bagaimana bahasa telah berevolusi secara historis. Kompiler C / C ++ paling awal tidak menggunakan register sama sekali untuk ini.
Gort the Robot

4

LIFO vs FIFO

LIFO adalah singkatan dari Last In, First Out. Seperti dalam, item terakhir yang dimasukkan ke dalam tumpukan adalah item pertama yang dikeluarkan dari tumpukan.

Apa yang Anda gambarkan dengan analogi hidangan Anda (dalam revisi pertama ), adalah antrian atau FIFO, First In, First Out.

Perbedaan utama antara keduanya, adalah bahwa LIFO / stack mendorong (menyisipkan) dan muncul (menghapus) dari ujung yang sama, dan FIFO / antrian melakukannya dari ujung yang berlawanan.

// Both:

Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]

// Stack            // Queue
Pop()               Pop()
-> [a, b]           -> [b, c]

Penunjuk tumpukan

Mari kita lihat apa yang terjadi di bawah kap tumpukan. Berikut adalah beberapa memori, setiap kotak adalah alamat:

...[ ][ ][ ][ ]...                       char* sp;
    ^- Stack Pointer (SP)

Dan ada penunjuk tumpukan yang menunjuk ke bagian bawah tumpukan yang saat ini kosong (apakah tumpukan tumbuh atau tumbuh tidak terlalu relevan di sini sehingga kita akan mengabaikan itu, tetapi tentu saja di dunia nyata, yang menentukan operasi mana yang ditambahkan , dan yang mengurangi dari SP).

Jadi mari kita dorong a, b, and clagi. Grafik di sebelah kiri, operasi "level tinggi" di tengah, kode semu C-ish di kanan:

...[a][ ][ ][ ]...        Push('a')      *sp = 'a';
    ^- SP
...[a][ ][ ][ ]...                       ++sp;
       ^- SP

...[a][b][ ][ ]...        Push('b')      *sp = 'b';
       ^- SP
...[a][b][ ][ ]...                       ++sp;
          ^- SP

...[a][b][c][ ]...        Push('c')      *sp = 'c';
          ^- SP
...[a][b][c][ ]...                       ++sp;
             ^- SP

Seperti yang dapat Anda lihat, setiap kali kita push, itu memasukkan argumen di lokasi penunjuk tumpukan saat ini menunjuk, dan menyesuaikan penunjuk tumpukan untuk menunjuk ke lokasi berikutnya.

Sekarang mari kita pop:

...[a][b][c][ ]...        Pop()          --sp;
          ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'c'
          ^- SP
...[a][b][c][ ]...        Pop()          --sp;
       ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'b'
       ^- SP

Popadalah kebalikan dari push, itu menyesuaikan penunjuk tumpukan untuk menunjuk pada lokasi sebelumnya dan menghapus item yang ada di sana (biasanya untuk mengembalikannya kepada siapa pun yang dipanggil pop).

Anda mungkin memperhatikan itu bdan cmasih dalam memori. Saya hanya ingin meyakinkan Anda bahwa itu bukan kesalahan ketik. Kami akan segera kembali ke situ.

Hidup tanpa stack pointer

Mari kita lihat apa yang terjadi jika kita tidak memiliki stack pointer. Mulai dengan mendorong lagi:

...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]...        Push(a)        ? = 'a';

Eh, hmm ... jika kita tidak memiliki stack pointer, maka kita tidak bisa memindahkan sesuatu ke alamat yang ditunjuknya. Mungkin kita bisa menggunakan pointer yang menunjuk ke pangkalan alih-alih bagian atas.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[a][ ][ ][ ]...        Push(a)        *bp = 'a';
    ^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]...        Push(b)        *bp = 'b';
    ^- bp

Uh oh. Karena kami tidak dapat mengubah nilai tetap dari basis tumpukan, kami hanya menimpa adengan mendorong bke lokasi yang sama.

Nah, mengapa kita tidak melacak berapa kali kita telah mendorong. Dan kita juga harus melacak waktu kita muncul.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);
                                         int count = 0;

...[a][ ][ ][ ]...        Push(a)        bp[count] = 'a';
    ^- bp
...[a][ ][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Push(a)        bp[count] = 'b';
    ^- bp
...[a][b][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Pop()          --count;
    ^- bp
...[a][b][ ][ ]...                       return bp[count]; //returns b
    ^- bp

Yah itu berfungsi, tetapi sebenarnya sangat mirip dengan sebelumnya, kecuali *pointerlebih murah daripada pointer[offset](tidak ada aritmatika tambahan), belum lagi kurang mengetik. Ini seperti kehilangan bagi saya.

Mari coba lagi. Alih-alih menggunakan gaya string Pascal untuk menemukan akhir koleksi berbasis array (melacak berapa banyak item dalam koleksi), mari kita coba gaya string C (memindai dari awal hingga akhir):

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[ ][ ][ ][ ]...        Push(a)        char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       *top = 'a';
    ^- bp    ^- top

...[ ][ ][ ][ ]...        Pop()          char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       --top;
    ^- bp       ^- top                   return *top; // returns '('

Anda mungkin sudah menebak masalahnya di sini. Memori yang tidak diinisialisasi tidak dijamin menjadi 0. Jadi ketika kita mencari bagian atas ke tempat a, kita akhirnya melewatkan sekelompok lokasi memori yang tidak digunakan yang memiliki sampah acak di dalamnya. Demikian pula, ketika kita memindai ke atas, kita akan melompati jauh melampaui akita hanya mendorong sampai kita akhirnya menemukan lokasi memori lain yang kebetulan 0, dan bergerak kembali dan mengembalikan sampah acak tepat sebelum itu.

Itu cukup mudah untuk diperbaiki, kita hanya perlu menambahkan operasi Pushdan Popuntuk memastikan bagian atas tumpukan selalu diperbarui untuk ditandai dengan 0, dan kita harus menginisialisasi tumpukan dengan terminator semacam itu. Tentu saja itu juga berarti kita tidak dapat memiliki 0(atau nilai apa pun yang kita pilih sebagai terminator) sebagai nilai sebenarnya dalam tumpukan.

Selain itu, kami juga mengubah operasi O (1) menjadi operasi O (n).

TL; DR

Penunjuk tumpukan melacak bagian atas tumpukan, tempat semua tindakan terjadi. Ada beberapa cara untuk menghilangkannya ( bp[count]dan toppada dasarnya masih stack pointer), tetapi keduanya akhirnya menjadi lebih rumit dan lebih lambat daripada hanya memiliki stack pointer. Dan tidak tahu di mana bagian atas tumpukan berarti Anda tidak dapat menggunakan tumpukan.

Catatan: Penunjuk tumpukan menunjuk ke "bawah" dari tumpukan runtime di x86 mungkin kesalahpahaman terkait dengan seluruh tumpukan runtime terbalik. Dengan kata lain, dasar tumpukan ditempatkan pada alamat memori tinggi, dan ujung tumpukan tumbuh menjadi alamat memori yang lebih rendah. Stack pointer tidak menunjuk ke ujung tumpukan mana semua tindakan terjadi, hanya saja tip di alamat memori lebih rendah dari dasar tumpukan.


2

Penunjuk tumpukan digunakan (dengan penunjuk bingkai) untuk tumpukan panggilan (ikuti tautan ke wikipedia, di mana ada gambar yang bagus).

Tumpukan panggilan berisi bingkai panggilan, yang berisi alamat pengirim, variabel lokal dan data lokal lainnya (khususnya, konten register yang tumpah ; formal).

Baca juga tentang panggilan ekor (beberapa panggilan rekursif ekor tidak memerlukan bingkai panggilan), penanganan pengecualian (seperti setjmp & longjmp , mungkin melibatkan popping banyak bingkai tumpukan sekaligus), sinyal & interupsi , dan kelanjutan . Lihat juga konvensi pemanggilan dan antarmuka biner aplikasi (ABI), khususnya x86-64 ABI (yang menetapkan bahwa beberapa argumen formal dilewatkan oleh register).

Juga, gcc -Wall -O -S -fverbose-asm buat kode beberapa fungsi sederhana dalam C, kemudian gunakan untuk mengompilasinya, dan lihat ke .s file assembler yang dihasilkan .

Appel menulis kertas tahun 1986 yang mengklaim bahwa pengumpulan sampah dapat lebih cepat daripada alokasi tumpukan (menggunakan Gaya Berlanjut-Kelanjutan dalam kompiler), tetapi ini mungkin salah pada prosesor x86 hari ini (terutama karena efek cache).

Perhatikan bahwa konvensi pemanggilan, ABI, dan tata letak tumpukan berbeda pada 32 bit i686 dan pada 64 bit x86-64. Juga, konvensi panggilan (dan siapa yang bertanggung jawab untuk mengalokasikan atau membuka frame panggilan) mungkin berbeda dengan bahasa yang berbeda (misalnya C, Pascal, Ocaml, SBCL Common Lisp memiliki konvensi panggilan yang berbeda ....)

BTW, ekstensi x86 baru-baru ini seperti AVX memaksakan batasan penyelarasan yang semakin besar pada penunjuk tumpukan (IIRC, bingkai panggilan pada x86-64 ingin disejajarkan dengan 16 byte, yaitu dua kata atau pointer).


1
Anda mungkin ingin menyebutkan bahwa menyelaraskan ke 16 byte pada x86-64 berarti dua kali ukuran / penyelarasan pointer, yang sebenarnya lebih menarik daripada hitungan byte.
Deduplicator

1

Secara sederhana, program peduli karena menggunakan data itu dan perlu melacak di mana menemukannya.

Jika Anda mendeklarasikan variabel lokal dalam suatu fungsi, tumpukan adalah tempat mereka disimpan. Juga, jika Anda memanggil fungsi lain, tumpukan adalah tempat ia menyimpan alamat kembali sehingga dapat kembali ke fungsi yang Anda masuki ketika yang Anda panggil selesai dan mengambil di mana ia tinggalkan.

Tanpa SP, pemrograman terstruktur seperti yang kita tahu pada dasarnya tidak mungkin. (Anda dapat bekerja di sekitar tanpa memilikinya, tetapi itu akan membutuhkan banyak penerapan versi Anda sendiri, jadi itu tidak banyak perbedaan.)


1
Pernyataan Anda bahwa pemrograman terstruktur tanpa tumpukan tidak mungkin adalah salah. Program yang dikompilasi ke dalam gaya kelanjutan meneruskan tidak menggunakan stack, tetapi mereka adalah program yang masuk akal.
Eric Lippert

@EricLippert: Untuk nilai-nilai "sangat masuk akal" cukup masuk akal bahwa mereka termasuk berdiri di atas kepala seseorang dan mengubah diri sendiri ke luar, mungkin. ;-)
Mason Wheeler

1
Dengan berlalunya kelanjutan , adalah mungkin untuk tidak perlu tumpukan panggilan sama sekali. Secara efektif, setiap panggilan adalah panggilan ekor dan kebalikannya daripada kembali. "Karena CPS dan TCO menghilangkan konsep pengembalian fungsi implisit, penggunaan kombinasi mereka dapat menghilangkan kebutuhan untuk stack run-time."

@MichaelT: Saya mengatakan "pada dasarnya" tidak mungkin karena suatu alasan. CPS secara teoritis dapat mencapai ini, tetapi dalam praktiknya menjadi sangat sulit sangat cepat untuk menulis kode dunia nyata dari segala kompleksitas dalam CPS, seperti yang ditunjukkan Eric dalam serangkaian posting blog tentang masalah ini .
Mason Wheeler

1
@MasonWheeler Eric berbicara tentang program yang dikompilasi ke dalam CPS. Misalnya, mengutip blog Jon Harrop : In fact, some compilers don’t even use stack frames [...], and other compilers like SML/NJ convert every call into continuation style and put stack frames on the heap, splitting every segment of code between a pair of function calls in the source into its own separate function in the compiled form.Itu berbeda dari "menerapkan versi [tumpukan] Anda sendiri".
Doval

1

Untuk tumpukan prosesor pada prosesor x86, analogi dari tumpukan piring benar-benar tidak akurat.
Karena berbagai alasan (kebanyakan historis), tumpukan prosesor tumbuh dari bagian atas memori ke bagian bawah memori, sehingga analogi yang lebih baik adalah rantai rantai yang tergantung dari langit-langit. Saat mendorong sesuatu ke tumpukan, tautan rantai ditambahkan ke tautan terendah.

Penunjuk tumpukan mengacu pada tautan terendah rantai dan digunakan oleh prosesor untuk "melihat" di mana tautan terendah berada, sehingga tautan dapat ditambahkan atau dihapus tanpa harus melewati seluruh rantai dari langit-langit ke bawah.

Dalam arti tertentu, di dalam prosesor x86, tumpukan terbalik tetapi ambang terminologi stack biasa digunakan, sehingga tautan terendah disebut sebagai bagian atas tumpukan.


Tautan berantai yang saya sebutkan di atas sebenarnya adalah sel-sel memori di komputer dan mereka terbiasa menyimpan variabel lokal dan beberapa hasil penghitungan perantara. Program komputer peduli di mana bagian atas tumpukan berada (yaitu di mana tautan paling rendah hang), karena sebagian besar variabel yang perlu diakses oleh suatu fungsi berada dekat dengan tempat penunjuk tumpukan merujuk dan akses cepat ke sana diinginkan.


1
The stack pointer refers to the lowest link of the chain and is used by the processor to "see" where that lowest link is, so that links can be added or removed without having to travel the entire chain from the ceiling down.Saya tidak yakin ini analogi yang bagus. Pada kenyataannya tautan tidak pernah ditambahkan atau dihapus. Penunjuk tumpukan lebih seperti sedikit pita yang Anda gunakan untuk menandai salah satu tautan. Jika Anda kehilangan rekaman itu, Anda tidak akan memiliki cara untuk mengetahui tautan mana yang paling bawah yang Anda gunakan sama sekali ; bepergian rantai dari langit-langit ke bawah tidak akan membantu Anda.
Doval

Jadi stack pointer memberikan titik referensi yang dapat digunakan oleh program / komputer untuk menemukan variabel lokal dari suatu fungsi?
moonman239

Jika itu masalahnya, lalu bagaimana komputer menemukan variabel lokal? Apakah itu hanya mencari setiap alamat memori dari bawah ke atas?
moonman239

@ moonman239: Tidak, saat mengkompilasi, kompiler melacak di mana setiap variabel disimpan relatif terhadap penunjuk tumpukan. Prosesor memahami pengalamatan relatif tersebut untuk memberikan akses langsung ke variabel.
Bart van Ingen Schenau

1
@ BartvanIngenSchenau Ah, oke. Agak seperti ketika Anda berada di tengah-tengah dari mana dan Anda membutuhkan bantuan, sehingga Anda memberi 911 ide di mana Anda relatif terhadap tengara. Penunjuk tumpukan, dalam hal ini, biasanya merupakan "landmark" terdekat dan oleh karena itu, mungkin, titik referensi terbaik.
moonman239

1

Jawaban ini mengacu khusus untuk para stack pointer dari benang saat ini (eksekusi) .

Dalam bahasa pemrograman prosedural, utas biasanya memiliki akses ke tumpukan 1 untuk tujuan berikut:

  • Mengontrol aliran, yaitu "call stack".
    • Ketika satu fungsi memanggil fungsi lain, tumpukan panggilan akan mengingat ke mana harus kembali.
    • Tumpukan panggilan diperlukan karena ini adalah bagaimana kita ingin "panggilan fungsi" berperilaku - "untuk mengambil tempat yang kita tinggalkan" .
    • Ada gaya pemrograman lain yang tidak memiliki panggilan fungsi di tengah eksekusi (mis. Hanya diizinkan untuk menentukan fungsi berikutnya ketika akhir fungsi saat ini tercapai), atau tidak memiliki panggilan fungsi sama sekali (hanya menggunakan goto dan conditional jumps) ). Gaya pemrograman ini mungkin tidak memerlukan tumpukan panggilan.
  • Parameter panggilan fungsi.
    • Ketika suatu fungsi memanggil fungsi lain, parameter dapat didorong ke stack.
    • Penelepon dan callee perlu mengikuti konvensi yang sama dengan siapa yang bertanggung jawab untuk membersihkan parameter dari stack, ketika panggilan selesai.
  • Variabel lokal yang hidup dalam panggilan fungsi.
    • Perhatikan bahwa variabel lokal milik penelepon dapat dibuat diakses oleh callee dengan melewatkan pointer ke variabel lokal ke callee.

Catatan 1 : didedikasikan untuk penggunaan utas, meskipun isinya sepenuhnya dapat dibaca - dan dapat dihancurkan - oleh utas lainnya.

Dalam pemrograman perakitan, C, dan C ++, ketiga tujuan dapat dipenuhi oleh tumpukan yang sama. Dalam beberapa bahasa lain, beberapa tujuan dapat dipenuhi oleh tumpukan yang terpisah, atau memori yang dialokasikan secara dinamis.


1

Berikut ini adalah versi sengaja disederhanakan untuk stack.

Bayangkan tumpukan itu sebagai tumpukan kartu indeks. Pointer tumpukan menunjuk ke kartu teratas.

Saat Anda memanggil suatu fungsi:

  • Anda menulis alamat kode segera setelah baris yang memanggil fungsi pada kartu dan meletakkannya di tumpukan. (Yaitu Anda menambah tumpukan penunjuk dengan satu dan menulis alamat ke tempat itu menunjuk ke)
  • Kemudian Anda menuliskan nilai-nilai yang terkandung dalam register ke beberapa kartu, dan meletakkannya di tumpukan. (yaitu Anda menambah tumpukan penunjuk dengan jumlah register, dan menyalin konten register ke tempat itu menunjuk ke)
  • Lalu Anda menaruh kartu penanda di tumpukan. (Yaitu Anda menghemat pointer stack saat ini.)
  • Kemudian Anda menulis nilai setiap parameter fungsi dipanggil, satu ke kartu, dan meletakkannya di tumpukan. (yaitu Anda menambah penunjuk tumpukan dengan jumlah parameter dan menulis parameter ke tempat penunjuk tumpukan menunjuk.)
  • Kemudian Anda menambahkan kartu untuk setiap variabel lokal, berpotensi menulis nilai awal di atasnya. (yaitu Anda menambah tumpukan penunjuk dengan jumlah variabel lokal.)

Pada titik ini, kode dalam fungsi berjalan. Kode dikompilasi untuk mengetahui di mana masing-masing kartu relatif ke atas. Jadi ia tahu bahwa variabel xadalah kartu ketiga dari atas (yaitu stack pointer - 3) dan parameternya yadalah kartu keenam dari atas (yaitu stack pointer - 6.)

Metode ini berarti bahwa alamat setiap variabel atau parameter lokal tidak perlu dimasukkan ke dalam kode. Alih-alih, semua item data ini ditangani relatif terhadap penunjuk tumpukan.

Ketika fungsi kembali, operasi sebaliknya hanyalah:

  • Cari kartu penanda dan buang semua kartu di atasnya. (yaitu mengatur stack pointer ke alamat yang disimpan.)
  • Kembalikan register dari kartu yang disimpan sebelumnya dan buang. (mis. kurangi nilai tetap dari stack pointer)
  • Mulai jalankan kode dari alamat pada kartu di atas dan kemudian buang. (mis. kurangi 1 dari stack pointer.)

Tumpukan sekarang kembali ke keadaan sebelum fungsi dipanggil.

Ketika Anda mempertimbangkan hal ini, catat dua hal: alokasi dan alokasi lokal adalah operasi yang sangat cepat karena hanya menambah angka atau mengurangi angka dari penunjuk tumpukan. Perhatikan juga bagaimana ini bekerja secara alami dengan rekursi.

Ini terlalu disederhanakan untuk tujuan penjelasan. Dalam praktiknya, parameter dan penduduk lokal dapat dimasukkan ke dalam register sebagai pengoptimalan, dan penunjuk tumpukan umumnya akan bertambah dan dikurangi dengan ukuran kata mesin, bukan satu. (Untuk menyebutkan beberapa hal.)


1

Bahasa pemrograman modern, seperti yang Anda ketahui, mendukung konsep panggilan subrutin (paling sering disebut "panggilan fungsi"). Ini berarti:

  1. Di tengah-tengah beberapa kode, Anda dapat memanggil beberapa fungsi lain di program Anda;
  2. Fungsi itu tidak secara eksplisit tahu dari mana ia dipanggil;
  3. Namun demikian, ketika pekerjaannya selesai dan returns, kontrol kembali ke titik tepat di mana panggilan dimulai, dengan semua nilai variabel lokal berlaku seperti ketika panggilan dimulai.

Bagaimana komputer melacaknya? Itu memelihara catatan yang sedang berlangsung dari fungsi mana yang menunggu panggilan untuk kembali. Catatan ini adalah tumpukan-dan karena itu merupakan salah satu yang penting, biasanya kita menyebutnya dengan stack.

Dan karena pola panggilan / pengembalian ini sangat penting, CPU telah lama dirancang untuk memberikan dukungan perangkat keras khusus untuknya. Penunjuk tumpukan adalah fitur perangkat keras dalam CPU — register yang khusus didedikasikan untuk melacak bagian atas tumpukan, dan digunakan oleh instruksi CPU untuk bercabang ke dalam subrutin dan kembali dari sana.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.