Stack dan heap adalah konsep perangkat lunak, bukan konsep perangkat keras. Apa yang disediakan oleh perangkat keras adalah memori. Menentukan zona memori, salah satunya disebut "tumpukan" dan salah satunya disebut "tumpukan", adalah pilihan program Anda.
Perangkat keras membantu dengan tumpukan. Sebagian besar arsitektur memiliki register khusus yang disebut stack pointer. Kegunaan yang dimaksudkan adalah bahwa ketika program membuat panggilan fungsi, parameter fungsi dan alamat kembali didorong ke stack, dan mereka muncul ketika fungsi berakhir dan kembali ke pemanggilnya. Mendorong ke tumpukan berarti menulis di alamat yang diberikan oleh penunjuk tumpukan dan mengurangi penunjuk tumpukan sesuai (atau bertambah, tergantung ke arah mana tumpukan tumbuh). Popping berarti menambah (atau mengurangi) penunjuk tumpukan; alamat pengirim dibaca dari alamat yang diberikan oleh penunjuk tumpukan.
Beberapa arsitektur (bukan ARM) memiliki instruksi panggilan subrutin yang menggabungkan lompatan dengan penulisan ke alamat yang diberikan oleh penunjuk tumpukan, dan instruksi pengembalian subrutin yang menggabungkan pembacaan dari alamat yang diberikan oleh penunjuk tumpukan dan melompat ke alamat ini. Pada ARM, penyimpanan dan pemulihan alamat dilakukan dalam register LR, instruksi panggilan dan pengembalian tidak menggunakan penunjuk tumpukan. Namun ada instruksi untuk memfasilitasi penulisan atau membaca banyak register ke alamat yang diberikan oleh stack pointer, untuk mendorong dan memunculkan argumen fungsi.
Untuk memilih ukuran tumpukan dan tumpukan, satu-satunya informasi yang relevan dari perangkat keras adalah jumlah total memori yang Anda miliki. Anda kemudian membuat pilihan tergantung pada apa yang ingin Anda simpan di memori (memungkinkan kode, data statis, dan program lainnya).
Suatu program biasanya akan menggunakan konstanta ini untuk menginisialisasi beberapa data dalam memori yang akan digunakan oleh sisa kode, seperti alamat bagian atas tumpukan, mungkin nilai di suatu tempat untuk memeriksa tumpukan kelebihan, batas untuk pengalokasi tumpukan , dll.
Dalam kode yang Anda lihat , Stack_Size
konstanta digunakan untuk memesan blok memori di area kode (melalui SPACE
arahan dalam perakitan ARM). Alamat atas dari blok ini diberi label __initial_sp
, dan disimpan dalam tabel vektor (prosesor menggunakan entri ini untuk mengatur SP setelah reset perangkat lunak) serta diekspor untuk digunakan dalam file sumber lain. The Heap_Size
konstan sama digunakan untuk cadangan blok memori dan label untuk batas-batasnya ( __heap_base
dan __heap_limit
) diekspor untuk digunakan dalam file sumber lain.
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
…
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
…
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
Ukuran tumpukan dan tumpukan ditentukan oleh aplikasi Anda, bukan di mana pun di lembar data mikrokontroler.
Tumpukan
Tumpukan ini digunakan untuk menyimpan nilai-nilai variabel lokal di dalam fungsi, nilai-nilai sebelumnya register CPU digunakan untuk variabel lokal (sehingga mereka dapat dikembalikan saat keluar dari fungsi), alamat program untuk kembali ketika meninggalkan fungsi-fungsi tersebut, ditambah beberapa overhead untuk pengelolaan tumpukan itu sendiri.
Saat mengembangkan sistem tertanam, Anda memperkirakan kedalaman panggilan maksimum yang Anda harapkan, tambahkan ukuran semua variabel lokal dalam fungsi dalam hierarki itu, lalu tambahkan beberapa padding untuk memungkinkan overhead yang disebutkan di atas, lalu tambahkan lagi untuk setiap gangguan yang mungkin terjadi selama eksekusi program Anda.
Metode estimasi alternatif (di mana RAM tidak dibatasi) adalah untuk mengalokasikan ruang stack jauh lebih banyak daripada yang Anda perlukan, isi stack dengan nilai sentinel, kemudian pantau berapa banyak yang sebenarnya Anda gunakan selama eksekusi. Saya telah melihat versi debug runtime bahasa C yang akan melakukan ini untuk Anda secara otomatis. Kemudian, ketika Anda selesai mengembangkan, Anda dapat mengurangi ukuran tumpukan jika Anda mau.
Tumpukan itu
Menghitung ukuran tumpukan yang Anda butuhkan bisa jadi lebih sulit. Heap digunakan untuk variabel yang dialokasikan secara dinamis, jadi jika Anda menggunakan
malloc()
danfree()
dalam program bahasa C, ataunew
dandelete
dalam C ++, di situlah variabel-variabel tersebut tinggal.Namun, khususnya di C ++, mungkin ada alokasi memori dinamis tersembunyi yang terjadi. Misalnya, jika Anda memiliki objek yang dialokasikan secara statis, bahasa tersebut mengharuskan destruktornya dipanggil saat program keluar. Saya mengetahui setidaknya satu runtime tempat alamat destruktor disimpan dalam daftar tertaut yang dialokasikan secara dinamis.
Jadi untuk memperkirakan ukuran tumpukan yang Anda butuhkan, lihat semua alokasi memori dinamis di setiap jalur melalui pohon panggilan Anda, hitung maksimum dan tambahkan beberapa lapisan. Runtime bahasa dapat memberikan diagnostik yang dapat Anda gunakan untuk memantau total penggunaan tumpukan, fragmentasi, dll.
sumber
Selain jawaban lain, saya ingin menambahkan bahwa ketika mengukir RAM antara stack dan heap space, Anda juga perlu mempertimbangkan ruang untuk data statis yang tidak konstan (misalnya file global, statika fungsi, dan program-lebar) global dari perspektif C, dan mungkin yang lain untuk C ++).
Cara alokasi tumpukan / tumpukan bekerja
Perlu dicatat bahwa file perakitan startup adalah salah satu cara untuk mendefinisikan wilayah; toolchain (baik lingkungan build Anda dan lingkungan run-time) kebanyakan peduli tentang simbol yang menentukan awal stackspace (digunakan untuk menyimpan pointer stack awal di Tabel Vektor) dan awal dan akhir ruang tumpukan (digunakan oleh dinamis pengalokasi memori, biasanya disediakan oleh libc Anda)
Dalam contoh OP, hanya 2 simbol yang didefinisikan, ukuran tumpukan di 1kiB dan ukuran tumpukan di 0B. Nilai-nilai ini digunakan di tempat lain untuk benar-benar menghasilkan ruang tumpukan dan tumpukan
Dalam contoh @Gilles, ukurannya didefinisikan dan digunakan dalam file assembly untuk mengatur ruang stack mulai dari mana pun dan berlangsungnya ukuran, diidentifikasi oleh simbol Stack_Mem dan menetapkan label __initial_sp di akhir. Demikian juga untuk heap, di mana spasi adalah simbol Heap_Mem (ukuran 0,5kiB), tetapi dengan label di awal dan akhir (__heap_base dan __heap_limit).
Ini diproses oleh linker, yang tidak akan mengalokasikan apa pun di dalam ruang stack dan heap space karena memori tersebut ditempati (oleh simbol Stack_Mem dan Heap_Mem), tetapi ini dapat menempatkan memori itu dan semua global di mana pun ia butuhkan. Label akhirnya menjadi simbol tanpa panjang di alamat yang diberikan. __Initial_sp digunakan langsung untuk tabel vektor pada waktu tautan, dan __heap_base dan __heap_limit oleh kode runtime Anda. Alamat simbol yang sebenarnya diberikan oleh penghubung berdasarkan tempat penempatannya.
Seperti yang saya singgung di atas, simbol-simbol ini sebenarnya tidak harus berasal dari file startup.s. Mereka dapat berasal dari konfigurasi linker Anda (Menyebarkan file di Keil, linkerscript di GNU), dan pada mereka Anda dapat memiliki kontrol yang lebih halus atas penempatan. Misalnya, Anda bisa memaksa tumpukan berada di awal atau akhir RAM, atau menjauhkan global Anda dari tumpukan, atau apa pun yang Anda inginkan. Anda bahkan dapat menentukan bahwa HEAP atau STACK hanya menempati RAM apa pun yang tersisa setelah global ditempatkan. Perhatikan bahwa Anda harus berhati-hati dalam menambahkan lebih banyak variabel statis sehingga memori Anda yang lain akan berkurang.
Namun, setiap toolchain berbeda, dan cara menulis file konfigurasi dan simbol apa yang akan digunakan pengalokasi memori dinamis Anda harus berasal dari dokumentasi lingkungan khusus Anda.
Ukuran Stack
Mengenai cara menentukan ukuran tumpukan, banyak toolchains dapat memberi Anda kedalaman tumpukan maksimum dengan menganalisis pohon panggilan fungsi program Anda, JIKA Anda tidak menggunakan rekursi atau pointer fungsi. Jika Anda menggunakannya, perkirakan ukuran tumpukan dan pra-mengisinya dengan nilai-nilai kardinal (mungkin melalui fungsi entri sebelum utama) dan kemudian periksa setelah program Anda berjalan untuk sementara waktu di mana kedalaman maksimumnya (di mana nilai-nilai kardinal akhir). Jika Anda telah sepenuhnya menjalankan program hingga batasnya, Anda akan tahu secara cukup akurat apakah Anda dapat mengecilkan tumpukan atau, jika program Anda macet atau tidak ada nilai kardinal yang tersisa, Anda perlu menambah tumpukan dan coba lagi.
Heap Sizing
Menentukan ukuran tumpukan lebih tergantung aplikasi. Jika Anda hanya melakukan alokasi dinamis selama startup, Anda bisa menambahkan ruang yang diperlukan dalam kode startup Anda (ditambah beberapa overhead untuk manajemen memori). Jika Anda memiliki akses ke sumber manajer memori Anda, Anda bisa tahu persis apa overhead-nya, dan bahkan mungkin menulis kode untuk menggerakkan memori untuk memberi Anda informasi penggunaan. Untuk aplikasi yang membutuhkan memori runtime dinamis (mis. Mengalokasikan buffer untuk frame ethernet masuk) yang terbaik yang bisa saya sarankan adalah dengan hati-hati mengasah stacksize Anda dan memberikan Heap segala yang tersisa setelah stack dan statika.
Catatan akhir (RTOS)
Pertanyaan OP ditandai dengan bare-metal, tapi saya ingin menambahkan catatan untuk RTOS. Seringkali (selalu?) Setiap tugas / proses / utas (saya hanya akan menulis tugas di sini untuk kesederhanaan) akan diberi ukuran tumpukan ketika tugas dibuat, dan selain tumpukan tugas, kemungkinan akan ada OS kecil tumpukan (digunakan untuk interupsi dan semacamnya)
Struktur akuntansi tugas dan tumpukan harus dialokasikan dari suatu tempat, dan ini akan sering berasal dari ruang tumpukan keseluruhan aplikasi Anda. Dalam kasus ini, ukuran tumpukan awal Anda sering tidak masalah, karena OS hanya akan menggunakannya selama inisialisasi. Saya telah melihat, misalnya, menentukan SEMUA ruang yang tersisa selama penautan dialokasikan ke HEAP dan menempatkan penumpukan tumpukan awal di ujung heap untuk tumbuh menjadi heap, mengetahui bahwa OS akan mengalokasikan mulai dari awal heap dan akan mengalokasikan tumpukan OS sebelum meninggalkan tumpukan initial_sp. Kemudian semua ruang digunakan untuk mengalokasikan tumpukan tugas dan memori yang dialokasikan secara dinamis lainnya.
sumber