Selamat siang Beberapa hari yang lalu saya mengalami masalah kecil dalam
proyek kami - di interrupt handler gdb, stack stack untuk Cortex-M ditampilkan secara tidak benar. Oleh karena itu, sekali lagi berguna untuk mencari tahu, dan dengan cara apa saya bisa mendapatkan jejak stack untuk ARM? Apa flag kompilasi yang memengaruhi keterlacakan stack pada ARM? Bagaimana ini diterapkan di kernel Linux? Berdasarkan penelitian, saya memutuskan untuk menulis artikel ini.
Mari kita lihat dua metode jejak tumpukan utama di kernel Linux.
Tumpuk bersantai melalui bingkai
Mari kita mulai dengan pendekatan sederhana yang dapat ditemukan di kernel Linux, tetapi yang saat ini memiliki status usang dalam GCC.
Bayangkan bahwa suatu program sedang berjalan pada stack dalam RAM, dan pada titik tertentu kita menghentikannya dan ingin memunculkan stack panggilan. Misalkan kita memiliki pointer ke instruksi saat ini yang dijalankan oleh prosesor (PC), serta pointer saat ini ke bagian atas tumpukan (SP). Sekarang, untuk "melompati" tumpukan ke fungsi sebelumnya, Anda perlu memahami fungsi apa itu dan di mana kita harus melompat ke fungsi ini. ARM menggunakan Link Register (LR) untuk tujuan ini.
Link Register (LR) adalah register R14. Ini menyimpan informasi kembali untuk subrutin, panggilan fungsi, dan pengecualian. Saat diatur ulang, prosesor menetapkan nilai LR ke 0xFFFFFFFF
Selanjutnya, kita perlu naik tumpukan dan memuat nilai-nilai baru register LR dari tumpukan. Struktur frame tumpukan untuk kompiler adalah sebagai berikut:
/* The stack backtrace structure is as follows: fp points to here: | save code pointer | [fp] | return link value | [fp, #-4] | return sp value | [fp, #-8] | return fp value | [fp, #-12] [| saved r10 value |] [| saved r9 value |] [| saved r8 value |] ... [| saved r0 value |] r0-r3 are not normally saved in a C function. */
Deskripsi ini diambil dari file header GCC gcc / gcc / config / arm / arm.h.
Yaitu kompiler (dalam kasus kami GCC) entah bagaimana dapat diinformasikan bahwa kami ingin melakukan jejak stack. Dan kemudian dalam prolog masing-masing fungsi kompiler akan menyiapkan semacam struktur tambahan. Anda dapat melihat bahwa di dalam struktur ini terdapat nilai “berikutnya” dari register LR yang kita butuhkan, dan, yang paling penting, itu berisi alamat frame berikutnya
| return fp value | [fp, #-12]
| return fp value | [fp, #-12]
Mode kompiler ini ditentukan oleh opsi -mapcs-frame. Ada disebutkan dalam deskripsi opsi tentang "Menentukan -fomit-frame-pointer dengan opsi ini menyebabkan frame tumpukan tidak dihasilkan untuk fungsi daun." Di sini, fungsi daun dipahami sebagai fungsi yang tidak membuat panggilan ke fungsi lain, sehingga dapat dibuat sedikit lebih mudah.
Anda juga mungkin bertanya-tanya apa yang harus dilakukan dengan fungsi assembler dalam kasus ini. Sebenarnya, tidak ada yang rumit - Anda harus memasukkan makro khusus. Dari file
tools / objtool / Documentation / stack-validation.txt di kernel Linux:
Setiap fungsi yang dapat dipanggil harus dijelaskan dengan ELF
jenis fungsi. Dalam kode asm, ini biasanya dilakukan menggunakan
Macro ENTRY / ENDPROC.
Tetapi dokumen yang sama membahas bahwa ini juga merupakan kerugian nyata dari pendekatan ini. Utilitas objtool memeriksa apakah semua fungsi dalam kernel ditulis dalam format yang benar untuk jejak stack.
Berikut ini adalah fungsi membuka gulungan tumpukan dari kernel Linux:
#if defined(CONFIG_FRAME_POINTER) && !defined(CONFIG_ARM_UNWIND) int notrace unwind_frame(struct stackframe *frame) { unsigned long high, low; unsigned long fp = frame->fp; frame->fp = *(unsigned long *)(fp - 12); frame->sp = *(unsigned long *)(fp - 8); frame->pc = *(unsigned long *)(fp - 4); return 0; } #endif
Tapi di sini saya ingin menandai baris dengan yang
defined(CONFIG_ARM_UNWIND)
. Dia mengisyaratkan bahwa kernel Linux juga menggunakan implementasi lain dari relax_frame, dan kita akan membicarakannya sedikit kemudian.
Opsi
-mapcs-frame hanya valid untuk set instruksi ARM. Tetapi diketahui bahwa mikrokontroler ARM memiliki satu set instruksi - Jempol (Jempol-1 dan Jempol-2, lebih tepatnya), digunakan terutama untuk seri Cortex-M. Untuk mengaktifkan pembuatan bingkai untuk mode Thumb, gunakan
flag -mtpcs-frame dan
-mtpcs-leaf-frame. Intinya, ini adalah analog dari -mapcs-frame. Menariknya, opsi-opsi ini saat ini hanya berfungsi untuk Cortex-M0 / M1. Untuk beberapa waktu saya tidak tahu mengapa saya tidak bisa mengkompilasi gambar yang diinginkan untuk Cortex-M3 / M4 / .... Setelah saya membaca kembali semua opsi gcc untuk ARM dan mencari di Internet, saya menyadari bahwa ini mungkin bug. Oleh karena itu, saya naik langsung ke kode sumber kompiler
arm-none-eabi-gcc . Setelah mempelajari bagaimana kompiler menghasilkan frame untuk ARM, Thumb-1 dan Thumb-2, saya sampai pada kesimpulan bahwa mereka melewati Thumb-2, yaitu saat ini frame dihasilkan hanya untuk Thumb-1 dan ARM. Setelah membuat
bug , pengembang GCC menjelaskan bahwa standar untuk ARM telah berubah beberapa kali dan flag-flag ini sangat ketinggalan jaman, tetapi untuk beberapa alasan mereka semua masih ada di kompiler. Di bawah ini adalah disassembler dari fungsi untuk mana frame dihasilkan.
static int my_func(int a) { my_func2(7); return 0; }
00008134 <my_func>: 8134: b084 sub sp, #16 8136: b580 push 8138: aa06 add r2, sp, #24 813a: 9203 str r2, [sp, #12] 813c: 467a mov r2, pc 813e: 9205 str r2, [sp, #20] 8140: 465a mov r2, fp 8142: 9202 str r2, [sp, #8] 8144: 4672 mov r2, lr 8146: 9204 str r2, [sp, #16] 8148: aa05 add r2, sp, #20 814a: 4693 mov fp, r2 814c: b082 sub sp, #8 814e: af00 add r7, sp, #0
Sebagai perbandingan, disassembler dengan fungsi yang sama untuk instruksi ARM
000081f8 <my_func>: 81f8: e1a0c00d mov ip, sp 81fc: e92dd800 push {fp, ip, lr, pc} 8200: e24cb004 sub fp, ip, #4 8204: e24dd008 sub sp, sp, #8
Pada pandangan pertama, sepertinya ini adalah hal yang sangat berbeda. Tetapi pada kenyataannya, frame persis sama, faktanya adalah bahwa dalam mode Thumb, instruksi push hanya memungkinkan register rendah (r0 - r7) dan register lr untuk ditumpuk. Untuk semua register lain, ini harus dilakukan dalam dua tahap melalui instruksi mov dan str, seperti pada contoh di atas.
Tumpuk bersantai melalui pengecualian
Pendekatan alternatif adalah tumpukan unwinding berdasarkan Exception Handling ABI untuk standar ARM Architecture (
EHABI ). Faktanya, contoh utama penggunaan standar ini adalah penanganan pengecualian dalam bahasa seperti C ++. Informasi yang disiapkan oleh kompiler untuk penanganan pengecualian juga dapat digunakan untuk melacak tumpukan. Mode ini diaktifkan dengan opsi GCC
-feksepsi (atau
-funwind-frame ).
Mari kita lihat lebih dekat bagaimana ini dilakukan. Untuk mulai dengan, dokumen ini (EHABI) memaksakan persyaratan tertentu pada kompiler untuk menghasilkan tabel tambahan .ARM.exidx dan .ARM.extab. Ini adalah bagaimana bagian .ARM.exidx ini didefinisikan dalam sumber kernel Linux. Dari
lengkungan file
/ arm / kernel / vmlinux.lds.h :
/* Stack unwinding tables */ #define ARM_UNWIND_SECTIONS \ . = ALIGN(8); \ .ARM.unwind_idx : { \ __start_unwind_idx = .; \ *(.ARM.exidx*) \ __stop_unwind_idx = .; \ } \
Standar "Penanganan Pengecualian ABI untuk Arsitektur ARM" mendefinisikan setiap elemen dari tabel .ARM.exidx sebagai struktur berikut:
struct unwind_idx { unsigned long addr_offset; unsigned long insn; };
Elemen pertama adalah offset relatif terhadap fungsi awal, dan elemen kedua adalah alamat dalam tabel instruksi yang perlu ditafsirkan dengan cara khusus untuk memutar tumpukan lebih lanjut. Dengan kata lain, setiap elemen dari tabel ini hanyalah urutan kata dan setengah kata, yang merupakan urutan instruksi. Kata pertama menunjukkan jumlah instruksi yang harus diselesaikan untuk memutar tumpukan ke frame berikutnya.
Petunjuk ini dijelaskan dalam standar EHABI yang telah disebutkan:

Lebih lanjut, implementasi utama penerjemah ini di Linux ada di file
arch / arm / kernel / melepas.cPenerapan fungsi relax_frame int unwind_frame(struct stackframe *frame) { unsigned long low; const struct unwind_idx *idx; struct unwind_ctrl_block ctrl; idx = unwind_find_idx(frame->pc); if (!idx) { pr_warn("unwind: Index not found %08lx\n", frame->pc); return -URC_FAILURE; } ctrl.vrs[FP] = frame->fp; ctrl.vrs[SP] = frame->sp; ctrl.vrs[LR] = frame->lr; ctrl.vrs[PC] = 0; if (idx->insn == 1) return -URC_FAILURE; else if ((idx->insn & 0x80000000) == 0) ctrl.insn = (unsigned long *)prel31_to_addr(&idx->insn); else if ((idx->insn & 0xff000000) == 0x80000000) ctrl.insn = &idx->insn; else { pr_warn("unwind: Unsupported personality routine %08lx in the index at %p\n", idx->insn, idx); return -URC_FAILURE; } if ((*ctrl.insn & 0xff000000) == 0x80000000) { ctrl.byte = 2; ctrl.entries = 1; } else if ((*ctrl.insn & 0xff000000) == 0x81000000) { ctrl.byte = 1; ctrl.entries = 1 + ((*ctrl.insn & 0x00ff0000) >> 16); } else { pr_warn("unwind: Unsupported personality routine %08lx at %p\n", *ctrl.insn, ctrl.insn); return -URC_FAILURE; } ctrl.check_each_pop = 0; while (ctrl.entries > 0) { int urc; if ((ctrl.sp_high - ctrl.vrs[SP]) < sizeof(ctrl.vrs)) ctrl.check_each_pop = 1; urc = unwind_exec_insn(&ctrl); if (urc < 0) return urc; if (ctrl.vrs[SP] < low || ctrl.vrs[SP] >= ctrl.sp_high) return -URC_FAILURE; } frame->fp = ctrl.vrs[FP]; frame->sp = ctrl.vrs[SP]; frame->lr = ctrl.vrs[LR]; frame->pc = ctrl.vrs[PC]; return URC_OK; }
Ini adalah implementasi dari fungsi escape_frame, yang digunakan jika opsi CONFIG_ARM_UNWIND diaktifkan. Saya memasukkan komentar dengan penjelasan dalam bahasa Rusia langsung ke teks sumber.
Berikut ini adalah contoh bagaimana elemen tabel .ARM.exidx mencari fungsi kernel_start di Embox:
$ arm-none-eabi-readelf -u build/base/bin/embox Unwind table index '.ARM.exidx' at offset 0xaa6d4 contains 2806 entries: <...> 0x1c3c <kernel_start>: @0xafe40 Compact model index: 1 0x9b vsp = r11 0x40 vsp = vsp - 4 0x84 0x80 pop {r11, r14} 0xb0 finish 0xb0 finish <...>
Dan ini dia disassemblernya:
00001c3c <kernel_start>: void kernel_start(void) { 1c3c: e92d4800 push {fp, lr} 1c40: e28db004 add fp, sp, #4 <...>
Mari kita melalui langkah-langkahnya. Kami melihat tugas
vps = r11
. (R11 ini adalah FP) dan kemudian
vps = vps - 4
. Ini sesuai dengan instruksi
add fp, sp, #4
. Selanjutnya muncul pop {r11, r14}, yang sesuai dengan instruksi
push {fp, lr}
. Instruksi
finish
melaporkan akhir eksekusi (jujur, saya masih tidak mengerti mengapa ada dua instruksi selesai di sana).
Sekarang mari kita lihat berapa banyak memori yang dirakit dengan flag flag
-funwind-frames.Untuk percobaan, saya mengkompilasi Embox untuk platform STM32F4-Discovery. Berikut adalah hasil objdump:
Dengan bendera -funwind-frames:Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0005a600 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00003fd8 0805a600 0805a600 0005e600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .ARM.extab 000049d0 0805e5d8 0805e5d8 000625d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .rodata 0003e380 08062fc0 08062fc0 00066fc0 2**5
Tanpa bendera:Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00058b1c 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00000008 08058b1c 08058b1c 0005cb1c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .rodata 0003e380 08058b40 08058b40 0005cb40 2**5
Sangat mudah untuk menghitung bahwa bagian .ARM.exidx dan .ARM.extab menempati sekitar 1/10 dari ukuran .text. Setelah itu, saya mengumpulkan gambar yang lebih besar - untuk ARM Integrator CP berdasarkan ARM9, dan ada bagian ini 1/12 dari ukuran bagian .text. Tetapi jelas bahwa rasio ini dapat bervariasi dari satu proyek ke proyek lainnya. Juga ternyata ukuran gambar yang menambahkan flag -macps-frame lebih kecil dari opsi pengecualian (yang diharapkan). Jadi, misalnya, ketika ukuran bagian .text adalah 600 Kb, ukuran total .ARM.exidx + .ARM.extab adalah 50 Kb, dan ukuran kode tambahan dengan flag -mapcs-frame hanya 10 Kb. Tetapi jika kita melihat di atas, apa prolog besar dihasilkan untuk Cortex-M1 (ingat, melalui mov / str?), Maka menjadi jelas bahwa dalam kasus ini praktis tidak ada perbedaan, yang berarti tidak mungkin menggunakan
-mtpcs-frame untuk mode Thumb setidaknya masuk akal.
Apakah jejak stack diperlukan untuk ARM sekarang? Apa alternatifnya?
Pendekatan ketiga adalah melacak tumpukan menggunakan debugger. Tampaknya banyak sistem operasi untuk bekerja dengan FreeRTOS, mikrokontroler NuttX saat ini
menyarankan opsi penelusuran khusus ini atau menawarkan untuk menonton disassembler.
Sebagai hasilnya, kami sampai pada kesimpulan bahwa jejak tumpukan untuk lengan dalam waktu berjalan sebenarnya tidak digunakan di mana pun. Ini mungkin konsekuensi dari keinginan untuk membuat kode yang paling efisien saat bekerja, dan untuk mengambil tindakan debugging (yang termasuk promosi tumpukan) secara offline. Di sisi lain, jika OS sudah menggunakan kode C ++, maka sangat mungkin untuk menggunakan implementasi tracing melalui .ARM.exidx.
Nah dan ya, masalah dengan output stack yang salah dalam interupsi di
Embox diselesaikan dengan sangat sederhana, ternyata cukup untuk menyimpan register LR di stack.