Memvalidasi alamat memori pada Cortex-M0 / M3 / M4 / M7

Halo, Habr!

Mengenai pelonggaran rezim di hari lain, kemarahan dalam komentar dari satu pos tetangga bahwa artikel tentang mikrokontroler semua hanya berkedip oleh LED, dan juga kematian terlalu dini dari blog standar saya, yang saya terlalu malas untuk mengembalikannya, saya akan mentransfer materi berguna tentang sedikit cahaya yang disesalkan. trik pers dalam bekerja dengan inti Cortex-M - memeriksa alamat acak untuk validitas.


Salah satu yang sangat berguna dan pada saat yang sama karena beberapa alasan fitur yang dibuat tidak dijelaskan di mana saja pada mikrokontroler Cortex-M (semua) adalah kemampuan untuk memeriksa kebenaran alamat dalam memori. Dengan itu, Anda dapat menentukan ukuran flash, RAM dan EEPROM, menentukan keberadaan pada prosesor tertentu dari periferal dan register tertentu, mengalahkan proses jatuh sambil menjaga kesehatan keseluruhan OS, dll.

Dalam mode normal, ketika Anda sampai ke alamat yang tidak ada di Cortex-M3 / M4 / M7, pengecualian BusFault dilemparkan, dan jika tidak ada handler, itu meningkat ke HardFault. Pada Cortex-M0 tidak ada pengecualian "terperinci" (MemFault, BusFault, UsageFault), dan setiap kegagalan segera meningkat ke HardFault.

Secara umum, Anda tidak dapat mengabaikan HardFault - ini mungkin karena kegagalan perangkat keras, misalnya, dan perilaku lebih lanjut dari perangkat akan menjadi tidak dapat diprediksi. Tetapi dalam kasus khusus ini dapat dan harus dilakukan.

Cortex-M3 dan Cortex-M4: BusFault yang tidak terpenuhi


Pada Cortex-M3 dan lebih tinggi, memeriksa validitas alamat cukup sederhana - Anda harus melarang semua pengecualian (kecuali, jelas, tidak dapat ditutup-tutupi) melalui register FAULTMASK, nonaktifkan pemrosesan BusFault secara khusus, dan kemudian masukkan ke alamat yang sedang diperiksa dan lihat apakah bendera BFARVALID dalam register BFAR , mis. Daftar Alamat Kesalahan Bus. Jika Anda telah mengambilnya, Anda baru saja mengalami BusFault, mis. Alamatnya salah.

Kode tersebut terlihat seperti ini, semua definisi dan fungsi dari CMSIS (non-vendor) standar, sehingga harus bekerja pada M3, M4 atau M7:

bool cpu_check_address(volatile const char *address) { /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */ static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; /* Clear BFARVALID flag by writing 1 to it */ SCB->CFSR |= BFARVALID_MASK; /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */ uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; /* probe address in question */ *address; /* Check BFARVALID flag */ if ((SCB->CFSR & BFARVALID_MASK) != 0) { /* Bus Fault occured reading the address */ is_valid = false; } /* Reenable BusFault by clearing BFHFNMIGN */ SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; } 

Cortex-M0 dan Cortex-M0 +


Dengan Cortex-M0 dan Cortex-M0 + semuanya lebih rumit, seperti yang saya katakan di atas, mereka tidak memiliki BusFault dan semua register yang sesuai, dan pengecualian segera ditingkatkan ke HardFault. Oleh karena itu, hanya ada satu jalan keluar - untuk membuat penangan HardFault dapat memahami bahwa pengecualian itu sengaja disebabkan, dan untuk kembali ke fungsi yang memanggilnya, melewati di sana bendera yang menunjukkan bahwa HardFault ada di sana.

