Debugging post-mortem pada Cortex-M

Latar belakang:
Baru-baru ini saya berpartisipasi dalam pengembangan perangkat atipikal untuk saya dari kelas elektronik konsumen. Sepertinya tidak ada yang rumit, sebuah kotak yang kadang-kadang harus keluar dari mode tidur, melapor ke server dan tertidur kembali.
Praktik cepat menunjukkan bahwa debugger tidak banyak membantu ketika bekerja dengan mikrokontroler yang terus-menerus masuk ke mode tidur nyenyak atau memotong kekuatannya. Pada dasarnya, karena kotak dalam mode uji coba tanpa debugger dan tanpa saya di dekatnya dan kadang - kadang buggy. Kira-kira sekali setiap beberapa hari.
UART debugging kacau pada nozel, di mana saya mulai membuat log. Menjadi lebih mudah, beberapa masalah diselesaikan. Tetapi kemudian suatu pernyataan terjadi dan itu semua terjadi.
Dalam kasus saya, makro untuk pernyataan terlihat seperti ini:#define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0)
__BKPT(0xAB)
adalah breakpoint perangkat lunak; jika pernyataan terjadi di bawah debugging, maka debugger hanya berhenti di baris masalah, itu sangat nyaman.
Untuk beberapa pernyataan, segera jelas apa yang menyebabkannya - karena log menunjukkan nama file dan nomor baris tempat pernyataan tersebut bekerja.
Tetapi menurut pernyataan itu, hanya jelas bahwa array meluap - lebih tepatnya, pembungkus darurat atas array, yang memeriksa jalan keluar. Karena itu, hanya nama file "super_array.h" dan nomor baris di dalamnya yang terlihat di log. Dan apa array spesifik tidak jelas. Dari kayu bulat di sekitarnya, juga tidak jelas.
Tentu saja, orang hanya bisa menggigit peluru dan membaca kode Anda, tapi saya terlalu malas, dan kemudian artikel itu tidak akan berfungsi.
Karena saya menulis di uVision Keil 5 dengan kompiler armcc, kode selanjutnya diperiksa hanya di bawahnya. Saya juga menggunakan C ++ 11, karena sudah 2019 di halaman, sudah waktunya.
Stacktrace
Tentu saja, hal pertama yang terlintas dalam pikiran adalah, sial, karena ketika pernyataan muncul pada komputer desktop normal, jejak tumpukan adalah output ke konsol, seperti pada KDPV. Dari jejak tumpukan, Anda biasanya dapat memahami urutan panggilan apa yang menyebabkan kesalahan.
Oke, jadi saya juga perlu trek tersembunyi. Bagaimana cara membuatnya?
Mungkin jika Anda melempar pengecualian, dia akan dideduksi?
Kami melemparkan pengecualian dan tidak menangkapnya, kami melihat output dari "SIGABRT" dan panggilan ke _sys_exit
. Bukan tumpangan, well, oke, tidak juga, dan aku benar-benar ingin mengizinkan pengecualian.
Googling bagaimana orang lain melakukannya.
Semua metode adalah platform- execinfo.h
(tidak terlalu mengejutkan), untuk gcc di bawah POSIX ada backtrace()
dan execinfo.h
. Tidak ada yang masuk akal untuk Cale. Kami menjatuhkan air mata yang kejam. Anda harus naik ke tumpukan dengan tangan Anda.
Kami naik ke tumpukan dengan tangan kami
Secara teoritis, semuanya cukup sederhana.
- Alamat kembali dari fungsi saat ini ada di register LR, alamat puncak saat ini dari stack (dalam arti elemen terakhir di stack) ada di register SP, alamat perintah saat ini ada di register PC.
- Entah bagaimana, kami menemukan ukuran bingkai tumpukan untuk fungsi saat ini, melangkah sepanjang tumpukan pada jarak seperti itu, menemukan alamat kembali untuk fungsi sebelumnya di sana dan ulangi sampai kami melangkah melalui tumpukan sampai akhir.
- Entah bagaimana kami mencocokkan alamat pengirim dengan nomor baris dalam file dengan kode sumber.
Ok, untuk permulaan - bagaimana saya tahu ukuran bingkai tumpukan?
Pada opsi secara default - tampaknya, tidak ada sama sekali, itu hanya hardcoded oleh kompiler ke dalam "prolog" dan "epilog" dari masing-masing fungsi, menjadi perintah yang mengalokasikan dan membebaskan sepotong tumpukan untuk frame.
Tapi, untungnya, armcc memiliki opsi --use_frame_pointer
, yang mengalokasikan register R11 di bawah Frame Pointer - yaitu. arahkan ke bingkai tumpukan fungsi sebelumnya. Hebat, sekarang Anda bisa berjalan melalui semua frame stack.
Sekarang - bagaimana mencocokkan alamat pengirim dengan string dalam file sumber?
Sial, tidak mungkin lagi. Informasi debug tidak dimasukkan ke dalam mikrokontroler (yang tidak mengejutkan, karena membutuhkan tempat yang layak). Dapatkah Cale masih membuatnya mem-flash di sana, saya tidak tahu, saya tidak bisa menemukannya.
Kami menghela nafas. Oleh karena itu, tumpukan jujur - sedemikian rupa sehingga nama fungsi dan nomor baris segera output ke output debug - tidak akan berfungsi. Tetapi Anda dapat menampilkan alamat, dan kemudian di komputer membandingkannya dengan fungsi dan nomor baris, karena masih ada info debugging di proyek.
Tapi itu terlihat sangat menyedihkan, karena Anda harus mem-parsing file .map, yang menunjukkan rentang alamat yang ditempati setiap fungsi. Dan kemudian parsing file secara terpisah dengan kode dibongkar untuk menemukan baris tertentu. Ada keinginan yang tajam untuk mencetak gol.
Plus, dengan hati-hati melihat dokumentasi untuk opsi --use_frame_pointer
memungkinkan --use_frame_pointer
melihat halaman ini , yang mengatakan bahwa opsi ini dapat menyebabkan crash di HardFault secara acak. Hmm.
Oke, pikirkan lebih jauh.
Bagaimana cara debugger melakukan ini?
Tetapi debugger entah bagaimana menunjukkan tumpukan panggilan bahkan tanpa frame pointer'a
. Yah, sudah jelas caranya, IDE memiliki semua info debug yang ada, mudah baginya untuk membandingkan alamat dan nama fungsi. Hm
Pada saat yang sama, Visual Studio yang sama memiliki hal seperti itu - minidump - ketika aplikasi yang macet menghasilkan file kecil, yang kemudian Anda beri makan studio dan mengembalikan keadaan aplikasi pada saat crash. Dan Anda dapat mempertimbangkan semua variabel, berjalan di atas tumpukan dengan nyaman. Hm lagi.
Tapi itu agak sederhana. Hanya butuh gosok kelanjutan Soviet tebal ke pantat setiap hari mengisi tumpukan dengan nilai-nilai yang ada di sana pada saat musim gugur dan, tampaknya, memulihkan keadaan register. Dan itu saja, sepertinya?
Sekali lagi, pecah ide ini menjadi subtugas.
- Pada mikrokontroler, Anda harus melewati tumpukan, untuk ini Anda perlu mendapatkan nilai SP saat ini dan alamat awal tumpukan.
- Pada mikrokontroler, Anda perlu menampilkan nilai register.
- Dalam IDE, Anda perlu mendorong semua nilai dari "minidump" kembali ke tumpukan. Dan nilai-nilai register juga.
Bagaimana cara mendapatkan nilai SP saat ini?
Lebih disukai, bukan perampok tangan pada assembler. Dalam Cale, untungnya, ada fungsi khusus (intrinsik) - __current_sp()
. Gcc tidak akan berfungsi, tetapi saya tidak perlu melakukannya.
Bagaimana cara mendapatkan alamat awal stack? Karena saya menggunakan skrip untuk melindungi dari luapan (yang saya tulis di sini ), tumpukan saya terletak di bagian tautan terpisah, yang saya sebut REGION_STACK
.
Ini berarti bahwa alamatnya dapat ditemukan di tautan menggunakan variabel aneh dengan dolar di namanya .
Dengan coba-coba, kami memilih nama yang diinginkan - Image$$REGION_STACK$$ZI$$Limit
, periksa, itu berfungsi.
PenjelasanIni adalah simbol ajaib yang dibuat pada tahap penautan, jadi sebenarnya, itu bukan konstan pada tahap kompilasi.
Untuk menggunakannya, Anda perlu dereferencing:
extern unsigned int Image$$REGION_STACK$$ZI$$Limit; using MemPointer = const uint32_t *;
Jika Anda tidak ingin repot, maka Anda dapat dengan mudah meng-hardcode ukuran stack, karena itu sangat jarang berubah. Dalam kasus terburuk, kita melihat di jendela tumpukan panggilan tidak semua panggilan, tetapi sebuah rintisan.
Bagaimana cara menampilkan nilai register?
Pada awalnya saya berpikir bahwa perlu untuk menampilkan semua register tujuan umum secara umum, saya mulai kacau dengan assembler, tetapi dengan cepat menyadari bahwa tidak akan ada gunanya dalam hal ini. Bagaimanapun, output dari minidump akan dilakukan oleh fungsi khusus untuk saya, tidak ada gunanya dalam nilai register dalam konteksnya.
Sebenarnya kita hanya perlu Link Register (LR), yang menyimpan alamat pengirim dari fungsi saat ini, SP, yang telah kita bahas, dan Program Counter (PC), yang menyimpan alamat perintah saat ini.
Sekali lagi, saya tidak dapat menemukan opsi yang akan berfungsi dengan kompiler apa pun, tetapi ada lagi fungsi intrinsik untuk Cale: __return_address()
untuk LR dan __current_pc()
untuk PC.
Bagus Tetap mendorong semua nilai dari minidump kembali ke tumpukan, dan nilai register ke register.
Bagaimana cara memuat minidump ke dalam memori?
Pada awalnya, saya berencana untuk menggunakan perintah LOAD debugger, yang memungkinkan Anda untuk memuat nilai dari file .hex atau .bin ke dalam memori, tetapi dengan cepat menemukan bahwa LOAD karena suatu alasan tidak memuat nilai ke dalam RAM.
Dan saya masih tidak dapat menyelesaikan register dengan perintah ini.
Baiklah, oke, masih akan memerlukan terlalu banyak gerakan, mengonversi teks ke nampan, mengonversi nampan ke hex ...
Untungnya, Cale memiliki simulator, dan untuk simulator Anda dapat menulis skrip dalam beberapa bahasa mirip-C celaka. Dan dalam bahasa ini ada peluang untuk menulis di memori! Ada fungsi khusus seperti _WDWORD
dan _WBYTE
. Kami mengumpulkan semua ide dalam tumpukan, dan mendapatkan kode seperti itu.
Semua kode: #define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ print_minidump(); \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0)
Untuk memuat minidump, kita perlu membuat file .ini, menyalin fungsi __load_minidump
ke dalamnya, menambahkan file ini ke autorun - Project -> Options for Target -> Debug
dan menulis file .ini ini di bagian “File inisialisasi” di bagian Use Simulator.
Sekarang kita masuk ke debugging pada simulator dan, tanpa memulai debugging, panggil fungsi __load_minidump()
di jendela perintah.
Dan voila, kita teleport ke fungsi print_minidump
pada baris di mana PC disimpan. Dan di jendela Callstack + Lokal Anda dapat melihat tumpukan panggilan.
Catatan:Fungsi ini secara khusus dinamai dengan dua garis bawah di awal, karena jika nama fungsi atau variabel dalam skrip simulasi secara tidak sengaja bertepatan dengan nama dalam kode proyek, maka Cale tidak akan dapat memanggilnya. Standar C ++ melarang penggunaan nama dengan dua garis bawah di awal, sehingga kemungkinan nama yang cocok berkurang.
Pada prinsipnya, itu saja. Sejauh yang saya bisa verifikasi, minidump berfungsi untuk fungsi reguler dan interrupt handler. Apakah ini akan berfungsi untuk semua jenis penyimpangan dengan setjmp/longjmp
atau alloca
- Saya tidak tahu, karena saya tidak mempraktikkan penyimpangan.
Saya cukup senang dengan apa yang terjadi; kode kecil, overhead - makro sedikit bengkak untuk menegaskan. Dalam hal ini, semua pekerjaan yang membosankan pada penguraian tumpukan jatuh di pundak IDE, di mana tempatnya.
Kemudian saya mencari di Google sedikit dan menemukan hal yang sama untuk gcc dan gdb - CrashCatcher .
Saya mengerti bahwa saya tidak menemukan sesuatu yang baru, tetapi saya tidak dapat menemukan resep yang sudah jadi yang mengarah ke hasil yang serupa. Saya akan berterima kasih jika mereka memberi tahu saya apa yang bisa dilakukan dengan lebih baik.