Bagaimana cara melindungi dari stack overflow (pada Cortex M)?

Jika Anda memprogram pada komputer "besar", maka Anda mungkin tidak memiliki pertanyaan seperti itu. Ada banyak tumpukan untuk melimpah, Anda harus mencoba. Dalam kasus terburuk, Anda mengklik OK pada jendela seperti ini dan mencari tahu.

gambar

Tetapi jika Anda memprogram mikrokontroler, maka masalahnya terlihat sedikit berbeda. Pertama, Anda perlu memperhatikan bahwa tumpukan sudah penuh.

Pada artikel ini saya akan berbicara tentang penelitian saya sendiri tentang topik ini. Karena saya memprogram terutama di bawah STM32 dan di bawah Milander 1986 - saya fokus pada mereka.

Pendahuluan


Mari kita bayangkan kasus paling sederhana - kita menulis kode single-threaded sederhana tanpa sistem operasi, mis. kami hanya memiliki satu tumpukan. Dan jika Anda, seperti saya, memprogram di uVision Keil, maka memori didistribusikan entah bagaimana seperti ini:



Dan jika Anda, seperti saya, menganggap memori dinamis pada mikrokontroler sebagai jahat, maka seperti ini:



Ngomong-ngomong
Jika Anda ingin melarang penggunaan heap, Anda dapat melakukannya seperti ini:
#pragma import(__use_no_heap_region) 

Detail di sini

OK, apa masalahnya? Masalahnya adalah bahwa Keil menempatkan tumpukan tepat di belakang area data statis. Dan tumpukan di Cortex-M tumbuh ke arah penurunan alamat. Dan ketika meluap, maka itu hanya merangkak keluar dari bagian memori yang dialokasikan. Dan menimpa variabel statis atau global.

Terutama hebat jika tumpukan meluap hanya saat memasuki interupsi. Atau, bahkan lebih baik, dalam interupsi bersarang! Dan diam-diam merusak beberapa variabel yang digunakan dalam bagian kode yang sama sekali berbeda. Dan program crash pada pernyataan tersebut. Jika anda beruntung Tas heisen bola, orang bisa mencari seminggu penuh dengan senter.

Segera buat reservasi bahwa jika Anda menggunakan heap, maka masalahnya tidak kemana-mana, hanya saja variabel global heap rampasan. Tidak jauh lebih baik.

Oke, masalahnya jelas. Apa yang harus dilakukan

MPU


Yang paling sederhana dan paling jelas adalah menggunakan MPU (dengan kata lain, Memory Protection Unit). Memungkinkan Anda untuk menetapkan atribut yang berbeda ke bagian memori yang berbeda; khususnya, Anda dapat mengelilingi tumpukan dengan wilayah hanya-baca dan menangkap MemFault saat menulis di sana.

Misalnya, dalam stm32f407 MPU adalah. Sayangnya, di banyak stm "junior" lainnya tidak. Dan dalam Milandrovsky 1986VE1 juga tidak ada.

Yaitu Solusinya bagus, tetapi tidak selalu terjangkau.

Kontrol manual


Saat kompilasi, Keil dapat membuat (dan melakukannya secara default) laporan html dengan grafik panggilan (opsi tautan "--info = stack"). Dan laporan ini juga memberikan informasi tentang tumpukan yang digunakan. Gcc dapat melakukannya juga (opsi -fstack-use). Karenanya, Anda terkadang dapat melihat laporan ini (atau menulis skrip yang melakukan ini untuk Anda, dan menyebutnya sebelum setiap pembuatan).

Selain itu, di bagian paling awal laporan, sebuah jalur ditulis yang mengarah ke penggunaan maksimum tumpukan:



Masalahnya adalah bahwa jika kode Anda memiliki panggilan fungsi oleh pointer atau metode virtual (dan saya memilikinya), maka laporan ini dapat sangat meremehkan kedalaman tumpukan maksimum. Yah, interupsi, tentu saja, tidak diperhitungkan. Bukan cara yang sangat andal.