Ini dilakukan murni di assembler. Dalam contoh di bawah ini, register R5 diatur ke 1, dan dua "angka ajaib" ditulis ke register R1 dan R2. Jika HardFault terjadi setelah mencoba memuat nilai ke alamat yang diperiksa, maka itu harus memeriksa nilai R1 dan R2, dan jika mereka menemukan angka yang diperlukan, atur R5 ke nol. Nilai R5 ditransfer ke kode syshech melalui variabel khusus yang terikat erat ke register ini, alamat yang akan dirakit menjadi assembler adalah implisit, kita hanya tahu bahwa di arm-none-eabi parameter pertama dari fungsi ditempatkan di R0.

 bool cpu_check_address(volatile const char *address) { /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */ (void)address; /* R5 will be set to 0 by HardFault handler */ /* to indicate HardFault has occured */ register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" /* set default R5 value */ "ldr r1, =0xDEADF00D \n" /* set magic number */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic to be sure */ "ldrb r3, [r0] \n" /* probe address */ ); return result; } 

Kode penangan HardFault dalam bentuknya yang paling sederhana terlihat seperti ini:

 __attribute__((naked)) void hard_fault_default(void) { /* Get stack pointer where exception stack frame lies */ __asm__ volatile ( /* decide if we need MSP or PSP stack */ "movs r0, #4 \n" /* r0 = 0x4 */ "mov r2, lr \n" /* r2 = lr */ "tst r2, r0 \n" /* if(lr & 0x4) */ "bne use_psp \n" /* { */ "mrs r0, msp \n" /* r0 = msp */ "b out \n" /* } */ " use_psp: \n" /* else { */ "mrs r0, psp \n" /* r0 = psp */ " out: \n" /* } */ /* catch intended HardFaults on Cortex-M0 to probe memory addresses */ "ldr r1, [r0, #0x04] \n" /* read R1 from the stack */ "ldr r2, =0xDEADF00D \n" /* magic number to be found */ "cmp r1, r2 \n" /* compare with the magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x08] \n" /* read R2 from the stack */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic number to be found */ "cmp r1, r2 \n" /* compare with 2nd magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x18] \n" /* read PC from the stack */ "add r1, r1, #2 \n" /* move to the next instruction */ "str r1, [r0, #0x18] \n" /* modify PC in the stack */ "ldr r5, =0 \n" /* set R5 to indicate HardFault */ "bx lr \n" /* exit the exception handler */ " regular_handler: \n" /* here comes the rest of the fucking owl */ ) 

Pada saat meninggalkan handler pengecualian, Cortex melempar register, yang dijamin akan rusak oleh handler (R0-R3, R12, LR, PC ...), ke stack. Fragmen pertama - sudah ada di sebagian besar penangan HardFault yang sudah jadi, kecuali yang ditulis dengan logam telanjang murni - menentukan tumpukan mana: ketika bekerja di OS, bisa berupa MSP atau PSP, dan mereka memiliki alamat yang berbeda. Dalam proyek bare metal, tumpukan MSP (Main Stack Pointer) biasanya dipasang apriori, tanpa memeriksa - karena PSP (Process Stack Pointer) tidak dapat berada di sana karena kurangnya proses.

Setelah menentukan tumpukan yang diinginkan dan meletakkan alamatnya di R0, kami membaca nilai R1 (offset 0x04) dan R2 (offset 0x08) darinya, bandingkan dengan kata-kata ajaib, jika keduanya cocok - kami membaca nilai PC (offset 0x18) dari tumpukan, tambahkan 2 (2 byte - ukuran instruksi pada Cortex-M *) dan simpan kembali ke stack. Jika ini tidak dilakukan, ketika kami kembali dari handler, kami akan menemukan diri kami pada instruksi yang sama yang sebenarnya menyebabkan pengecualian, dan kami akan selalu berjalan berputar-putar. Lampiran 2 menggerakkan kita ke instruksi berikutnya pada saat kembali.

* Pembaruan. Dalam komentar, muncul pertanyaan tentang ukuran instruksi pada Cortex-M, saya akan membuat jawaban yang benar di sini: dalam kasus ini, crash menyebabkan instruksi LDRB, yang tersedia dalam arsitektur ARMv7-M dalam dua versi - 16-bit dan 32-bit. Opsi kedua akan dipilih jika setidaknya salah satu syarat terpenuhi:

  • penulis dengan jelas menunjukkan instruksi LDRB.W bukan LDRB (kami tidak)
  • register di atas R7 digunakan (untuk kita - R0 dan R3)
  • Offset yang lebih besar dari 31 byte ditentukan (kami tidak memiliki offset)


Dalam semua kasus lainnya (mis., Ketika operan cocok dengan format versi 16-bit instruksi), assembler harus memilih versi 16-bit.

Oleh karena itu, dalam kasus kami, akan selalu ada instruksi 2-byte yang perlu diloncati, tetapi jika Anda mengedit kode dengan kuat, opsi dimungkinkan.

Selanjutnya, tulis 0 di R5, yang berfungsi sebagai indikator untuk masuk ke HardFault. Register setelah R3 tidak disimpan ke stack sebelum register khusus, dan ketika keluar dari handler, mereka tidak dipulihkan dengan cara apa pun, jadi hati nurani kita untuk merusak atau tidak merusaknya. Dalam hal ini, kami mengubah R5 dari 1 menjadi 0 dengan sengaja.

Pengembalian dari interrupt handler dilakukan persis dengan satu cara. Ketika memasuki handler, nilai khusus dituliskan dalam register LR yang disebut EXC_RETURN, yang diperlukan untuk menulis ke PC untuk keluar dari handler - dan tidak hanya menulisnya, tetapi melakukannya dengan perintah POP atau BX (yaitu, “pc pc, lr, misalnya, tidak berfungsi , meskipun pertama kali menurut Anda itu berfungsi). BX LR terlihat seperti upaya untuk pergi ke alamat yang tidak berarti (di LR akan ada sesuatu seperti 0xFFFFFFF1 yang tidak ada hubungannya dengan alamat sebenarnya dari prosedur yang perlu kita kembalikan), tetapi pada kenyataannya prosesor melihat nilai ini pada PC (ke mana ia akan pergi secara otomatis), ini akan mengembalikan register dari stack dan melanjutkan untuk menjalankan prosedur kami - dengan prosedur selanjutnya setelah memanggil HardFault karena fakta bahwa kami secara manual meningkatkan PC dalam stack ini sebanyak 2.

Anda dapat membaca tentang semua offset dan perintah dengan jelas di mana , tentu saja.

Nah, atau jika angka ajaib tidak terlihat, maka semuanya akan pergi ke regular_handler, setelah itu prosedur pemrosesan HardFault mengikuti - sebagai aturan, ini adalah fungsi yang mencetak nilai register ke konsol, memutuskan apa yang harus dilakukan selanjutnya dengan prosesor, dll.

Penentuan ukuran RAM


Menggunakan semua ini sederhana dan mudah. Kami ingin menulis firmware yang berfungsi pada beberapa mikrokontroler dengan jumlah RAM yang berbeda, sementara setiap kali menggunakan RAM dalam program penuh?

Ya mudah:

 static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) { char *address = base; do { address += block; if (!cpu_check_address(address)) { break; } } while ((uint32_t)(address - base) < maxsize); return (uint32_t)(address - base); } uint32_t get_cpu_ram_size(void) { return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024); } 

