Guten Tag! Vor einigen Tagen bin ich in
unserem Projekt auf ein kleines Problem gestoßen - im GDB-Interrupt-Handler wurde die Stapelverfolgung für Cortex-M falsch angezeigt. Daher war es erneut hilfreich herauszufinden, und auf welche Weise kann ich einen Stack-Trace für ARM erhalten? Welche Kompilierungsflags wirken sich auf die Stapelverfolgbarkeit in ARM aus? Wie ist das im Linux-Kernel implementiert? Aufgrund der Recherche habe ich beschlossen, diesen Artikel zu schreiben.
Schauen wir uns die beiden wichtigsten Stack-Trace-Methoden im Linux-Kernel an.
Stapeln Sie sich durch Frames
Beginnen wir mit einem einfachen Ansatz, der im Linux-Kernel zu finden ist, aber derzeit in GCC einen veralteten Status hat.
Stellen Sie sich vor, ein bestimmtes Programm läuft auf dem Stapel im RAM, und irgendwann unterbrechen wir es und möchten den Aufrufstapel aufrufen. Angenommen, wir haben einen Zeiger auf den aktuellen Befehl, der vom Prozessor (PC) ausgeführt wird, sowie den aktuellen Zeiger auf die Oberseite des Stapels (SP). Um den Stapel zur vorherigen Funktion hochzuspringen, müssen Sie verstehen, um welche Art von Funktion es sich handelt und wo wir zu dieser Funktion springen sollten. ARM verwendet zu diesem Zweck das Link Register (LR).
Das Verbindungsregister (LR) ist das Register R14. Es speichert die Rückgabeinformationen für Unterprogramme, Funktionsaufrufe und Ausnahmen. Beim Zurücksetzen setzt der Prozessor den LR-Wert auf 0xFFFFFFFF
Als nächstes müssen wir den Stapel hochgehen und die neuen Werte der LR-Register vom Stapel laden. Die Struktur des Stapelrahmens für den Compiler ist wie folgt:
/* 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. */
Diese Beschreibung stammt aus der GCC-Header-Datei gcc / gcc / config / arm / arm.h.
Das heißt, Der Compiler (in unserem Fall GCC) kann irgendwie darüber informiert werden, dass wir einen Stack-Trace durchführen möchten. Und dann bereitet der Compiler im Prolog jeder Funktion eine Art Hilfsstruktur vor. Sie können feststellen, dass in dieser Struktur der „nächste“ Wert des LR-Registers liegt, den wir benötigen, und vor allem die Adresse des nächsten Frames enthält
| return fp value | [fp, #-12]
Dieser Compilermodus wird durch die Option -mapcs-frame angegeben. In der Beschreibung der Option zum Thema "Wenn Sie -fomit-frame-pointer mit dieser Option angeben, werden die Stapelrahmen nicht für Blattfunktionen generiert." Unter Blattfunktionen werden hier diejenigen verstanden, die keine anderen Funktionen aufrufen, so dass sie etwas einfacher gemacht werden können.
Möglicherweise fragen Sie sich auch, was Sie in diesem Fall mit Assembler-Funktionen tun sollen. Eigentlich nichts kniffliges - Sie müssen spezielle Makros einfügen. Aus der Datei
tools / objtool / Documentation / stack-validation.txt im Linux-Kernel:
Jede aufrufbare Funktion muss als solche mit dem ELF versehen werden
Funktionstyp. Im asm-Code erfolgt dies normalerweise mit dem
ENTRY / ENDPROC-Makros.
In demselben Dokument wird jedoch erörtert, dass dies auch ein offensichtlicher Nachteil dieses Ansatzes ist. Das Dienstprogramm objtool prüft, ob alle Funktionen im Kernel im richtigen Format für den Stack-Trace geschrieben sind.
Das Folgende ist die Funktion zum Abwickeln eines Stacks vom Linux-Kernel:
#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
Aber hier möchte ich die Zeile mit
defined(CONFIG_ARM_UNWIND)
. Sie weist darauf hin, dass der Linux-Kernel auch eine andere Implementierung von unwind_frame verwendet, und wir werden etwas später darüber sprechen.
Die Option
-mapcs-frame ist nur für den ARM-Befehlssatz gültig. Es ist jedoch bekannt, dass ARM-Mikrocontroller einen anderen Befehlssatz haben - Thumb (Thumb-1 und Thumb-2, genauer gesagt), der hauptsächlich für die Cortex-M-Serie verwendet wird. Verwenden Sie die
Flags -mtpcs-frame und
-mtpcs-leaf-frame, um die Frame-Generierung für den Thumb-Modus zu
aktivieren . Im Wesentlichen ist es ein Analogon von -mapcs-frame. Interessanterweise funktionieren diese Optionen derzeit nur für den Cortex-M0 / M1. Für einige Zeit konnte ich nicht herausfinden, warum ich das gewünschte Bild für Cortex-M3 / M4 / .... nicht kompilieren konnte. Nachdem ich alle gcc-Optionen für ARM erneut gelesen und im Internet gesucht hatte, stellte ich fest, dass dies wahrscheinlich ein Fehler war. Daher bin ich direkt in den Quellcode des
arm-none-eabi-gcc- Compilers
geklettert . Nachdem ich untersucht hatte, wie der Compiler Frames für ARM, Thumb-1 und Thumb-2 generiert, kam ich zu dem Schluss, dass sie Thumb-2 umgangen haben, d. H. Im Moment werden Frames nur für Thumb-1 und ARM generiert. Nach dem Erstellen der
Fehler erklärten die GCC-Entwickler, dass sich der Standard für ARM bereits mehrmals geändert hat und diese Flags sehr veraltet sind, aber aus irgendeinem Grund alle noch im Compiler vorhanden sind. Unten sehen Sie den Disassembler der Funktion, für die der Frame generiert wird.
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
Im Vergleich dazu ein Disassembler der gleichen Funktion für ARM-Befehle
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
Auf den ersten Blick scheint es, dass dies völlig andere Dinge sind. Tatsächlich sind die Frames jedoch genau gleich. Tatsache ist, dass im Thumb-Modus mit dem Push-Befehl nur niedrige Register (r0 - r7) und das lr-Register gestapelt werden können. Für alle anderen Register muss dies in zwei Schritten durch die Anweisungen mov und str erfolgen, wie im obigen Beispiel.
Stapel durch Ausnahmen abwickeln
Ein alternativer Ansatz ist das Abwickeln des Stapels basierend auf dem ABI für die Ausnahmebehandlung für den ARM-Architekturstandard (
EHABI ). Das Hauptbeispiel für die Verwendung dieses Standards ist die Ausnahmebehandlung in Sprachen wie C ++. Informationen, die vom Compiler für die Ausnahmebehandlung vorbereitet wurden, können auch zum Verfolgen des Stapels verwendet werden. Dieser Modus wird mit der Option GCC
-fexceptions (oder
-funwind-frame )
aktiviert .
Schauen wir uns genauer an, wie das gemacht wird. Dieses Dokument (EHABI) stellt zunächst bestimmte Anforderungen an den Compiler, um die Hilfstabellen .ARM.exidx und .ARM.extab zu generieren. So wird dieser Abschnitt .ARM.exidx in den Linux-Kernelquellen definiert. Aus der Datei
arch / 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 = .; \ } \
Der Standard „ABI für die Ausnahmebehandlung für die ARM-Architektur“ definiert jedes Element der Tabelle .ARM.exidx wie folgt:
struct unwind_idx { unsigned long addr_offset; unsigned long insn; };
Das erste Element ist der Versatz relativ zum Beginn der Funktion, und das zweite Element ist die Adresse in der Befehlstabelle, die auf besondere Weise interpretiert werden muss, um den Stapel weiter zu drehen. Mit anderen Worten, jedes Element dieser Tabelle ist einfach eine Folge von Wörtern und Halbwörtern, die eine Folge von Anweisungen sind. Das erste Wort gibt die Anzahl der Anweisungen an, die ausgeführt werden müssen, um den Stapel zum nächsten Frame zu drehen.
Diese Anweisungen sind im bereits erwähnten EHABI-Standard beschrieben:

Die Hauptimplementierung dieses Interpreters unter Linux befindet sich in der Datei
arch / arm / kernel / unwind.cImplementierung der Funktion unwind_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; }
Dies ist eine Implementierung der Funktion unwind_frame, die verwendet wird, wenn die Option CONFIG_ARM_UNWIND aktiviert ist. Ich habe die Kommentare mit Erklärungen in russischer Sprache direkt in den Quelltext eingefügt.
Das folgende Beispiel zeigt, wie das Tabellenelement .ARM.exidx nach der Funktion kernel_start in Embox sucht:
$ 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 <...>
Und hier ist ihr Disassembler:
00001c3c <kernel_start>: void kernel_start(void) { 1c3c: e92d4800 push {fp, lr} 1c40: e28db004 add fp, sp, #4 <...>
Gehen wir die Schritte durch. Wir sehen die Zuordnung
vps = r11
. (R11 das ist FP) und dann
vps = vps - 4
. Dies entspricht der Anweisung
add fp, sp, #4
. Als nächstes kommt pop {r11, r14}, was der Anweisung
push {fp, lr}
. Die letzte
finish
das Ende der Ausführung an (um ehrlich zu sein, verstehe ich immer noch nicht, warum es dort zwei Zielanweisungen gibt).
Nun wollen wir sehen, wie viel Speicher die Assembly mit dem
Flag -funwind-frame belegt.Für das Experiment habe ich Embox für die STM32F4-Discovery-Plattform kompiliert. Hier sind die Objdump-Ergebnisse:
Mit dem Flag -funwind-frame: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
Ohne Flagge: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
Es ist leicht zu berechnen, dass die Abschnitte .ARM.exidx und .ARM.extab ungefähr 1/10 der Textgröße einnehmen. Danach habe ich ein größeres Bild gesammelt - für ARM Integrator CP basierend auf ARM9, und dort waren diese Abschnitte 1/12 der Größe des Textabschnitts. Es ist jedoch klar, dass dieses Verhältnis von Projekt zu Projekt variieren kann. Es stellte sich auch heraus, dass die Größe des Bildes, das das Flag -macps-frame hinzufügt, kleiner ist als die erwartete Ausnahmeoption. Wenn beispielsweise die Größe des Textabschnitts 600 KB betrug, betrug die Gesamtgröße von .ARM.exidx + .ARM.extab 50 KB, und die Größe des zusätzlichen Codes mit dem Flag -mapcs-frame betrug nur 10 KB. Aber wenn wir oben schauen, was für ein großer Prolog für Cortex-M1 generiert wurde (denken Sie daran, durch mov / str?), Dann wird klar, dass es in diesem Fall praktisch keinen Unterschied gibt, was bedeutet, dass es unwahrscheinlich ist,
-mtpcs-frame für den Thumb-Modus zu verwenden macht zumindest einen Sinn.
Wird eine solche Stapelverfolgung jetzt für ARM benötigt? Was sind die Alternativen?
Ein dritter Ansatz besteht darin, den Stapel mithilfe eines Debuggers zu verfolgen. Es scheint, dass viele Betriebssysteme für die Arbeit mit FreeRTOS- und NuttX-Mikrocontrollern derzeit diese spezielle Ablaufverfolgungsoption
vorschlagen oder anbieten, einen Disassembler zu überwachen.
Als Ergebnis kamen wir zu dem Schluss, dass die Stapelverfolgung für Waffen zur Laufzeit eigentlich nirgendwo verwendet wird. Dies ist wahrscheinlich eine Folge des Wunsches, während der Arbeit den effizientesten Code zu erstellen und die Debugging-Aktionen (einschließlich der Heraufstufung des Stacks) offline auszuführen. Wenn das Betriebssystem jedoch bereits C ++ - Code verwendet, ist es durchaus möglich, die Implementierung der Ablaufverfolgung über .ARM.exidx zu verwenden.
Nun ja, das Problem mit der falschen
Stapelausgabe im Interrupt in
Embox wurde sehr einfach gelöst. Es stellte sich heraus, dass es ausreichte, um das LR-Register auf dem Stapel zu speichern.