Penempatan Tacky Stack


Saya belajar tentang metode ini dari artikel ini . Artikel ini tentang karat, tetapi ide utamanya adalah ini:



Saat menggunakan gcc, ini bisa dilakukan menggunakan " tautan ganda ".

Dan di Keil, lokasi area dapat diubah menggunakan skrip Anda sendiri untuk tautan (file sebar dalam terminologi Keil). Untuk melakukan ini, buka opsi proyek dan hapus centang "Gunakan tata letak memori dari dialog target". Maka file default akan muncul di bidang "Scatter file". Itu terlihat seperti ini:

 ; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (+RW +ZI) } } 

Apa yang harus dilakukan selanjutnya? Opsi yang memungkinkan. Dokumentasi resmi menyarankan mendefinisikan bagian dengan nama yang dipesan - ARM_LIB_HEAP dan ARM_LIB_STACK. Tapi ini memerlukan konsekuensi yang tidak menyenangkan, setidaknya bagi saya - ukuran tumpukan dan tumpukan harus diatur dalam file pencar.

Di semua proyek yang saya gunakan, ukuran tumpukan dan tumpukan diatur dalam file startup assembler (yang dihasilkan Keil saat membuat proyek). Saya benar-benar tidak ingin mengubahnya. Saya hanya ingin memasukkan file sebar baru dalam proyek, dan semuanya akan baik-baik saja. Jadi saya pergi dengan cara yang sedikit berbeda:

Spoiler
 #! armcc -E ; with that we can use C preprocessor #define RAM_BEGIN 0x20000000 #define RAM_SIZE_BYTES (4*1024) #define FLASH_BEGIN 0x8000000 #define FLASH_SIZE_BYTES (32*1024) ; This scatter file places stack before .bss region, so on stack overflow ; we get HardFault exception immediately LR_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load region size_region ER_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ; Stack region growing down REGION_STACK RAM_BEGIN { *(STACK) } ; We have to define heap region, even if we don't actually use heap REGION_HEAP ImageLimit(REGION_STACK) { *(HEAP) } ; this will place .bss region above the stack and heap and allocate RAM that is left for it RW_IRAM1 ImageLimit(REGION_HEAP) (RAM_SIZE_BYTES - ImageLength(REGION_STACK) - ImageLength(REGION_HEAP)) { *(+RW +ZI) } } 


Lalu saya mengatakan bahwa semua objek bernama STACK harus terletak di wilayah REGION_STACK, dan semua objek HEAP harus terletak di wilayah REGION_HEAP. Dan yang lainnya ada di wilayah RW_IRAM1. Dan dia mengatur daerah dalam urutan ini - awal operasi, tumpukan, tumpukan, segala sesuatu yang lain. Perhitungannya adalah bahwa dalam file startup assembler tumpukan dan tumpukan diatur menggunakan kode ini (mis., Sebagai array dengan nama STACK dan HEAP):

Spoiler
 Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB 


Oke, Anda mungkin bertanya, tapi apa artinya ini bagi kita? Dan inilah yang terjadi. Sekarang, ketika keluar dari tumpukan, prosesor mencoba untuk menulis (atau membaca) memori yang tidak ada. Dan pada STM32, gangguan terjadi karena pengecualian - HardFault.

Ini tidak semudah MemFault karena MPU, karena HardFault dapat terjadi karena berbagai alasan, tetapi setidaknya kesalahannya keras dan tidak sepi. Yaitu itu terjadi segera, dan bukan setelah periode waktu yang tidak diketahui, seperti sebelumnya.

Yang terbaik dari semuanya, kami tidak membayar apa pun untuk itu, tidak ada runtime overhead! Wow Tapi ada satu masalah.

Ini tidak berfungsi pada Milander.