maxsize di sini diperlukan kemudian, bahwa pada jumlah maksimum yang mungkin RAM antara itu dan blok alamat berikutnya mungkin tidak ada celah di mana cpu_check_address akan pecah. Dalam contoh ini, 80 KB. Juga tidak masuk akal untuk menyelidiki semua alamat - lihat saja lembar data untuk melihat apa langkah minimum yang mungkin antara dua model pengontrol dan setel sebagai blok.

Transisi terprogram ke bootloader yang terletak di antah berantah


Kadang-kadang Anda dapat melakukan trik yang lebih rumit - misalnya, bayangkan Anda ingin secara terprogram melompat ke bootloader pabrik pabrik STM32 untuk beralih ke mode pembaruan firmware melalui UART atau USB, tanpa repot-repot menulis bootloader Anda.

Bootloader STM32 terletak di area yang disebut System Memory, yang perlu Anda kunjungi, tetapi ada satu masalah - area ini memiliki alamat yang berbeda tidak hanya pada seri prosesor yang berbeda, tetapi pada model berbeda dari seri yang sama (pelat epik dapat ditemukan di AN2606 pada halaman 22 hingga 26). Saat Anda menambahkan fungsionalitas yang sesuai ke platform secara umum, dan bukan hanya ke produk tertentu, Anda menginginkan keserbagunaan.

Dalam file CMSIS, alamat awal Memori Sistem juga tidak ada. Tidak mungkin untuk menentukannya dengan ID Bootloader, karena ini masalah ayam dan telur - ID Bootloader terletak di byte terakhir Memori Sistem, yang membawa kita kembali ke pertanyaan tentang alamat.

Namun, jika kita melihat kartu memori STM32, kita akan melihat sesuatu seperti ini:


Dalam hal ini, kami tertarik dengan lingkungan Memori Sistem - misalnya, di atas adalah area yang dapat diprogram (tidak semua STM32) dan Opsi byte (semuanya). Struktur ini diamati tidak hanya pada model yang berbeda, tetapi pada garis STM32 yang berbeda, dengan satu-satunya perbedaan dalam keberadaan OTP dan adanya kesenjangan dalam alamat antara memori sistem dan opsi.

Tetapi bagi kami dalam hal ini, hal yang paling penting adalah bahwa alamat mulai dari Opsi Bytes ada di header CMSIS biasa - disebut OB_BASE di sana.

Lebih sederhana. Kami menulis fungsi untuk mencari alamat yang valid atau tidak valid turun atau naik dari yang ditentukan:

 char *cpu_find_next_valid_address(char *start, char *stop, bool valid) { char *address = start; while (true) { if (address == stop) { return NULL; } if (cpu_check_address(address) == valid) { return address; } if (stop > start) { address++; } else { address--; } }; return NULL; } 

Dan lihat ke bawah dari Opsi byte, pertama akhir dari memori sistem, atau OTP yang berdekatan dengannya, dan kemudian awal memori sistem dalam dua lintasan:

 /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; 

Dan tanpa banyak kesulitan, kami mengatur ini menjadi fungsi yang menemukan awal memori sistem dan melompat ke atasnya, yaitu meluncurkan bootloader:

 static void jump_to_bootloader(void) __attribute__ ((noreturn)); /* Sets up and jumps to the bootloader */ static void jump_to_bootloader(void) { /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; if (!c) { NVIC_SystemReset(); } uint32_t boot_addr = (uint32_t)c; uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr); uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4); void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr); /* Reset the stack pointer */ __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); } 

Itu tergantung pada model prosesor spesifik ... tidak ada yang bergantung. Logika tidak akan berfungsi pada model yang memiliki lubang antara OTP dan memori sistem - tetapi saya belum memeriksa apakah ada sama sekali. Akan aktif bekerja dengan OTP - centang.

Trik lain hanya berlaku pada prosedur biasa untuk memanggil bootloader dari kode Anda - jangan lupa untuk mereset penunjuk tumpukan dan memanggil prosedur untuk meninggalkan bootloader sebelum menginisialisasi periferal, kecepatan jam, dll. Karena minimalis, bootloader dapat menyumbat menginisialisasi periferal dan berharap bahwa itu dalam keadaan default. Pilihan yang baik untuk memanggil bootloader dari tempat yang sewenang-wenang dalam program Anda adalah menulis ke Register Cadangan RTC atau hanya ke alamat yang diketahui dalam memori nomor ajaib, mem-boot ulang program dan memeriksa pada tahap pertama menginisialisasi nomor ini.

PS Karena semua alamat dalam kartu memori prosesor disejajarkan dalam kasus terburuk dengan 4, prosedur di atas akan sangat dipercepat oleh gagasan melangkah melalui mereka dalam peningkatan 4 byte, bukan satu.

Pemberitahuan penting


NB: harap dicatat bahwa pada pengontrol tertentu validitas alamat tertentu tidak selalu menunjukkan keberadaan fungsionalitas aktual yang mungkin terletak di alamat itu. Misalnya, alamat register yang mengendalikan beberapa blok periferal opsional mungkin valid, meskipun blok itu sendiri tidak ada dalam model ini. Dari sisi pabrikan, trik paling menarik adalah mungkin, biasanya berakar pada penggunaan kristal yang sama untuk model prosesor yang berbeda. Namun, dalam kebanyakan kasus, prosedur ini bekerja dan terbukti sangat bermanfaat.

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


All Articles