Ya Tentu saja, pada Milandra (saya terutama tertarik pada 1986BE1 dan BE91), kartu memori terlihat berbeda. Di STM32, sebelum dimulainya operasi, tidak ada apa-apa, dan di Milandra, sebelum operasi, terletak area bus eksternal.

Tetapi bahkan jika Anda tidak menggunakan bus eksternal, Anda tidak akan menerima HardFault. Atau mungkin mendapatkannya. Atau mungkin mendapatkannya, tetapi tidak segera. Saya tidak dapat menemukan informasi mengenai hal ini (yang tidak mengejutkan bagi Milander), dan percobaan tidak memberikan hasil yang masuk akal. HardFault kadang-kadang terjadi jika ukuran tumpukan kelipatan 256. Kadang-kadang HardFault terjadi jika tumpukan terlalu jauh ke memori yang tidak ada.

Tapi itu bahkan tidak masalah. Jika HardFault tidak terjadi setiap waktu, maka cukup memindahkan tumpukan ke awal RAM tidak lagi menyelamatkan kita. Dan sejujurnya, STM juga tidak berkewajiban untuk melemparkan pengecualian pada saat yang sama, spesifikasi inti Cortex-M tampaknya tidak mengatakan apa pun yang konkret tentang hal ini.

Jadi, bahkan pada STM lebih seperti peretasan, tidak terlalu kotor.

Jadi, Anda perlu mencari cara lain.

Akses breakpoint dalam catatan


Jika kita memindahkan tumpukan ke awal RAM, maka nilai batas tumpukan akan selalu sama - 0x20000000. Dan kita bisa menempatkan breakpoint pada catatan di sel ini. Ini dapat dilakukan dengan perintah dan bahkan terdaftar di autorun menggunakan file .ini:

 // breakpoint on stackoverflow BS Write 0x20000000, 1 

Tapi ini bukan cara yang bisa diandalkan. Breakpoint ini akan menyala setiap kali stack diinisialisasi. Sangat mudah untuk mengalahkannya secara tidak sengaja dengan mengeklik "Bunuh semua breakpoint". Dan dia akan melindungi Anda hanya di hadapan debugger. Tidak bagus

Perlindungan overflow dinamis


Pencarian cepat pada subjek ini mengarahkan saya ke opsi Keil --protect_stack dan --protect_stack_all. Opsi yang berguna, sayangnya, mereka melindungi tidak meluap seluruh tumpukan, tetapi dari muncul fungsi lain ke dalam bingkai tumpukan. Misalnya, jika kode Anda melampaui batas array atau gagal dengan sejumlah parameter variabel. Gcc, tentu saja, bisa melakukannya juga (-fstack-protector).

Inti dari opsi ini adalah sebagai berikut: "variabel penjaga" ditambahkan ke setiap bingkai tumpukan, yaitu, nomor penjaga. Jika nomor ini telah berubah setelah keluar dari fungsi, maka fungsi penangan kesalahan dipanggil. Detail di sini .

Suatu hal yang bermanfaat, tetapi tidak cukup apa yang saya butuhkan. Saya perlu pemeriksaan yang lebih sederhana - sehingga ketika memasuki setiap fungsi, nilai register SP (Stack Pointer) diperiksa terhadap nilai minimum yang diketahui sebelumnya. Tetapi jangan menulis tes ini dengan tangan Anda di pintu masuk ke setiap fungsi?

Kontrol SP dinamis


Untungnya, gcc memiliki opsi luar biasa "-finstrument-functions", yang memungkinkan Anda untuk memanggil fungsi yang ditentukan pengguna saat Anda memasuki setiap fungsi dan saat Anda keluar dari setiap fungsi. Ini biasanya digunakan untuk menampilkan informasi debug, tetapi apa bedanya?

Yang lebih untungnya, Keil sengaja menyalin fungsionalitas gcc, dan ada opsi yang sama tersedia dengan nama "--gnu_instrument" ( detail ).

Setelah itu, Anda hanya perlu menulis kode ini:

Spoiler
 //   ,    //   ,         scatter- extern unsigned int Image$$REGION_STACK$$RW$$Base; //    ,   static const uint32_t stack_lower_address = (uint32_t) &( Image$$REGION_STACK$$RW$$Base ); //         extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_enter( void * current_func, void * callsite ) { (void)current_func; (void)callsite; ASSERT( __current_sp() >= stack_lower_address ); } //   -   extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_exit( void * current_func, void * callsite ) { (void)current_func; (void)callsite; } 


Dan voila! Sekarang, setelah memasuki setiap fungsi (termasuk penangan interrupt), pemeriksaan akan dilakukan untuk stack overflow. Dan jika tumpukan meluap, akan ada penegasan.

Sedikit penjelasan:
  • Ya, tentu saja, Anda perlu memeriksa luapan dengan margin, jika tidak ada risiko "melompat" di atas tumpukan.
  • Image $$ REGION_STACK $$ RW $$ Base adalah keajaiban khusus untuk mendapatkan informasi tentang area memori menggunakan konstanta yang dihasilkan oleh linker. Detail (meskipun tidak terlalu jelas di beberapa tempat) di sini .


Apakah solusinya sempurna? Tentu saja tidak.

Pertama, pemeriksaan ini jauh dari gratis, kode dari itu membengkak sebesar 10 persen. Nah, kode itu akan bekerja lebih lambat (walaupun saya tidak mengukurnya). Apakah itu penting atau tidak, itu terserah Anda; Menurut pendapat saya, ini adalah harga yang wajar untuk keamanan.

Kedua, ini kemungkinan besar tidak akan berfungsi ketika menggunakan perpustakaan yang sudah dikompilasi (tapi karena saya tidak menggunakannya sama sekali, saya tidak memeriksa).

Tetapi solusi ini berpotensi cocok untuk program multi-utas, karena kami melakukan verifikasi sendiri. Tapi saya belum benar-benar memikirkan ide ini, jadi saya akan menahannya untuk saat ini.

Untuk meringkas


Ternyata untuk menemukan solusi bekerja untuk FM32 dan untuk Milander, meskipun untuk yang terakhir saya harus membayar dengan beberapa overhead.

Bagi saya, yang terpenting adalah perubahan kecil dalam paradigma berpikir. Sebelum artikel tersebut di atas, saya tidak berpikir sama sekali bahwa Anda dapat melindungi diri Anda dari tumpukan yang meluap. Saya tidak menganggap ini sebagai masalah yang perlu diselesaikan, melainkan sebagai fenomena alam tertentu - kadang hujan, dan kadang-kadang tumpukan meluap, yah, tidak ada yang harus dilakukan, Anda harus menggigit peluru dan mentolerir.

Dan saya biasanya cukup sering memperhatikan sendiri (dan untuk orang lain) ini - alih-alih menghabiskan 5 menit di Google dan menemukan solusi sepele - Saya telah hidup dengan masalah saya selama bertahun-tahun.

Itu semua untuk saya. Saya mengerti bahwa saya belum menemukan sesuatu yang secara fundamental baru, tetapi saya belum menemukan artikel yang sudah jadi dengan keputusan seperti itu (setidaknya Joseph Yu sendiri tidak menawarkan ini secara langsung dalam artikel tentang hal ini). Saya berharap dalam komentar mereka akan memberi tahu saya apakah saya benar atau tidak, dan apa jebakan dari pendekatan ini.

UPD: Jika, ketika menambahkan file pencar, Keil mulai mengeluarkan peringatan yang tidak dapat dipahami ala "AppData \ Local \ Temp \ p17af8-2 (33): peringatan: # 1-D: baris terakhir file berakhir tanpa baris baru" - tetapi file ini sendiri tidak terbuka, karena sifatnya sementara, maka cukup tambahkan jeda baris dengan karakter terakhir di file sebar.

Source: https://habr.com/ru/post/id425071/


All Articles